@defdone/annon 0.0.0-stage → 0.1.0-alpha.0

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 (214) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/CLI.md +378 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +7 -0
  5. package/README.md +581 -2
  6. package/dist/api/annon-types.d.ts +108 -0
  7. package/dist/api/annon-types.js +2 -0
  8. package/dist/api/annon.d.ts +117 -0
  9. package/dist/api/annon.js +1152 -0
  10. package/dist/api/hosted-record.d.ts +13 -0
  11. package/dist/api/hosted-record.js +149 -0
  12. package/dist/api/sharing-types.d.ts +109 -0
  13. package/dist/api/sharing-types.js +1 -0
  14. package/dist/api/sharing.d.ts +36 -0
  15. package/dist/api/sharing.js +879 -0
  16. package/dist/browser/download.d.ts +19 -0
  17. package/dist/browser/download.js +52 -0
  18. package/dist/browser/index.d.ts +23 -0
  19. package/dist/browser/index.js +22 -0
  20. package/dist/browser/login-bundle.js +116 -0
  21. package/dist/browser/login.d.ts +1 -0
  22. package/dist/browser/login.js +177 -0
  23. package/dist/browser/purchase-bundle.js +84 -0
  24. package/dist/browser/purchases.d.ts +98 -0
  25. package/dist/browser/purchases.js +735 -0
  26. package/dist/browser/record.d.ts +55 -0
  27. package/dist/browser/record.js +16 -0
  28. package/dist/browser/selling.d.ts +146 -0
  29. package/dist/browser/selling.js +418 -0
  30. package/dist/browser/wallets.d.ts +53 -0
  31. package/dist/browser/wallets.js +286 -0
  32. package/dist/bytes.d.ts +4 -0
  33. package/dist/bytes.js +16 -0
  34. package/dist/download/download.d.ts +59 -0
  35. package/dist/download/download.js +384 -0
  36. package/dist/download/host.d.ts +56 -0
  37. package/dist/download/host.js +161 -0
  38. package/dist/download/http.d.ts +23 -0
  39. package/dist/download/http.js +87 -0
  40. package/dist/download/proof.d.ts +20 -0
  41. package/dist/download/proof.js +79 -0
  42. package/dist/download/upload.d.ts +130 -0
  43. package/dist/download/upload.js +408 -0
  44. package/dist/envelope/evm.d.ts +4 -0
  45. package/dist/envelope/evm.js +32 -0
  46. package/dist/envelope/pay2.d.ts +176 -0
  47. package/dist/envelope/pay2.js +541 -0
  48. package/dist/envelope/replay.d.ts +45 -0
  49. package/dist/envelope/replay.js +186 -0
  50. package/dist/envelope/stream.d.ts +78 -0
  51. package/dist/envelope/stream.js +372 -0
  52. package/dist/errors.d.ts +21 -0
  53. package/dist/errors.js +300 -0
  54. package/dist/index.d.ts +25 -0
  55. package/dist/index.js +23 -0
  56. package/dist/keys/near-ckd.d.ts +47 -0
  57. package/dist/keys/near-ckd.js +96 -0
  58. package/dist/limits.d.ts +4 -0
  59. package/dist/limits.js +4 -0
  60. package/dist/near/nep413.d.ts +22 -0
  61. package/dist/near/nep413.js +39 -0
  62. package/dist/near/rpc.d.ts +170 -0
  63. package/dist/near/rpc.js +423 -0
  64. package/dist/near/texts.d.ts +39 -0
  65. package/dist/near/texts.js +60 -0
  66. package/dist/net/ip.d.ts +7 -0
  67. package/dist/net/ip.js +62 -0
  68. package/dist/net/retry.d.ts +3 -0
  69. package/dist/net/retry.js +31 -0
  70. package/dist/node/adapter-checks.d.ts +6 -0
  71. package/dist/node/adapter-checks.js +13 -0
  72. package/dist/node/adapter.d.ts +8 -0
  73. package/dist/node/adapter.js +99 -0
  74. package/dist/node/agent.d.ts +49 -0
  75. package/dist/node/agent.js +266 -0
  76. package/dist/node/amounts.d.ts +2 -0
  77. package/dist/node/amounts.js +20 -0
  78. package/dist/node/browser-login.d.ts +18 -0
  79. package/dist/node/browser-login.js +212 -0
  80. package/dist/node/budget.d.ts +24 -0
  81. package/dist/node/budget.js +59 -0
  82. package/dist/node/buy-commands.d.ts +24 -0
  83. package/dist/node/buy-commands.js +301 -0
  84. package/dist/node/cli.d.ts +60 -0
  85. package/dist/node/cli.js +201 -0
  86. package/dist/node/command-context.d.ts +82 -0
  87. package/dist/node/command-context.js +77 -0
  88. package/dist/node/config.d.ts +37 -0
  89. package/dist/node/config.js +189 -0
  90. package/dist/node/core-adapter.d.ts +43 -0
  91. package/dist/node/core-adapter.js +122 -0
  92. package/dist/node/errors.d.ts +8 -0
  93. package/dist/node/errors.js +12 -0
  94. package/dist/node/index.d.ts +7 -0
  95. package/dist/node/index.js +5 -0
  96. package/dist/node/io.d.ts +13 -0
  97. package/dist/node/io.js +112 -0
  98. package/dist/node/keychain.d.ts +10 -0
  99. package/dist/node/keychain.js +67 -0
  100. package/dist/node/main.d.ts +2 -0
  101. package/dist/node/main.js +21 -0
  102. package/dist/node/offers.d.ts +16 -0
  103. package/dist/node/offers.js +91 -0
  104. package/dist/node/origin.d.ts +11 -0
  105. package/dist/node/origin.js +80 -0
  106. package/dist/node/paths.d.ts +3 -0
  107. package/dist/node/paths.js +71 -0
  108. package/dist/node/payment-terms.d.ts +26 -0
  109. package/dist/node/payment-terms.js +127 -0
  110. package/dist/node/purchase-commands.d.ts +16 -0
  111. package/dist/node/purchase-commands.js +237 -0
  112. package/dist/node/purchase-guard.d.ts +19 -0
  113. package/dist/node/purchase-guard.js +53 -0
  114. package/dist/node/relay-transport.d.ts +6 -0
  115. package/dist/node/relay-transport.js +173 -0
  116. package/dist/node/resources.d.ts +2 -0
  117. package/dist/node/resources.js +26 -0
  118. package/dist/node/retry-gate.d.ts +15 -0
  119. package/dist/node/retry-gate.js +53 -0
  120. package/dist/node/seller-commands.d.ts +12 -0
  121. package/dist/node/seller-commands.js +207 -0
  122. package/dist/node/sharing.d.ts +3 -0
  123. package/dist/node/sharing.js +28 -0
  124. package/dist/node/shops.d.ts +17 -0
  125. package/dist/node/shops.js +74 -0
  126. package/dist/node/signer-mock.d.ts +11 -0
  127. package/dist/node/signer-mock.js +79 -0
  128. package/dist/node/signer-wallet.d.ts +8 -0
  129. package/dist/node/signer-wallet.js +147 -0
  130. package/dist/node/signing.d.ts +29 -0
  131. package/dist/node/signing.js +200 -0
  132. package/dist/node/storage-commands.d.ts +11 -0
  133. package/dist/node/storage-commands.js +50 -0
  134. package/dist/node/sync-dir.d.ts +1 -0
  135. package/dist/node/sync-dir.js +11 -0
  136. package/dist/node/wallets.d.ts +13 -0
  137. package/dist/node/wallets.js +146 -0
  138. package/dist/oneclick/oneclick.d.ts +132 -0
  139. package/dist/oneclick/oneclick.js +342 -0
  140. package/dist/pay/backup.d.ts +70 -0
  141. package/dist/pay/backup.js +402 -0
  142. package/dist/pay/base.d.ts +135 -0
  143. package/dist/pay/base.js +393 -0
  144. package/dist/pay/eip3009.d.ts +59 -0
  145. package/dist/pay/eip3009.js +72 -0
  146. package/dist/pay/pay.d.ts +86 -0
  147. package/dist/pay/pay.js +297 -0
  148. package/dist/pay/purchase.d.ts +103 -0
  149. package/dist/pay/purchase.js +20 -0
  150. package/dist/pay/recover.d.ts +31 -0
  151. package/dist/pay/recover.js +34 -0
  152. package/dist/pay/seller.d.ts +130 -0
  153. package/dist/pay/seller.js +444 -0
  154. package/dist/pay/settle.d.ts +96 -0
  155. package/dist/pay/settle.js +308 -0
  156. package/dist/pay/split.d.ts +115 -0
  157. package/dist/pay/split.js +256 -0
  158. package/dist/pay/store-node.d.ts +8 -0
  159. package/dist/pay/store-node.js +224 -0
  160. package/dist/pay/store.d.ts +54 -0
  161. package/dist/pay/store.js +135 -0
  162. package/dist/pay/x402.d.ts +95 -0
  163. package/dist/pay/x402.js +136 -0
  164. package/dist/relay/admission.d.ts +8 -0
  165. package/dist/relay/admission.js +58 -0
  166. package/dist/relay/capabilities.d.ts +36 -0
  167. package/dist/relay/capabilities.js +232 -0
  168. package/dist/relay/fees.d.ts +17 -0
  169. package/dist/relay/fees.js +105 -0
  170. package/dist/relay/index.d.ts +10 -0
  171. package/dist/relay/index.js +8 -0
  172. package/dist/relay/keeper-balance.d.ts +17 -0
  173. package/dist/relay/keeper-balance.js +46 -0
  174. package/dist/relay/keeper-runner.d.ts +114 -0
  175. package/dist/relay/keeper-runner.js +703 -0
  176. package/dist/relay/keeper.d.ts +38 -0
  177. package/dist/relay/keeper.js +73 -0
  178. package/dist/relay/ledger.d.ts +155 -0
  179. package/dist/relay/ledger.js +392 -0
  180. package/dist/relay/relay-checks.d.ts +3 -0
  181. package/dist/relay/relay-checks.js +244 -0
  182. package/dist/relay/relay-reconciliation.d.ts +15 -0
  183. package/dist/relay/relay-reconciliation.js +293 -0
  184. package/dist/relay/relay-request.d.ts +6 -0
  185. package/dist/relay/relay-request.js +139 -0
  186. package/dist/relay/relay-transactions.d.ts +23 -0
  187. package/dist/relay/relay-transactions.js +182 -0
  188. package/dist/relay/relay-types.d.ts +128 -0
  189. package/dist/relay/relay-types.js +17 -0
  190. package/dist/relay/relay.d.ts +102 -0
  191. package/dist/relay/relay.js +426 -0
  192. package/dist/relay/reservation.d.ts +29 -0
  193. package/dist/relay/reservation.js +132 -0
  194. package/dist/relay/sponsorship.d.ts +42 -0
  195. package/dist/relay/sponsorship.js +184 -0
  196. package/dist/routes.d.ts +2 -0
  197. package/dist/routes.js +1 -0
  198. package/dist/settle-near/codec.d.ts +17 -0
  199. package/dist/settle-near/codec.js +86 -0
  200. package/dist/settle-near/identity.d.ts +27 -0
  201. package/dist/settle-near/identity.js +80 -0
  202. package/dist/settle-near/purchase.d.ts +40 -0
  203. package/dist/settle-near/purchase.js +153 -0
  204. package/dist/settle-near/terms.d.ts +37 -0
  205. package/dist/settle-near/terms.js +89 -0
  206. package/dist/shops/discovery.d.ts +95 -0
  207. package/dist/shops/discovery.js +384 -0
  208. package/dist/wallet/account-only.d.ts +11 -0
  209. package/dist/wallet/account-only.js +6 -0
  210. package/dist/wallet/uncertain.d.ts +8 -0
  211. package/dist/wallet/uncertain.js +13 -0
  212. package/dist/wallet/wallet.d.ts +100 -0
  213. package/dist/wallet/wallet.js +204 -0
  214. package/package.json +135 -3
