orders.serve(app): the human-present checkout, wired in one call (#97) - #98
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 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".
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>
This was referenced Jul 22, 2026
TheBlackBit
reviewed
Jul 23, 2026
…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
self-requested a review
July 23, 2026 17:24
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>
This was referenced Jul 25, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:What you're approving
adds
credentagent.ordersalongside what's already there.examples/orders-checkout/) you can boot and clickthrough, plus the library API behind it.
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).
presence-only-demo— thecryptography 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
For reviewers — the detail
Graduates the
ordersprototype from #95 (the two demo programs that stood in for the API) intolibrary 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 unwiredMoneyhelper removed):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.
orders.serve(app)— binds the existing, proven pieces (mountCeremonyfor the rails,completeOrderfor the shared fail-closed completion,renderRequirementsfor the one checkoutpage) over the created-order store. There is no second enforcement surface — it reuses the
same completion path every other rail uses.
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):
orders.create) isthe price + age authority; the catalog re-derives amount and age threshold from the stored
lines, never from the link.
through the fail-closed wallet rails. The instant-demo "Complete purchase" path is refused (403)
for gated orders, server-side — hiding the button is not enforcement. This is the load-bearing
bypass test in
orders-serve.test.ts(proven RED when the guard is removed).another (a dedicated test).
Honesty:
trust_levelispresence-only-demothroughout — no overclaim; the instant-democompletion 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 failwhen 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