Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/credentagent-gate/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export { MemoryVerificationStore } from "./store.js";
// `await credentagent.orders.create({ order, policy })` → { id, approveUrl, manifest };
// `credentagent.orders.retrieve(id)` → the door (ok | pending+approveUrl | reason).
export { Orders, MemoryOrderStore } from "./orders.js";
export type { OrderStore, CreatedOrder, CompletedOrder, OrderDoor } from "./orders.js";
export type { OrderStore, CreatedOrder, CompletedOrder, OrderDoor, OrderDoorCode } from "./orders.js";

// ── Webhooks (spec 010) — the REAL HTTP completion signal ───────────────────
// SEND: `new CredentAgent({ webhooks: { endpoints: [{ url, secret }] } })` → every settled order
Expand Down
13 changes: 10 additions & 3 deletions packages/credentagent-gate/src/orders.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { CredentAgent } from "./client.js";
import { age, payment, required } from "./credentials.js";
import type { CreatedOrder, OrderStore } from "./orders.js";
import type { CreatedOrder, OrderStore, OrderDoorCode } from "./orders.js";

// Amounts are dollars, matching the checkout page's formatter ($21.00 — not minor units).
const anOrder = () => ({
Expand Down Expand Up @@ -81,8 +81,15 @@ describe("credentagent.orders", () => {
expect(seen).toEqual([id]);
});

it("retrieve of an unknown id is a typed refusal, not a throw", async () => {
it("retrieve of an unknown id is a typed refusal with the typed OrderDoorCode, not a throw", async () => {
const ca = new CredentAgent({ walletOrigin: "https://shop.example" });
expect(await ca.orders.retrieve("ord_nope")).toMatchObject({ ok: false, code: "not-found" });
const res = await ca.orders.retrieve("ord_nope");
expect(res).toMatchObject({ ok: false, code: "not-found" });
// `code` is the typed union OrderDoorCode (not `string`); the compile-time guard that keeps it
// narrow lives in orders.ts (revert it to `string` and the build fails). Here we pin the value.
if (!res.ok && "code" in res) {
const code: OrderDoorCode = res.code; // narrows to the union — a `string` would need a cast
Comment thread
dzuluaga marked this conversation as resolved.
Outdated
expect(code).toBe("not-found");
}
});
});
14 changes: 13 additions & 1 deletion packages/credentagent-gate/src/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,23 @@ export class MemoryOrderStore<T> implements OrderStore<T> {
clear(id: string): void { this.m.delete(id); }
}

/**
* The refusal codes the door's `{ ok:false, code }` branch can carry — a TYPED union, not
* `string`, so `res.code === "not-found"` autocompletes, a typo fails to compile, and a
* `switch` on it is exhaustiveness-checked (#95 review — a bare string is a footgun).
*
* Today `orders.retrieve` reads stores and only refuses with `"not-found"`; it does not surface
* policy failures — an age/payment gate refuses at the ceremony, not here. Widen this union
* DELIBERATELY when a new orders refusal path lands; the compile error at each `switch` is the
* reminder to handle it. (Sibling of the delegated `RefusalCode` and the webhook `WebhookRefusalCode`.)
*/
export type OrderDoorCode = "not-found";

/** The one result shape every consent path shares (spec 009 FR-003). */
export type OrderDoor =
| { ok: true; mandateBundle?: unknown; authorization: "direct"; trustLevel: TrustLevel; completion: Omit<CompletedOrder, "orderId" | "mandateBundle"> }
| { ok: false; pending: true; approveUrl: string; trustLevel: TrustLevel }
| { ok: false; code: string; credential?: string; trustLevel: TrustLevel };
| { ok: false; code: OrderDoorCode; credential?: string; trustLevel: TrustLevel };

export interface OrdersDeps {
walletOrigin: string;
Expand Down
Loading