- PowerShell 79.9%
- C# 20.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| _bmad-output/implementation-artifacts | ||
| config | ||
| docs | ||
| intune | ||
| packaging | ||
| qa | ||
| scripts | ||
| src/PrintagoKiosk | ||
| store-assets | ||
| tests | ||
| .gitignore | ||
| ARTIFACT-SIGNING.md | ||
| CI.md | ||
| CURRENT-RELEASE-STATUS.md | ||
| HOME-INSTALL.md | ||
| INTUNE-ASSIGNED-ACCESS.md | ||
| INTUNE-WIN32.md | ||
| LAB-PILOT.md | ||
| LICENSE | ||
| MICROSOFT-STORE.md | ||
| MIGRATION.md | ||
| PrintagoKiosk.sln | ||
| PRIVACY.md | ||
| README.md | ||
| RELEASE-HANDOFF.md | ||
| UNSIGNED-PORTABLE.md | ||
Printago Windows kiosk
Recommended production distribution
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
AllowExternalDropproperty is disabled); in-page drag interactions are unaffected; - every external URI-scheme launch is canceled by WebView2's
LaunchingExternalUriSchemeevent before Windows can invoke a protocol handler; - camera, autoplay, and persistent-storage permission is granted
only to that origin; every other
PermissionRequestedkind 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.jsonclient-navigation fetches asX-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. IfSERIAL_DEVICEis 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 exactlySERIAL_DEVICE(for exampleCOM3) atSERIAL_BAUD. Complete CR/LF-delimited scans are re-injected with WindowsSendInput, 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:
src/PrintagoKiosk/appsettings.json;%ProgramData%\PrintagoKiosk\kiosk-config.env(or--config PATH);- process environment variables
SCANNER_MODE,SERIAL_DEVICE,SERIAL_BAUD; --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, andnavigator.serialpresent 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.