@agimon-ai/po-web-security
Shared security primitives for the Po web cockpit: sealed channels and signed bundle manifests for independently trusted verifiers.
Package details
Install @agimon-ai/po-web-security from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@agimon-ai/po-web-security- Package
@agimon-ai/po-web-security- Version
0.0.0-alpha.83- Published
- Oct 30, 2026
- Downloads
- 32.2K/mo · 21K/wk
- Author
- agiflow-ai
- License
- BUSL-1.1
- Types
- package
- Size
- 378.6 KB
- Dependencies
- 0 dependencies · 0 peers
Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@agimon-ai/po-web-security
Shared security primitives for the Po web cockpit: the sealed-channel envelope, its node:crypto
and WebCrypto halves, and the signed bundle manifest.
This package holds no policy and starts nothing. It exists because three different programs need to agree byte for byte on the same crypto: the cockpit hub (Node), the cockpit page (browser), and every web plugin bundled into that page.
Why it is a package
A plugin in another package cannot import from a client application, and the sealed transport is a module singleton whose nonce counters every caller must share. A second copy would start counting at zero and the receiver would reject everything it sent as a replay. One package, deduped in the bundle, is what makes that impossible.
Subpaths
| Import | Runs where | What it carries |
|---|---|---|
@agimon-ai/po-web-security |
both | The envelope contract, the bundle manifest shape, the canonical serialization both sides sign and verify |
@agimon-ai/po-web-security/browser |
the page | The WebCrypto channel and sealedTransport, the shared instance plugins use |
@agimon-ai/po-web-security/node |
the hub | The node:crypto channel, the host handshake, and the bundle signer |
The sealed channel
A hosted tunnel terminates TLS at its provider's edge, so without application sealing everything the cockpit carries is plaintext to them. Sealing the payload underneath their TLS leaves them a relay that sees timing and sizes but not content.
The QR pairing path anchors key exchange out of band. The host's ephemeral P-256 public key is printed on the screen the user is holding, so the relay cannot substitute its own. A returning device instead receives the current public key after proving a passkey. That path depends on the trusted code-delivery edge described in The signed bundle-manifest primitive, and the key itself is public rather than secret.
Three properties worth knowing, because getting any of them wrong is silent:
- Separate keys per direction. Derived with different HKDF info strings, so a message the server sent can never be replayed back at it as though the client had sent it.
- Nonces are a random per-channel prefix plus a monotonic counter, never random per message. A repeated nonce under one AES-GCM key leaks the XOR of two plaintexts and the authentication subkey; 96 bits is small enough that random nonces collide at a rate worth caring about.
- Both directions are serialized. Sealing advances the counter and opening demands it strictly
increase, so overlapping asynchronous calls are a correctness bug, not a race worth tolerating.
createSerialQueueis what prevents a burst of socket sends from reordering into a self-inflicted replay.
Every failure names itself. A decryption failure otherwise shows up as a blank page with nothing to go on.
The signed bundle-manifest primitive
The Node signer traverses a public asset root without following symlinks, hashes each byte sequence, and signs
a strict canonical manifest v2 with ECDSA P-256. Every manifest is checked against a profile. The web profile
requires /index.html. The native profile requires one or more top-level .js modules (such as /<hash>.js), each
text/javascript and at most 4 MiB, so a manifest can never pass both profiles. Signer and verifier take the same profile. Revision state and the private key
are persisted atomically at mode 0600; malformed or replaced state fails closed instead of rotating
silently.
The browser verifier accepts only the public SPKI and minimum revision pinned by the physical QR. Po's package-owned service worker verifies the envelope and every asset before committing a Cache Storage revision, then atomically records the active bundle in IndexedDB. A same-revision digest conflict, signer mismatch, missing cache, bad MIME metadata, length mismatch, or hash mismatch is refused. The last-known-good bundle remains active during a failed refresh.
This does not make the package-owned bootstrap self-protecting. A TLS edge that replaces /pair or /sw.js
could remove the verifier before it runs. Once the intended bootstrap is installed, however, host and plugin
JavaScript cannot execute until its signed bytes pass verification.
The same key also signs tagged attestations. attestationBytes(tag, fields) is
UTF-8(tag + "\n" + JSON.stringify(fields)), where the tag matches po/<name>/v<n> and fields is an ordered
list of strings. A canonical manifest starts with 2\n and every tag starts with p, so neither signature can
stand in for the other. verifyAttestation (WebCrypto) and verifyDeviceSignature (node:crypto, P-256 keys only)
check them.
canonicalManifest is hand-rolled rather than JSON.stringify of the whole object, because a signature
is only worth anything if signer and verifier agree byte for byte, and key order in a JSON object is an
implementation detail.
Public API
import { canonicalManifest, describeSealedFailure, isSealedEnvelope } from '@agimon-ai/po-web-security';
import { sealedTransport } from '@agimon-ai/po-web-security/browser';
import { createBundleSigner, createHostHandshake } from '@agimon-ai/po-web-security/node';
Federation destination verification
deviceProof exports createFederationVerifier, FederationReplayStore, and
createFederationIssuerHttp. The verifier checks ES256 issuer JWTs, C3 device proofs,
C4 host proofs and C5 grants, exact recovery epochs, request scope/body binding and
atomic durable nonce admission. C1 binds the device key thumbprint; C2 binds the
calling host key thumbprint. Token org/role authority lasts only until its conservative
expiry. It does not create membership or authorize a workspace by itself.
Issuer origins and initial JWKS must be locally pinned at claim/join. Unknown kid
rotation and device-key/feed HTTP requests use only that origin with redirects refused.
The HTTP adapter persists cursor/feed state through injected ports; missing state or
state older than ten minutes fails closed when the issuer is unavailable. Device keys
come from a trusted local registry or the host-authenticated issuer lookup, not envelopes.
The destination supplies its home-host ACL/provider-consent authorization callback.
All replay times and envelope dates are milliseconds; JWT NumericDate claims are seconds.
Browser/native signers use federationEnvelopeFields from browser with
attestationBytes(tag, fields). browser also exports FEDERATION_FRESHNESS.
Development
pnpm build
pnpm typecheck
pnpm test
pnpm lint
The round-trip test is the one that matters: it seals with node:crypto and opens with real
WebCrypto, so the two implementations cannot drift apart without a failure.
Maintained by Agimon.