@m-kopa/platform-auth-container
v0.1.0
Published
Container-side identity adapter for the Launchpad Worker. Reads the Worker-injected X-User-* identity headers and exposes a typed User to HTTP handlers. Optional Express middleware adapter.
Downloads
92
Readme
@m-kopa/platform-auth-container
Container-side identity adapter for the Launchpad Worker.
The Launchpad Worker (see ADR 0015 Decision 4) is the canonical
trust boundary for the container shape. The target state is for
the Worker to verify Cf-Access-Jwt-Assertion against Cloudflare
Access and forward the trusted claims as X-User-* headers on
every request it dispatches into the container Durable Object.
The container trusts those headers because the DO stub is the
only ingress — Cloudflare's edge will not route external
traffic to the container directly.
Status today: the chunk-C Worker only does a fail-closed
presence check on Cf-Access-Jwt-Assertion — full RS256 + iss +
aud verification and X-User-* injection still need to land on
the Worker side (see Worker contract (status)
below).
This package is the container-side counterpart. It:
- Reads the Worker-injected
X-User-*headers. - Exposes a typed
Userto HTTP handlers. - Optionally provides an Express middleware adapter.
Status
Pre-1.0. Consumers MUST pin exact versions. New error codes and
new optional User fields may land in minor versions; existing
field shapes are stable.
Worker contract (status)
At time of writing the Worker only does a fail-closed presence
check on Cf-Access-Jwt-Assertion — it does not yet inject
the X-User-* headers this package reads. A follow-up will add
JWT verification + header injection on the Worker side. Until
then the container's /me-style routes will see
MISSING_IDENTITY and respond 401.
The Worker is expected to set the following headers, all parsed by
the canonical names exported as IDENTITY_HEADER:
| Header | Required | Meaning |
| --- | --- | --- |
| X-User-Sub | yes | Cloudflare Access sub claim (stable subject). |
| X-User-Preferred-Username | no | JWT preferred_username claim. |
| X-User-Email | no | JWT email claim. |
| X-User-Groups | no | Comma-separated group memberships. |
Install
bun add @m-kopa/[email protected].npmrc must scope @m-kopa to GitHub Packages:
@m-kopa:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}Usage — runtime-agnostic
import { readUserFromHeaders } from "@m-kopa/platform-auth-container";
app.get("/api/me", (req, res) => {
const user = readUserFromHeaders(req.headers);
res.json(user);
});readUserFromHeaders accepts either a Web-standard Headers
instance or a Node IncomingHttpHeaders-shaped dictionary.
Usage — Express middleware
import {
createPlatformAuthMiddleware,
type PlatformAuthRequest,
} from "@m-kopa/platform-auth-container/express";
const app = express();
app.use(createPlatformAuthMiddleware());
app.get("/api/me", (req, res) => {
// The parsed `User` is on `req.user` by default. Pass `userKey:
// "principal"` to the factory to change the field name; cast to
// `PlatformAuthRequest<"principal">` accordingly.
const typedReq = req as PlatformAuthRequest;
res.json(typedReq.user);
});express is declared as an optional peer dependency — the
main entrypoint of this package does not import it.
Error handling
A single sealed error class, IdentityHeaderError, surfaces
parse failures. Consumers MUST narrow on code, never on
message.includes(...).
import { IdentityHeaderError } from "@m-kopa/platform-auth-container";
try {
const user = readUserFromHeaders(req.headers);
} catch (err) {
if (err instanceof IdentityHeaderError) {
switch (err.code) {
case "MISSING_IDENTITY":
// No X-User-Sub header — request did not arrive through
// the Worker's identity-injection path. Respond 401.
return res.status(401).end();
case "MALFORMED_IDENTITY":
// X-User-Sub was present but empty after trim. This
// indicates a Worker-side bug — log it loudly.
return res.status(401).end();
}
}
throw err;
}A non-throwing variant is also exported:
import { tryReadUserFromHeaders } from "@m-kopa/platform-auth-container";
const user = tryReadUserFromHeaders(req.headers); // User | null
if (!user) return res.status(401).end();tryReadUserFromHeaders only swallows MISSING_IDENTITY. A
MALFORMED_IDENTITY still throws because silently treating it as
anonymous would mask a Worker bug.
Sanitisation contract
IdentityHeaderError.message names the structural fact that
failed (which header was missing, "empty after trim"). It MUST
NOT include the raw header value bytes — even though the
X-User-* headers are not secrets per se, they are
user-identifying and a wide-net error log should not leak them.
The parser constructs messages from header names, not values.
TODO (follow-up work)
- Worker-side injection. The chunk-C Worker
(
launchpad-template/container/worker/index.ts) currently forwards onlyX-Bindings-Proxy-*andX-Request-ID. A follow-up needs to add the JWT verification step (using@m-kopa/platform-auth) and inject the fourX-User-*headers documented above. - Group-membership helpers. A
hasGroup(user, name)/requireGroup(name)middleware may follow once we have real RBAC requirements from a launching app.
References
- Milestone: M-1071 (chunk D2 of the seven-chunk plan).
- ADR 0015 Decision 4 — container auth posture.
- ADR 0016 — container-shape TF wiring.
@m-kopa/platform-auth— JWT verification (used inside the Worker; this package is the container-side counterpart).@m-kopa/platform-bindings-client— sibling package, same package shape and pre-1.0 conventions.
