Proof #3 now supports the production asymmetric path, not just the dev shared secret.
- context-attestation.ts: PublicKeyResolverPort seam; verify() picks ES256 (JWKS by
kid) when the client has a jwksUri, else HS256 (dev). signDevAttestationES256 helper.
- attestation-stores.ts: JwksPublicKeyResolver (jwks-rsa, one cached client per URI,
refetch on rotation, fail-closed to NO_KEY).
- attestation.module.ts: inject the resolver into the verifier; dev-seed the client as
clientType APPSHELL (it is the CRM browser-flow parent per the July-12 notes).
- rename the registered client crm-support-widget -> appshell-crm (the name reflects the
attesting parent, not a widget).
- specs: ES256 (valid via JWKS, wrong key -> BAD_SIGNATURE, unknown kid -> NO_KEY, no
resolver -> NO_KEY) + two stolen-token gate cases (wrong app binding -> APP_MISMATCH,
wrong audience -> WRONG_AUDIENCE).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies the context-attestation verifier at the request boundary via a guard, so a
stolen/forged/replayed attestation is rejected before IIOS acts — while staying safe
to roll out.
- ContextAttestationGuard: if an X-Context-Attestation header is present, verify it
(its app_id must match the actor token's app_id) → 403 on any failure; if absent,
allow only when IIOS_REQUIRE_ATTESTATION != 1 (dev/zero-trust), else 403. The flag is
read per-request and defaults OFF, so existing callers keep working until AppShell/
be-crm start forwarding attestations.
- AttestationModule (@Global): provides the verifier + Prisma stores + guard, and
dev-seeds the crm-support-widget client so locally minted attestations verify.
- Guard applied to ThreadsController + SupportController.
- Tests (5 pass, no DB): not-required-allows, required-rejects, valid, forged, replayed.
Workload/mTLS (proof #2) stays a mesh concern. Next: SDK header passthrough + demote
be-crm IiosClient to forward an attestation, then flip the flag to prove end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an opt-in RedisIoAdapter (wired in main.ts when REDIS_URL is set) so
socket.io room emits fan out across instances via Redis pub/sub — a client on
replica B now receives messages emitted by replica A. Without REDIS_URL the
in-memory adapter is kept (single-instance dev unchanged). Adds redis to
docker-compose, REDIS_URL to the env contract, and smoke-realtime-cluster.mjs
which proves cross-instance delivery fails without Redis and passes with it.
Delivery is at-least-once at N>1; clients dedupe by message id (documented).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a multi-stage Dockerfile for iios-service (build → pnpm deploy prune →
slim non-root runtime), a docker-entrypoint that runs `prisma migrate deploy`
then starts the server as PID 1, a .dockerignore, a fully-commented
.env.example config contract, and docs/DEPLOYMENT.md (topology, scaling,
release strategy, prod-readiness gaps). Moves prisma to dependencies so the
CLI ships in the prod bundle. Image builds, migrates, and serves /health 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>