Adds examples/auth-showcase/app/services/greeter-client.ts so the showcase
demonstrates both halves - the review noted the example was callee-only, so a
developer had no working reference for making a call.
Raises integration coverage to the planned 3 tests and adds the missing
rpc-endpoint cases. Also wires the prod build path for services.
304 tests pass across rpc/router/dev-server/cli; typecheck, lint, format and
check:public-api all clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
verifyJwt gates its maxAge check on iat being a number, so a token forged
without iat was honoured at any maxAgeSeconds - the same shape as the
audience and exp fail-opens closed in the previous round. A future-dated iat
did the same via a negative age. Both refused now.
The import side never checked aud was a single string, and verifyJwt compares
with includes(), so a multi-audience token verified at several apps. The
mint-side guard's invariant now holds where it is enforced.
ctx.tenant present with a null id minted an authenticated credential with no
tenant claim, which the callee reads as global. Absent ctx.tenant means
untenanted; a present tenant with an unusable id is an error.
Adds six tests pinning behaviours that mutation testing showed were free to
delete without any test noticing: no-exp, no-iat, the 300s default max age,
an array audience on import, a non-string tenant claim on import, and a null
tenant id at mint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- importSubjectContext now rejects a non-string/empty selfApp before
verifying. verifyJwt skips the audience check entirely when audience
is undefined, so an unvalidated selfApp (the natural shape of
currentAppName(): string | undefined) accepted every token from every
app for every audience.
- exportSubjectContext now rejects a non-string/empty targetApp, so an
array can no longer mint one token valid at multiple apps.
- Both directions now reject a present-but-non-string tenant id instead
of silently dropping it (was: callee reads missing tenantId as
global/unscoped -> cross-tenant exposure).
- importSubjectContext now requires exp to be present and independently
bounds accepted token age via a new maxAge/ImportOptions.maxAgeSeconds
(default 300s), so a caller cannot mint a long-lived token via a huge
ttlSeconds and have it honoured indefinitely.
- SubjectContext.callerApp doc now states it is self-asserted (the
signing secret is workspace-wide) and must never be an authz input.
- index.ts also exports the new ImportOptions type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>