@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,212 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:http';
5
+ import { TransactionUncertain } from '../wallet/uncertain.js';
6
+ const equal = (a, b) => Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual(Buffer.from(a), Buffer.from(b));
7
+ export async function startBrowserSession(o) {
8
+ let asset = o.asset;
9
+ if (asset === undefined) {
10
+ try {
11
+ asset = await readFile(new URL('../browser/login-bundle.js', import.meta.url), 'utf8');
12
+ }
13
+ catch (error) {
14
+ if (error.code !== 'ENOENT')
15
+ throw error;
16
+ try {
17
+ asset = await readFile(new URL('../../dist/browser/login-bundle.js', import.meta.url), 'utf8');
18
+ }
19
+ catch {
20
+ throw new Error('Browser login requires the bundle. Run pnpm --filter @defdone/annon build first.');
21
+ }
22
+ }
23
+ }
24
+ const capability = randomBytes(32).toString('base64url'), pairingCode = randomBytes(3).toString('hex').toUpperCase();
25
+ let origin = '', closed = false;
26
+ let bootstrapped = false;
27
+ // Keep in-flight and completed outcomes so popup/opener races verify only once.
28
+ const results = new Map();
29
+ const canonical = (v) => v && typeof v === 'object'
30
+ ? Array.isArray(v)
31
+ ? `[${v.map(canonical)}]`
32
+ : `{${Object.keys(v)
33
+ .sort()
34
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
35
+ .join(',')}}`
36
+ : JSON.stringify(v);
37
+ let pending;
38
+ const server = createServer((req, res) => {
39
+ void (async () => {
40
+ const headers = {
41
+ 'cache-control': 'no-store',
42
+ 'referrer-policy': 'no-referrer',
43
+ 'x-content-type-options': 'nosniff',
44
+ 'content-security-policy': "default-src 'none'; script-src 'self'; connect-src 'self' https:; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
45
+ };
46
+ const send = (status, body, type = 'application/json') => res.writeHead(status, { ...headers, 'content-type': type }).end(body);
47
+ if (closed || req.headers.host !== new URL(origin).host)
48
+ return void send(403, '{}');
49
+ const path = req.url;
50
+ if (path === '/api/bootstrap' && req.method === 'POST') {
51
+ if (o.noOpen || bootstrapped)
52
+ return void send(404, '{}');
53
+ if (req.headers.origin !== origin)
54
+ return void send(403, '{}');
55
+ bootstrapped = true;
56
+ return void send(200, JSON.stringify({ capability }));
57
+ }
58
+ if (path?.startsWith('/api/')) {
59
+ if (req.headers.origin !== origin ||
60
+ !equal(req.headers.authorization ?? '', `Bearer ${capability}`))
61
+ return void send(403, '{}');
62
+ if (path === '/api/action' && ['GET', 'POST'].includes(req.method ?? ''))
63
+ return void send(200, JSON.stringify({ pairingCode, action: pending?.action ?? null }));
64
+ if (path !== '/api/result' || req.method !== 'POST')
65
+ return void send(404, '{}');
66
+ let size = 0;
67
+ const parts = [];
68
+ for await (const chunk of req) {
69
+ size += chunk.length;
70
+ if (size > 131072)
71
+ return void send(413, '{}');
72
+ parts.push(chunk);
73
+ }
74
+ const b = JSON.parse(Buffer.concat(parts).toString());
75
+ const key = canonical(b.result);
76
+ const previous = results.get(b.id);
77
+ if (previous && Date.now() < previous.expiresAt) {
78
+ if (previous.key !== key)
79
+ return void send(409, '{}');
80
+ return void send(await previous.outcome, '{}');
81
+ }
82
+ const p = pending;
83
+ if (!p || p.busy || b.id !== p.action.id || Date.now() >= p.action.expiresAt)
84
+ return void send(409, '{}');
85
+ p.busy = true; // consumed before async verification, including forged results
86
+ let complete = () => { };
87
+ const outcome = new Promise((resolve) => {
88
+ complete = resolve;
89
+ });
90
+ results.set(b.id, { key, expiresAt: p.action.expiresAt, outcome });
91
+ try {
92
+ await p.verify(b.result);
93
+ if (closed || pending !== p || Date.now() >= p.action.expiresAt)
94
+ throw new Error('expired');
95
+ clearTimeout(p.timer);
96
+ if (pending === p) {
97
+ pending = undefined;
98
+ armIdle();
99
+ }
100
+ p.resolve(b.result);
101
+ complete(200);
102
+ send(200, '{}');
103
+ }
104
+ catch (error) {
105
+ clearTimeout(p.timer);
106
+ if (pending === p) {
107
+ pending = undefined;
108
+ armIdle();
109
+ }
110
+ p.reject(error instanceof TransactionUncertain ? error : new Error('wallet result rejected'));
111
+ complete(400);
112
+ send(400, '{}');
113
+ }
114
+ return;
115
+ }
116
+ if (req.method !== 'GET')
117
+ return void send(405, '{}');
118
+ if (path === '/app.js')
119
+ return void send(200, asset, 'text/javascript');
120
+ if (path === '/' || path === '/callback')
121
+ return void send(200, '<!doctype html><html><head><meta charset="utf-8"><title>Annon wallet approval</title><script src="/app.js" defer></script></head><body><h1>Annon wallet approval</h1><p id="pair"></p><pre id="action"></pre><button id="approve">Review in wallet</button><p id="status"></p></body></html>', 'text/html; charset=utf-8');
122
+ send(404, '{}');
123
+ })().catch(() => {
124
+ if (!res.headersSent)
125
+ res.writeHead(400, { 'cache-control': 'no-store' }).end();
126
+ });
127
+ });
128
+ server.on('upgrade', (_req, socket) => socket.destroy());
129
+ await new Promise((resolve, reject) => {
130
+ server.once('error', reject);
131
+ server.listen(0, '127.0.0.1', resolve);
132
+ });
133
+ origin = `http://127.0.0.1:${server.address().port}`;
134
+ const close = async () => {
135
+ if (closed)
136
+ return;
137
+ closed = true;
138
+ clearTimeout(timeout);
139
+ if (pending) {
140
+ clearTimeout(pending.timer);
141
+ pending.reject(new Error('session closed'));
142
+ pending = undefined;
143
+ }
144
+ server.closeAllConnections();
145
+ await new Promise((resolve) => server.close(() => resolve()));
146
+ };
147
+ let timeout;
148
+ const armIdle = () => {
149
+ clearTimeout(timeout);
150
+ if (!closed && !pending)
151
+ timeout = setTimeout(() => {
152
+ void close();
153
+ }, o.timeoutMs ?? 300000);
154
+ };
155
+ armIdle();
156
+ const url = `${origin}/#${capability}`;
157
+ if (!o.noOpen)
158
+ (o.open ??
159
+ ((target) => {
160
+ execFile(process.platform === 'darwin'
161
+ ? 'open'
162
+ : process.platform === 'win32'
163
+ ? 'explorer.exe'
164
+ : 'xdg-open', [target], { env: browserOpenerEnv(process.env) }, () => { });
165
+ }))(`${origin}/`);
166
+ return {
167
+ url,
168
+ pairingCode,
169
+ close,
170
+ request: (action, verify) => new Promise((resolve, reject) => {
171
+ for (const [id, r] of results)
172
+ if (Date.now() >= r.expiresAt)
173
+ results.delete(id);
174
+ if (closed || pending || results.has(action.id) || action.expiresAt <= Date.now()) {
175
+ reject(new Error('session closed, busy or expired'));
176
+ return;
177
+ }
178
+ clearTimeout(timeout);
179
+ const timer = setTimeout(() => {
180
+ if (pending?.timer === timer) {
181
+ pending = undefined;
182
+ armIdle();
183
+ }
184
+ reject(new Error('action expired'));
185
+ }, action.expiresAt - Date.now());
186
+ pending = {
187
+ action: JSON.parse(JSON.stringify(action)),
188
+ busy: false,
189
+ resolve: resolve,
190
+ reject,
191
+ verify: verify,
192
+ timer,
193
+ };
194
+ }),
195
+ };
196
+ }
197
+ /** Only desktop routing and executable lookup reach the browser opener. */
198
+ export function browserOpenerEnv(env) {
199
+ return Object.fromEntries([
200
+ 'PATH',
201
+ 'HOME',
202
+ 'DISPLAY',
203
+ 'BROWSER',
204
+ 'XDG_CURRENT_DESKTOP',
205
+ 'DESKTOP_SESSION',
206
+ 'LANG',
207
+ 'WAYLAND_DISPLAY',
208
+ 'XDG_RUNTIME_DIR',
209
+ 'DBUS_SESSION_BUS_ADDRESS',
210
+ 'SystemRoot',
211
+ ].flatMap((key) => (env[key] === undefined ? [] : [[key, env[key]]])));
212
+ }
@@ -0,0 +1,24 @@
1
+ import { AnnonError } from '../errors.js';
2
+ import type { Store, Versioned } from '../pay/store.js';
3
+ export declare class BudgetError extends AnnonError {
4
+ constructor(message?: string);
5
+ }
6
+ export interface BudgetRecord extends Versioned {
7
+ day: number;
8
+ hour: number;
9
+ daily: string;
10
+ hourly: string;
11
+ reconciled?: string[];
12
+ }
13
+ /** Reserve in UTC windows; refund once after a definite refusal or retain verified final spend. */
14
+ export declare function reserveSpend(store: Store<BudgetRecord>, scope: string, amount: bigint, limits: {
15
+ daily: bigint;
16
+ hourly?: bigint;
17
+ }, now?: number, reservationId?: string): Promise<(retained?: bigint) => Promise<void>>;
18
+ export interface SpendReservation {
19
+ id?: string;
20
+ amount: string;
21
+ at: number;
22
+ }
23
+ /** Idempotent across keeper restarts; old UTC windows are never refunded into new ones. */
24
+ export declare function reconcileSpend(store: Store<BudgetRecord>, scope: string, reservation: SpendReservation, retained: bigint): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { AnnonError } from '../errors.js';
2
+ export class BudgetError extends AnnonError {
3
+ constructor(message) {
4
+ super('budget_exceeded', message);
5
+ this.name = 'BudgetError';
6
+ }
7
+ }
8
+ /** Reserve in UTC windows; refund once after a definite refusal or retain verified final spend. */
9
+ export async function reserveSpend(store, scope, amount, limits, now = Date.now(), reservationId) {
10
+ if (amount < 0n || limits.daily < 0n || (limits.hourly !== undefined && limits.hourly < 0n))
11
+ throw new TypeError('invalid spend limit');
12
+ const day = Math.floor(now / 86400000), hour = Math.floor(now / 3600000);
13
+ for (;;) {
14
+ const old = await store.load(scope);
15
+ if (old && (old.day > day || old.hour > hour))
16
+ throw new BudgetError('Clock moved backwards; spending is paused.');
17
+ const daily = BigInt(old?.day === day ? old.daily : '0') + amount;
18
+ const hourly = BigInt(old?.hour === hour ? old.hourly : '0') + amount;
19
+ if (daily > limits.daily || (limits.hourly !== undefined && hourly > limits.hourly))
20
+ throw new BudgetError('The persisted spending budget is exhausted.');
21
+ if (await store.compareAndSet(scope, old, {
22
+ id: scope,
23
+ revision: (old?.revision ?? -1) + 1,
24
+ day,
25
+ hour,
26
+ daily: String(daily),
27
+ hourly: String(hourly),
28
+ ...(old?.day === day && old.reconciled ? { reconciled: old.reconciled } : {}),
29
+ }))
30
+ break;
31
+ }
32
+ let released;
33
+ return (retained = 0n) => (released ??= reconcileSpend(store, scope, { amount: String(amount), at: now, ...(reservationId ? { id: reservationId } : {}) }, retained));
34
+ }
35
+ /** Idempotent across keeper restarts; old UTC windows are never refunded into new ones. */
36
+ export async function reconcileSpend(store, scope, reservation, retained) {
37
+ const amount = BigInt(reservation.amount);
38
+ if (retained < 0n)
39
+ throw new TypeError('invalid retained spend');
40
+ const refund = amount > retained ? amount - retained : 0n;
41
+ const day = Math.floor(reservation.at / 86400000), hour = Math.floor(reservation.at / 3600000);
42
+ for (;;) {
43
+ const old = await store.load(scope);
44
+ if (!old || old.day !== day || (reservation.id && old.reconciled?.includes(reservation.id)))
45
+ return;
46
+ const daily = BigInt(old.daily) - refund;
47
+ const hourly = BigInt(old.hourly) - (old.hour === hour ? refund : 0n);
48
+ if (daily < 0n || hourly < 0n)
49
+ throw new Error('invalid budget refund');
50
+ if (await store.compareAndSet(scope, old, {
51
+ ...old,
52
+ revision: old.revision + 1,
53
+ daily: String(daily),
54
+ hourly: String(hourly),
55
+ ...(reservation.id ? { reconciled: [...(old.reconciled ?? []), reservation.id] } : {}),
56
+ }))
57
+ return;
58
+ }
59
+ }
@@ -0,0 +1,24 @@
1
+ import type { CommandSession, RequestContext } from './command-context.js';
2
+ type PreparedBuy = Awaited<ReturnType<typeof prepareBuy>>;
3
+ /** `verify`: a local file's header and body, or a remote file's header, against its listing. */
4
+ declare function verifyFile(ctx: RequestContext): Promise<Record<string, unknown>>;
5
+ /**
6
+ * `buy`, checks that run before the backup secret is read: agent policy, the price cap, the
7
+ * offer and its header against the registered listing.
8
+ */
9
+ declare function prepareBuy(ctx: RequestContext): Promise<{
10
+ found: {
11
+ pr: import("../pay/x402.js").PaymentRequired;
12
+ url: string;
13
+ header: Uint8Array<ArrayBufferLike>;
14
+ fileId: string;
15
+ };
16
+ f: import("../near/rpc.js").FileInfo;
17
+ maxPrice: bigint;
18
+ }>;
19
+ /**
20
+ * `buy`: confirms the exact terms, holds the per-file purchase guard and the agent budget, then
21
+ * lets the SDK pay.
22
+ */
23
+ declare function buy(ctx: CommandSession, preparedBuy: PreparedBuy | undefined): Promise<Record<string, unknown>>;
24
+ export { buy, prepareBuy, verifyFile };
@@ -0,0 +1,301 @@
1
+ /** Buyer commands before a purchase exists: `verify` a file and `buy` it. */
2
+ import { mkdir, readdir, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { inspect, inspectHeader, MAX_HEADER, parseHeader } from '../envelope/pay2.js';
5
+ import { AnnonError } from '../errors.js';
6
+ import { fileStorageBalance, getConfig, getPurchase } from '../near/rpc.js';
7
+ import { OneClick } from '../oneclick/oneclick.js';
8
+ import { purchaseKey } from '../pay/purchase.js';
9
+ import { fileStore } from '../pay/store-node.js';
10
+ import { checkBaseSponsorship } from '../relay/capabilities.js';
11
+ import { hex } from '../settle-near/codec.js';
12
+ import { backupPath, fail, isId } from './adapter-checks.js';
13
+ import { amount } from './amounts.js';
14
+ import { reserveSpend } from './budget.js';
15
+ import { AdapterError, usage } from './errors.js';
16
+ import { offer, resourceHeader, response } from './offers.js';
17
+ import { originFor } from './origin.js';
18
+ import { humanPaymentTerms, quotedSource, savedPayment, validatePaymentToken, } from './payment-terms.js';
19
+ import { claimPurchase } from './purchase-guard.js';
20
+ import { resourceAllowed } from './resources.js';
21
+ /** `verify`: a local file's header and body, or a remote file's header, against its listing. */
22
+ async function verifyFile(ctx) {
23
+ const { c, fetcher, o, readListing } = ctx;
24
+ if (!o.target)
25
+ throw usage();
26
+ const remote = /^https?:/.test(o.target);
27
+ const data = remote
28
+ ? await resourceHeader(o.target, c, fetcher)
29
+ : new Uint8Array(await readFile(o.target));
30
+ const parsed = parseHeader(new Uint8Array(data.subarray(0, MAX_HEADER)));
31
+ const fileId = hex(parsed.fileId);
32
+ const { listing } = await readListing(fileId);
33
+ const result = await (remote
34
+ ? inspectHeader(data, listing, c.contractId)
35
+ : inspect(data, listing, c.contractId));
36
+ if (!result.usable)
37
+ return fail('no_usable_stanza', 'The file has no key stanza for the trusted contract.');
38
+ return {
39
+ fileId,
40
+ verified: true,
41
+ checked: remote ? 'header' : 'header-and-body',
42
+ contractId: c.contractId,
43
+ };
44
+ }
45
+ /**
46
+ * `buy`, checks that run before the backup secret is read: agent policy, the price cap, the
47
+ * offer and its header against the registered listing.
48
+ */
49
+ async function prepareBuy(ctx) {
50
+ const { c, d, fetcher, o, readListing } = ctx;
51
+ if (!o.target)
52
+ throw usage();
53
+ if (d.agent && !/^https?:/.test(o.target))
54
+ return fail('resource_not_allowed', 'Agent purchases require a secure resource URL; offer files are refused.', 2);
55
+ const cap = o.maxPrice ?? c.maxPrice;
56
+ if (!cap)
57
+ return fail('max_price_required', 'Set --max-price, ANNON_MAX_PRICE or maxPrice in human configuration before buying.', 2);
58
+ const maxPrice = BigInt(amount(cap));
59
+ if (c.maxPrice && maxPrice > BigInt(amount(c.maxPrice)))
60
+ return fail('price_above_max', 'The requested cap exceeds the configured maximum.', 2);
61
+ const resource = /^https?:/.test(o.target) ? o.target : o.url;
62
+ if (d.agent && (!resource || !resourceAllowed(c.allowedResources, resource)))
63
+ return fail('resource_not_allowed', 'The resource URL is outside the configured agent policy.', 2);
64
+ if (d.agent && (o.refundTo || o.fromChain || o.fromAsset))
65
+ return fail('origin_not_allowed', 'Use a human-configured origin name.', 2);
66
+ const found = await offer(o.target, c, fetcher, undefined, o.url);
67
+ const { f, listing } = await readListing(found.fileId);
68
+ await inspectHeader(found.header, listing, c.contractId);
69
+ if (o.route === 'base' && !f.terms.base)
70
+ throw new AnnonError('route_unavailable');
71
+ return { found, f, maxPrice };
72
+ }
73
+ /**
74
+ * `buy`: confirms the exact terms, holds the per-file purchase guard and the agent budget, then
75
+ * lets the SDK pay.
76
+ */
77
+ async function buy(ctx, preparedBuy) {
78
+ const { c, d, env, fetcher, o, resources, sdk, storeDir, wallet } = ctx;
79
+ if (!preparedBuy)
80
+ throw usage();
81
+ const { found, f, maxPrice } = preparedBuy;
82
+ const buyer = await wallet();
83
+ const route = o.route ??
84
+ (o.fromChain || o.origin ? '1click' : buyer.identity.kind === 'near' ? 'near' : 'base');
85
+ if (d.agent && route === '1click' && !o.origin)
86
+ return fail('origin_not_allowed', 'Use a human-configured origin name.', 2);
87
+ if (route === 'near' && buyer.identity.kind !== 'near')
88
+ return fail('near_wallet_required', 'The direct NEAR route requires a NEAR wallet; use 1click for another chain.', 2);
89
+ if (route === 'base' && !f.terms.base)
90
+ throw new AdapterError(o.route ? 'route_unavailable' : 'origin_required', 'This file has no direct Base route. Use --route 1click --from-chain base to pay from Base.', 2);
91
+ if (route === 'base' && (buyer.identity.kind !== 'evm' || !('signTypedData' in buyer)))
92
+ return fail('base_wallet_required', 'The Base route requires an EVM payment wallet.', 2);
93
+ if (route === 'base' && !c.baseRpc)
94
+ return fail('base_rpc_required', 'Configure baseRpc before a Base purchase.', 2);
95
+ const origin = route === '1click' ? await originFor(o, env, buyer, fetcher, c) : undefined;
96
+ const before = await resources.load(found.fileId);
97
+ if (!(await resources.compareAndSet(found.fileId, before, {
98
+ id: found.fileId,
99
+ revision: (before?.revision ?? -1) + 1,
100
+ contractId: c.contractId,
101
+ url: found.url,
102
+ header: hex(found.header),
103
+ })))
104
+ throw new Error('resource conflict');
105
+ const price = route === 'base' ? f.terms.base?.price : f.terms.near.price;
106
+ if (!price || BigInt(price) > maxPrice)
107
+ return fail('price_above_max', 'The registered price exceeds maxPrice.');
108
+ const paymentConfig = await getConfig(c.nearRpc, c.contractId);
109
+ const paymentTerms = {
110
+ file: found.fileId,
111
+ resource: found.url,
112
+ network: route === 'base' ? 'eip155:8453' : c.network,
113
+ route,
114
+ price,
115
+ recipients: f.terms.near.recipients,
116
+ remainder: f.terms.near.remainder,
117
+ baseRecipient: f.terms.base?.pay_to,
118
+ token: route === 'base' ? f.terms.base?.usdc : f.terms.near.token,
119
+ };
120
+ if (paymentConfig.network !== c.network)
121
+ return fail('network_mismatch', 'The contract network differs from the configured network.', 2);
122
+ validatePaymentToken(paymentTerms, paymentConfig);
123
+ const baseSponsorship = route === 'base'
124
+ ? await checkBaseSponsorship({
125
+ deployment: {
126
+ network: c.network,
127
+ contractId: c.contractId,
128
+ nearUsdc: paymentConfig.nearUsdc,
129
+ baseUsdc: paymentConfig.usdc,
130
+ },
131
+ file: f,
132
+ baseRpc: c.baseRpc,
133
+ relayer: c.relayer ?? '',
134
+ fetch: fetcher,
135
+ })
136
+ : undefined;
137
+ if (route !== 'base') {
138
+ const budget = await fileStorageBalance(c.nearRpc, c.contractId, f.terms.file_id);
139
+ if (budget.available < budget.admissionRequirement)
140
+ return fail('file_storage_low', 'This file needs more purchase storage funding before a NEAR or 1Click payment.', 2);
141
+ }
142
+ if (origin && !d.agent && !d.browserApprovals && !o.json && !d.prompt)
143
+ return fail('confirmation_required', 'Review and confirm the exact payment terms before paying.', 3);
144
+ const confirmPayment = async (source) => {
145
+ const review = { ...paymentTerms, ...(source ? { source } : {}) };
146
+ d.paymentTerms?.(review);
147
+ if (!d.agent && !d.browserApprovals) {
148
+ d.notify?.(o.json ? JSON.stringify(review) : humanPaymentTerms(review, paymentConfig));
149
+ if (!(await (o.json ? undefined : d.prompt)?.('Type yes to pay: ')))
150
+ return fail('confirmation_required', 'Review and confirm the exact payment terms before paying.', 3);
151
+ }
152
+ };
153
+ if (d.agent && c.dailyBudget === undefined)
154
+ return fail('budget_required', 'Configure a daily budget before buying.', 2);
155
+ const guardDir = join(c.stateDir, 'purchase-guards');
156
+ await mkdir(guardDir, { recursive: true, mode: 0o700 });
157
+ const known = [];
158
+ for (const name of await readdir(storeDir)) {
159
+ if (!/^[0-9a-f]{64}\.json$/.test(name))
160
+ continue;
161
+ const envelope = JSON.parse(await readFile(join(storeDir, name), 'utf8'));
162
+ if (!isId(envelope.id))
163
+ continue;
164
+ const saved = await sdk.store.load(purchaseKey(envelope.id));
165
+ if (saved?.purchase.file_id === found.fileId &&
166
+ saved.purchase.contract === c.contractId &&
167
+ saved.terms.network === (saved.purchase.route === 'base-usdc' ? 'eip155:8453' : c.network))
168
+ known.push(saved.id);
169
+ }
170
+ const guard = await claimPurchase(fileStore(guardDir), JSON.stringify([c.network, c.contractId, found.fileId]), {
171
+ known,
172
+ ...(o.force === undefined ? {} : { force: o.force }),
173
+ live: async (id, claimUntil) => {
174
+ const saved = await sdk.store.load(purchaseKey(id));
175
+ if (await (d.purchase ?? getPurchase)(c.nearRpc, c.contractId, id))
176
+ return true;
177
+ if (!saved)
178
+ return Date.now() / 1000 < claimUntil;
179
+ return (Number(saved.purchase.expires_at) > Date.now() / 1000 ||
180
+ (saved.purchase.route === 'base-usdc' && saved.attempts.length > 0));
181
+ },
182
+ });
183
+ let renewalError;
184
+ const heartbeat = setInterval(() => {
185
+ void guard.renew().catch((error) => {
186
+ renewalError = error;
187
+ });
188
+ }, 30000);
189
+ let refundBudget;
190
+ const reserve = async (spend) => {
191
+ if (!d.agent)
192
+ return;
193
+ if (c.dailyBudget === undefined)
194
+ return fail('budget_required', 'Configure a daily budget before buying.', 2);
195
+ const budgetDir = join(c.stateDir, 'budgets');
196
+ await mkdir(budgetDir, { recursive: true, mode: 0o700 });
197
+ refundBudget = await reserveSpend(fileStore(budgetDir), 'agent:USDC', spend, {
198
+ daily: c.dailyBudget === '0' ? 0n : BigInt(amount(c.dailyBudget)),
199
+ });
200
+ };
201
+ let completed = false;
202
+ try {
203
+ if (!origin || (o.json && !d.browserApprovals))
204
+ await confirmPayment();
205
+ if (!origin)
206
+ await reserve(BigInt(price));
207
+ let source;
208
+ const bought = await sdk.buy({
209
+ fileId: found.fileId,
210
+ header: found.header,
211
+ beforeSave: async (record) => {
212
+ if (renewalError)
213
+ throw renewalError;
214
+ await guard.saved(record.id);
215
+ },
216
+ recovery: buyer,
217
+ ...(baseSponsorship ? { baseSponsorship } : {}),
218
+ route,
219
+ refund: buyer.identity,
220
+ ...(route === '1click' ? {} : { payer: buyer }),
221
+ maxPrice,
222
+ moderators: o.moderator ? [o.moderator] : (c.moderators ?? []),
223
+ paymentRequired: found.pr,
224
+ url: found.url,
225
+ ...(origin
226
+ ? {
227
+ origin,
228
+ oneClick: new OneClick({ fetch: fetcher }),
229
+ onQuote: async (quote) => {
230
+ source = quotedSource(quote, origin.sourceAsset);
231
+ let spend = BigInt(price);
232
+ if (d.agent) {
233
+ const sourceAmount = BigInt(source.amount);
234
+ if (origin.maxSourceAmount !== undefined &&
235
+ sourceAmount > BigInt(origin.maxSourceAmount))
236
+ return fail('price_above_max', 'The quoted source amount exceeds the configured origin cap.', 2);
237
+ if (source.symbol === 'USDC' && source.decimals !== undefined) {
238
+ const scale = 10n ** BigInt(source.decimals);
239
+ spend = (sourceAmount * 1000000n + scale - 1n) / scale;
240
+ if (spend > maxPrice)
241
+ return fail('price_above_max', 'The quoted source USDC amount exceeds maxPrice.', 2);
242
+ }
243
+ else if (!origin.maxSourceAmount)
244
+ return fail('origin_not_allowed', 'Configure maxSourceAmount in source base units for this origin before buying.', 2);
245
+ }
246
+ await reserve(spend);
247
+ await confirmPayment(source);
248
+ },
249
+ }
250
+ : {}),
251
+ submitBase: async (payment) => {
252
+ const r = await response(found.url, fetcher, { 'PAYMENT-SIGNATURE': payment });
253
+ await r.body?.cancel();
254
+ if (![200, 206, 402, 503].includes(r.status))
255
+ throw new AdapterError('host_payment_failed', `Payment submission failed: host HTTP ${r.status}. Keep the saved purchase and check status before retrying.`);
256
+ },
257
+ });
258
+ await guard.saved(bought.record.id);
259
+ completed = true;
260
+ let payment;
261
+ try {
262
+ payment = savedPayment(bought.record, source);
263
+ const next = { ...bought.record, revision: bought.record.revision + 1, payment };
264
+ await sdk.store.compareAndSet(purchaseKey(next.id), bought.record, next);
265
+ }
266
+ catch {
267
+ // Keep the completed purchase when saving source details fails.
268
+ }
269
+ // Raw purchase records and signed payment payloads never cross the adapter boundary.
270
+ return {
271
+ purchaseId: bought.record.id,
272
+ fileId: found.fileId,
273
+ route: bought.route,
274
+ ...(payment ? { payment } : {}),
275
+ state: bought.uncertain
276
+ ? 'uncertain'
277
+ : bought.instructions
278
+ ? 'deposit_required'
279
+ : 'submitted',
280
+ ...(bought.uncertain ? { txHash: bought.uncertain.txHash } : {}),
281
+ backup: backupPath(c, bought.record.id),
282
+ ...(bought.instructions ? { instructions: bought.instructions } : {}),
283
+ ...(source ? { source } : {}),
284
+ next: 'Fund the deposit if instructed, then check status and open this purchase.',
285
+ };
286
+ }
287
+ catch (error) {
288
+ // Only failures before the SDK returns can release a 1Click reservation.
289
+ if (origin && !completed)
290
+ await refundBudget?.().catch(() => { });
291
+ throw error;
292
+ }
293
+ finally {
294
+ clearInterval(heartbeat);
295
+ if (completed)
296
+ await guard.release().catch(() => { });
297
+ else
298
+ await guard.release();
299
+ }
300
+ }
301
+ export { buy, prepareBuy, verifyFile };