Brings the uncommitted body of work under version control so it cannot be
lost. Gates are green: 152 tests pass across rpc/router/dev-server,
typecheck, lint, format and check:public-api all clean.
NOT YET REVIEWED. None of Tasks 5-11 has had an independent task review, and
Task 4's second fix round was never re-reviewed either.
Known gaps against the plan, recorded here rather than discovered later:
- packages/rpc/test/{transport,server,client}.test.ts are ABSENT. The plan
required a test file for each. server.ts holds the fail-closed identity and
permission checks and currently has no direct coverage at all.
- rpc-endpoint.test.ts has 3 tests where the plan specified 9. Missing:
unknown service, non-POST, malformed body, non-rpc passthrough, and the
isInternalCaller sweep. This is the task where a reachable
/__wrnexus/rpc/* makes every permission check in the workspace bypassable.
- http.test.ts has 3 of 7; integration.test.ts 2 of 3;
services-discovery.test.ts 1 of 4.
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>
- isRetryableStatus now fails closed for out-of-range values (600+, negative,
NaN) by bounding the 5xx check on both sides (>= 500 && <= 599), instead of
an unbounded >= 500 that classified garbage statuses like 1000 as retryable.
- 408 Request Timeout is now retryable, matching the RPC_TRANSPORT doc
comment (connection, timeout, 5xx) — a timeout surfaced as 408 is no longer
treated differently from the same timeout surfaced as 504.
- Add RPC_MALFORMED: the callee answered, but not with a ServiceResult (HTML
error page, truncated body, unexpected shape). Distinct from RPC_TRANSPORT
since something DID respond; non-retryable via the existing retryableFor,
no new branch needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Drop the redundant eslint-disable on AnyProcedures; no-explicit-any
is off repo-wide so the directive itself was the warning. Doc
comment now explains why none is needed.
- Rename test's schema binding to _schema per the lint config's
underscore-prefix rule for read-only-as-type bindings.