Skip to content

orders.serve(app): the human-present checkout, wired in one call (#97) - #98

Merged
dzuluaga merged 8 commits into
mainfrom
feat/97-orders-surface
Jul 23, 2026
Merged

orders.serve(app): the human-present checkout, wired in one call (#97)#98
dzuluaga merged 8 commits into
mainfrom
feat/97-orders-surface

Conversation

@dzuluaga

@dzuluaga dzuluaga commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

In plain terms

An AI agent that wants to buy something for you — say a bottle of wine — can now do it through
a real, tested checkout, not a throwaway demo. The agent starts the order and gets back a
link; it hands you the link; you open it, prove what's required (being 21+ for the wine) and
pay; the order is marked paid. This PR turns that flow into actual library code you can ship:
credentagent.orders.

The headline is how little you write. Two lines run once at startup (wire the checkout,
subscribe to completion); orders.create() runs per purchase, inside a request handler:

// ── once, at startup ──────────────────────────────────────
credentagent.orders.serve(app);                              // wire the whole checkout onto your app
credentagent.on("order.settled", ({ id }) => fulfill(id));   // fires when ANY order is paid

// ── per purchase — inside a request handler ───────────────
app.post("/buy-wine", async (_req, res) => {
  const { approveUrl } = await credentagent.orders.create({ order, policy });
  res.json({ approveUrl });                                   // hand the link to the human
});

What you're approving

  • New code only — nothing existing changes. No one using the SDK has to touch anything; this
    adds credentagent.orders alongside what's already there.
  • What ships: a runnable checkout example (examples/orders-checkout/) you can boot and click
    through, plus the library API behind it.
  • Worst case if it's wrong: a checkout page or the "order paid" step misbehaves. The one thing
    that must never break — a wine order can't be completed without actually proving you're 21+ — is
    locked down by a test that fails the moment that check is removed (I verified it goes red).
  • What stays safe: no real money moves. The trust level is presence-only-demo — the
    cryptography is real, but there's no issuer trust anchor yet, so this is a flow demo, not a safety
    control. And the price is always recomputed on the server, never trusted from the link.

How to test

npm run build && npm test                    # gate: 328 pass · storefront: 86 pass
node examples/orders-checkout/smoke.mjs       # → ALL SMOKE CHECKS PASSED (8/8), no browser needed
node examples/orders-checkout/server.mjs      # boots on http://localhost:4000
#   curl -X POST http://localhost:4000/buy-wine   → { id, approveUrl }
#   open the approveUrl in a browser             → the checkout page (wine + a 21+ gate)
#   curl http://localhost:4000/orders/<id>       → { ok: true } once it settles

For reviewers — the detail

Graduates the orders prototype from #95 (the two demo programs that stood in for the API) into
library code, per #97 (which is a sub-issue of epic #12 — Human-Not-Present delegation). Grants
(the human-not-present half) is the next increment; this PR is the human-present checkout only.

The commits, additive (plus review-round fixes — awaited create() persistence, an idempotent demo place path, dollar amounts everywhere, and the unwired Money helper removed):

  1. credentagent.orders.*await orders.create({ order, policy }){ id, approveUrl, manifest } (async: it awaits persistence before handing out the link); orders.retrieve(id)
    the one result door (ok | pending + approveUrl | code); on("order.settled")
    the in-process completion event.
  2. orders.serve(app) — binds the existing, proven pieces (mountCeremony for the rails,
    completeOrder for the shared fail-closed completion, renderRequirements for the one checkout
    page) over the created-order store. There is no second enforcement surface — it reuses the
    same completion path every other rail uses.
  3. The runnable example + README/API-surface docs.

Files: orders.ts, orders-serve.ts (new); client.ts, types.ts, index.ts
(wiring/exports); orders.test.ts, orders-serve.test.ts (new); examples/orders-checkout/.

Security invariants held (spelled out):

Honesty: trust_level is presence-only-demo throughout — no overclaim; the instant-demo
completion is a convenience for ungated orders only, so the flow is clickable without a wallet.

Tests: orders.test.ts (6) + orders-serve.test.ts (8) — each security test verified to fail
when its control is deleted. Full suite green: gate 326, storefront 86.

Refs #97 · epic #12 · design #95 (spec 009-ap2-mandate-chain-dx)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Harfquzhqg5SCCURyYRbHD

dzuluaga added 3 commits July 21, 2026 19:42
…out API

Graduates the orders prototype into library code. New surface on the CredentAgent
client (spec 009):
- orders.create({ order, policy }) → { id, approveUrl, manifest } (manifest resolved
  server-side via requirements()); orders.retrieve(id) → the door (ok | pending +
  approveUrl | reason).
- usd Money — opaque, currency-checked, integer minor units (FR-005).
- on("order.settled") — the completion webhook (fired by the completed-store write),
  so you retrieve ONCE, never poll.
- Order stores default in-memory, injectable (invariant 4: per-order keyed).

Wraps existing machinery (requirements + renderRequirements + completeOrder); the
approveUrl page + completion wiring land in the interface increment next.

Tests: 6 new (orders.test.ts) incl. a load-bearing bypass (an order is 'ok' only once
completed — proven RED when the control is removed) + per-order isolation + the webhook.

Refs #97 #92

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
Graduates the orders prototype's server into the library. One call wires the whole
human-present checkout onto an Express app:

  ca.orders.serve(app);                       // rails + checkout page + completion
  ca.on("order.settled", ({ id }) => fulfill(id));
  const { approveUrl } = ca.orders.create({ order, policy });  // a REAL, completable link

What serve() binds (reusing the SAME proven pieces, no second enforcement surface):
- the ceremony rails (mountCeremony) over the created-order store — the price authority;
- GET /credentagent/orders/:id — the shared checkout page (renderRequirements);
- POST /credentagent/orders/:id/place — instant-demo completion, UNGATED orders only;
- GET  /credentagent/orders/:id/status — the completion poll;
- the completion seam = the shared completeOrder, bound so its record write flows into
  orders._complete → order.settled.

The stored order is the amount + age authority (invariant 2): the catalog re-derives from
the STORED lines, never the token. Gated orders (age / payment) complete ONLY through the
fail-closed rails — the instant-demo path refuses them server-side (invariant 1).

Tests: 6 new (orders-serve.test.ts) incl. a load-bearing bypass — a gated order POSTed to
the instant-demo place path is refused and never completes unverified (proven RED when the
guard is removed). Full suite green: gate 326, storefront 86.

Refs #97 #92

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
…ve API)

A shippable interface built on the real credentagent.orders API — a checkout an agent
can drive, wired in one line (orders.serve(app)):

- examples/orders-checkout/server.mjs — boots on :4000; POST /buy-wine → approveUrl,
  open it in a browser to prove age + pay, order.settled fires on completion.
- examples/orders-checkout/smoke.mjs — drives the built package over HTTP and asserts:
  a gated order is refused on the instant-demo path (403) + stays pending; an ungated
  order completes → order.settled → retrieve ok. Runnable with no browser/wallet.
- gate README + examples index: an 'Orders — a checkout without a storefront' section.

Verified: node examples/orders-checkout/smoke.mjs → ALL SMOKE CHECKS PASSED (7/7).

Refs #97

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

vercel Bot commented Jul 22, 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 23, 2026 4:35pm

@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: e2b7e7bc00

ℹ️ 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.ts Outdated
Comment thread packages/credentagent-gate/src/orders-serve.ts Outdated
Found by driving the checkout in a browser: after proving age, the rail redirected the
buyer to `/checkout?order=<id>` — the storefront's route — which the orders interface
doesn't serve, so they hit a 'Cannot GET /checkout' dead end. The order state was correct
(age WAS verified); only the return link was wrong.

Fix: the ceremony context now carries a returnUrl(orderId) builder (absent ⇒ each rail's
existing `/checkout` default, so the storefront is byte-unchanged). orders.serve() sets it
to `/credentagent/orders/:id`, so all three rails (age/credential, passkey, dc-payment)
send the buyer back to the right checkout page.

Also: the example wine now prices at $21 (was a stray 2100 → $2,100.00 — the checkout page
renders amounts in major units).

Tests: +1 regression (the credential rail's page returns the orders URL, not /checkout) —
proven RED when the returnUrl thread is removed. Full suite green: gate 327, storefront 86,
orders-checkout smoke 7/7. Verified end-to-end in a browser: checkout → prove age → back on
the orders page with Age ✓ and payment unlocked.

Refs #97

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
…er-purchase)

The condensed snippet read as three flat lines, so it was unclear when each runs. Mark it:
serve(app) + on('order.settled') run ONCE at startup; orders.create() runs PER purchase,
inside a request handler. Applied to the gate README, the example README, and server.mjs.
Also corrects the stray $2,100 wine to $21 in the READMEs (matches the example).

No code change; smoke still 7/7.

Refs #97

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

on('order.settled') is a local, same-process event listener — no HTTP, no retry, no cross-
instance delivery. Calling it a 'webhook' overclaimed. Corrected the on() doc, orders.ts
comments, two test labels, and the README to say what it actually is, and to point at
orders.retrieve(id) as the durable cross-instance signal (a real HTTP webhook isn't built yet).

No behavior change; gate 327 tests pass.

Refs #97

Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
Comment thread examples/orders-checkout/smoke.mjs Outdated
…t demo place, dollars everywhere

- orders.create() is now async and awaits the created-order write before
  returning approveUrl (Codex P1): with an injected async/shared store the
  fire-and-forget write could still be in flight when the human opened the
  link — a 404 on an order create() reported as created. Matches retrieve(),
  which was already async. The new test goes red without the await.
- The instant-demo place path is idempotent (Codex P2): a retried or
  double-clicked POST no longer re-records the order or re-fires
  order.settled, whose listener triggers fulfillment. The new unit test and
  smoke check go red without the completed-store guard.
- Amounts are dollars everywhere (review): smoke.mjs and the unit tests used
  minor units (2100 rendered as $2,100.00 by the checkout page) while
  server.mjs used dollars. The unwired Money helper (usd.*) is removed —
  nothing consumed it and its cents convention contradicted the repo's
  dollar-number amounts; the grants increment can introduce it wired in.

Both suites green (420 tests); smoke 8/8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MphtihdwkALRVgLhhW3dm
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
The README snippets are what people copy, and the copy-pasteable line
`on("order.settled", ({ id }) => fulfill(id))` silently misbehaves on
serverless: the emit is in-process and synchronous, so async work started
in the listener can be frozen with the instance once the response is sent.
Say so next to the snippet (not only in the client.ts docstring), point at
shared stores + orders.retrieve(id) as the durable signal, and link the
webhook increment (#101).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MphtihdwkALRVgLhhW3dm
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>

@TheBlackBit TheBlackBit left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@dzuluaga
dzuluaga merged commit 3bfd948 into main Jul 23, 2026
7 checks passed
@dzuluaga
dzuluaga deleted the feat/97-orders-surface branch July 23, 2026 21:07
TheBlackBit pushed a commit that referenced this pull request Jul 24, 2026
Brings the branch up to date with Demo PKI (#100), signed order.settled webhooks
(#101/#102), orders.serve(app) (#97/#98) and the Vercel deploy smoke CI (#105).

Conflict: packages/credentagent-gate/src/client.ts — main added `ordersServed` and
008 added `delegated` as adjacent private fields. Kept BOTH (independent features).
Also refreshed the `delegated` comment to match the two-step routing committed in
c3e8637: only the PAYMENT goes to the delegated ceremony; identity gates (age) and a
discount stay on the built-in credential rail.

451 tests pass across both packages; both typecheck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ever Morales <ever.morales@koombea.com>
dzuluaga added a commit that referenced this pull request Jul 25, 2026
…efusalCode, denied state, Money reconciliation

Addresses the #95 review (TheBlackBit + Codex):
- Age gate: headline example now uses .when() so 21+ applies ONLY to age-restricted carts,
  not every order (the example is the DX test).
- Intent bounds gain an optional `allow` constraint (SKUs/categories/attributes) — a human
  who's away bounds WHAT the agent buys, not just how much; enforced fail-closed each spend
  (new code `not-allowed`). (FR-010)
- RefusalCode is documented as a TYPED union, not string (autocomplete + exhaustiveness).
- Grant lifecycle: all four states documented incl. `denied` (never-authorized, terminal)
  vs `revoked` (authorized-then-cancelled), plus the revoke-wins-fail-closed race. (FR-007)
- Prototypes note refreshed: orders graduated in #98; the caller-controlled-id wrapper was
  removed (Codex P1); grants graduates under #104.
- Honest reconciliation: #98 shipped plain dollar numbers, not the opaque Money type —
  flagged as an open decision for #104 rather than pretending Money shipped.

Codex P1s already fixed on-branch: orders-proto wrapper deleted; grants-proto CartMandate
lines now carry { id, quantity, unitPrice, lineTotal }.

Refs #95 #92 · epic #97

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>
dzuluaga added a commit that referenced this pull request Jul 26, 2026
… main)

main advanced 11 commits past this branch's fork point (the orders / webhooks /
grants merges — #98, #102, #112, #118, #121, #123 — and the #120 browse-products
fix). GitHub flagged PR #103 CONFLICTING. Reconcile MERGE (not rebase — the repo's
precedent is reconcile merges, and rebasing would rewrite pushed, DCO-signed history).

Two files conflicted (both-add — the two features touched adjacent lines, neither
restructured the other's seam); resolved by keeping BOTH sides:

- packages/credentagent-storefront/src/server.ts — the StorefrontOptions interface.
  BOTH won: kept the branch's `verifier?: DelegatedVerifier` (008 delegated ceremony)
  AND main's `grants?: Grants` + `merchant?: string` (spec 009 human-not-present).
  They are independent optional fields; a delegated storefront and a grants storefront
  compose. (Restored a `/**` the raw resolution dropped — caught by tsc, re-verified.)

- packages/credentagent-storefront/src/server.test.ts — two separate describe blocks
  appended at end of file, sharing the trailing closers. BOTH won: kept the branch's
  "008 — delegated ceremony completes through the storefront (S5)" suite (the e2e
  delegated payment + the wrong-amount refusal) AND main's "browse-products is
  agent-legible (#120)" suite; closed each block explicitly.

Auto-merged with both sides preserved (no hand resolution needed): client.ts (main's
grants/webhooks wiring AND the branch's `this.delegated` verifier wiring), index.ts
(main's grants exports AND the branch's DelegatedVerifier exports), the two READMEs.
The #103 double-settle fix (completion.ts, ceremony/types.ts, keyed-mutex.ts) was
branch-only — main never touched completion.ts, so it merged untouched.

Verified after resolving: build (both packages typecheck), gate suite 380 + storefront
99 green, lint clean, and the security-critical files re-run explicitly — the delegated
verify refusals + amount binding (delegated-payment 37, server delegated e2e), the new
double-settle test (completion 33), and main's grants tests (grants 14 + grants-tools 10)
all pass. Neither feature regressed the other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5Y1aX9ffAF26hP4fgQFi7
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.

2 participants