Public Windows WebView2 kiosk client and signed release channel for Printago dashboards.
  • PowerShell 79.9%
  • C# 20.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-16 17:44:21 -04:00
.forgejo/workflows fix: recover managed Store kiosk release 2026-08-10 02:42:05 -04:00
_bmad-output/implementation-artifacts docs: record Store handoff evidence 2026-08-10 02:46:35 -04:00
config fix: recover managed Store kiosk release 2026-08-10 02:42:05 -04:00
docs fix: make Windows auto-update trustworthy 2026-08-01 17:23:01 -04:00
intune Launch Store desktop app through stable alias 2026-08-16 17:44:21 -04:00
packaging feat: add Microsoft Store distribution 2026-08-02 12:43:00 -04:00
qa docs: align Windows production trust and lifecycle 2026-07-22 14:44:55 -04:00
scripts fix: recover managed Store kiosk release 2026-08-10 02:42:05 -04:00
src/PrintagoKiosk feat: add Microsoft Store distribution 2026-08-02 12:43:00 -04:00
store-assets feat: add Microsoft Store distribution 2026-08-02 12:43:00 -04:00
tests Launch Store desktop app through stable alias 2026-08-16 17:44:21 -04:00
.gitignore Harden signed URL persistence and installer elevation 2026-07-21 13:29:33 -04:00
ARTIFACT-SIGNING.md Harden Windows stable promotion evidence 2026-07-24 10:21:14 -04:00
CI.md Harden LAB release source trust 2026-07-21 10:47:26 -04:00
CURRENT-RELEASE-STATUS.md feat: add Microsoft Store distribution 2026-08-02 12:43:00 -04:00
HOME-INSTALL.md fix: recover managed Store kiosk release 2026-08-10 02:42:05 -04:00
INTUNE-ASSIGNED-ACCESS.md Launch Store desktop app through stable alias 2026-08-16 17:44:21 -04:00
INTUNE-WIN32.md feat: package unsigned kiosk for Intune Win32 2026-07-23 18:58:05 -04:00
LAB-PILOT.md docs: align Windows production trust and lifecycle 2026-07-22 14:44:55 -04:00
LICENSE Initialize standalone Printago Windows kiosk 2026-07-20 17:49:42 -04:00
MICROSOFT-STORE.md Launch Store desktop app through stable alias 2026-08-16 17:44:21 -04:00
MIGRATION.md Initialize standalone Printago Windows kiosk 2026-07-20 17:49:42 -04:00
PrintagoKiosk.sln Harden Windows update trust and stable promotion 2026-07-22 13:45:15 -04:00
PRIVACY.md feat: add Microsoft Store distribution 2026-08-02 12:43:00 -04:00
README.md fix: recover managed Store kiosk release 2026-08-10 02:42:05 -04:00
RELEASE-HANDOFF.md feat: add Microsoft Store distribution 2026-08-02 12:43:00 -04:00
UNSIGNED-PORTABLE.md feat: add certificate-free portable deployment 2026-07-22 19:24:30 -04:00

Printago Windows kiosk

The long-term distribution channel is now the Microsoft Store: it gives ordinary PCs a trusted install and Store-managed updates while Intune can assign the same Store product to managed kiosks. The source includes a fail-closed, unsigned Store-submission workflow; Microsoft supplies the final signature. Exact Partner Center identity values are required and are never guessed.

See MICROSOFT-STORE.md for the Partner Center handoff, personal-PC behavior, Intune configuration, scheduled maintenance validation, canary migration, and rollback. The legacy Forgejo App Installer channel below remains unchanged during migration and must not be removed before the Store canary passes.

The authoritative 1.0.1.0 Store handoff is the protected artifact produced by the manual Store workflow from the exact clean protected-main commit. A local unsigned build is only diagnostic evidence. The repository never submits to Partner Center automatically; submission and publication remain explicit human actions.

Current public download status is tracked in CURRENT-RELEASE-STATUS.md. Check it before giving an Intune or home-install tester a link.

To verify the stable channel from an unauthenticated machine, run:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-PublicReleaseStatus.ps1

Add -RequireReady in a handoff or release gate; a 404, missing content length, or wrong MIME type means there is still no production-safe installer.

