@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.
- package/CHANGELOG.md +7 -0
- package/README.md +18 -8
- package/dist/browser/login-bundle.js +112 -77
- package/dist/browser/login.js +45 -6
- package/dist/browser/purchase-bundle.js +45 -45
- package/dist/node/adapter.js +0 -1
- package/dist/node/browser-login.d.ts +7 -0
- package/dist/node/browser-login.js +9 -2
- package/dist/node/signer-wallet.js +3 -1
- package/dist/pay/base.js +36 -43
- package/dist/wallet/account-only.d.ts +1 -1
- package/dist/wallet/account-only.js +1 -1
- package/package.json +3 -2
package/dist/node/adapter.js
CHANGED
|
@@ -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':
|
|
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,
|
|
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
|
-
/**
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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:
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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/
|
|
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",
|