Skip to content

Type OrderDoor.code as a union, not string (#95 review follow-up) - #111

Merged
dzuluaga merged 2 commits into
mainfrom
fix/orders-door-code-union
Jul 25, 2026
Merged

Type OrderDoor.code as a union, not string (#95 review follow-up)#111
dzuluaga merged 2 commits into
mainfrom
fix/orders-door-code-union

Conversation

@dzuluaga

Copy link
Copy Markdown
Contributor

In plain terms

When you ask an order for its status and it comes back refused, it tells you why with a short
code. That code was typed as a plain string, so a typo like res.code === "budget-exceded"
compiled fine — no autocomplete, no warning. This types it as a fixed set instead, so the typo is
caught the moment you write it.

This is the small code follow-up to a point Ever raised on #95 (the grants/delegated side already
had the typed version; orders didn't).

What you're approving

  • Type-only, additive — no runtime behavior changes; orders.retrieve returns exactly what it did.
  • Worst case if wrong: a compile error in your editor, not a runtime bug.
  • The union is "not-found" today — the accurate set orders.retrieve emits (it reads stores; age/
    payment failures refuse at the ceremony, not here), with a doc comment marking where to widen it later.

How to test

npm run build && npm test   # gate 345 · storefront 86 · lint clean

res.code === "typo" no longer compiles; res.code === "not-found" autocompletes.


For reviewers — the detail

OrderDoor.code: stringOrderDoor.code: OrderDoorCode (a new exported union type), in
packages/credentagent-gate/src/orders.ts; exported from index.ts; the retrieve-unknown-id test now
also narrows res.code to OrderDoorCode. I deliberately did not type it as the spec's fuller door
vocabulary (under-age / budget-exceeded / …) because orders.retrieve cannot emit those — that would
be an aspirational type that invites dead case branches. Widening is a deliberate one-line change with a
doc-comment reminder. Closes Ever's typed-code point (#95) in shipped orders code; the delegated
RefusalCode and webhook WebhookRefusalCode unions already exist.

Refs #95 · epic #97

🤖 Generated with Claude Code

https://claude.ai/code/session_01Harfquzhqg5SCCURyYRbHD

…review)

Ever's #95 point: a door `code` typed `string` is a footgun — a typo like
`res.code === "budget-exceded"` compiles, there's no autocomplete, and no exhaustiveness.
The grants/delegated side already ships a typed RefusalCode; orders did not.

Fix: `OrderDoor.code` is now the exported union `OrderDoorCode`. Today `orders.retrieve`
only refuses with "not-found" (it reads stores; policy failures refuse at the ceremony,
not here), so the union is `"not-found"` — accurate, not an aspirational list of codes it
can't emit. A doc comment marks the deliberate widening point for future refusal paths.

Additive + type-only (no runtime change). Build/lint green; gate 345, storefront 86.

Refs #95 · epic #97

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
credentagent-demo Ready Ready Preview, Comment Jul 25, 2026 4:59pm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1434e587ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/credentagent-gate/src/orders.test.ts Outdated
…test-d.ts (#111 Codex)

Codex (P2): the compile-time assertion in orders.test.ts never ran through tsc —
tsconfig.test.json type-checks ONLY src/types.test-d.ts, and Vitest transpiles tests
without type-checking, so reverting OrderDoor.code to string left the build green.

Fix: put the real guard in types.test-d.ts (the file tsc -p tsconfig.test.json checks) —
a @ts-expect-error on a non-member code that goes UNUSED (TS2578 → build fails) if code
regresses to string. Verified: reverting OrderDoor.code to string now breaks the build.
orders.test.ts keeps only the runtime value assertion (no misleading untyped guard).

Build/lint green; gate 345, storefront 86.

Refs #95 #111

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
@dzuluaga
dzuluaga merged commit 6b8ddb5 into main Jul 25, 2026
9 checks passed
@dzuluaga
dzuluaga deleted the fix/orders-door-code-union branch July 25, 2026 17:01
dzuluaga added a commit that referenced this pull request Jul 25, 2026
…API; facade deleted

Answering the duplication audit on #112:
- examples/grants-proto/grants.mjs (the #95 hand-rolled GrantsProto facade + usd shim +
  REASON_MAP) DELETED — it duplicated what grants.ts now ships. Per the spec's graduation
  promise, server.mjs is REWIRED onto the real credentagent.grants API (same two-pane UI;
  verified over HTTP: create+authorize → wine ✓ $20 → case ✗ per-spend-exceeded →
  replay ✓ → revoke → ✗ revoked).
- examples/hnp-draws: marked as the lower-level DelegatedGate seams reference, superseded
  for everyday use by grants + the demo hub.
- examples/README: HNP section points at the graduated demo.

Audited, NOT duplicated: orders-proto already consumes the real API (no facade); Ever's #103
is the external verify/settle rail — zero grants surface (only client.ts/index.ts merge-order
overlap). #44 remains recommended for closure (superseded by #41 + this).

Merged main in (brings #95 protos + #111). Gate 359 + storefront 86 green; lint clean.

Refs #104 · addresses the #112 duplication audit

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
dzuluaga added a commit that referenced this pull request Jul 25, 2026
…kable (#104) (#112)

* examples(demo-hub): one page to SEE it work — drive the checkout, watch the signed webhook verify live

Closes the reviewer's confidence gap: a stakeholder who can't read diffs can now open one page
and validate the product by USING it. Built on the real library (orders.serve + webhooks):
- a checkout an agent drives (ungated = click-to-complete, no wallet; wine = the real 21+ gate),
- a live feed that shows each order.settled webhook arrive and get SIGNATURE-VERIFIED in real time
  (the invisible cross-service notification, made visible).

Plus GUARANTEES.md — plain-English 'what each piece guarantees + the one command that proves it',
every command a test that fails if the protection is removed. Verified the promised output matches.

  node examples/demo-hub/hub.mjs   # → http://localhost:4000

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>

* feat(gate,#104): credentagent.grants — approve once, the agent spends while you're away (visible in the hub)

The first NEW capability the original demo never had, shipped end-to-end and CLICKABLE:

Library (grants.ts, over the real DelegatedGate engine):
- grants.create({ merchant, budget, perSpend, allow? }) → pending grant; the human approves
  once (the _authorize/_deny seam — today the demo page, the wallet ceremony later calls the
  SAME seam); grants.retrieve(id) rehydrates; grant.spend()/revoke().
- allow bounds: WHAT may be bought (SKUs/categories), enforced server-side fail-closed →
  'not-allowed' (#95 review, Ever's point — now in shipped code, not just the spec).
- Typed GrantDoorCode door mapping the engine refusals (per-spend-exceeded, budget-exceeded,
  wrong-merchant, step-up, revoked…). Age is NON-delegable: wine refuses step-up/needs-human.
- Lifecycle per spec FR-007: pending → authorized/denied(terminal) → revoked; idempotent replay.
- CatalogEntry gains optional category (additive); client gains { catalog } option.

Hub (Section 3): pre-approve '$100 · $30/purchase · Beverages' → YOU approve/deny → play the
agent: coffee ✓ ($82 left), headphones ✗ not-allowed, espresso ✗ per-spend, wine ✗ step-up,
6th coffee ✗ budget-exceeded, revoke → ✗ revoked — every outcome lands in the live feed.

Money decision (spec FR-005 reconciliation): grants use plain dollar numbers, consistent with
the shipped orders surface — recorded for #104.

Tests: 9 (grants.test.ts); the two NEW controls (allow bounds, status gate) proven RED when
removed. Full suite green: gate 354, storefront 86; lint clean. Verified end-to-end headless
AND in the browser (feed screenshot).

Refs #104 · epic #97 · spec 009

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>

* fix(gate,#104): address PR #112 review — sealed bounds immutable, approveUrl real, honest doors

All five Codex findings on #112:
- P1 sealed bounds: create() now deep-freezes a structuredClone snapshot — neither mutating
  grant.allow nor the caller's original options object can widen what the human approved.
  Proven RED when the freeze is removed.
- P1 approveUrl: grants.serve(app) (new grants-serve.ts) serves the approve/deny page at
  /credentagent/grants/:id + the POST actions — the documented create-and-send-the-link flow
  works end-to-end (verified over HTTP: page 200 → public approve → authorized → spend ok).
  The page is the demo stand-in for the wallet ceremony; #71 replaces it via the same seams.
- P2 refusal replay: an idempotency key now replays the ORIGINAL outcome, refusal included —
  a refused key can't be repurposed with a cheaper item. Proven RED when reverted.
- P2 multi-item: items must be exactly one → typed 'invalid-request' (key NOT consumed);
  nothing is silently dropped.
- P2 honest mapping: every engine RefusalCode mapped deliberately (invalid-amount surfaces
  as itself; unpermitted-presentment → step-up; not-yet-valid → expired); the unreachable
  integrity class coarsens to the catch-all 'refused' — never misreported as 'revoked'.

Lifecycle-status refusals stay deliberately uncached (pending → authorized transitions must
let a retry proceed) — documented inline.

Tests: 14 (was 9); gate 359 + storefront 86 green; lint clean.

Refs #104 · addresses #112 review

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>

* chore(#104): de-duplicate — grants-proto now runs on the REAL grants API; facade deleted

Answering the duplication audit on #112:
- examples/grants-proto/grants.mjs (the #95 hand-rolled GrantsProto facade + usd shim +
  REASON_MAP) DELETED — it duplicated what grants.ts now ships. Per the spec's graduation
  promise, server.mjs is REWIRED onto the real credentagent.grants API (same two-pane UI;
  verified over HTTP: create+authorize → wine ✓ $20 → case ✗ per-spend-exceeded →
  replay ✓ → revoke → ✗ revoked).
- examples/hnp-draws: marked as the lower-level DelegatedGate seams reference, superseded
  for everyday use by grants + the demo hub.
- examples/README: HNP section points at the graduated demo.

Audited, NOT duplicated: orders-proto already consumes the real API (no facade); Ever's #103
is the external verify/settle rail — zero grants surface (only client.ts/index.ts merge-order
overlap). #44 remains recommended for closure (superseded by #41 + this).

Merged main in (brings #95 protos + #111). Gate 359 + storefront 86 green; lint clean.

Refs #104 · addresses the #112 duplication audit

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>

---------

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
dzuluaga added a commit that referenced this pull request Jul 25, 2026
Version bump for both packages (gate dep → ^0.3.0 in the storefront). Since 0.2.0:
- credentagent.orders.* + orders.serve(app) — the checkout in one call (#98)
- credentagent.webhooks — signed HTTP order.settled + constructEvent (#102)
- credentagent.grants.* + grants.serve(app) — approve once, spend while away;
  allow bounds, typed GrantDoorCode, immutable sealed bounds, refusal replay (#112)
- OrderDoorCode typed union (#111); demo hub + guarantees (#112)

README: adds the missing Grants section (published docs must match the surface);
honesty labels unchanged (presence-only-demo / delegated-demo / server-issued-demo).

The quickstart's dep bump to ^0.3.0 follows AFTER publish (its CI smoke installs
from the registry, so bumping first would 404).

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
dzuluaga added a commit that referenced this pull request Jul 25, 2026
…115)

Version bump for both packages (gate dep → ^0.3.0 in the storefront). Since 0.2.0:
- credentagent.orders.* + orders.serve(app) — the checkout in one call (#98)
- credentagent.webhooks — signed HTTP order.settled + constructEvent (#102)
- credentagent.grants.* + grants.serve(app) — approve once, spend while away;
  allow bounds, typed GrantDoorCode, immutable sealed bounds, refusal replay (#112)
- OrderDoorCode typed union (#111); demo hub + guarantees (#112)

README: adds the missing Grants section (published docs must match the surface);
honesty labels unchanged (presence-only-demo / delegated-demo / server-issued-demo).

The quickstart's dep bump to ^0.3.0 follows AFTER publish (its CI smoke installs
from the registry, so bumping first would 404).

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
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