This directory contains a self-contained .NET 8 WPF application with a single WebView2 surface. Its public-source placeholder is https://dashboard.example.invalid/; configure your farm URL before deployment. It has no browser chrome, and keeps its WebView2 user-data folder at the logical path %LOCALAPPDATA%\Printago\Kiosk\WebView2Profile. The MSIX explicitly excludes that tree from package virtualization so it remains per-account and persistent across app and Windows restarts. Cookies and the dashboard login therefore survive restarts. It never uses private/incognito mode.

The app is one containment layer. Windows Assigned Access is the OS containment layer that prevents shell escape and relaunches the app after an unexpected exit. Deploy both; a borderless desktop window by itself is not a secure kiosk.

What the app enforces

  • maximized borderless, non-resizable, topmost WebView2 window;
  • exactly one top-level origin derived from DASHBOARD_URL (scheme, IDN host, and effective port must match); paths, queries, and fragments may change;
  • all new windows, downloads, credential-bearing URLs, context menus, DevTools, zoom/navigation gestures, and browser accelerators are canceled;
  • external files and links cannot be dropped onto the page (the supported per-app AllowExternalDrop property is disabled); in-page drag interactions are unaffected;
  • every external URI-scheme launch is canceled by WebView2's LaunchingExternalUriScheme event before Windows can invoke a protocol handler;
  • camera, autoplay, and persistent-storage permission is granted only to that origin; every other PermissionRequested kind is denied;
  • page-to-host WebView messaging is disabled; the host reads only four boolean device-capability values with a bounded script after a successful load;
  • when locked-kiosk capability mode is required, a DPAPI-protected token is attached only to same-origin HTML documents and the explicitly required SvelteKit */__data.json client-navigation fetches as X-Printago-Kiosk-Capability; cross-origin redirects and every other subresource/API request never receive it, and it is never placed in DOM, JavaScript, URL, config output, or logs;
  • WebView2 browser-process recovery and retry after a failed navigation;
  • fail-closed startup if inherited WEBVIEW2_* process variables or WebView2 loader policy under the kiosk account's HKCU attempt to replace the persistent profile/runtime, select another runtime channel, add browser flags, or enable script debugging; administrator-owned HKLM WebView2 policy remains a separate deployment boundary;
  • a single app instance to protect the persistent profile from concurrent use.

The app does not weaken TLS, SmartScreen, or the WebView2 sandbox.

Scanner modes

Use the shared keys in config/kiosk-config.env.example:

  • auto (default): keyboard-wedge scanners work as ordinary trusted keyboard input. If SERIAL_DEVICE is also set, the serial bridge is enabled.
  • hid-keyboard: explicitly select a USB/Bluetooth HID scanner that emulates a keyboard. Its scan reaches the focused page through the same input path as in a normal browser.
  • serial: opens exactly SERIAL_DEVICE (for example COM3) at SERIAL_BAUD. Complete CR/LF-delimited scans are re-injected with Windows SendInput, including Enter, so the page receives normal OS key input. A disconnect shows an on-screen warning and retries the same configured port every five seconds; it never silently selects another device.
  • web: leaves browser-owned device selection to the dashboard. The current dashboard exposes explicit Connect HID POS scanner and Connect serial scanner controls; those clicks must remain the user gestures that open the chooser. Generic WebUSB is not claimed without a reviewed device-specific adapter.

The current WebView2 SDK's host CoreWebView2PermissionKind enum has no USB, HID, or Serial member. The host cannot honestly pre-grant those device choosers. The page must request the device from a user gesture, the current Evergreen WebView2 runtime must expose the API, and Assigned Access must not suppress the picker. A startup capability report is written to the kiosk log, but only the physical tests in qa/acceptance-test-plan.md prove a real device works.

The dashboard's authenticated locked-kiosk capability now provides the supported source-side path around WebView2's missing native open-picker event: when the server validates the same-origin capability header for an authenticated request, it does not render Scan QR image. LOCK-09 remains a production blocker until that exact dashboard revision is deployed and the real Assigned Access device proves the upload control and native picker are absent after login, redirects, refresh, reboot, and WebView2 restart. Live camera scanning and keyboard/WebHID POS/WebSerial/manual/touch paths must remain available. See docs/LOCK-09-WEBVIEW2-FILE-PICKER.md.

