Conversation
A registered application can now send its own Authorization header to any TinyNode CRUD route and have it forwarded to RERUM verbatim, so the write is attributed to the caller's agent instead of the instance's. This gives small annotation apps an auth-wrapping backend call without cloning TinyThings. - routes/helpers/passthrough.js: resolveAuthorization picks the caller's header when present, requirePassthroughAllowed rejects with 403 when an operator sets ALLOW_PASSTHROUGH_TOKENS=false (no silent misattribution) - tokens.js: checkAccessToken skips the instance refresh cycle for passthrough requests, so a failed refresh cannot fail a request that does not use the instance token - all routes: pass upstream 401/403 through with their real status codes so callers can debug their own tokens; other failures remain 502 - docs: README Passthrough Token Mode section, sample.env flag, TESTING.md passthrough coverage notes - openapi: bearerAuth security scheme, artifact bumped to 0.2.0-alpha.1 Closes #133
thehabes
left a comment
There was a problem hiding this comment.
Looks good but I found a couple of things while testing that claude really fleshed out. I think we'll need to address these.
Static Review Comments
Branch: 133-passthrough-token-support
Review Date: 2026-09-22
Reviewer: Pair Static Review - Claude & @thehabes
Claude and Bryan make mistakes. Verify all issues and suggestions. Avoid unnecessary scope creep.
| Category | Issues Found |
|---|---|
| 🔴 Critical | 0 |
| 🟠 Major | 1 |
| 🟡 Minor | 1 |
| 🔵 Suggestions | 0 |
Major Issues 🟠
🟠 Issue 1: RERUM 401/403 errors bypass TinyNode's 502 error contract
File: routes/create.js:33-44, routes/update.js:32-42, routes/overwrite.js:54-64, routes/delete.js:32-42, routes/delete.js:77-87, routes/query.js:48-58
Category: Breaking change
Problem:
TinyNode's agreed contract is that RERUM back-end errors surface as 502 from TinyNode.
The response body carries RERUM's actual status code and error text, for example 401: <url> - <RERUM message>.
The 409 version conflict on /overwrite is the one deliberate exception.
This PR adds a branch to all six RERUM calls that returns RERUM's 401 and 403 with their own status codes.
That breaks the contract for every caller, not only for passthrough callers, because the branch has no passthrough condition.
Passthrough doesn't need special handling: a caller whose token is rejected gets a 502 whose body starts with 401: and includes RERUM's reason, the same as any other RERUM error.
Reproduced live with no Authorization header, comparing main with 133-passthrough-token-support:
| Request | main |
This PR |
|---|---|---|
PUT /overwrite on an object owned by another agent |
502 |
401 |
DELETE /delete/:id on an object owned by another agent |
502 |
401 |
PUT /overwrite on a deleted object |
502 |
403 |
POST /create when the instance ACCESS_TOKEN is invalid |
502 |
401 |
POST /create with Authorization: Bearer garbage (passthrough) |
n/a | 401 |
Current Code (repeated in all six places):
// Pass through 401/403 so callers can see RERUM's rejection of their
// own token rather than a misleading 502. Everything else is a bad
// gateway from TinyNode's point of view.
if (resp.status === 401 || resp.status === 403) {
let rerumAuthMessage
try {
rerumAuthMessage = `${resp.status}: ${createURL} - ${await resp.text()}`
} catch (e) {
rerumAuthMessage = `${resp.status}: ${createURL} - A RERUM error occurred`
}
throw httpError(rerumAuthMessage, resp.status)
}Suggested Fix:
Delete the 401/403 block from all six places and let these responses fall through to the existing 502 handling.
Then update the things that describe or test the pass-through:
routes/create.js:3androutes/delete.js:3:httpErrorwas imported only for this block; remove it from those imports. (update.js,overwrite.js, andquery.jsalready used it.)- The per-route tests titled "Passes an upstream 401/403 through with the real status code" (
create.test.js:246,create.test.js:263,update.test.js:200,overwrite.test.js:125,delete.test.js:219,query.test.js:96): assert502and that the body starts with401:or403:. test/TESTING.md:64andtest/TESTING.md:89: describe the502contract instead of "401/403 status fidelity."README.md:95and thebearerAuthdescription inopenapi/components/tinynode-shared-components.openapi.yaml: see Issue 2.- The PR description's "Behavior" section, which says
401/403pass through with their real status codes.
Minor Issues 🟡
🟡 Issue 2: The kill switch blocks public reads on /query
File: routes/query.js:10, routes/query.js:35
Category: Logic
Problem:
Forwarding the caller's token on /query has no effect upstream. The only visible effect is that ALLOW_PASSTHROUGH_TOKENS=false now rejects public reads from any client that sends an Authorization header on every call (a common shared-fetch-wrapper pattern).
The kill switch exists to prevent writes being attributed to the wrong agent.
A query attributes nothing, so rejecting it doesn't serve that goal.
Suggested Fix:
Leave /query as it was on main. If you keep query in scope "for uniformity," at least drop requirePassthroughAllowed from the query chain so reads are never refused.
router.post('/', verifyJsonContentType, async (req, res, next) => {Suggestions 🔵
None.
If there are significant code changes in response to this review please test those changes. Run the application manually and test or perform internal application tests when applicable.
The ALLOW_PASSTHROUGH_TOKENS=false kill switch exists to prevent writes from being attributed to the wrong agent. A /query attributes nothing, so rejecting it served no purpose and broke public reads for clients that send an Authorization header on every call. Keep resolveAuthorization so /query still forwards caller tokens for uniformity, but remove requirePassthroughAllowed from the route chain. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Upstream RERUM 401/403 responses now surface as TinyNode 502 errors with RERUM's status and message in the body, matching the existing contract. The only deliberate exception remains /overwrite returning 409 for version conflicts. - Remove 401/403 pass-through branches from create, update, overwrite, delete (both handlers), and query route handlers. - Remove now-unused httpError imports from create.js and delete.js. - Update per-route tests to assert 502 status while preserving the upstream 401:/403: prefix in the response body. - Update TESTING.md, README.md, and OpenAPI bearerAuth description to describe the 502 contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
For modification routes (create, update, overwrite, delete), when the caller supplied an Authorization header that is forwarded to RERUM, upstream 401/403 responses now pass through with their real status codes so the caller can debug their own token. Instance-token requests (no Authorization header) continue to receive TinyNode's 502 contract with RERUM's status and message in the body. - Add isPassthroughRequest checks before the 401/403 pass-through branch in create, update, overwrite, and both delete handlers. - Re-import httpError in create.js and delete.js for the pass-through branch. - Split per-route tests to cover both passthrough (401/403 real status) and instance-token (502) cases. - Update TESTING.md, README.md, and OpenAPI bearerAuth description to describe the conditional behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Closes #133
What this does
A registered application can make machine-to-machine calls through a running TinyNode without cloning the whole thing. Send your own access token in the
Authorizationheader of any/create,/update,/overwrite,/delete(both forms), or/queryrequest, and TinyNode forwards it to RERUM verbatim. The write is attributed to your registered agent, not the instance's. That makes the minimum machinery for a small annotation app an auth-wrapping backend call instead of a full driver clone of TinyThings.Behavior
/create,/update,/overwrite,/delete), when the request carried a caller-suppliedAuthorizationheader, an upstream401or403is passed back to the caller with its real status code so the caller can debug their own token. For headerless (instance-token) requests, RERUM's401/403is reported as a TinyNode502whose body starts with401:or403:and includes RERUM's message, preserving TinyNode's existing error contract.Authorizationbehave exactly as before, using the instance'sACCESS_TOKEN.ALLOW_PASSTHROUGH_TOKENS=falsein.envrejects any write-request carrying the header with403("Token passthrough is not allowed on this TinyNode instance"). Rejection, not silence: falling back to the instance identity would misattribute the write, which is the exact failure this issue exists to prevent. Default istrueso tiny.rerum.io can relay out of the box.checkAccessTokenskips the instance token refresh when the request is a passthrough one, so a failed refresh cannot fail a request that never uses the instance token./queryforwards anAuthorizationheader when present, but it does not enforce the kill-switch guard or attribute anything, so public reads from clients that always send a header are not blocked. Upstream401/403on/queryfollows the normal502contract./overwritestill returns409for version conflicts regardless of passthrough mode.Implementation
routes/helpers/passthrough.js(new):resolveAuthorization,requirePassthroughAllowed,isPassthroughAllowed,isPassthroughRequesttokens.js: passthrough short-circuit incheckAccessTokencreate,update,overwrite,deleteboth forms): guard middleware in the chain,resolveAuthorization(req)for the upstream header, conditional 401/403 pass-through whenisPassthroughRequest(req)is true/query:resolveAuthorization(req)for upstream header, no guard middleware, no special 401/403 handlingopenapi/components/tinynode-shared-components.openapi.yaml:bearerAuthHTTP security scheme documenting the passthrough contract and the kill switch; version bumped0.1.0-alpha.1->0.2.0-alpha.1(additive, so the receiver repo can tell something arrived). The sync workflow will pick this up on merge.Tests
All mocked per
test/TESTING.mdconventions; no live RERUM calls.test/routes/passthrough.test.js(new): helper semantics (default-on, exact-"false"kill switch, verbatim forwarding including non-****** env fallback), guard 403/next() behavior, andcheckAccessTokeninteraction (skips refresh for passthrough, refreshes otherwise)__mock_functions __coresuites for create, update, overwrite, delete (both forms), and query: verbatim upstream contract, 403 when disabled with proof no upstream fetch fires, passthrough 401/403 pass-through with real status codes, instance-token 401/403 mapped to 502, 502 preserved for other failuresopenapi_sync_artifacts.test.js: guard asserting thebearerAuthscheme exists with its kill-switch documentationLocal results:
npm test(111 pass).Docs
curlexample, alongside the existing Client App and Centralized Client API modessample.env:ALLOW_PASSTHROUGH_TOKENSwith commenttest/TESTING.md: what the passthrough tests validate and why (403 over silent misattribution, conditional 401/403 handling)