Skip to content

Passthrough token support - #134

Open
cubap wants to merge 4 commits into
mainfrom
133-passthrough-token-support
Open

cubap wants to merge 4 commits into
mainfrom
133-passthrough-token-support

Conversation

@cubap

@cubap cubap commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

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 Authorization header of any /create, /update, /overwrite, /delete (both forms), or /query request, 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

  • Verbatim forwarding. The caller's header value reaches RERUM unchanged. No scheme rewriting, no local validation. RERUM is the authority.
  • Passthrough error visibility. For modification routes (/create, /update, /overwrite, /delete), when the request carried a caller-supplied Authorization header, an upstream 401 or 403 is passed back to the caller with its real status code so the caller can debug their own token. For headerless (instance-token) requests, RERUM's 401/403 is reported as a TinyNode 502 whose body starts with 401: or 403: and includes RERUM's message, preserving TinyNode's existing error contract.
  • No header, no change. Requests without Authorization behave exactly as before, using the instance's ACCESS_TOKEN.
  • Operator kill switch. ALLOW_PASSTHROUGH_TOKENS=false in .env rejects any write-request carrying the header with 403 ("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 is true so tiny.rerum.io can relay out of the box.
  • Refresh cycle. checkAccessToken skips 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.
  • Query is read-only. /query forwards an Authorization header 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. Upstream 401/403 on /query follows the normal 502 contract.
  • Overwrite 409 exception. /overwrite still returns 409 for version conflicts regardless of passthrough mode.

Implementation

  • routes/helpers/passthrough.js (new): resolveAuthorization, requirePassthroughAllowed, isPassthroughAllowed, isPassthroughRequest
  • tokens.js: passthrough short-circuit in checkAccessToken
  • Write routes (create, update, overwrite, delete both forms): guard middleware in the chain, resolveAuthorization(req) for the upstream header, conditional 401/403 pass-through when isPassthroughRequest(req) is true
  • /query: resolveAuthorization(req) for upstream header, no guard middleware, no special 401/403 handling
  • openapi/components/tinynode-shared-components.openapi.yaml: bearerAuth HTTP security scheme documenting the passthrough contract and the kill switch; version bumped 0.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.md conventions; 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, and checkAccessToken interaction (skips refresh for passthrough, refreshes otherwise)
  • Per-route __mock_functions __core suites 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 failures
  • openapi_sync_artifacts.test.js: guard asserting the bearerAuth scheme exists with its kill-switch documentation

Local results: npm test (111 pass).

Docs

  • README: "Passthrough Token Mode" section with a curl example, alongside the existing Client App and Centralized Client API modes
  • sample.env: ALLOW_PASSTHROUGH_TOKENS with comment
  • test/TESTING.md: what the passthrough tests validate and why (403 over silent misattribution, conditional 401/403 handling)

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
@cubap
cubap requested a review from thehabes as a code owner September 18, 2026 16:33
@cubap
cubap deployed to development September 18, 2026 16:34 — with GitHub Actions Active

@thehabes thehabes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:3 and routes/delete.js:3: httpError was imported only for this block; remove it from those imports. (update.js, overwrite.js, and query.js already 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): assert 502 and that the body starts with 401: or 403:.
  • test/TESTING.md:64 and test/TESTING.md:89: describe the 502 contract instead of "401/403 status fidelity."
  • README.md:95 and the bearerAuth description in openapi/components/tinynode-shared-components.openapi.yaml: see Issue 2.
  • The PR description's "Behavior" section, which says 401/403 pass 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>
@cubap
cubap deployed to development September 22, 2026 20:08 — with GitHub Actions Active
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>
@cubap
cubap deployed to development September 23, 2026 15:51 — with GitHub Actions Active
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>
@cubap
cubap deployed to development September 23, 2026 16:52 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
development — 0f3dccf2 Deployed Sep 23, 2026 by cubap via deploy (24, vlcdhp02) #203
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Passthrough token support

2 participants