@agenticprimitives/payments 0.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/dist/entitlement/index.d.ts +125 -0
  4. package/dist/entitlement/index.d.ts.map +1 -0
  5. package/dist/entitlement/index.js +128 -0
  6. package/dist/entitlement/index.js.map +1 -0
  7. package/dist/entitlement/voucher.d.ts +85 -0
  8. package/dist/entitlement/voucher.d.ts.map +1 -0
  9. package/dist/entitlement/voucher.js +109 -0
  10. package/dist/entitlement/voucher.js.map +1 -0
  11. package/dist/index.d.ts +135 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +60 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mandate-sign.d.ts +187 -0
  16. package/dist/mandate-sign.d.ts.map +1 -0
  17. package/dist/mandate-sign.js +177 -0
  18. package/dist/mandate-sign.js.map +1 -0
  19. package/dist/mandate.d.ts +33 -0
  20. package/dist/mandate.d.ts.map +1 -0
  21. package/dist/mandate.js +40 -0
  22. package/dist/mandate.js.map +1 -0
  23. package/dist/ops.d.ts +55 -0
  24. package/dist/ops.d.ts.map +1 -0
  25. package/dist/ops.js +66 -0
  26. package/dist/ops.js.map +1 -0
  27. package/dist/rails/escrow.d.ts +156 -0
  28. package/dist/rails/escrow.d.ts.map +1 -0
  29. package/dist/rails/escrow.js +88 -0
  30. package/dist/rails/escrow.js.map +1 -0
  31. package/dist/rails/invoice.d.ts +65 -0
  32. package/dist/rails/invoice.d.ts.map +1 -0
  33. package/dist/rails/invoice.js +57 -0
  34. package/dist/rails/invoice.js.map +1 -0
  35. package/dist/rails/recurring.d.ts +56 -0
  36. package/dist/rails/recurring.d.ts.map +1 -0
  37. package/dist/rails/recurring.js +58 -0
  38. package/dist/rails/recurring.js.map +1 -0
  39. package/dist/rails/wallet.d.ts +16 -0
  40. package/dist/rails/wallet.d.ts.map +1 -0
  41. package/dist/rails/wallet.js +20 -0
  42. package/dist/rails/wallet.js.map +1 -0
  43. package/dist/rails/x402/executor.d.ts +118 -0
  44. package/dist/rails/x402/executor.d.ts.map +1 -0
  45. package/dist/rails/x402/executor.js +243 -0
  46. package/dist/rails/x402/executor.js.map +1 -0
  47. package/dist/rails/x402/index.d.ts +11 -0
  48. package/dist/rails/x402/index.d.ts.map +1 -0
  49. package/dist/rails/x402/index.js +8 -0
  50. package/dist/rails/x402/index.js.map +1 -0
  51. package/dist/rails/x402/nonce-store.d.ts +30 -0
  52. package/dist/rails/x402/nonce-store.d.ts.map +1 -0
  53. package/dist/rails/x402/nonce-store.js +35 -0
  54. package/dist/rails/x402/nonce-store.js.map +1 -0
  55. package/dist/rails/x402/quote.d.ts +41 -0
  56. package/dist/rails/x402/quote.d.ts.map +1 -0
  57. package/dist/rails/x402/quote.js +84 -0
  58. package/dist/rails/x402/quote.js.map +1 -0
  59. package/dist/rails/x402/resource.d.ts +42 -0
  60. package/dist/rails/x402/resource.d.ts.map +1 -0
  61. package/dist/rails/x402/resource.js +71 -0
  62. package/dist/rails/x402/resource.js.map +1 -0
  63. package/dist/rails/x402/wire.d.ts +81 -0
  64. package/dist/rails/x402/wire.d.ts.map +1 -0
  65. package/dist/rails/x402/wire.js +125 -0
  66. package/dist/rails/x402/wire.js.map +1 -0
  67. package/dist/receipt.d.ts +36 -0
  68. package/dist/receipt.d.ts.map +1 -0
  69. package/dist/receipt.js +50 -0
  70. package/dist/receipt.js.map +1 -0
  71. package/dist/refund.d.ts +29 -0
  72. package/dist/refund.d.ts.map +1 -0
  73. package/dist/refund.js +19 -0
  74. package/dist/refund.js.map +1 -0
  75. package/dist/split.d.ts +27 -0
  76. package/dist/split.d.ts.map +1 -0
  77. package/dist/split.js +33 -0
  78. package/dist/split.js.map +1 -0
  79. package/dist/transfer.d.ts +51 -0
  80. package/dist/transfer.d.ts.map +1 -0
  81. package/dist/transfer.js +54 -0
  82. package/dist/transfer.js.map +1 -0
  83. package/package.json +61 -0
  84. package/spec.md +5 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agentic Trust Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @agenticprimitives/payments
