Windows tray agent (.NET 8) that exposes a wss:// JSON-RPC API on localhost so paired HTTPS web pages can list local printers and silent-print PDFs without going through the browser dialog.
Inspired by tools such as QZ Tray and Dymo Web Service, but kept intentionally minimal and focused on PDF printing.
+------------------------------------+ +-----------------------------------+
| Web page (https://app.example.com)| | Windows desktop |
| | | |
| await pa.getLocalPrinters() | <-----> | PrintAgent.exe (system tray) |
| await pa.print({ pdfBase64, ... })| wss:// | Kestrel + self-signed cert |
| | 127. | Origin pairing prompt |
| printagent-client.js | 0.0.1: | SumatraPDF (silent printing) |
+------------------------------------+ 8443+ +-----------------------------------+
- The user installs the agent once. A self-signed certificate is added to the user's Trusted Root store so browsers accept
wss://localhost. - A web page imports the bundled TypeScript client and calls
connect(). The first time a given origin connects, the agent shows a Windows prompt asking the user to authorize that origin. - Once paired, the page can list installed printers, send PDF jobs, and receive asynchronous events (
job.statusChanged,printers.changed).
| Folder | Purpose |
|---|---|
agent/ |
.NET 8 solution: the tray agent + xUnit test project. |
client/ |
Standalone TypeScript client (no npm publish, copy-paste). |
installer/ |
Velopack build script producing the Windows installer. |
- Windows 10/11 x64
- .NET 8 SDK
- Node.js 20+ (for building the TypeScript client)
.NET tool: vpk— auto-installed globally bybuild.ps1if not present (dotnet tool install -g vpk)- SumatraPDF — already vendored at
agent/PrintAgent/Resources/SumatraPDF.exe(3.6.1, ~19 MB, portable 64-bit). It is copied to the publish folder next toPrintAgent.exe, never embedded or extracted at runtime. To bump the version, replace the file and updateSUMATRAPDF-NOTICE.txt.
cd agent
dotnet publish PrintAgent/PrintAgent.csproj -c Release -r win-x64Output: agent/PrintAgent/bin/Release/net8.0-windows/win-x64/publish/ - a self-contained folder (~190 MB, ~390 files) containing PrintAgent.exe, the .NET runtime, and SumatraPDF.exe. Velopack packages the whole folder.
Do not add -p:PublishSingleFile=true. A self-extracting bundle is a packer signature for Defender's ML heuristics, and unsigned builds get flagged as Trojan:Win32/Sabsik.FL.A!ml. See "Antivirus false positives" below.
cd agent
dotnet testcd client
npm install
npm run buildOutput: client/dist/printagent-client.js and printagent-client.d.ts. These two files are also committed to the repo so consumers can grab them directly without running the build.
cd installer
./build.ps1 -Version 0.1.0build.ps1 wipes installer/Output/ before packing, so every build starts from a clean slate.
Output in installer/Output/:
PrintAgent-win-Setup.exe- run on the target machine to install (per-user, no elevation required)PrintAgent-win-Portable.zip- no-install variantRELEASES/releases.win.json- Velopack release feed for auto-updatePrintAgent-<version>-full.nupkg- full update package
The installer is built with Velopack. The vpk dotnet tool is installed automatically by build.ps1 if not already present.
Releases are full-only (--delta None). Delta generation would diff against whichever older .nupkg files happen to be in Output/, which makes builds depend on local leftovers. Auto-update still works - Velopack falls back to the full package - but each update downloads the whole ~92 MB payload instead of a small patch.
PrintAgent is not code-signed, so Windows Defender's machine-learning heuristics may flag the build as Trojan:Win32/Sabsik.FL.A!ml - the generic bucket for unsigned Windows executables of unknown reputation. Two build choices exist specifically to keep that surface small:
- No
PublishSingleFile. A self-extracting bundle looks like a packed binary. Velopack packages the publish folder anyway, so the flag buys nothing. SumatraPDF.exeis shipped, not extracted. Writing an executable to disk at runtime and spawning it is the "dropper" pattern; shipping it directly also keeps SumatraPDF's own Authenticode signature intact.
What remains is the unsigned Setup.exe and zero SmartScreen reputation. Until the project is signed:
- Submit each release to Microsoft as a false positive: https://www.microsoft.com/en-us/wdsi/filesubmission ("Software developer" tab). The correction propagates to all machines, but it is per-hash, so it must be redone every release.
- For local development, exclude the build output directories rather than individual files (hashes change on every publish):
Add-MpPreference -ExclusionPath "<repo>\build\agent\PrintAgent\bin"
Add-MpPreference -ExclusionPath "<repo>\build\installer"The real fix is an Authenticode signature. See the project notes on SignPath Foundation (free for OSS) and Azure Trusted Signing.
A standalone HTML demo is shipped alongside the client at client/dist/index.html. It connects to a running PrintAgent, lists printers, lets you upload a PDF and watch job events stream in.
cd client/dist
npx http-server -p 8080
# then open http://localhost:8080/Note: the page must be served over HTTPS (or from a https://*.dev.localhost vhost trusted by your browser) so the wss://127.0.0.1:8443 connection passes the same-origin and mixed-content checks. By default the agent rejects http:// origins; flip AllowInsecureOrigins to true in appsettings.json only for local dev.
Copy client/dist/printagent-client.js and printagent-client.d.ts into your project (for example under src/lib/).
import { PrintAgent } from './lib/printagent-client'
const pa = new PrintAgent()
await pa.connect() // user sees the pairing prompt the first time
const printers = await pa.getLocalPrinters()
const { jobId } = await pa.print({
printerName: 'HP LaserJet',
pdfBase64,
options: { copies: 1, paperSize: 'A4' }
})
pa.on('job.statusChanged', (event) => {
if (event.jobId === jobId) console.log(event.status)
})Release binaries are signed using free code signing provided by SignPath.io, with a certificate from the SignPath Foundation. See CODE_SIGNING_POLICY.md for the build, signing, roles, and privacy details.
- Maintained by DragonOfMercy
This package is open-sourced software licensed under the MIT license.
PrintAgent embeds a copy of SumatraPDF (3.6.1) for silent PDF printing. SumatraPDF is licensed under the GNU GPL v3.0 or later. PrintAgent invokes it as a separate subprocess, so the two programs form an aggregate under section 5 of the GPL and PrintAgent's MIT license is unaffected. See agent/PrintAgent/Resources/SUMATRAPDF-NOTICE.txt for the full attribution and links to the source code.
If this project helps to increase your productivity, you can give me a cup of coffee :)
