@karpo.dev/token
v0.4.0
Published
Token verification for the Karpo API gateway: ES256 end-user access tokens and RS256 WorkOS operator tokens. WebCrypto only, Workers-compatible.
Readme
@karpo.dev/token
Token verification for the Karpo API gateway — the TypeScript mirror of
crates/karpo-token (the identity engine signs what that crate verifies).
WebCrypto only, no Node built-ins, so it runs unchanged in a Cloudflare Workers
isolate; verification is async.
Deliberately its own package rather than a @karpo.dev/protocol module, so the
gateway does not pull the whole protocol surface into a cold isolate. The
identifier schemas are local copies kept structurally identical to the
protocol package's.
Cross-language parity with the Rust crate is pinned by the fixture vectors in
testdata/token/verify-vectors.json — both implementations must return the same
verdict for every vector. Contract: docs/110-identity/040-tokens.md.
Two tokens, two allowed algorithm sets, one body
The gateway is presented with two kinds of bearer, and this package verifies both through one function with two configurations:
| Export | Token | Algorithm | Claims |
| --- | --- | --- | --- |
| verifyAccessToken | the end user's Karpo access token, minted by the identity engine | ES256 only | AccessClaimsSchema, strict |
| verifyOperatorToken | the operator's WorkOS AuthKit token, presented to /admin/v1/* and /v1/dev/* | RS256 only | OperatorClaimsSchema, open |
The split is the whole point of src/algorithms.ts, which mirrors
crates/karpo-token/src/algorithms.rs: one key picker, taking an allowed
algorithm set, and a JWK that is not usable for one of those algorithms is
treated as absent (unknown-kid) rather than as an error. So an RSA key
published under a Karpo access token's kid is unreachable from
verifyAccessToken no matter what the header claims, and alg: none and HS256
are refused by name, before any key lookup — the HS256 case being the
algorithm-confusion attack, where a published RSA modulus is handed to HMAC as
a shared secret. RSA keys shorter than MIN_RSA_MODULUS_BITS (2048) are
unusable, not weak-but-fine.
Why one schema is strict and the other is not: Karpo mints its own access
tokens, so an unknown claim there is a rolling-upgrade discipline the platform
owes itself. The operator claim set belongs to WorkOS — sid, jti,
entitlements, and whatever it adds next — and refusing a token for carrying a
claim WorkOS invented would be an outage we could not fix. The four claims the
gateway reads (sub, org_id, role, permissions) are typed; the rest pass
through.
This package answers whose token it is, never what that person may do.
Authority — owner and admin from the claim alone, member and viewer through a
WorkOS project assignment — is the gateway's operator seam, per
docs/superpowers/decisions/2026-08-18-workos-operator-identity.md.
Parity with the Rust crate
verifyAccessToken is a strict mirror: same check order, same verdict
spellings, same serde_json integer semantics for the temporal claims, and the
committed vectors prove it. verifyOperatorToken has no Rust counterpart and
needs none — WorkOS tokens are read by the gateway, which is TypeScript, while
crates/karpo-token verifies what the identity engine mints. Its fixtures
(testdata/token/operator-vectors.json) are produced and consumed by this
package alone.