Configuration precedence

From lowest to highest precedence:

  1. src/PrintagoKiosk/appsettings.json;
  2. %ProgramData%\PrintagoKiosk\kiosk-config.env (or --config PATH);
  3. process environment variables SCANNER_MODE, SERIAL_DEVICE, SERIAL_BAUD;
  4. --url, --scanner-mode, --serial-device, --serial-baud, --kiosk-capability-mode.

DASHBOARD_URL and capability mode deliberately have no process-environment override; each comes only from package defaults, the admin-owned machine file, or administrator-controlled launch arguments. DASHBOARD_URL is a lockdown control that pins the kiosk origin, so it must never be settable from a user-writable process environment variable (e.g. HKCU\Environment) that a standard kiosk account can write. The token has no environment-variable or command-line precedence and is loaded only from the fixed ACL-restricted DPAPI file.

For a Store-installed managed kiosk, run the single supported provisioning command from an elevated PowerShell window:

.\scripts\Provision-HomeKiosk.ps1 `
  -DashboardUrl 'https://printagotesting.humpf.online/' `
  -KioskAccount '.\PrintagoKiosk' `
  -UpdateAuthority microsoft-store

The masked prompt accepts the dashboard's server-issued kiosk capability token, not a Microsoft Store, Partner Center, Microsoft Entra, or Intune credential. The wrapper installs the DPAPI-protected token and explicit managed/required configuration transactionally. Legacy App Installer provisioning uses the same wrapper without -UpdateAuthority and retains its appinstaller default.

The configured origin is never logged with credentials, query, or fragment.

Certificate-free portable option

For an owner-managed Windows PC that cannot obtain a public signing identity, the repository also provides an explicitly unsigned, manual-update portable ZIP. It requires no certificate installation or administrator access and keeps the same persistent WebView2 profile across versioned installs and explicit rollbacks.

Build it with scripts/Build-PortableRelease.ps1; validate and install it with scripts/Install-PortableRelease.ps1. The installer requires the exact expected ZIP SHA-256, validates every packaged file, blocks implicit downgrades, and creates a current-user Startup shortcut. See UNSIGNED-PORTABLE.md for the complete lifecycle.

This is not a way to label unsigned bytes as a trusted production release. Windows may show SmartScreen warnings, a same-host checksum is not publisher authentication, direct owner-managed updates are manual, and a Startup shortcut is not Assigned Access. The signed MSIX/App Installer path below remains available when public publisher trust, an Intune LOB MSIX, or an authenticated internet App Installer channel is needed.

Enrolled kiosks can instead use Microsoft Intune as the trusted distribution and update authority for this unsigned portable build. The repeatable scripts/Build-IntuneWin32Package.ps1 workflow emits a real .intunewin, version-aware detection script, and exact upload settings. See INTUNE-WIN32.md. The package must use System install behavior and a device-group Required assignment. Each reviewed higher version is published as a new Win32 app that supersedes the prior version without uninstalling it first.

Build, test, and sign

Pull requests use the separate unprivileged CI / verify gate documented in CI.md. It builds and inspects an unsigned, non-deployable MSIX on the dedicated printago-windows-ci runner and has no signing or publication access.

Prerequisites: Windows 10/11 x64, .NET 8 SDK, Windows 10/11 SDK (MakeAppx and SignTool), and the Evergreen WebView2 Runtime on the target device.

For the legacy direct/App Installer channel, the implemented public-trust signer is Azure Artifact Signing (formerly Trusted Signing). Preinstall Microsoft.Azure.ArtifactSigningClientTools on the protected Windows runner. The Forgejo direct-channel workflow uses it when signing_mode=azure-artifact-signing. Unsigned and TEST-ONLY outputs never become deployable. The former generic PFX fallback was removed: Windows trust on one runner cannot prove that a CA/private root belongs to the public root program trusted by an unmanaged home PC.

The workflow never places Azure credentials in the unsigned build, test, .NET publish, or MakeAppx environment. Only the signer step receives them. The stable App Installer and versioned package URLs are bound to this repository's Forgejo release channel; an arbitrary mirror URL is rejected unless a future explicit mirror publication/attestation contract is implemented.

Protected Forgejo secrets are AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET, WINDOWS_KIOSK_ARTIFACT_SIGNING_ENDPOINT, WINDOWS_KIOSK_ARTIFACT_SIGNING_ACCOUNT, WINDOWS_KIOSK_ARTIFACT_SIGNING_PROFILE, WINDOWS_KIOSK_SIGNING_PUBLISHER (the profile certificate's exact Subject), and WINDOWS_KIOSK_PRODUCTION_AUMID (the reviewed output of Get-PackageAumid.ps1). Scope the service principal to the profile with the Artifact Signing Certificate Profile Signer role and never expose its rotated secret to pull-request workflows. GitHub Actions users should use Microsoft's official azure/trusted-signing-action@v0.

Optional candidate publication additionally uses a protected, release-scoped FORGEJO_RELEASE_TOKEN. Keep publish_release=false for build-only evidence. When enabled, the signing workflow publishes only an immutable prerelease candidate; it cannot modify stable. The separate promotion workflow requires protected values matching the accepted MSIX hash, AUMID, physical-evidence hash, deployed dashboard revision, and Assigned Access XML hash plus PROMOTE-ACCEPTED-CANDIDATE-TO-STABLE. The protected values are WINDOWS_KIOSK_ACCEPTED_PACKAGE_SHA256, WINDOWS_KIOSK_ACCEPTED_AUMID, WINDOWS_KIOSK_ACCEPTED_PHYSICAL_EVIDENCE_SHA256, WINDOWS_KIOSK_ACCEPTED_DASHBOARD_REVISION, and WINDOWS_KIOSK_ACCEPTED_ASSIGNED_ACCESS_XML_SHA256.

Publisher must exactly match the signing certificate Subject. The build runs the origin-policy tests, publishes the app self-contained, generates package assets, creates the MSIX, signs and verifies it, and writes metadata containing the signer-dependent AUMID. Never commit a signing private key or credential. The generated artifacts/, bin/, and obj/ trees are intentionally ignored; publish the approved MSIX, checksum, and metadata as protected release assets, not source files. DashboardUrl bakes a safe farm URL into the package default so a home operator can double-click a production-trusted package without running a configuration script. Intune can still override it silently with the machine configuration. For unmanaged devices every future URL change is delivered only by a strictly higher production-signed MSIX through App Installer. The earlier independent runtime URL feed was removed because its user-writable anti-replay state could be deleted.

For lab use only, scripts/New-TestSignedPackage.ps1 creates an ephemeral self-signed certificate, timestamps and signs a package labeled TEST-ONLY, exports only its public CER, and never writes the signer to a certificate store. Intune may silently trust the CER for a dedicated lab device group only. This is not a production signature and must never be presented to a production user. The immutable v1.0.1-lab.1 package also has a pinned disposable-PC recovery bootstrap in scripts/Install-LabPilot.ps1; see LAB-PILOT.md. It verifies the exact release and uses LocalMachine\TrustedPeople, but remains explicitly unsuitable for production/home rollout.

Production deployment has a hard zero-prompt rule. The recommended path is the Microsoft Store workflow documented above. The legacy stable App Installer pipeline accepts only Microsoft Azure Artifact Signing Public Trust; generic PFX, public-CA, and private-enterprise identities are not alternatives for that direct channel. An end user must never see or approve a certificate dialog or PowerShell window. An unmanaged Store install uses the normal Microsoft Store UI. The legacy direct channel still lacks a real tenant signing identity, so its replacement production-signed MSIX remains blocked.

For the Azure path, the stable workflow accepts only the Microsoft Artifact Signing Public Trust chain; Private Trust and Public Trust Test profiles are rejected even if their roots were installed on the build runner. Public trust eliminates manual certificate installation, but Microsoft explicitly does not guarantee instant SmartScreen reputation for a new publisher/file. If the accepted home experience must contain no new-publisher warning, retain that as an external release gate until it is observed on a clean unmanaged PC; do not substitute another signing path that this repository does not implement.

Forgejo App Installer updates

Production release builds also emit PrintagoKiosk.appinstaller. Its 2021 schema checks every launch (HoursBetweenUpdateChecks=0) and in the background. Updates are silent: ShowPrompt=false and UpdateBlocksActivation=false. The workflow re-reads the signed package and requires the protected Publisher and AUMID baseline before emitting it, preserving package identity and the WebView2 LocalCache login profile.

The update association is not created merely by installing the raw MSIX. Unmanaged devices must install the first production release by downloading and opening the stable .appinstaller file. An Intune Line-of-Business MSIX assignment must also deploy the Enterprise Modern App Management settings in intune/AppUpdateSettings.template.csv (or let Intune own every later package version). See INTUNE-ASSIGNED-ACCESS.md; do not claim Forgejo auto-update from a raw-MSIX-only deployment.

Publish the versioned signed MSIX as a prerelease candidate first. Replace the stable manifest only through the separate protected promotion workflow after signature, identity, checksum, physical evidence, and public checks pass. Both must be publicly reachable over HTTPS without cookies. Serve the manifest as application/appinstaller with a correct Content-Length and the MSIX as application/vnd.ms-appx. Use a direct .appinstaller download; Microsoft disables the ms-appinstaller: protocol by default.

The package URL is only a default. Intune can redeploy Install-KioskConfig.ps1 to change %ProgramData%\PrintagoKiosk\kiosk-config.env without reinstalling. Same-origin changes retain login; a different hostname cannot share cookies and requires one login.

For unmanaged/home kiosks, changing dashboard_url takes effect only when a strictly higher package version installs through the stable channel. For managed kiosks, the machine configuration has higher precedence, so changing the baked package default does not override an Intune configuration. Every versioned release records both the reviewed URL and its origin before rollout.

The checked-in source has moved beyond the locally built lab output below. That package is retained only as historical lab evidence and must be rebuilt before any further pilot. See RELEASE-HANDOFF.md for the exact release gate and Intune-admin handoff.

Historical locally built lab output (20 July 2026):

  • artifacts/PrintagoKiosk-1.0.0.0-x64-TEST-ONLY-signed.msix;
  • SHA-256 E019866EB8FE51234FD216A58A7C00085AC5FFBAD98421FA5098D0542D5F4FF2;
  • signer SHA-1 B2DFE862DBA68753DD7C875636C27D330E3C3E21 (matches the exported TEST-ONLY public CER);
  • test AUMID Printago.Kiosk_sy2r47zzwsv8c!App;
  • 14/14 origin-policy tests passed; MSIX creation and timestamped signature succeeded;
  • an earlier TEST-ONLY package with the same manifest Publisher was explicitly trusted on this lab PC and installed with package status Ok; its execution alias launched PID 86532, WebView2 initialized, and the persistent profile produced a Cookie database. The final disposable signer above is intentionally not trusted or installed because it is only a reproducibility artifact;
  • the live Evergreen runtime reported navigator.hid, navigator.usb, and navigator.serial present in a secure context. This proves API exposure, not that a physical scanner or permission picker has passed acceptance;
  • no test-signing private key remains. The public test certificate remains trusted only for lab installation and must be removed after testing. A production AUMID will differ when the Publisher changes.

Assigned Access and Intune

See INTUNE-ASSIGNED-ACCESS.md. The shipped profile uses the stable MSIX execution alias as a classic desktop kiosk app. The AUMID is still emitted for package inventory and validation, but must not be guessed: it changes with the signing Publisher.

For an existing named local Windows account or a nontechnical home deployment, also see HOME-INSTALL.md. The source supports both the Windows-managed auto-logon account and an explicitly named local standard account.

Production signing and Forgejo update-channel setup are in ARTIFACT-SIGNING.md.

Logs and release evidence

The app writes the logical path %LOCALAPPDATA%\Printago\Kiosk\Logs\kiosk.log (physically under the package family's LocalCache for an installed MSIX). It does not log scan values, cookies, passwords, URL queries, or URL fragments. Complete every Windows row in the shared acceptance plan and retain the evidence record before calling a device production-ready.