Skip to content

Bump to 2.8.0: surface the pairwise operator handle through the Gate - #107

Merged
vvillait88 merged 2 commits into
mainfrom
operator-handle
Aug 12, 2026
Merged

Bump to 2.8.0: surface the pairwise operator handle through the Gate#107
vvillait88 merged 2 commits into
mainfrom
operator-handle

Conversation

@vvillait88

Copy link
Copy Markdown
Contributor

Summary

Adds getOperatorHandle(ctx) across all five identity adapters, plus ctx.operatorHandle inside Checkout hooks. It returns the stable pairwise oph_... handle for the account behind the request's operator token.

What it is for. Merchants keying durable state on identity, prepaid balances above all, need a value that outlives a credential. An opc_ lives 24h and rotates silently off a 90-day refresh, so state keyed on the token instance is stranded daily, and revoking a leaked token would forfeit the balance it was holding. The handle derives from the account, so rotation, expiry and revocation are free, and it is pairwise per merchant so handles never correlate across stores.

It costs nothing extra. The handle rides the /v1/assess response the gate already fetches, so the accessor is a synchronous cache read exactly like getSignerVerdict: no second round trip on a merchant's hot path, and nothing additional metered against their quota.

An earlier draft of this resolved the handle lazily through a separate endpoint. That was dropped deliberately: it doubled the calls on a metered path, it would have shipped every merchant an endpoint that bypasses quota, and it forced the accessor to be async for no benefit a caller could see.

Available on the deny branch too, unlike data. This is identity rather than a verdict, so a merchant recording a denial against the buyer gets it on exactly the path where its handler never runs.

Anything that is not a well-formed oph_ string reads as absent rather than being passed through, so a half-configured API can never hand a merchant a value it would write balance rows against.

Type of change

  • Bug fix (no breaking change)
  • New feature (no breaking change)
  • Breaking change (existing callers must update)
  • Docs, tests, or internal maintenance only

Public API

Additive only; no migration for existing callers.

  • getOperatorHandle(ctx) exported from identity/{hono,express,fastify}; gate.operatorHandle on the identity/{nextjs,web} wrapper surface.
  • ctx.operatorHandle on CheckoutContext.
  • OperatorHandle type and AssessResult.operator_handle exported from the root.

Requires an API that returns operator_handle on /v1/assess. Against an older API the field is simply absent and every accessor returns undefined, so upgrading this package alone changes no behavior.

Test plan

New tests/operator_handle.test.ts (6 cases) covers the surface end to end against a stubbed assess: the handle reaches the handler; exactly one API call is made and it is assess (the claim most worth holding still, since a regression here silently reintroduces a second round trip); it survives the gate's own cache so a second request still keys correctly; it is present on the deny path; and wallet-path, malformed, and gate-never-ran cases all read as absent.

Verified the tests FAIL without the wiring rather than only passing with it: removing the outcome plumbing drops the allow-path and cache-path cases immediately. Restored from a copy taken beforehand, then re-ran green.

Full local gates: bun run lint clean, bun run typecheck clean across the package and examples, bun run test 1814 passed / 4 skipped, bun run knip clean, bun run build producing CJS + ESM.

Checklist

  • Tests cover the new behavior, and the suite passes locally
  • Lint, format, and type checks pass
  • Docs and README examples updated if the public surface changed
  • No secrets, credentials, or personal data in the diff or the tests

Merchants keying durable state on identity (prepaid balances first) need a
value that outlives a credential. An opc_ lives 24h and rotates silently off
a 90-day refresh, so state keyed on the token instance is stranded daily and
revoking a leaked token would forfeit the balance it held. The handle derives
from the account instead, and is pairwise per merchant so handles never
correlate across stores.

It rides the /v1/assess response the gate already fetches, so reading it is a
synchronous cache read like getSignerVerdict, costs no second round trip on a
merchant's hot path, and meters nothing extra. An earlier draft resolved it
lazily through a separate endpoint; that was dropped because it doubled calls
on a metered path and would have shipped every merchant an unmetered
endpoint.

Available on the DENY branch too, unlike `data`: this is identity rather than
a verdict, and a merchant recording a denial against the buyer needs it on
exactly the path where its handler never runs.

All five adapters plus Checkout's ctx. Anything that is not a well-formed
oph_ string reads as absent rather than passing through, so a
half-configured API cannot hand a merchant a value it would write balance
rows against.
Minor rather than patch: this adds exported surface (getOperatorHandle on
every adapter, ctx.operatorHandle on Checkout, the OperatorHandle type)
rather than only changing behavior.
@vvillait88 vvillait88 changed the title Surface the pairwise operator handle through the Gate Bump to 2.8.0: surface the pairwise operator handle through the Gate Aug 12, 2026
@vvillait88
vvillait88 merged commit 09a7e83 into main Aug 12, 2026
6 checks passed
@vvillait88
vvillait88 deleted the operator-handle branch August 12, 2026 12:12
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.

1 participant