@defdone/annon 0.1.0-alpha.0 → 0.1.0-alpha.1

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.
@@ -56,7 +56,6 @@ export function createAdapter(env = process.env, d = {}) {
56
56
  message: challenge,
57
57
  recipient: c.contractId || 'annon-cli',
58
58
  nonce: [...crypto.getRandomValues(new Uint8Array(32))],
59
- callbackUrl: new URL('/callback', s.url).href,
60
59
  });
61
60
  let identity;
62
61
  await s.request(q, async (r) => {
@@ -1,3 +1,10 @@
1
+ /**
2
+ * The approval page talks only to this loopback server, the NEAR RPC its Wallet Selector uses
3
+ * (fixed per network in browser/login.ts, independent of the CLI's configured RPC) and HOT
4
+ * Wallet's relay; it frames only the HOT widget. EVM wallets are reached through the injected
5
+ * provider, which needs no connection from the page.
6
+ */
7
+ export declare const LOGIN_PAGE_CSP = "default-src 'none'; script-src 'self'; connect-src 'self' https://free.rpc.fastnear.com https://test.rpc.fastnear.com https://h4n.app; frame-src https://hot-labs.org; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
1
8
  export interface BrowserSession {
2
9
  url: string;
3
10
  pairingCode: string;
@@ -4,6 +4,13 @@ import { readFile } from 'node:fs/promises';
4
4
  import { createServer } from 'node:http';
5
5
  import { TransactionUncertain } from '../wallet/uncertain.js';
6
6
  const equal = (a, b) => Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual(Buffer.from(a), Buffer.from(b));
7
+ /**
8
+ * The approval page talks only to this loopback server, the NEAR RPC its Wallet Selector uses
9
+ * (fixed per network in browser/login.ts, independent of the CLI's configured RPC) and HOT
10
+ * Wallet's relay; it frames only the HOT widget. EVM wallets are reached through the injected
11
+ * provider, which needs no connection from the page.
12
+ */
13
+ export const LOGIN_PAGE_CSP = "default-src 'none'; script-src 'self'; connect-src 'self' https://free.rpc.fastnear.com https://test.rpc.fastnear.com https://h4n.app; frame-src https://hot-labs.org; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
7
14
  export async function startBrowserSession(o) {
8
15
  let asset = o.asset;
9
16
  if (asset === undefined) {
@@ -41,7 +48,7 @@ export async function startBrowserSession(o) {
41
48
  'cache-control': 'no-store',
42
49
  'referrer-policy': 'no-referrer',
43
50
  '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'",
51
+ 'content-security-policy': LOGIN_PAGE_CSP,
45
52
  };
46
53
  const send = (status, body, type = 'application/json') => res.writeHead(status, { ...headers, 'content-type': type }).end(body);
47
54
  if (closed || req.headers.host !== new URL(origin).host)
@@ -118,7 +125,7 @@ export async function startBrowserSession(o) {
118
125
  if (path === '/app.js')
119
126
  return void send(200, asset, 'text/javascript');
120
127
  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');
128
+ 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><p><label for="near-wallet">NEAR wallet</label> <select id="near-wallet"><option value="meteor-wallet">Meteor Wallet</option><option id="near-wallet-hot" value="hot-wallet">HOT Wallet (mainnet)</option></select></p><button id="approve">Review in wallet</button><p id="status"></p></body></html>', 'text/html; charset=utf-8');
122
129
  send(404, '{}');
123
130
  })().catch(() => {
124
131
  if (!res.headersSent)
@@ -139,7 +139,9 @@ export async function configuredWallet(s, c, o, env, keychain, browser, terms) {
139
139
  throw error;
140
140
  }
141
141
  },
142
- }, id.id, (text, context) => proveNearControl(id.id, text, { ...context, ...(session ? { callbackUrl: new URL('/callback', session.url).href } : {}) }, signMessage));
142
+ }, id.id,
143
+ // Browser NEAR wallets return NEP-413 signatures in the page, so the payload has no callbackUrl.
144
+ (text, context) => proveNearControl(id.id, text, context, signMessage));
143
145
  return {
144
146
  ...wallet,
145
147
  signUpload: (intent) => signNearUpload({ getAccounts: async () => [{ accountId: id.id }], signMessage }, intent, c.nearRpc),
package/dist/pay/base.js CHANGED
@@ -298,32 +298,44 @@ async function lastBlock(url, from, validBefore, now) {
298
298
  return at;
299
299
  }
300
300
  }
301
- /** The block of an `AuthorizationCanceled` for this nonce in `[from, to]`, searched in chunks. */
302
- async function canceledAfter(url, e, from, to) {
303
- // Note: 10,000 blocks per query, the common RPC limit; a long-expired purchase takes several.
304
- for (let at = from; at <= to; at += 10_000) {
305
- const end = Math.min(to, at + 9_999);
306
- const topics = [
307
- TOPICS.authorizationCanceled,
308
- toHex(addressWord(e.from)),
309
- e.nonce.toLowerCase(),
310
- ];
301
+ /** Note: blocks per eth_getLogs, the common RPC limit (QuickNode refuses a wider range). */
302
+ const LOG_RANGE = 10_000;
303
+ /**
304
+ * USDC's logs for `topics` (a list in a position matches any of it) in `[from, to]`, read in
305
+ * chunks of `LOG_RANGE` blocks, earliest first. Each chunk's logs must answer that chunk's query.
306
+ * Stops after the first chunk with a log `enough` accepts; an empty range reads nothing.
307
+ */
308
+ async function usdcLogs(url, usdc, topics, from, to, enough) {
309
+ const all = [];
310
+ for (let at = from; at <= to; at += LOG_RANGE) {
311
+ const end = Math.min(to, at + LOG_RANGE - 1);
311
312
  const found = await ask(url, 'eth_getLogs', [
312
- { address: e.usdc, fromBlock: qty(at), toBlock: qty(end), topics },
313
+ { address: usdc, fromBlock: qty(at), toBlock: qty(end), topics },
313
314
  ]);
314
315
  if (!Array.isArray(found))
315
316
  return malformed('eth_getLogs result');
316
- for (const l of found.map(evmLog).filter((l) => !l.removed)) {
317
- if (l.address !== strip(e.usdc) ||
318
- l.topics.join() !== topics.map(strip).join() ||
317
+ const logs = found.map(evmLog).filter((l) => !l.removed);
318
+ // Every log must answer the query; anything else is an RPC that can't be believed.
319
+ for (const l of logs) {
320
+ if (l.address !== strip(usdc) ||
321
+ l.topics.length !== topics.length ||
322
+ !topics.every((t, i) => [t].flat().some((x) => strip(x) === l.topics[i])) ||
319
323
  l.block_number < at ||
320
324
  l.block_number > end) {
321
325
  return malformed('eth_getLogs returned a log the query did not ask for');
322
326
  }
323
- return l.block_number;
324
327
  }
328
+ all.push(...logs);
329
+ if (logs.some(enough))
330
+ break;
325
331
  }
326
- return null;
332
+ return all;
333
+ }
334
+ /** The block of an `AuthorizationCanceled` for this nonce in `[from, to]`. */
335
+ async function canceledAfter(url, e, from, to) {
336
+ const topics = [TOPICS.authorizationCanceled, toHex(addressWord(e.from)), e.nonce.toLowerCase()];
337
+ const [first] = await usdcLogs(url, e.usdc, topics, from, to, () => true);
338
+ return first ? first.block_number : null;
327
339
  }
328
340
  /**
329
341
  * Where a signed purchase stands, found by its nonce alone, so a lost transaction hash loses
@@ -338,33 +350,14 @@ export async function paymentStatus(url, e) {
338
350
  const final = await block(url, 'finalized');
339
351
  const now = await head(url);
340
352
  const last = await lastBlock(url, e.block, e.validBefore, now);
341
- const query = {
342
- address: e.usdc,
343
- fromBlock: qty(e.block),
344
- toBlock: qty(last),
345
- topics: [
346
- [TOPICS.authorizationUsed, TOPICS.authorizationCanceled],
347
- toHex(addressWord(e.from)),
348
- e.nonce.toLowerCase(),
349
- ],
350
- };
351
- const found = await ask(url, 'eth_getLogs', [query]);
352
- if (!Array.isArray(found))
353
- return malformed('eth_getLogs result');
354
- const logs = found.map(evmLog).filter((l) => !l.removed);
355
- // Every log must answer the query; anything else is an RPC that can't be believed.
356
- for (const l of logs) {
357
- if (l.address !== strip(e.usdc) ||
358
- l.topics.length !== 3 ||
359
- !query.topics[0].some((t) => strip(t) === l.topics[0]) ||
360
- l.topics[1] !== topicOf(e.from) ||
361
- l.topics[2] !== strip(e.nonce) ||
362
- l.block_number < e.block ||
363
- l.block_number > last) {
364
- return malformed('eth_getLogs returned a log the query did not ask for');
365
- }
366
- }
367
- const used = logs.find((l) => l.topics[0] === strip(TOPICS.authorizationUsed));
353
+ // A used authorization in any chunk wins over a cancellation, so only that ends the search.
354
+ const usedTopic = strip(TOPICS.authorizationUsed);
355
+ const logs = await usdcLogs(url, e.usdc, [
356
+ [TOPICS.authorizationUsed, TOPICS.authorizationCanceled],
357
+ toHex(addressWord(e.from)),
358
+ e.nonce.toLowerCase(),
359
+ ], e.block, last, (l) => l.topics[0] === usedTopic);
360
+ const used = logs.find((l) => l.topics[0] === usedTopic);
368
361
  if (used) {
369
362
  const receipt = await ask(url, 'eth_getTransactionReceipt', [`0x${used.transaction_hash}`]);
370
363
  const paid = paymentLogs(receipt, e);
@@ -1,4 +1,4 @@
1
- /** Never supply contractId/methodNames: MyNearWallet otherwise generates a local secret. */
1
+ /** Never supply contractId/methodNames: a wallet would otherwise add a function-call key for them. */
2
2
  export declare function signInAccountOnly(wallet: {
3
3
  signOut?: () => Promise<void>;
4
4
  signIn(params: {
@@ -1,4 +1,4 @@
1
- /** Never supply contractId/methodNames: MyNearWallet otherwise generates a local secret. */
1
+ /** Never supply contractId/methodNames: a wallet would otherwise add a function-call key for them. */
2
2
  export async function signInAccountOnly(wallet) {
3
3
  // A reused loopback origin may contain older connector authority. Never let it sign silently.
4
4
  await wallet.signOut?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defdone/annon",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.1",
4
4
  "description": "Files only the buyer can open: encrypted on the client, the key released by NEAR once the payment is verified.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://annon.dev",
@@ -115,7 +115,8 @@
115
115
  ],
116
116
  "devDependencies": {
117
117
  "@near-wallet-selector/core": "10.1.4",
118
- "@near-wallet-selector/my-near-wallet": "10.1.4",
118
+ "@near-wallet-selector/hot-wallet": "10.1.4",
119
+ "@near-wallet-selector/meteor-wallet": "10.1.4",
119
120
  "@types/node": "26.6.5",
120
121
  "buffer": "6.0.3",
121
122
  "esbuild": "0.28.2",