@communication-engine/host-issuer
v0.1.0-alpha.0
Published
Host-handoff identity SDK for Communication Engine. Generate Ed25519 keypairs, mint host JWTs, publish JWKS.
Readme
@communication-engine/host-issuer
Host-handoff identity SDK for Communication Engine.
This package is the TypeScript distribution of the host-issuer SDK (per CONTEXT D-01.2). It gives a host platform (the Three.js platform, future hosts) everything it needs to participate in CE's host-hand-off identity flow:
- Generate an Ed25519 keypair.
- Mint short-lived host JWTs with the canonical claim shape CE's auth service accepts.
- Publish the matching JWKS document at
/.well-known/jwks.jsonso the CE backend can verify those JWTs.
Phase 1 status
| Surface | Status |
| ------------------------ | --------------------------------- |
| generateHostIssuerKeys | WORKING (Ed25519 via jose) |
| mintHostJwt | WORKING (alg pinned to EdDSA) |
| serveJwks + toJwks | WORKING (Node middleware) |
| ce-host-issuer CLI | WORKING (generate-keys, serve) |
This is a real consumable SDK package — not a dev-only stub. Per D-01.1 the Three.js host platform integrates against this package from Phase 1 onward.
Quickstart (Three.js host integration, Express example)
import express from 'express'
import {
generateHostIssuerKeys,
mintHostJwt,
serveJwks,
} from '@communication-engine/host-issuer'
// On host startup: load (or generate) the keypair from your secret store.
// In production: persist to KMS / Vault / encrypted-at-rest. NEVER commit.
const keys = await loadFromSecretStore() ?? await generateHostIssuerKeys()
await persistToSecretStore(keys)
const app = express()
// Publish JWKS so the CE backend can verify your minted JWTs.
app.get('/.well-known/jwks.json', serveJwks(keys))
// Endpoint your host calls when a logged-in user wants to start a CE session.
app.post('/internal/ce-token', async (req, res) => {
const jwt = await mintHostJwt({
iss: 'threejs-host.example.com',
aud: 'ce.example.com',
sub: req.user.id,
host_tenant_id: 'threejs-host.example.com', // single-tenant Phase 1
expirySeconds: 300,
}, keys)
res.json({ assertion: jwt })
})
app.listen(8080)The browser then exchanges that assertion for a CE session JWT against
POST /v1/auth/exchange on the CE auth service (Plan 03).
Phase 1 dev quickstart (docker-compose stack)
The CE dev stack (Plan 04 — infra/docker-compose/compose.yaml) wires the
auth service to fetch JWKS at:
http://host.docker.internal:3001/.well-known/jwks.json(The host.docker.internal:host-gateway extra_host makes this work uniformly
on Mac, Windows, and Linux — Pitfall 12 fix.)
The CLI bundled with this package serves that endpoint:
# 1) Generate dev keys (writes ./.dev/host-keys.json — gitignored).
npx ce-host-issuer generate-keys --kid dev-host-1
# 2) Serve the JWKS at port 3001.
npx ce-host-issuer serve --port 3001 --keys ./.dev/host-keys.json
# 3) Bring up the CE stack — it will fetch from the URL above.
CE_SESSION_HMAC_SECRET=$(openssl rand -hex 32) \
docker compose -f infra/docker-compose/compose.yaml up -d --waitAlgorithm pinning (D-01.5)
mintHostJwt HARDCODES alg: "EdDSA" in the protected header. There is no
parameter to change it, defending against algorithm-confusion CVEs at the
mint site. The validator at services/auth (Plan 03) pins EdDSA via
jwt.WithValidMethods — belt-and-suspenders.
Security: privateJwk handling
The HostIssuerKeys object contains both privateJwk and publicJwk. Only
publicJwk should ever leave your host's secret boundary.
serveJwks(keys) and toJwks(keys) both project to public material only
(T-05-01 mitigation, asserted by test/jwks.test.ts).
For dev, the CLI writes the full keypair to ./.dev/host-keys.json — that
file is covered by .gitignore. Never commit it. Never log it. Production
hosts must persist privateJwk to a real secret store (AWS KMS, HashiCorp
Vault, GCP Secret Manager, etc.).
Key rotation playbook
- Generate a fresh keypair:
await generateHostIssuerKeys({ kid: 'v2' }). - Persist the new keypair alongside the old one.
- Switch
mintHostJwtto use the new one (instant — every new JWT carries the newkid). - Continue serving BOTH keys via
serveJwks([newKeys, oldKeys])until the old key's max JWT lifetime has elapsed (≥ 5 minutes for a 300s expiry). - Drop the old key.
The CE auth service's MicahParks/keyfunc cache (Plan 03) refetches on unknown-kid (rate-limited to 60s — Pitfall 2 mitigation), so the rotation is seamless without a coordinated bounce.
Where this fits in the stack
host (Three.js platform)
└── @communication-engine/host-issuer ← THIS PACKAGE
├── generateHostIssuerKeys() → Ed25519 keypair
├── mintHostJwt({iss,aud,sub,...}) → host JWT (EdDSA)
└── serveJwks(keys) → /.well-known/jwks.json
browser (CE Web SDK)
└── @communication-engine/core-sdk
└── generateDpopProof(...) → DPoP proof per request
network ──────────────────────────────────────────────────────────
CE backend (services/auth — Plan 03)
├── verifies host JWT against host-issuer JWKS ← consumes THIS package's output
├── enforces alg=EdDSA (CRYPTO-08)
├── replay-defends host JWT jti (5min TTL in Valkey)
├── verifies DPoP proof (RFC 9449)
└── mints DPoP-bound CE session JWTFuture plans
- Plan 06 (UE spike) ships a minimal C++ host-issuer shim that mirrors this package's claim shape. Phase 4 production UE plugin will integrate against the same shape documented here.
- Plan 07 (CI + dev-setup) uses
npx ce-host-issuer generate-keysas the dev-key bootstrap step inscripts/dev-setup.sh. - Phase 2 publishes this package to npm via the changesets release flow.