@@ -0,0 +1,444 @@
1
+ /**
2
+ * The seller's side of `specs/x402-annon.md`: the 402 for an Annon file, built from its
3
+ * registration, and the check a server runs on `PAYMENT-SIGNATURE` before it settles. Settlement
4
+ * itself stays with the server's facilitator: any standard x402 one, through `forFacilitator`.
5
+ */
6
+ import { secureUrl } from '../download/download.js';
7
+ import { getConfig, getFile } from '../near/rpc.js';
8
+ import { baseNonceV2, parsePurchaseMessage, purchaseId, } from '../settle-near/purchase.js';
9
+ import { fail, hasCode, head } from './base.js';
10
+ import { digest, signer } from './eip3009.js';
11
+ import { MAX_CLOCK_SKEW_SECONDS, MAX_VALIDITY_SECONDS } from './pay.js';
12
+ import { ANNON_EXACT, ANNON_NEAR_USDC, BASE_NETWORK, BAZAAR, BUILDER_CODE, BUILDER_CODE_PATTERN, codes, isPaymentId, MAX_ECHOED_SERVICE_CODES, PAYMENT_IDENTIFIER, paymentIdentifierDeclaration, } from './x402.js';
13
+ /**
14
+ * An icon URL bazaar keeps (its `isValidIconUrl`), https only: at most 2048 characters, no
15
+ * credentials, and a host name, not an IP address or loopback.
16
+ */
17
+ function iconUrl(v) {
18
+ if (v.length > 2048 || /\p{Cc}/u.test(v) || !URL.canParse(v))
19
+ return false;
20
+ const u = new URL(v);
21
+ const h = u.hostname.toLowerCase();
22
+ return (u.protocol === 'https:' &&
23
+ !u.username &&
24
+ !u.password &&
25
+ !h.startsWith('[') &&
26
+ !['localhost', 'localhost.localdomain', 'ip6-localhost', 'ip6-loopback'].includes(h) &&
27
+ // WHATWG parses an all-digit or 0x host as IPv4, so this covers bazaar's digit and hex rules.
28
+ !/^\d{1,3}(\.\d{1,3}){3}$/.test(h));
29
+ }
30
+ /** x402's `declareBuilderCodeExtension(appCode)`. */
31
+ const builderCodeDeclaration = (a) => {
32
+ if (!BUILDER_CODE_PATTERN.test(a))
33
+ throw new TypeError('appCode must match ^[a-z0-9_]{1,32}$');
34
+ const code = { type: 'string', pattern: '^[a-z0-9_]{1,32}$' };
35
+ return {
36
+ info: { a },
37
+ schema: {
38
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
39
+ type: 'object',
40
+ properties: {
41
+ a: { ...code, description: 'App builder code' },
42
+ w: { ...code, description: 'Wallet builder code' },
43
+ s: { type: 'array', maxItems: 11, items: code, description: 'Service builder codes' },
44
+ },
45
+ additionalProperties: false,
46
+ },
47
+ };
48
+ };
49
+ /** x402's `declareDiscoveryExtension` for a file: a GET that returns PAY2 bytes. */
50
+ const discoveryDeclaration = () => ({
51
+ info: {
52
+ input: { type: 'http', method: 'GET' },
53
+ output: { type: 'binary', format: 'application/vnd.annon.pay2' },
54
+ },
55
+ schema: {
56
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
57
+ type: 'object',
58
+ properties: {
59
+ input: {
60
+ type: 'object',
61
+ properties: {
62
+ type: { type: 'string', const: 'http' },
63
+ method: { type: 'string', enum: ['GET', 'HEAD', 'DELETE'] },
64
+ },
65
+ required: ['type', 'method'],
66
+ additionalProperties: false,
67
+ },
68
+ output: {
69
+ type: 'object',
70
+ properties: { type: { type: 'string' }, format: { type: 'string' } },
71
+ required: ['type'],
72
+ },
73
+ },
74
+ required: ['input'],
75
+ },
76
+ });
77
+ /**
78
+ * The `PaymentRequired` for a registered file, every value from the contract. Throws `PayError`
79
+ * `sales_stopped` or `taken_down` for a file that must not be offered (answer 410), and
80
+ * `unknown_file` for one that isn't registered.
81
+ */
82
+ async function offerFor(o) {
83
+ const file = await getFile(o.nearRpc, o.contractId, o.fileId);
84
+ if (!file)
85
+ return fail('unknown_file', 'the file is not registered on the Annon contract');
86
+ if (file.takenDown)
87
+ fail('taken_down', `the file was taken down (${file.takenDown})`);
88
+ if (file.salesStoppedAt !== null)
89
+ fail('sales_stopped', 'the seller stopped selling this file');
90
+ const config = await getConfig(o.nearRpc, o.contractId);
91
+ if (file.terms.network !== config.network ||
92
+ file.terms.near.token !== config.nearUsdc ||
93
+ (file.terms.base && file.terms.base.usdc !== config.usdc))
94
+ fail('network_mismatch');
95
+ return paymentRequired(o, file, config.usdc);
96
+ }
97
+ /**
98
+ * The `PaymentRequired` for a registration already read: the offer while sales are open, none once
99
+ * they are stopped: buyers still prove their purchase grant to download.
100
+ */
101
+ export function paymentRequired(o, file, usdc) {
102
+ // An Annon client won't sign without the header, and the file itself goes over https only
103
+ // (specs/download.md).
104
+ if (typeof o.header !== 'string' || !URL.canParse(o.header)) {
105
+ throw new TypeError('header must be an absolute URL');
106
+ }
107
+ if (typeof o.url !== 'string')
108
+ throw new TypeError('url must be an https URL');
109
+ secureUrl(o.url);
110
+ const offer = {
111
+ scheme: ANNON_EXACT,
112
+ network: BASE_NETWORK,
113
+ amount: file.price.toString(),
114
+ asset: usdc,
115
+ payTo: file.payTo,
116
+ maxTimeoutSeconds: o.maxTimeoutSeconds ?? 300,
117
+ // Extension v3: settled before the file is served (x402 v2 section 6.1).
118
+ extra: { name: 'USD Coin', version: '2', paymentFlow: 'upfront', nonceVersion: 2 },
119
+ };
120
+ const info = {
121
+ version: 3,
122
+ network: 'near:mainnet',
123
+ contract: o.contractId,
124
+ fileId: o.fileId,
125
+ header: o.header,
126
+ ...(o.public ? { public: true } : {}),
127
+ };
128
+ const d = o.discovery;
129
+ // Bazaar's service metadata rules: a name and up to five distinct tags (case aside) of 1 to 32
130
+ // printable ASCII, and an icon an https URL a catalog would keep. Refused, not trimmed.
131
+ const label = (v) => typeof v === 'string' && /^[\x20-\x7e]{1,32}$/.test(v);
132
+ const tags = d?.tags;
133
+ if ((d?.serviceName !== undefined && !label(d.serviceName)) ||
134
+ (tags !== undefined &&
135
+ !(tags.length <= 5 &&
136
+ tags.every(label) &&
137
+ new Set(tags.map((t) => t.toLowerCase())).size === tags.length)) ||
138
+ (d?.iconUrl !== undefined && !iconUrl(d.iconUrl))) {
139
+ throw new TypeError('discovery: serviceName and up to 5 distinct tags of 1-32 printable ASCII, iconUrl a public https URL');
140
+ }
141
+ return {
142
+ x402Version: 2,
143
+ resource: {
144
+ url: o.url,
145
+ ...(d?.description ? { description: d.description } : {}),
146
+ ...(d?.mimeType ? { mimeType: d.mimeType } : {}),
147
+ ...(d?.serviceName ? { serviceName: d.serviceName } : {}),
148
+ ...(d?.tags ? { tags: d.tags } : {}),
149
+ ...(d?.iconUrl ? { iconUrl: d.iconUrl } : {}),
150
+ },
151
+ accepts: file.salesStoppedAt === null
152
+ ? [
153
+ {
154
+ scheme: ANNON_NEAR_USDC,
155
+ network: file.terms.network,
156
+ amount: file.terms.near.price,
157
+ asset: file.terms.near.token,
158
+ payTo: o.contractId,
159
+ maxTimeoutSeconds: 3600,
160
+ extra: {
161
+ contract: o.contractId,
162
+ file_id: o.fileId,
163
+ terms_hash: file.termsHash,
164
+ },
165
+ },
166
+ ...(file.terms.base ? [offer] : []),
167
+ ]
168
+ : [],
169
+ extensions: {
170
+ annon: { info, schema: ANNON_SCHEMA },
171
+ [PAYMENT_IDENTIFIER]: paymentIdentifierDeclaration(),
172
+ ...(o.appCode ? { [BUILDER_CODE]: builderCodeDeclaration(o.appCode) } : {}),
173
+ ...(d ? { [BAZAAR]: discoveryDeclaration() } : {}),
174
+ },
175
+ };
176
+ }
177
+ /** JSON Schema of `extensions.annon.info` (the buyer adds `buyerKey`). */
178
+ const ANNON_SCHEMA = {
179
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
180
+ type: 'object',
181
+ required: ['version', 'network', 'contract', 'fileId'],
182
+ properties: {
183
+ version: { const: 3 },
184
+ network: { const: 'near:mainnet' },
185
+ contract: { type: 'string' },
186
+ fileId: { type: 'string', pattern: '^0x[0-9a-f]{64}$' },
187
+ header: { type: 'string' },
188
+ public: { type: 'boolean' },
189
+ buyerKey: { type: 'string', pattern: '^0x[0-9a-f]{96}$' },
190
+ },
191
+ };
192
+ const b64json = (s) => {
193
+ try {
194
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(atob(s.trim()), (c) => c.charCodeAt(0))));
195
+ }
196
+ catch {
197
+ return null;
198
+ }
199
+ };
200
+ /** Structural equality of JSON values: key order doesn't matter, as in any JSON parser. */
201
+ function same(a, b) {
202
+ if (a === b)
203
+ return true;
204
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
205
+ return false;
206
+ if (Array.isArray(a) !== Array.isArray(b))
207
+ return false;
208
+ const ka = Object.keys(a).filter((k) => a[k] !== undefined);
209
+ const kb = Object.keys(b).filter((k) => b[k] !== undefined);
210
+ return (ka.length === kb.length &&
211
+ ka.every((k) => same(a[k], b[k])));
212
+ }
213
+ /**
214
+ * What a server runs before it settles: the registration read again now (a file stopped or taken
215
+ * down since the 402 is refused), then `checkPayment` against the offer it gives, with "now" taken
216
+ * from the earlier of this server's clock and Base's (USDC compares `validBefore` with Base time).
217
+ * New settlements require sales to be open and a short outstanding authorization window.
218
+ */
219
+ export async function verifyPayment(header, o) {
220
+ // Time first, registration second: "now" must not be later than the moment the file was seen on
221
+ // sale, or a slow read would stretch the window.
222
+ const local = Math.floor(Date.now() / 1000);
223
+ const block = await head(o.baseRpc);
224
+ const base = block.timestamp;
225
+ if (Math.abs(base - local) > MAX_CLOCK_SKEW_SECONDS) {
226
+ fail('transport', 'this server and Base disagree on the time; refusing to settle');
227
+ }
228
+ const pr = await offerFor(o);
229
+ const checked = checkPayment(header, pr, Math.min(base, local));
230
+ if (checked.validBefore < BigInt(base + (o.paymentMinValiditySeconds ?? 60)))
231
+ fail('payment_expired', 'authorization expires too soon');
232
+ if (await hasCode(o.baseRpc, checked.payer, block.number))
233
+ fail('bad_signature', 'payer must be an EOA');
234
+ return checked;
235
+ }
236
+ /** The base64 JSON of `PAYMENT-SIGNATURE`, as an x402 v2 payload. */
237
+ export function readPayload(header) {
238
+ const p = b64json(header);
239
+ if (p?.x402Version !== 2)
240
+ return fail('bad_requirements', 'not an x402 v2 payment');
241
+ return p;
242
+ }
243
+ /** The payload's `payment-identifier` id, checked when it is there. */
244
+ function paymentIdOf(p) {
245
+ const ext = p.extensions?.[PAYMENT_IDENTIFIER];
246
+ if (ext === undefined)
247
+ return undefined;
248
+ // Optional: an echo without an id is a payment without one.
249
+ const id = ext?.info?.id;
250
+ if (id === undefined)
251
+ return undefined;
252
+ if (!isPaymentId(id))
253
+ fail('bad_requirements', 'the payment id is not a valid payment-identifier');
254
+ return id;
255
+ }
256
+ /**
257
+ * What never changes for a purchase (x402-annon.md section 10): the authorization signed by `from`, for
258
+ * the Annon nonce of this file and the buyer key, paying `offer` exactly. Shared by a new payment
259
+ * (`checkPayment`) and by a retry of one already settling (`settle`).
260
+ */
261
+ function checkSigned(p, contract, fileId, offer) {
262
+ const info = p.extensions?.annon?.info;
263
+ let purchase;
264
+ let id;
265
+ try {
266
+ purchase = parsePurchaseMessage(info?.purchase, { baseUsdc: offer.asset });
267
+ id = purchaseId(purchase, { baseUsdc: offer.asset });
268
+ }
269
+ catch {
270
+ return fail('bad_requirements', 'invalid purchase commitment');
271
+ }
272
+ if (info?.version !== 3 ||
273
+ info.purchase_id !== id ||
274
+ purchase.route !== 'base-usdc' ||
275
+ purchase.contract !== contract ||
276
+ purchase.file_id !== fileId ||
277
+ purchase.asset !== offer.asset ||
278
+ purchase.price !== offer.amount)
279
+ return fail('bad_requirements', 'purchase does not match offer');
280
+ const buyerKey = purchase.buyer_key;
281
+ const nonce = baseNonceV2(id);
282
+ const a = p.payload?.authorization ?? {};
283
+ const str = (k) => (typeof a[k] === 'string' ? a[k] : '');
284
+ if (!/^0x[0-9a-fA-F]{40}$/.test(str('from')) || !/^\d{1,78}$/.test(str('validBefore'))) {
285
+ fail('bad_requirements', 'malformed authorization');
286
+ }
287
+ if (str('nonce').toLowerCase() !== nonce) {
288
+ fail('bad_requirements', 'the nonce is not the Annon nonce of this file and buyer key');
289
+ }
290
+ if (str('to').toLowerCase() !== offer.payTo.toLowerCase() || str('value') !== offer.amount) {
291
+ fail('bad_requirements', 'the authorization pays another amount or recipient');
292
+ }
293
+ if (str('validAfter') !== '0')
294
+ fail('bad_requirements', 'validAfter must be 0');
295
+ const from = str('from').toLowerCase();
296
+ const validBefore = BigInt(str('validBefore'));
297
+ if (validBefore > BigInt(purchase.expires_at))
298
+ fail('bad_requirements', 'authorization exceeds purchase expiry');
299
+ const auth = {
300
+ from,
301
+ to: offer.payTo.toLowerCase(),
302
+ value: BigInt(offer.amount),
303
+ validAfter: 0n,
304
+ validBefore,
305
+ nonce,
306
+ };
307
+ const sig = p.payload?.signature;
308
+ if (typeof sig !== 'string' || signer(digest(offer.asset.toLowerCase(), auth), sig) !== from) {
309
+ fail('bad_signature', "the signature is not the payer's, or not in the form USDC takes");
310
+ }
311
+ return { payer: from, buyerKey, nonce, validBefore, offer, purchase, purchaseId: id };
312
+ }
313
+ /**
314
+ * The purchase terms of a payload against the registration and the contract's configuration,
315
+ * without the seller's settings, the sale status or the deadline (x402-annon.md section 10, order of
316
+ * checks): what a retry of a payment already settling must still pass.
317
+ */
318
+ export function checkTerms(header, t) {
319
+ const p = readPayload(header);
320
+ const o = p.accepted;
321
+ const extra = (o?.extra ?? {});
322
+ if (o?.scheme !== ANNON_EXACT ||
323
+ o.network !== BASE_NETWORK ||
324
+ typeof o.asset !== 'string' ||
325
+ o.asset.toLowerCase() !== t.usdc ||
326
+ o.amount !== t.price.toString() ||
327
+ typeof o.payTo !== 'string' ||
328
+ o.payTo.toLowerCase() !== t.payTo ||
329
+ extra.name !== 'USD Coin' ||
330
+ extra.version !== '2' ||
331
+ extra.nonceVersion !== 2) {
332
+ fail('bad_requirements', 'the payment is not for this file on its registered terms');
333
+ }
334
+ const info = p.extensions?.annon?.info ?? {};
335
+ if (info.contract !== t.contractId || info.fileId !== t.fileId) {
336
+ fail('bad_requirements', 'the payment is for another Annon contract or file');
337
+ }
338
+ const checked = checkSigned(p, t.contractId, t.fileId, o);
339
+ if (checked.purchase.terms_hash !== t.termsHash)
340
+ fail('bad_requirements', 'wrong terms hash');
341
+ const paymentId = paymentIdOf(p);
342
+ return {
343
+ ...checked,
344
+ ...(paymentId ? { paymentId } : {}),
345
+ };
346
+ }
347
+ /**
348
+ * @internal Use `verifyPayment`: this checks against a `PaymentRequired` that may be stale.
349
+ * Checks `PAYMENT-SIGNATURE` against the `PaymentRequired` this server sent (x402-annon.md section 3):
350
+ * the offer echoed unchanged, the `annon` extension echoed with a valid buyer key, an advertised
351
+ * `payment-identifier` echoed with a valid id, and the nonce the one that key gives. Run it before
352
+ * settling: a payment with any other nonce would move the buyer's money and never release the key.
353
+ * Throws `PayError` `bad_signature` or `bad_requirements`.
354
+ */
355
+ export function checkPayment(header, pr, now = Math.floor(Date.now() / 1000)) {
356
+ const p = readPayload(header);
357
+ const offer = pr.accepts.find((o) => same(o, p.accepted));
358
+ if (!offer)
359
+ return fail('bad_requirements', 'the payment is for an offer this server did not make');
360
+ const sent = p.extensions?.annon?.info ?? {};
361
+ const advertised = pr.extensions?.annon?.info ?? {};
362
+ for (const [k, v] of Object.entries(advertised)) {
363
+ if (!same(sent[k], v))
364
+ fail('bad_requirements', `the annon extension changed ${k}`);
365
+ }
366
+ // payment-identifier: optional; when sent, the advertised object with only `info.id` added.
367
+ const declared = pr.extensions?.[PAYMENT_IDENTIFIER];
368
+ const echoed = p.extensions?.[PAYMENT_IDENTIFIER];
369
+ if (echoed !== undefined) {
370
+ const { id: _, ...info } = echoed?.info ?? {};
371
+ if (!declared || !same({ ...echoed, info }, declared)) {
372
+ fail('bad_requirements', 'the payment-identifier extension is not the one advertised');
373
+ }
374
+ }
375
+ const paymentId = paymentIdOf(p);
376
+ if (declared?.info?.required === true && !paymentId) {
377
+ fail('bad_requirements', 'this server requires a payment id');
378
+ }
379
+ checkBuilderCode(pr, p);
380
+ if (offer.scheme !== ANNON_EXACT || offer.extra?.nonceVersion !== 2)
381
+ fail('bad_requirements');
382
+ const c = checkSigned(p, String(advertised.contract), String(advertised.fileId), offer);
383
+ const near = pr.accepts.find((o) => o.scheme === ANNON_NEAR_USDC);
384
+ if (c.purchase.terms_hash !== near?.extra?.terms_hash ||
385
+ BigInt(c.purchase.expires_at) < c.validBefore ||
386
+ BigInt(c.purchase.expires_at) <= BigInt(now))
387
+ fail('bad_requirements', 'wrong or expired purchase terms');
388
+ // Bound outstanding payment authorizations, regardless of which client signed them.
389
+ if (c.validBefore > BigInt(now + MAX_VALIDITY_SECONDS + MAX_CLOCK_SKEW_SECONDS)) {
390
+ fail('bad_requirements', 'the authorization is valid for too long');
391
+ }
392
+ return { ...c, ...(paymentId ? { paymentId } : {}) };
393
+ }
394
+ /** builder-code (x402-annon.md section 11): the declared app code and every declared service code echoed. */
395
+ function checkBuilderCode(pr, p) {
396
+ const declared = pr.extensions?.[BUILDER_CODE]?.info;
397
+ const sent = p.extensions?.[BUILDER_CODE];
398
+ const echo = () => fail('bad_requirements', 'extension_echo_mismatch: builder-code');
399
+ if (sent === undefined) {
400
+ if (pr.extensions?.[BUILDER_CODE] !== undefined)
401
+ echo();
402
+ return;
403
+ }
404
+ const info = (sent?.info ?? {});
405
+ if (info.a !== undefined && info.a !== declared?.a)
406
+ echo();
407
+ if (declared?.a !== undefined && info.a !== declared.a)
408
+ echo();
409
+ if (info.w !== undefined)
410
+ echo();
411
+ const s = codes(info.s);
412
+ const mine = codes(declared?.s);
413
+ if (s.length > MAX_ECHOED_SERVICE_CODES ||
414
+ !s.every((c) => typeof c === 'string' && BUILDER_CODE_PATTERN.test(c)) ||
415
+ !mine.every((c) => s.includes(c))) {
416
+ echo();
417
+ }
418
+ }
419
+ /**
420
+ * A checked `annon-exact` payment as a standard x402 facilitator's `/verify` and `/settle` take it:
421
+ * the same signed authorization, every extension kept, with the scheme named `exact` in both the
422
+ * payload and the requirements (the official facilitator checks both). Run `verifyPayment` first.
423
+ */
424
+ export function forFacilitator(header, offer) {
425
+ if (offer.scheme !== ANNON_EXACT) {
426
+ return fail('method_mismatch', 'only annon-exact payments settle through a standard facilitator');
427
+ }
428
+ const p = b64json(header);
429
+ if (!p)
430
+ return fail('bad_requirements', 'PAYMENT-SIGNATURE is not base64 JSON');
431
+ const paymentRequirements = { ...offer, scheme: 'exact' };
432
+ // bazaar stays out: a facilitator would catalog the offer renamed `exact`, which an ordinary x402
433
+ // client cannot pay (x402-annon.md section 11).
434
+ const { [BAZAAR]: _, ...extensions } = (p.extensions ?? {});
435
+ return {
436
+ x402Version: 2,
437
+ paymentPayload: {
438
+ ...p,
439
+ accepted: paymentRequirements,
440
+ ...(p.extensions ? { extensions } : {}),
441
+ },
442
+ paymentRequirements,
443
+ };
444
+ }
@@ -0,0 +1,96 @@
1
+ import { forFacilitator, type OfferOptions } from './seller.js';
2
+ import type { Store, Versioned } from './store.js';
3
+ import { BASE_NETWORK } from './x402.js';
4
+ /** A payment id, bound to the payload it first came with. Kept at least until `keepUntil`. */
5
+ export interface IdBinding extends Versioned {
6
+ kind: 'id';
7
+ fingerprint: string;
8
+ /** The payload's `validBefore`: after it, at a finalized block, the payload can't settle. */
9
+ keepUntil: string;
10
+ purchaseKey?: string;
11
+ }
12
+ /** A purchase and its settlement. */
13
+ export interface SellerPurchase extends Versioned {
14
+ kind: 'purchase';
15
+ state: 'processing' | 'submitted' | 'settled' | 'unpaid';
16
+ /** The payload settled, or to settle again. */
17
+ header: string;
18
+ validBefore: string;
19
+ searchFromBlock: number;
20
+ transaction?: string;
21
+ /** Unix seconds: until then a facilitator call may be in flight and nobody calls again. */
22
+ leaseUntil: number;
23
+ submissionAttempted?: boolean;
24
+ }
25
+ export type SellerRecord = IdBinding | SellerPurchase;
26
+ /** What a facilitator's `/settle` answers (x402 v2 `SettleResponse`). */
27
+ export interface SettleResponse {
28
+ success: boolean;
29
+ retryAfterSeconds?: number;
30
+ transaction?: string;
31
+ network?: string;
32
+ errorReason?: string;
33
+ payer?: string;
34
+ /** Trusted local facilitator evidence that no broadcast was attempted. */
35
+ submissionAttempted?: boolean;
36
+ }
37
+ /**
38
+ * Posts the body to the facilitator's `/settle` and returns its answer. It must stop when `signal`
39
+ * aborts (pass it to `fetch`): `settle` gives up on the call then, and a later request may call
40
+ * again. A second call of the same payload settles nothing twice (the nonce settles once), but
41
+ * it is a call the facilitator did not need.
42
+ */
43
+ export type Facilitator = (body: ReturnType<typeof forFacilitator>, o: {
44
+ signal: AbortSignal;
45
+ }) => Promise<SettleResponse>;
46
+ export interface SettleOptions extends OfferOptions {
47
+ baseRpc: string;
48
+ store: Store<SellerRecord>;
49
+ facilitator: Facilitator;
50
+ /**
51
+ * Where payment ids are unique (x402 payment-identifier: the seller and the route). Default:
52
+ * the contract, the file and `url`.
53
+ */
54
+ scope?: string;
55
+ settlementPollSeconds?: number;
56
+ /** How long a facilitator call may take before it is aborted; default 50 s. */
57
+ facilitatorTimeoutMs?: number;
58
+ /**
59
+ * Called with a purchase's record key and payer before its record is first written (after the
60
+ * new-payment checks, or once Base shows the payment), so an index of it is never behind the
61
+ * record. May run more than once for one purchase.
62
+ */
63
+ /** Admission for a new, unpaid settlement, after verification and before any write. */
64
+ beforeSettle?: (file: import('../near/rpc.js').FileInfo) => Promise<void>;
65
+ beforeRecord?: (key: string, payer: string) => Promise<void>;
66
+ }
67
+ /** The answer to a payload; `paymentResponse` turns it into the `PAYMENT-RESPONSE` a server sends. */
68
+ export type Settlement = {
69
+ state: 'settled';
70
+ transaction: string;
71
+ network: typeof BASE_NETWORK;
72
+ payer: string;
73
+ buyerKey: string;
74
+ nonce: string;
75
+ }
76
+ /** `errorReason: "settlement_pending"`: broadcast, not yet at Base safe. Send it again later. */
77
+ | {
78
+ state: 'pending';
79
+ transaction: string;
80
+ network: typeof BASE_NETWORK;
81
+ };
82
+ /** After a facilitator call that did not settle, the next one waits this long. */
83
+ export declare const RETRY_SECONDS = 10;
84
+ /**
85
+ * Settles a payload, or answers it from the state of its purchase. Throws `PayError`:
86
+ * `taken_down` (410) first, whatever the state; `bad_requirements`, `bad_signature` for a payload
87
+ * whose purchase terms are wrong; `payment_id_conflict` (409); the refusals of `verifyPayment` for
88
+ * a new payment (`sales_stopped`, a deadline too long, a changed offer); `payment_expired` for a
89
+ * purchase that can no longer be paid; `settlement_pending` when it is under way or unclear: send
90
+ * the same payload again later.
91
+ */
92
+ /** Where `settle` keeps a purchase: its identity (x402-annon.md section 10). */
93
+ export declare const purchaseRecordKey: (usdc: string, payer: string, nonce: string) => string;
94
+ export declare function settle(header: string, o: SettleOptions): Promise<Settlement>;
95
+ /** The x402 v2 `SettleResponse` for a settlement: the `PAYMENT-RESPONSE` header's content. */
96
+ export declare function paymentResponse(s: Settlement): SettleResponse;