2
+
3
+ > **Status: STUB** (Wave 0.5 of the W1 implementation wave). Typed primitives and invariant guards ship today; the EIP-712 mandate signer and the three W1 rails land in Wave 6 per the [w1 implementation wave plan](../../docs/architecture/w1-implementation-wave-plan.md). A recent self-audit flagged this package's docs for overclaiming ([NEW-PMT-1](../../docs/audits/findings.yaml)); this README is written to the actual surface.
4
+
5
+ Machine payments arrived. x402 made HTTP-native pay-per-request real; Google's AP2 defined mandates for agent-initiated spend. What neither ships — what nobody ships — is machine payments scoped by the *same* delegation, custody, and audit substrate that governs everything else the agent does. "This agent may spend up to this amount, for this task, on this rail, until Friday — signed by a smart account, bound to the work it pays for, receipted into an attestation registry" is the combination the agent economy actually needs, and it only exists when payments are a layer of one system rather than a bolted-on rail.
6
+
7
+ This package is the designed payments slice of that substrate — spine Layer 9b, the `PaymentMandate` — spec'd in full, scaffolded now, landing in the implementation waves.
8
+
9
+ Part of [agenticprimitives](../../README.md) — the trust substrate for the agent economy: one canonical Smart Agent identity with custody, delegation, naming, credentials, and audit evidence designed as one system.
10
+
11
+ ## What ships today
12
+
13
+ Typed primitives and pure invariant guards — no signing, no execution, no money moves:
14
+
15
+ - **`PaymentMandate` type** — payer, payee, granter, rail, `AmountPolicy` (exact / range / formula), `maxRedemptions`, validity window, `mode`, and an optional `delegationRef` linking the mandate to the delegation that authorized it.
16
+ - **`ContextBinding` + `assertContextBindingValid`** — PMT-3.1 enforced: every mandate must bind to at least one of intent / agreement / task / artifact / HTTP resource. No context-free spend.
17
+ - **`MandateConstraints`** — AP2-aligned aggregate scope: cumulative caps, redemption frequency windows, category allow/deny, geo-fencing.
18
+ - **Open/closed mode discrimination + `assertClosedMandateInvariants`** — PMT-INV-14 enforced: a closed (final-charge) mandate is always one-shot.
19
+ - **`computeMandateId`** — deterministic keccak-256 mandate identity over payer + nonce + rail + chain.
20
+ - **`PaymentRailExecutor` interface + `registerRail` / `getRail`** — the extension point every rail (current and future, including the reserved confidential family) implements.
21
+
22
+ ## What lands in Wave 6 (designed, not shipped)
23
+
24
+ The EIP-712 typed-data builder, SA signing and ERC-1271 verification (no raw EOA signatures — PMT-INV-12), `redeemPaymentMandate`, and the three W1 rails: `rails/wallet` (SA-to-SA transfer via UserOp), `rails/x402` (HTTP-native per [x402.org](https://www.x402.org/) + reference facilitator), and `rails/sponsored-userop` (paymaster-sponsored, no value moved). Plus `PaymentReceipt` — an immutable, non-revocable credential asserted into the attestation registry per [ADR-0023](../../docs/architecture/decisions/0023-attestation-registry-eas-aligned-bilateral-consent.md), so every redemption leaves first-class evidence. Confidential rails (Aztec-style, Zcash-style, ZK paymasters) are a reserved W2 sub-module family.
25
+
26
+ ## Where this is heading / market context
27
+
28
+ - **x402** proved HTTP-native machine payments; it is one of our three W1 rails, not a competitor.
29
+ - **Google AP2** defined mandate semantics for agent spend; `MandateConstraints` is deliberately AP2-aligned so the substrate speaks the emerging lingua franca.
30
+ - The gap both leave open: x402 and AP2 authorize a *payment*; neither anchors it to a canonical on-chain identity with custody policy, a revocable delegation chain, and an audit trail behind it. Delegation-scoped machine payments — mandate references delegation, delegation references the Smart Agent, every step receipted — is the combination this layer exists to ship.
31
+
32
+ **Authoritative spec:** [spec 243 — payments](../../specs/243-payments.md) (see [`spec.md`](./spec.md)). Owns spine layer 9b; bounded surface in `CLAUDE.md` and `capability.manifest.json`.
33
+
34
+ ## Build
35
+
36
+ ```bash
37
+ pnpm --filter @agenticprimitives/payments typecheck
38
+ pnpm --filter @agenticprimitives/payments test
39
+ pnpm --filter @agenticprimitives/payments build
40
+ ```
41
+
42
+ ## Status — honest version
43
+
44
+ STUB. Nothing here signs or settles a payment today; treat the rails and the EIP-712 surface as spec'd commitments. The audit finding that this package's docs once claimed more ([NEW-PMT-1](../../docs/audits/findings.yaml)) is public — transparency about the gap is the point. Wave sequencing: [w1-implementation-wave-plan.md](../../docs/architecture/w1-implementation-wave-plan.md).
@@ -0,0 +1,125 @@
1
+ /**
2
+ * spec 272 §10 — entitlements: pay-once-then-access.
3
+ *
4
+ * A successful settlement mints an `EntitlementRecord`. Later access reads CONSUME
5
+ * the entitlement (decrement `usesLeft`) with NO new settlement — one access lane
6
+ * (X402-D8): a request is served against {a fresh charge} OR {a live entitlement},
7
+ * never both. **Prepaid credits = an entitlement with `maxUses: N`** — same machinery.
8
+ *
9
+ * Two bindings:
10
+ * - `'sa'` — held by a specific SA; the presenter MUST be that SA.
11
+ * - `'bearer'`— held by whoever presents the matching voucher (tier A3, unlinkable);
12
+ * this module gates on `voucherId`, the voucher crypto lives in `./voucher`.
13
+ *
14
+ * Fail-closed: an unknown binding, scope mismatch, expiry, or exhausted uses all DENY.
15
+ * Pure (no store): `consumeEntitlement` returns the decremented record; the app persists — single-writer
16
+ * only. For a shared/persisted entitlement use `EntitlementStore.claimUse` (atomic conditional decrement,
17
+ * the ARCH-H3 sibling) so two concurrent redemptions can't double-spend a prepaid credit.
18
+ */
19
+ import { type Address } from 'viem';
20
+ import type { Hex32 } from '../index.js';
21
+ export * as voucher from './voucher.js';
22
+ export type EntitlementBinding = 'sa' | 'bearer';
23
+ export interface EntitlementRecord {
24
+ entitlementId: Hex32;
25
+ binding: EntitlementBinding;
26
+ /** what this grants access to (e.g. keccak of the resource/skill scope) */
27
+ scopeHash: Hex32;
28
+ /** for `'sa'` binding — the SA that may present it */
29
+ subject?: Address;
30
+ /** for `'bearer'` binding — the voucher this entitlement is tied to */
31
+ voucherId?: Hex32;
32
+ /** expiry (unix seconds) */
33
+ ttl: number;
34
+ maxUses: number;
35
+ usesLeft: number;
36
+ provenance: {
37
+ mandateId: Hex32;
38
+ settlementHash: Hex32;
39
+ };
40
+ }
41
+ export interface MintEntitlementInput {
42
+ binding: EntitlementBinding;
43
+ scopeHash: Hex32;
44
+ ttl: number;
45
+ maxUses: number;
46
+ mandateId: Hex32;
47
+ settlementHash: Hex32;
48
+ /** required for `'sa'` binding */
49
+ subject?: Address;
50
+ /** required for `'bearer'` binding */
51
+ voucherId?: Hex32;
52
+ }
53
+ /** Mint an entitlement from a settled payment. Throws if the binding's required handle is missing. */
54
+ export declare function mintEntitlementOnPayment(input: MintEntitlementInput): EntitlementRecord;
55
+ /** Prepaid credit pack = an SA-bound entitlement with `maxUses = count`. */
56
+ export declare function mintCredits(input: Omit<MintEntitlementInput, 'binding' | 'maxUses'> & {
57
+ subject: Address;
58
+ count: number;
59
+ }): EntitlementRecord;
60
+ export interface EntitlementContext {
61
+ scopeHash: Hex32;
62
+ now: number;
63
+ /** the SA presenting (required to match an `'sa'` binding) */
64
+ presenter?: Address;
65
+ /** the voucher id presented (required to match a `'bearer'` binding) */
66
+ voucherId?: Hex32;
67
+ }
68
+ export type EntitlementCheck = {
69
+ ok: true;
70
+ } | {
71
+ ok: false;
72
+ reason: string;
73
+ };
74
+ /** Fail-closed validity check (does NOT consume). */
75
+ export declare function checkEntitlement(record: EntitlementRecord, ctx: EntitlementContext): EntitlementCheck;
76
+ export interface ConsumptionReceipt {
77
+ entitlementId: Hex32;
78
+ scopeHash: Hex32;
79
+ usesLeftAfter: number;
80
+ at: number;
81
+ }
82
+ export type ConsumeResult = {
83
+ ok: true;
84
+ record: EntitlementRecord;
85
+ consumption: ConsumptionReceipt;
86
+ } | {
87
+ ok: false;
88
+ reason: string;
89
+ };
90
+ /**
91
+ * Validate + decrement (PURE — returns the updated record; does NOT persist).
92
+ *
93
+ * entitlement-consume-toctou-no-atomic-claim (2026-07-04 self-audit): on its own this is a TOCTOU seam.
94
+ * A caller that does `checkEntitlement()` (or `consumeEntitlement()`) and THEN persists `record.usesLeft`
95
+ * has a read→decrement→write window: two concurrent redemptions of a prepaid `usesLeft:1` credit can both
96
+ * read 1 and both write 0, double-spending a money-equivalent pack. Use this only in-process/single-writer.
97
+ * For a shared/persisted entitlement, go through {@link EntitlementStore.claimUse}, whose atomic conditional
98
+ * decrement is the ARCH-H3 sibling of `SpentSet.claim`.
99
+ */
100
+ export declare function consumeEntitlement(record: EntitlementRecord, ctx: EntitlementContext): ConsumeResult;
101
+ export interface EntitlementStore {
102
+ /**
103
+ * entitlement-consume-toctou / ARCH-H3 sibling: ATOMIC validate + single-use decrement of a STORED
104
+ * entitlement. Returns ok ONLY if this call claimed a use — the record exists, matches `ctx` (scope +
105
+ * binding + presenter/voucher), is unexpired, and had `usesLeft > 0` at the instant of decrement. Two
106
+ * concurrent redemptions of a `usesLeft:1` credit: exactly one gets ok, the other `entitlement exhausted`.
107
+ * A distributed backend MUST implement the decrement as a conditional write so atomicity lives in the
108
+ * interface, not each caller: D1 `UPDATE … SET usesLeft = usesLeft - 1 WHERE entitlementId = ? AND
109
+ * usesLeft > 0 RETURNING usesLeft` (rows-affected == 1 is the claim), a Durable Object transaction, or a
110
+ * conditional put. Mirrors `SpentSet.claim` / x402 `NonceReservationStore.reserve`.
111
+ */
112
+ claimUse(entitlementId: Hex32, ctx: EntitlementContext): ConsumeResult | Promise<ConsumeResult>;
113
+ }
114
+ /**
115
+ * Reference in-memory {@link EntitlementStore}. In-process JS is single-threaded, so the validate +
116
+ * decrement below is naturally atomic — the interface demands the same guarantee of every (distributed)
117
+ * implementer. Seed it with the minted records; `claimUse` mutates `usesLeft` in place under the claim.
118
+ */
119
+ export declare function createMemoryEntitlementStore(records?: Iterable<EntitlementRecord>): EntitlementStore & {
120
+ put(record: EntitlementRecord): void;
121
+ get(id: Hex32): EntitlementRecord | undefined;
122
+ };
123
+ /** Convenience: keccak of a resource/skill scope string → `scopeHash`. */
124
+ export declare function scopeHashOf(scope: string): Hex32;
125
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entitlement/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * spec 272 §10 — entitlements: pay-once-then-access.
3
+ *
4
+ * A successful settlement mints an `EntitlementRecord`. Later access reads CONSUME
5
+ * the entitlement (decrement `usesLeft`) with NO new settlement — one access lane
6
+ * (X402-D8): a request is served against {a fresh charge} OR {a live entitlement},
7
+ * never both. **Prepaid credits = an entitlement with `maxUses: N`** — same machinery.
8
+ *
9
+ * Two bindings:
10
+ * - `'sa'` — held by a specific SA; the presenter MUST be that SA.
11
+ * - `'bearer'`— held by whoever presents the matching voucher (tier A3, unlinkable);
12
+ * this module gates on `voucherId`, the voucher crypto lives in `./voucher`.
13
+ *
14
+ * Fail-closed: an unknown binding, scope mismatch, expiry, or exhausted uses all DENY.
15
+ * Pure (no store): `consumeEntitlement` returns the decremented record; the app persists — single-writer
16
+ * only. For a shared/persisted entitlement use `EntitlementStore.claimUse` (atomic conditional decrement,
17
+ * the ARCH-H3 sibling) so two concurrent redemptions can't double-spend a prepaid credit.
18
+ */
19
+ import { keccak256, encodeAbiParameters, toBytes } from 'viem';
20
+ // tier A3 — blind bearer vouchers (Privacy Pass / VOPRF).
21
+ export * as voucher from './voucher.js';
22
+ const ZERO32 = ('0x' + '00'.repeat(32));
23
+ function entitlementId(args) {
24
+ return keccak256(encodeAbiParameters([{ type: 'string' }, { type: 'bytes32' }, { type: 'bytes32' }, { type: 'bytes32' }], [
25
+ args.binding,
26
+ args.scopeHash,
27
+ args.binding === 'sa'
28
+ ? keccak256(toBytes(args.subject ?? '0x'))
29
+ : (args.voucherId ?? ZERO32),
30
+ args.mandateId,
31
+ ]));
32
+ }
33
+ /** Mint an entitlement from a settled payment. Throws if the binding's required handle is missing. */
34
+ export function mintEntitlementOnPayment(input) {
35
+ if (input.binding === 'sa' && !input.subject)
36
+ throw new Error("[entitlement] 'sa' binding requires subject");
37
+ if (input.binding === 'bearer' && !input.voucherId)
38
+ throw new Error("[entitlement] 'bearer' binding requires voucherId");
39
+ if (input.maxUses < 1)
40
+ throw new Error('[entitlement] maxUses must be >= 1');
41
+ return {
42
+ entitlementId: entitlementId(input),
43
+ binding: input.binding,
44
+ scopeHash: input.scopeHash,
45
+ subject: input.subject,
46
+ voucherId: input.voucherId,
47
+ ttl: input.ttl,
48
+ maxUses: input.maxUses,
49
+ usesLeft: input.maxUses,
50
+ provenance: { mandateId: input.mandateId, settlementHash: input.settlementHash },
51
+ };
52
+ }
53
+ /** Prepaid credit pack = an SA-bound entitlement with `maxUses = count`. */
54
+ export function mintCredits(input) {
55
+ return mintEntitlementOnPayment({ ...input, binding: 'sa', maxUses: input.count });
56
+ }
57
+ /** Fail-closed validity check (does NOT consume). */
58
+ export function checkEntitlement(record, ctx) {
59
+ if (record.binding !== 'sa' && record.binding !== 'bearer')
60
+ return { ok: false, reason: 'unknown entitlement binding' };
61
+ if (record.scopeHash.toLowerCase() !== ctx.scopeHash.toLowerCase())
62
+ return { ok: false, reason: 'scope mismatch' };
63
+ if (ctx.now >= record.ttl)
64
+ return { ok: false, reason: 'entitlement expired' };
65
+ if (record.usesLeft <= 0)
66
+ return { ok: false, reason: 'entitlement exhausted' };
67
+ if (record.binding === 'sa') {
68
+ if (!ctx.presenter || !record.subject || ctx.presenter.toLowerCase() !== record.subject.toLowerCase()) {
69
+ return { ok: false, reason: 'presenter is not the entitlement subject' };
70
+ }
71
+ }
72
+ else {
73
+ if (!ctx.voucherId || !record.voucherId || ctx.voucherId.toLowerCase() !== record.voucherId.toLowerCase()) {
74
+ return { ok: false, reason: 'voucher mismatch' };
75
+ }
76
+ }
77
+ return { ok: true };
78
+ }
79
+ /**
80
+ * Validate + decrement (PURE — returns the updated record; does NOT persist).
81
+ *
82
+ * entitlement-consume-toctou-no-atomic-claim (2026-07-04 self-audit): on its own this is a TOCTOU seam.
83
+ * A caller that does `checkEntitlement()` (or `consumeEntitlement()`) and THEN persists `record.usesLeft`
84
+ * has a read→decrement→write window: two concurrent redemptions of a prepaid `usesLeft:1` credit can both
85
+ * read 1 and both write 0, double-spending a money-equivalent pack. Use this only in-process/single-writer.
86
+ * For a shared/persisted entitlement, go through {@link EntitlementStore.claimUse}, whose atomic conditional
87
+ * decrement is the ARCH-H3 sibling of `SpentSet.claim`.
88
+ */
89
+ export function consumeEntitlement(record, ctx) {
90
+ const v = checkEntitlement(record, ctx);
91
+ if (!v.ok)
92
+ return v;
93
+ const updated = { ...record, usesLeft: record.usesLeft - 1 };
94
+ return {
95
+ ok: true,
96
+ record: updated,
97
+ consumption: { entitlementId: record.entitlementId, scopeHash: record.scopeHash, usesLeftAfter: updated.usesLeft, at: ctx.now },
98
+ };
99
+ }
100
+ /**
101
+ * Reference in-memory {@link EntitlementStore}. In-process JS is single-threaded, so the validate +
102
+ * decrement below is naturally atomic — the interface demands the same guarantee of every (distributed)
103
+ * implementer. Seed it with the minted records; `claimUse` mutates `usesLeft` in place under the claim.
104
+ */
105
+ export function createMemoryEntitlementStore(records = []) {
106
+ const byId = new Map();
107
+ for (const r of records)
108
+ byId.set(r.entitlementId.toLowerCase(), { ...r });
109
+ return {
110
+ put: (r) => byId.set(r.entitlementId.toLowerCase(), { ...r }),
111
+ get: (id) => byId.get(id.toLowerCase()),
112
+ claimUse: (id, ctx) => {
113
+ const record = byId.get(id.toLowerCase());
114
+ if (!record)
115
+ return { ok: false, reason: 'entitlement not found' };
116
+ const result = consumeEntitlement(record, ctx);
117
+ if (!result.ok)
118
+ return result;
119
+ byId.set(id.toLowerCase(), result.record); // commit the decrement under the same synchronous claim
120
+ return result;
121
+ },
122
+ };
123
+ }
124
+ /** Convenience: keccak of a resource/skill scope string → `scopeHash`. */
125
+ export function scopeHashOf(scope) {
126
+ return keccak256(toBytes(`entitlement-scope:${scope}`));
127
+ }
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/entitlement/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * spec 272 §10 tier A3 — blind bearer vouchers (Privacy Pass / RFC 9497 VOPRF).
3
+ *
4
+ * Pay once → receive a pack of UNLINKABLE one-use vouchers → redeem each from a
5
+ * separate context with no link back to the purchase. Built on ristretto255 VOPRF
6
+ * (`@noble/curves`) — the issuer never sees the unblinded token at issuance, so the
7
+ * blinded request it signs and the token presented at redemption cannot be linked.
8
+ *
9
+ * Flow:
10
+ * client blindVoucherRequest() → { request(blinded), secret(tokenId,blind,blinded) }
11
+ * issuer issueVoucher(key, request) → { evaluated, proof } (VOPRF blind-evaluate)
12
+ * client unblindVoucher(secret, issued, pubKey) → Voucher (verifies the issuer proof)
13
+ * issuer redeemVoucher(key, voucher, spentSet) → ok | reason (VOPRF verify + double-spend)
14
+ *
15
+ * One-use is enforced by the spent-set keyed on `voucherId = keccak(tokenId)`.
16
+ */
17
+ import type { Hex } from '@agenticprimitives/types';
18
+ import type { Hex32 } from '../index.js';
19
+ export interface VoucherIssuerKey {
20
+ secretKey: Hex;
21
+ publicKey: Hex;
22
+ }
23
+ export interface BlindedVoucherRequest {
24
+ blinded: Hex;
25
+ }
26
+ /** The client's unblinding state — kept private until `unblindVoucher`. */
27
+ export interface VoucherSecret {
28
+ tokenId: Hex;
29
+ blind: Hex;
30
+ blinded: Hex;
31
+ }
32
+ export interface IssuedVoucher {
33
+ evaluated: Hex;
34
+ proof: Hex;
35
+ }
36
+ /** A redeemable one-use bearer token. */
37
+ export interface Voucher {
38
+ voucherId: Hex32;
39
+ tokenId: Hex;
40
+ output: Hex;
41
+ }
42
+ export declare function generateVoucherIssuerKey(): VoucherIssuerKey;
43
+ /** Deterministic issuer key from a 32-byte seed (stable across processes). */
44
+ export declare function deriveVoucherIssuerKey(seed: Hex, info?: string): VoucherIssuerKey;
45
+ /** `voucherId` — the public, spent-set key derived from the token (no link to the blinded request). */
46
+ export declare function voucherIdOf(tokenId: Hex): Hex32;
47
+ /** Client: blind a fresh random token (or a supplied one) for issuance. */
48
+ export declare function blindVoucherRequest(tokenId?: Hex): {
49
+ request: BlindedVoucherRequest;
50
+ secret: VoucherSecret;
51
+ };
52
+ /** Issuer: blind-evaluate one request (VOPRF — issuer learns nothing about the token). */
53
+ export declare function issueVoucher(key: VoucherIssuerKey, request: BlindedVoucherRequest): IssuedVoucher;
54
+ /** Issuer: issue a whole pack. */
55
+ export declare function issueVouchers(key: VoucherIssuerKey, requests: BlindedVoucherRequest[]): IssuedVoucher[];
56
+ /**
57
+ * Client: unblind into a finished voucher. Throws if the issuer's VOPRF proof fails
58
+ * (a malicious issuer using the wrong key is detected here — verifiability).
59
+ */
60
+ export declare function unblindVoucher(secret: VoucherSecret, issued: IssuedVoucher, issuerPublicKey: Hex): Voucher;
61
+ /** Issuer: VOPRF validity of a presented voucher (does NOT check double-spend). Fail-closed. */
62
+ export declare function verifyVoucher(key: VoucherIssuerKey, voucher: Voucher): {
63
+ ok: boolean;
64
+ reason?: string;
65
+ };
66
+ export interface SpentSet {
67
+ /**
68
+ * ARCH-H3 (seam audit): ATOMIC claim of a one-use id. Returns `true` if THIS call claimed the id
69
+ * (it was fresh), `false` if it was already spent. Replaces the former TOCTOU `has()` + `add()` pair —
70
+ * a check-then-add across an `await` let two concurrent redemptions of a money-equivalent one-use
71
+ * voucher BOTH succeed. A distributed backend MUST implement this as an insert-if-absent (D1 `INSERT
72
+ * ... ON CONFLICT DO NOTHING` + rows-affected, a Durable Object transaction, or a conditional put) so
73
+ * the atomicity lives in the interface, not in each caller. Mirrors x402 `NonceReservationStore.reserve`.
74
+ */
75
+ claim(id: Hex32): boolean | Promise<boolean>;
76
+ }
77
+ export declare function createMemorySpentSet(): SpentSet & {
78
+ size(): number;
79
+ };
80
+ /** Issuer: verify + one-shot redeem. Rejects an invalid voucher and a replayed one. */
81
+ export declare function redeemVoucher(key: VoucherIssuerKey, voucher: Voucher, spent: SpentSet): Promise<{
82
+ ok: boolean;
83
+ reason?: string;
84
+ }>;
85
+ //# sourceMappingURL=voucher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voucher.d.ts","sourceRoot":"","sources":["../../src/entitlement/voucher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAezC,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,GAAG,CAAC;IACf,SAAS,EAAE,GAAG,CAAC;CAChB;AACD,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,GAAG,CAAC;CACd;AACD,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,GAAG,CAAC;IACb,KAAK,EAAE,GAAG,CAAC;IACX,OAAO,EAAE,GAAG,CAAC;CACd;AACD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,GAAG,CAAC;IACf,KAAK,EAAE,GAAG,CAAC;CACZ;AACD,yCAAyC;AACzC,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC;IACb,MAAM,EAAE,GAAG,CAAC;CACb;AAED,wBAAgB,wBAAwB,IAAI,gBAAgB,CAG3D;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,SAA2B,GAAG,gBAAgB,CAGnG;AAED,uGAAuG;AACvG,wBAAgB,WAAW,CAAC,OAAO,EAAE,GAAG,GAAG,KAAK,CAE/C;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG;IAAE,OAAO,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAO5G;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,qBAAqB,GAAG,aAAa,CAGjG;AAED,kCAAkC;AAClC,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,EAAE,qBAAqB,EAAE,GAAG,aAAa,EAAE,CAEvG;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,eAAe,EAAE,GAAG,GAAG,OAAO,CAU1G;AASD,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAUvG;AAED,MAAM,WAAW,QAAQ;IACvB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C;AAED,wBAAgB,oBAAoB,IAAI,QAAQ,GAAG;IAAE,IAAI,IAAI,MAAM,CAAA;CAAE,CAapE;AAED,uFAAuF;AACvF,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAMvI"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * spec 272 §10 tier A3 — blind bearer vouchers (Privacy Pass / RFC 9497 VOPRF).
3
+ *
4
+ * Pay once → receive a pack of UNLINKABLE one-use vouchers → redeem each from a
5
+ * separate context with no link back to the purchase. Built on ristretto255 VOPRF
6
+ * (`@noble/curves`) — the issuer never sees the unblinded token at issuance, so the
7
+ * blinded request it signs and the token presented at redemption cannot be linked.
8
+ *
9
+ * Flow:
10
+ * client blindVoucherRequest() → { request(blinded), secret(tokenId,blind,blinded) }
11
+ * issuer issueVoucher(key, request) → { evaluated, proof } (VOPRF blind-evaluate)
12
+ * client unblindVoucher(secret, issued, pubKey) → Voucher (verifies the issuer proof)
13
+ * issuer redeemVoucher(key, voucher, spentSet) → ok | reason (VOPRF verify + double-spend)
14
+ *
15
+ * One-use is enforced by the spent-set keyed on `voucherId = keccak(tokenId)`.
16
+ */
17
+ import { ristretto255_oprf } from '@noble/curves/ed25519.js';
18
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
19
+ import { keccak256 } from 'viem';
20
+ const voprf = ristretto255_oprf.voprf;
21
+ const enc = new TextEncoder();
22
+ // @noble bytesToHex returns NO `0x` prefix; add it so values are real `Hex` and `.slice(2)` strips it.
23
+ const hx = (b) => ('0x' + bytesToHex(b));
24
+ export function generateVoucherIssuerKey() {
25
+ const kp = voprf.generateKeyPair();
26
+ return { secretKey: hx(kp.secretKey), publicKey: hx(kp.publicKey) };
27
+ }
28
+ /** Deterministic issuer key from a 32-byte seed (stable across processes). */
29
+ export function deriveVoucherIssuerKey(seed, info = 'agentic-voucher-issuer') {
30
+ const kp = voprf.deriveKeyPair(hexToBytes(seed.slice(2)), enc.encode(info));
31
+ return { secretKey: hx(kp.secretKey), publicKey: hx(kp.publicKey) };
32
+ }
33
+ /** `voucherId` — the public, spent-set key derived from the token (no link to the blinded request). */
34
+ export function voucherIdOf(tokenId) {
35
+ return keccak256(tokenId);
36
+ }
37
+ /** Client: blind a fresh random token (or a supplied one) for issuance. */
38
+ export function blindVoucherRequest(tokenId) {
39
+ const tok = tokenId ? hexToBytes(tokenId.slice(2)) : globalThis.crypto.getRandomValues(new Uint8Array(32));
40
+ const { blind, blinded } = voprf.blind(tok);
41
+ return {
42
+ request: { blinded: hx(blinded) },
43
+ secret: { tokenId: hx(tok), blind: hx(blind), blinded: hx(blinded) },
44
+ };
45
+ }
46
+ /** Issuer: blind-evaluate one request (VOPRF — issuer learns nothing about the token). */
47
+ export function issueVoucher(key, request) {
48
+ const be = voprf.blindEvaluate(hexToBytes(key.secretKey.slice(2)), hexToBytes(key.publicKey.slice(2)), hexToBytes(request.blinded.slice(2)));
49
+ return { evaluated: hx(be.evaluated), proof: hx(be.proof) };
50
+ }
51
+ /** Issuer: issue a whole pack. */
52
+ export function issueVouchers(key, requests) {
53
+ return requests.map((r) => issueVoucher(key, r));
54
+ }
55
+ /**
56
+ * Client: unblind into a finished voucher. Throws if the issuer's VOPRF proof fails
57
+ * (a malicious issuer using the wrong key is detected here — verifiability).
58
+ */
59
+ export function unblindVoucher(secret, issued, issuerPublicKey) {
60
+ const output = voprf.finalize(hexToBytes(secret.tokenId.slice(2)), hexToBytes(secret.blind.slice(2)), hexToBytes(issued.evaluated.slice(2)), hexToBytes(secret.blinded.slice(2)), hexToBytes(issuerPublicKey.slice(2)), hexToBytes(issued.proof.slice(2)));
61
+ return { voucherId: voucherIdOf(secret.tokenId), tokenId: secret.tokenId, output: hx(output) };
62
+ }
63
+ function ctEqual(a, b) {
64
+ if (a.length !== b.length)
65
+ return false;
66
+ let d = 0;
67
+ for (let i = 0; i < a.length; i++)
68
+ d |= a[i] ^ b[i];
69
+ return d === 0;
70
+ }
71
+ /** Issuer: VOPRF validity of a presented voucher (does NOT check double-spend). Fail-closed. */
72
+ export function verifyVoucher(key, voucher) {
73
+ try {
74
+ if (voucher.voucherId.toLowerCase() !== voucherIdOf(voucher.tokenId).toLowerCase()) {
75
+ return { ok: false, reason: 'voucherId does not match tokenId' };
76
+ }
77
+ const expected = voprf.evaluate(hexToBytes(key.secretKey.slice(2)), hexToBytes(voucher.tokenId.slice(2)));
78
+ return ctEqual(expected, hexToBytes(voucher.output.slice(2))) ? { ok: true } : { ok: false, reason: 'invalid voucher' };
79
+ }
80
+ catch {
81
+ return { ok: false, reason: 'malformed voucher' };
82
+ }
83
+ }
84
+ export function createMemorySpentSet() {
85
+ const set = new Set();
86
+ return {
87
+ // In-process JS is single-threaded, so test-and-set here is naturally atomic; the interface demands
88
+ // the same guarantee of every (distributed) implementer.
89
+ claim: (id) => {
90
+ const k = id.toLowerCase();
91
+ if (set.has(k))
92
+ return false;
93
+ set.add(k);
94
+ return true;
95
+ },
96
+ size: () => set.size,
97
+ };
98
+ }
99
+ /** Issuer: verify + one-shot redeem. Rejects an invalid voucher and a replayed one. */
100
+ export async function redeemVoucher(key, voucher, spent) {
101
+ const v = verifyVoucher(key, voucher);
102
+ if (!v.ok)
103
+ return v;
104
+ // ARCH-H3: atomic claim — no check-then-add window. Two concurrent redemptions can't both win.
105
+ if (!(await spent.claim(voucher.voucherId)))
106
+ return { ok: false, reason: 'voucher already spent' };
107
+ return { ok: true };
108
+ }
109
+ //# sourceMappingURL=voucher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voucher.js","sourceRoot":"","sources":["../../src/entitlement/voucher.ts"],"names":[],"mappings":"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"}