@defdone/annon 0.1.0-alpha.0 → 0.1.0-alpha.2
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 +15 -0
- package/CLI.md +116 -10
- package/README.md +57 -15
- package/dist/api/annon-types.d.ts +2 -1
- package/dist/api/annon.js +9 -2
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/login-bundle.js +113 -77
- package/dist/browser/login-dom.d.ts +13 -0
- package/dist/browser/login-dom.js +67 -0
- package/dist/browser/login-dropdown.d.ts +32 -0
- package/dist/browser/login-dropdown.js +129 -0
- package/dist/browser/login-format.d.ts +16 -0
- package/dist/browser/login-format.js +52 -0
- package/dist/browser/login-status.d.ts +70 -0
- package/dist/browser/login-status.js +85 -0
- package/dist/browser/login-terms.d.ts +10 -0
- package/dist/browser/login-terms.js +69 -0
- package/dist/browser/login-view.d.ts +34 -0
- package/dist/browser/login-view.js +227 -0
- package/dist/browser/login-wallet.d.ts +28 -0
- package/dist/browser/login-wallet.js +179 -0
- package/dist/browser/login.js +115 -101
- package/dist/browser/purchase-bundle.js +45 -45
- package/dist/browser/purchases.d.ts +4 -0
- package/dist/browser/purchases.js +14 -0
- package/dist/browser/reconcile.d.ts +41 -0
- package/dist/browser/reconcile.js +77 -0
- package/dist/node/adapter.js +5 -3
- package/dist/node/approval-status.d.ts +34 -0
- package/dist/node/approval-status.js +71 -0
- package/dist/node/backup-secret-init.d.ts +9 -0
- package/dist/node/backup-secret-init.js +103 -0
- package/dist/node/backup-secret.d.ts +35 -0
- package/dist/node/backup-secret.js +104 -0
- package/dist/node/browser-login.d.ts +9 -0
- package/dist/node/browser-login.js +75 -14
- package/dist/node/buy-commands.js +9 -2
- package/dist/node/cli.js +23 -7
- package/dist/node/command-context.js +1 -1
- package/dist/node/config.d.ts +2 -0
- package/dist/node/config.js +3 -1
- package/dist/node/login-page.d.ts +7 -0
- package/dist/node/login-page.js +71 -0
- package/dist/node/purchase-commands.d.ts +2 -2
- package/dist/node/purchase-commands.js +10 -6
- package/dist/node/saved-state.d.ts +4 -0
- package/dist/node/saved-state.js +50 -0
- package/dist/node/signer-wallet.js +3 -1
- package/dist/node/version.d.ts +1 -0
- package/dist/node/version.js +3 -0
- package/dist/node/wallets.d.ts +1 -1
- package/dist/node/wallets.js +2 -10
- package/dist/pay/base.js +36 -43
- package/dist/pay/purchase.d.ts +2 -1
- package/dist/wallet/account-only.d.ts +1 -1
- package/dist/wallet/account-only.js +1 -1
- package/dist/wallet/uncertain.d.ts +6 -3
- package/dist/wallet/uncertain.js +4 -1
- package/package.json +3 -2
|
@@ -3,8 +3,18 @@ import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
5
|
import { TransactionUncertain } from '../wallet/uncertain.js';
|
|
6
|
+
import { APPROVED_LINE, cancelledError, expiredError, failureLine, isPageReport, openingLine, sendUncertain, usableHash, waitingLine, } from './approval-status.js';
|
|
7
|
+
import { LOGIN_PAGE_CSS, LOGIN_PAGE_HTML } from './login-page.js';
|
|
6
8
|
const equal = (a, b) => Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
9
|
+
/**
|
|
10
|
+
* The approval page talks only to this loopback server, the NEAR RPC its Wallet Selector uses
|
|
11
|
+
* (fixed per network in browser/login.ts, independent of the CLI's configured RPC) and HOT
|
|
12
|
+
* Wallet's relay; it frames only the HOT widget. EVM wallets are reached through the injected
|
|
13
|
+
* provider, which needs no connection from the page.
|
|
14
|
+
*/
|
|
15
|
+
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
16
|
export async function startBrowserSession(o) {
|
|
17
|
+
const notify = o.notify ?? ((line) => void process.stderr.write(`${line}\n`));
|
|
8
18
|
let asset = o.asset;
|
|
9
19
|
if (asset === undefined) {
|
|
10
20
|
try {
|
|
@@ -34,6 +44,15 @@ export async function startBrowserSession(o) {
|
|
|
34
44
|
.map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
|
|
35
45
|
.join(',')}}`
|
|
36
46
|
: JSON.stringify(v);
|
|
47
|
+
/** Ends a request whose transaction may be on chain, with the CLI's own warning. */
|
|
48
|
+
const uncertain = (p, txHash) => {
|
|
49
|
+
const { line, error } = sendUncertain(p.action, txHash);
|
|
50
|
+
notify(line);
|
|
51
|
+
return error;
|
|
52
|
+
};
|
|
53
|
+
/** A transaction request whose send started, or that returned transaction evidence at all. */
|
|
54
|
+
const sent = (p) => p.sending === true ||
|
|
55
|
+
p.action.operation === 'near.signTransaction';
|
|
37
56
|
let pending;
|
|
38
57
|
const server = createServer((req, res) => {
|
|
39
58
|
void (async () => {
|
|
@@ -41,7 +60,7 @@ export async function startBrowserSession(o) {
|
|
|
41
60
|
'cache-control': 'no-store',
|
|
42
61
|
'referrer-policy': 'no-referrer',
|
|
43
62
|
'x-content-type-options': 'nosniff',
|
|
44
|
-
'content-security-policy':
|
|
63
|
+
'content-security-policy': LOGIN_PAGE_CSP,
|
|
45
64
|
};
|
|
46
65
|
const send = (status, body, type = 'application/json') => res.writeHead(status, { ...headers, 'content-type': type }).end(body);
|
|
47
66
|
if (closed || req.headers.host !== new URL(origin).host)
|
|
@@ -60,8 +79,12 @@ export async function startBrowserSession(o) {
|
|
|
60
79
|
!equal(req.headers.authorization ?? '', `Bearer ${capability}`))
|
|
61
80
|
return void send(403, '{}');
|
|
62
81
|
if (path === '/api/action' && ['GET', 'POST'].includes(req.method ?? ''))
|
|
63
|
-
return void send(200, JSON.stringify({
|
|
64
|
-
|
|
82
|
+
return void send(200, JSON.stringify({
|
|
83
|
+
pairingCode,
|
|
84
|
+
action: pending?.action ?? null,
|
|
85
|
+
sendStarted: pending?.sending === true,
|
|
86
|
+
}));
|
|
87
|
+
if (!['/api/result', '/api/report'].includes(path) || req.method !== 'POST')
|
|
65
88
|
return void send(404, '{}');
|
|
66
89
|
let size = 0;
|
|
67
90
|
const parts = [];
|
|
@@ -72,6 +95,31 @@ export async function startBrowserSession(o) {
|
|
|
72
95
|
parts.push(chunk);
|
|
73
96
|
}
|
|
74
97
|
const b = JSON.parse(Buffer.concat(parts).toString());
|
|
98
|
+
if (path === '/api/report') {
|
|
99
|
+
// A fixed reason for the current, unconsumed request. Only `cancelled` and
|
|
100
|
+
// `send_uncertain` end it, and never as cancelled once the page started a send.
|
|
101
|
+
const p = pending;
|
|
102
|
+
if (!p || p.busy || b.id !== p.action.id || !isPageReport(b.reason))
|
|
103
|
+
return void send(409, '{}');
|
|
104
|
+
if (b.reason === 'send_started') {
|
|
105
|
+
// One send per request: a reloaded or second page cannot start another.
|
|
106
|
+
if (p.sending)
|
|
107
|
+
return void send(409, '{}');
|
|
108
|
+
p.sending = true;
|
|
109
|
+
}
|
|
110
|
+
else if (b.reason === 'cancelled' || b.reason === 'send_uncertain') {
|
|
111
|
+
clearTimeout(p.timer);
|
|
112
|
+
pending = undefined;
|
|
113
|
+
armIdle();
|
|
114
|
+
p.reject(p.sending || b.reason === 'send_uncertain' ? uncertain(p) : cancelledError());
|
|
115
|
+
}
|
|
116
|
+
else if (!p.sending && p.failure !== b.reason) {
|
|
117
|
+
// After a send started no failure can be retried, so none is announced as one.
|
|
118
|
+
p.failure = b.reason;
|
|
119
|
+
notify(failureLine(b.reason, p.action.expiresAt - Date.now()));
|
|
120
|
+
}
|
|
121
|
+
return void send(200, '{}');
|
|
122
|
+
}
|
|
75
123
|
const key = canonical(b.result);
|
|
76
124
|
const previous = results.get(b.id);
|
|
77
125
|
if (previous && Date.now() < previous.expiresAt) {
|
|
@@ -97,6 +145,7 @@ export async function startBrowserSession(o) {
|
|
|
97
145
|
pending = undefined;
|
|
98
146
|
armIdle();
|
|
99
147
|
}
|
|
148
|
+
notify(APPROVED_LINE);
|
|
100
149
|
p.resolve(b.result);
|
|
101
150
|
complete(200);
|
|
102
151
|
send(200, '{}');
|
|
@@ -107,7 +156,13 @@ export async function startBrowserSession(o) {
|
|
|
107
156
|
pending = undefined;
|
|
108
157
|
armIdle();
|
|
109
158
|
}
|
|
110
|
-
|
|
159
|
+
// After a send, a result that cannot be verified is never evidence of success or of
|
|
160
|
+
// failure: the request ends as uncertain, with any usable hash, so it stays saved.
|
|
161
|
+
p.reject(sent(p)
|
|
162
|
+
? uncertain(p, usableHash(error instanceof TransactionUncertain ? error.txHash : b?.result?.txHash))
|
|
163
|
+
: error instanceof TransactionUncertain
|
|
164
|
+
? error
|
|
165
|
+
: new Error('wallet result rejected'));
|
|
111
166
|
complete(400);
|
|
112
167
|
send(400, '{}');
|
|
113
168
|
}
|
|
@@ -117,8 +172,10 @@ export async function startBrowserSession(o) {
|
|
|
117
172
|
return void send(405, '{}');
|
|
118
173
|
if (path === '/app.js')
|
|
119
174
|
return void send(200, asset, 'text/javascript');
|
|
175
|
+
if (path === '/app.css')
|
|
176
|
+
return void send(200, LOGIN_PAGE_CSS, 'text/css; charset=utf-8');
|
|
120
177
|
if (path === '/' || path === '/callback')
|
|
121
|
-
return void send(200,
|
|
178
|
+
return void send(200, LOGIN_PAGE_HTML, 'text/html; charset=utf-8');
|
|
122
179
|
send(404, '{}');
|
|
123
180
|
})().catch(() => {
|
|
124
181
|
if (!res.headersSent)
|
|
@@ -154,6 +211,9 @@ export async function startBrowserSession(o) {
|
|
|
154
211
|
};
|
|
155
212
|
armIdle();
|
|
156
213
|
const url = `${origin}/#${capability}`;
|
|
214
|
+
// The capability-free page URL: in open mode the page bootstraps the capability once.
|
|
215
|
+
if (!o.noOpen)
|
|
216
|
+
notify(openingLine(`${origin}/`));
|
|
157
217
|
if (!o.noOpen)
|
|
158
218
|
(o.open ??
|
|
159
219
|
((target) => {
|
|
@@ -176,21 +236,22 @@ export async function startBrowserSession(o) {
|
|
|
176
236
|
return;
|
|
177
237
|
}
|
|
178
238
|
clearTimeout(timeout);
|
|
179
|
-
const
|
|
180
|
-
if (pending?.timer === timer) {
|
|
181
|
-
pending = undefined;
|
|
182
|
-
armIdle();
|
|
183
|
-
}
|
|
184
|
-
reject(new Error('action expired'));
|
|
185
|
-
}, action.expiresAt - Date.now());
|
|
186
|
-
pending = {
|
|
239
|
+
const entry = {
|
|
187
240
|
action: JSON.parse(JSON.stringify(action)),
|
|
188
241
|
busy: false,
|
|
189
242
|
resolve: resolve,
|
|
190
243
|
reject,
|
|
191
244
|
verify: verify,
|
|
192
|
-
timer
|
|
245
|
+
timer: setTimeout(() => {
|
|
246
|
+
if (pending === entry) {
|
|
247
|
+
pending = undefined;
|
|
248
|
+
armIdle();
|
|
249
|
+
}
|
|
250
|
+
reject(entry.sending ? uncertain(entry) : expiredError(entry.failure));
|
|
251
|
+
}, action.expiresAt - Date.now()),
|
|
193
252
|
};
|
|
253
|
+
pending = entry;
|
|
254
|
+
notify(waitingLine(action.expiresAt - Date.now()));
|
|
194
255
|
}),
|
|
195
256
|
};
|
|
196
257
|
}
|
|
@@ -176,6 +176,10 @@ async function buy(ctx, preparedBuy) {
|
|
|
176
176
|
return true;
|
|
177
177
|
if (!saved)
|
|
178
178
|
return Date.now() / 1000 < claimUntil;
|
|
179
|
+
// A NEAR send that may be on chain blocks another payment until reconciled or --force,
|
|
180
|
+
// even after the purchase expired: a late transfer still moves tokens.
|
|
181
|
+
if (saved.nearSubmission?.state === 'uncertain')
|
|
182
|
+
return true;
|
|
179
183
|
return (Number(saved.purchase.expires_at) > Date.now() / 1000 ||
|
|
180
184
|
(saved.purchase.route === 'base-usdc' && saved.attempts.length > 0));
|
|
181
185
|
},
|
|
@@ -277,11 +281,14 @@ async function buy(ctx, preparedBuy) {
|
|
|
277
281
|
: bought.instructions
|
|
278
282
|
? 'deposit_required'
|
|
279
283
|
: 'submitted',
|
|
280
|
-
|
|
284
|
+
// A browser send that failed after the wallet's send call has no hash to report.
|
|
285
|
+
...(bought.uncertain?.txHash ? { txHash: bought.uncertain.txHash } : {}),
|
|
281
286
|
backup: backupPath(c, bought.record.id),
|
|
282
287
|
...(bought.instructions ? { instructions: bought.instructions } : {}),
|
|
283
288
|
...(source ? { source } : {}),
|
|
284
|
-
next:
|
|
289
|
+
next: bought.uncertain
|
|
290
|
+
? `The wallet may have sent this payment. Do not pay again. Run annon status ${bought.record.id} until it is accepted.`
|
|
291
|
+
: 'Fund the deposit if instructed, then check status and open this purchase.',
|
|
285
292
|
};
|
|
286
293
|
}
|
|
287
294
|
catch (error) {
|
package/dist/node/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { publicError, usage } from './errors.js';
|
|
3
|
+
import { version } from './version.js';
|
|
3
4
|
const common = [
|
|
4
5
|
'no-open',
|
|
5
6
|
'json',
|
|
@@ -57,6 +58,7 @@ const perCommand = {
|
|
|
57
58
|
verify: [],
|
|
58
59
|
status: [],
|
|
59
60
|
};
|
|
61
|
+
const bareInit = ['json', 'config', 'stateDir', 'backupSecretFile'];
|
|
60
62
|
export function parseCli(argv) {
|
|
61
63
|
try {
|
|
62
64
|
const keys = [...new Set([...common, ...Object.values(perCommand).flat()])];
|
|
@@ -117,7 +119,10 @@ export function parseCli(argv) {
|
|
|
117
119
|
else if (!values.price || !values.split)
|
|
118
120
|
throw usage();
|
|
119
121
|
}
|
|
120
|
-
|
|
122
|
+
// Bare init only sets up the backup secret; signer and deployment options need --agent.
|
|
123
|
+
if (cmd === 'init' &&
|
|
124
|
+
values.agent !== true &&
|
|
125
|
+
Object.keys(values).some((k) => !bareInit.includes(k)))
|
|
121
126
|
throw usage();
|
|
122
127
|
if (cmd === 'recover' && !values.recovery)
|
|
123
128
|
throw usage();
|
|
@@ -136,10 +141,12 @@ export function parseCli(argv) {
|
|
|
136
141
|
throw usage();
|
|
137
142
|
}
|
|
138
143
|
}
|
|
144
|
+
const versionFlags = ['--version', '-v'];
|
|
139
145
|
const HELP = `annon <command> [options]
|
|
140
146
|
shops [--search <q>] [--limit <n>] [--offset <n>]
|
|
141
147
|
shop add <url> [--id <alias>]
|
|
142
148
|
catalog [--shop <id>] [--search <q>] [--limit <n>] [--offset <n>]
|
|
149
|
+
init [--backup-secret-file <file>] [--state-dir <dir>] [--config <file>]
|
|
143
150
|
login [--wallet evm|near] [--no-open]
|
|
144
151
|
init --agent --daily-budget <USDC> [--recovery <kind:id>] [--key-env <name> | --keychain-entry <name> | --new-key | --signer <https origin> (--credential-env <name> | --credential-keychain <name>)]
|
|
145
152
|
share <file> --price <USDC> [--shop <id>] [--title <title>] [--description <text>] [--listing-storage <NEAR>] [--storage-budget <NEAR>]
|
|
@@ -158,9 +165,11 @@ Common: --json --config <file> --contract-id <account> --near-rpc <url> --base-r
|
|
|
158
165
|
--network <near:mainnet> --state-dir <dir> --relayer <url> --trust-contract
|
|
159
166
|
--wallet evm|solana|near --evm-key-file <file> --solana-key-file <file> --near-key-file <file>
|
|
160
167
|
--near-account <account> --near-public-key <ed25519:key> --backup-secret-file <file>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
168
|
+
Backup secret: run init once; it creates ~/.config/annon/backup-secret (owner-only) and never
|
|
169
|
+
overwrites. --backup-secret-file or ANNON_BACKUP_SECRET_FILE, then ANNON_BACKUP_SECRET, override it.
|
|
170
|
+
Secrets: env ANNON_EVM_PRIVATE_KEY, ANNON_SOLANA_KEYPAIR, ANNON_NEAR_CREDENTIALS; or explicitly
|
|
171
|
+
named files. See packages/annon/CLI.md. --version prints the installed version.
|
|
172
|
+
Exit: 0 success (buy may need deposit), 1 operation failed, 2 arguments/config, 3 trust required, 4 purchase pending or uncertain.
|
|
164
173
|
`;
|
|
165
174
|
export async function runCli(argv, execute, write) {
|
|
166
175
|
const json = argv.includes('--json') || argv.includes('--json=true');
|
|
@@ -168,6 +177,12 @@ export async function runCli(argv, execute, write) {
|
|
|
168
177
|
write(HELP);
|
|
169
178
|
return 0;
|
|
170
179
|
}
|
|
180
|
+
// Answered before any config, agent policy, wallet, network or backup-secret step.
|
|
181
|
+
if (argv.some((a) => versionFlags.includes(a)) &&
|
|
182
|
+
argv.every((a) => [...versionFlags, '--json'].includes(a))) {
|
|
183
|
+
write(json ? JSON.stringify({ ok: true, result: { version } }) : version);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
171
186
|
try {
|
|
172
187
|
const input = parseCli(argv);
|
|
173
188
|
const result = await execute(input);
|
|
@@ -185,11 +200,12 @@ export async function runCli(argv, execute, write) {
|
|
|
185
200
|
show(item);
|
|
186
201
|
}
|
|
187
202
|
write(JSON.stringify({ ok: true, result }, null, json ? undefined : 2));
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
// 4: not settled. A pending status, or a buy whose payment may be on chain (uncertain).
|
|
204
|
+
return result &&
|
|
190
205
|
typeof result === 'object' &&
|
|
191
206
|
'state' in result &&
|
|
192
|
-
result.state === 'pending'
|
|
207
|
+
((input.command === 'status' && result.state === 'pending') ||
|
|
208
|
+
(input.command === 'buy' && result.state === 'uncertain'))
|
|
193
209
|
? 4
|
|
194
210
|
: 0;
|
|
195
211
|
}
|
|
@@ -65,7 +65,7 @@ async function pendingSetup(ctx, purchaseId) {
|
|
|
65
65
|
return {
|
|
66
66
|
purchaseId,
|
|
67
67
|
state: 'pending',
|
|
68
|
-
next: 'Check the payment
|
|
68
|
+
next: 'Check the payment. To resume Base unlock, keep the saved state readable and restore its matching backup secret (or select its existing copy with ANNON_BACKUP_SECRET_FILE). 1Click SUCCESS alone is not acceptance.',
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
/** Loads the command's wallet once; later calls reuse it. */
|
package/dist/node/config.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface Config extends DiscoveryConfig {
|
|
|
32
32
|
bps: number;
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
/** Human default for saved records and backups; agent and explicit settings override it. */
|
|
36
|
+
export declare const defaultStateDir: () => string;
|
|
35
37
|
export declare function loadConfig(o: Input, env: Environment, login?: boolean, forcedAgent?: boolean): Promise<Config>;
|
|
36
38
|
export type TrustPrompt = (description: string) => Promise<boolean>;
|
|
37
39
|
export declare function trustContract(c: Config, explicit: boolean, prompt?: TrustPrompt): Promise<void>;
|
package/dist/node/config.js
CHANGED
|
@@ -8,6 +8,8 @@ import { validateDiscoveryConfig } from '../shops/discovery.js';
|
|
|
8
8
|
import { agentPolicy, configPath, validateSigner } from './agent.js';
|
|
9
9
|
import { AdapterError } from './errors.js';
|
|
10
10
|
import { resourceAllowed } from './resources.js';
|
|
11
|
+
/** Human default for saved records and backups; agent and explicit settings override it. */
|
|
12
|
+
export const defaultStateDir = () => join(homedir(), '.local', 'share', 'annon');
|
|
11
13
|
export async function loadConfig(o, env, login = false, forcedAgent = false) {
|
|
12
14
|
const pinnedAgent = await agentPolicy(o, env, forcedAgent);
|
|
13
15
|
try {
|
|
@@ -41,7 +43,7 @@ export async function loadConfig(o, env, login = false, forcedAgent = false) {
|
|
|
41
43
|
stateDir: o.stateDir ??
|
|
42
44
|
(agent ? undefined : env.ANNON_STATE_DIR) ??
|
|
43
45
|
file.stateDir ??
|
|
44
|
-
|
|
46
|
+
defaultStateDir(),
|
|
45
47
|
};
|
|
46
48
|
Object.assign(c, validateDiscoveryConfig(file));
|
|
47
49
|
const workDir = (env.ANNON_AGENT === 'true' ? env.ANNON_WORK_DIR : undefined) ?? file.workDir;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval page shell and its stylesheet. Both are static: the page script fills in every
|
|
3
|
+
* request value as text. The stylesheet is served from the page's own origin (`style-src 'self'`),
|
|
4
|
+
* with no inline styles, web fonts or other resources.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LOGIN_PAGE_HTML = "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light\"><title>Annon wallet approval</title><link rel=\"stylesheet\" href=\"/app.css\"><script src=\"/app.js\" defer></script></head><body><main class=\"card\">\n<header class=\"brand\"><svg class=\"mark\" viewBox=\"0 0 24 28\" width=\"22\" height=\"26\" aria-hidden=\"true\"><path d=\"M2 27V12a10 10 0 0 1 20 0v15h-5V12a5 5 0 0 0-10 0v15z\"/></svg><p class=\"eyebrow\">ANNON CLI</p></header>\n<h1 id=\"title\">Waiting for the CLI</h1>\n<section class=\"pair\" aria-labelledby=\"pair-label\"><p class=\"label\" id=\"pair-label\">Pairing code</p><p id=\"pair\" class=\"code\">------</p><p class=\"hint\">Check that your terminal shows the same code.</p></section>\n<section id=\"summary\" class=\"summary\" hidden><p id=\"amount\" class=\"amount\" hidden></p><dl id=\"rows\" class=\"rows\"></dl><div id=\"message-box\" class=\"message\" hidden><p id=\"message-label\" class=\"label\"></p><pre id=\"message\"></pre></div><ul id=\"warnings\" class=\"warnings\" hidden></ul><div id=\"terms-box\" class=\"terms\" hidden><p class=\"label\">Terms from the CLI</p><p class=\"hint\">For review. Your wallet signs only the request above.</p><dl id=\"terms\" class=\"rows\"></dl></div><p id=\"note\" class=\"note\"></p></section>\n<section class=\"act\"><div id=\"wallet-field\" class=\"field\" hidden><span class=\"label\" id=\"near-wallet-label\">NEAR wallet</span><div class=\"picker\"><div id=\"near-wallet\" class=\"picker-button\" role=\"combobox\" tabindex=\"0\" aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-controls=\"near-wallet-list\" aria-labelledby=\"near-wallet-label near-wallet-value\"><span id=\"near-wallet-value\">Meteor Wallet</span></div><ul id=\"near-wallet-list\" class=\"picker-list\" role=\"listbox\" aria-labelledby=\"near-wallet-label\" hidden><li id=\"near-wallet-meteor\" class=\"option\" role=\"option\" aria-selected=\"true\">Meteor Wallet<span class=\"option-note\">Extension or web popup</span></li><li id=\"near-wallet-hot\" class=\"option\" role=\"option\" aria-selected=\"false\">HOT Wallet<span class=\"option-note\">Mainnet only</span></li></ul></div></div><div class=\"buttons\"><button id=\"approve\" class=\"primary\" disabled>Review in wallet</button><button id=\"cancel\" class=\"secondary\" hidden>Cancel</button></div><p id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></p></section>\n<details class=\"details\"><summary>Technical details</summary><pre id=\"action\"></pre></details>\n</main></body></html>";
|
|
7
|
+
export declare const LOGIN_PAGE_CSS = ":root{--paper:#FAF8F2;--alt:#F1EDE3;--ink:#111111;--soft:#2E2C28;--muted:#4A4741;--line:#D9D3C7;--red:#B3261E;--serif:Newsreader,\"Iowan Old Style\",Georgia,serif;--mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Menlo,monospace}\n*{box-sizing:border-box;border-radius:0}\nhtml{background:var(--paper)}\nbody{margin:0;background:var(--paper);color:var(--ink);font:16px/1.5 var(--serif);-webkit-text-size-adjust:100%}\n.card{max-width:640px;margin:0 auto;padding:40px 16px 56px}\n.brand{display:flex;align-items:center;gap:10px;margin-bottom:20px}\n.mark{fill:var(--ink);flex:none}\n.eyebrow,.label,dt,.details summary,button{font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;font-size:12px}\n.eyebrow{margin:0;color:var(--soft)}\nh1{font-weight:500;font-size:clamp(28px,6vw,36px);line-height:1.15;margin:0 0 24px;letter-spacing:-.01em}\n.label{margin:0 0 6px;color:var(--muted)}\n.hint{margin:6px 0 0;color:var(--muted);font-size:15px}\n.pair{border:1px solid var(--ink);background:var(--alt);padding:16px 20px;margin-bottom:28px}\n.code{margin:0;font:600 clamp(30px,9vw,40px)/1.1 var(--mono);letter-spacing:.18em;word-break:break-all}\n.summary{border-top:1px solid var(--line);padding-top:20px}\n.amount{margin:0 0 12px;font-size:clamp(30px,8vw,42px);line-height:1.1}\n.rows{margin:0}\n.row{display:grid;grid-template-columns:minmax(0,11em) minmax(0,1fr);gap:4px 16px;padding:9px 0;border-bottom:1px solid var(--line)}\ndt{color:var(--muted);padding-top:3px}\ndd{margin:0;color:var(--soft);overflow-wrap:anywhere}\ndd.mono{font:14px/1.5 var(--mono)}\n.message,.terms{margin-top:20px}\npre{margin:0;padding:14px 16px;background:var(--alt);border:1px solid var(--line);font:13px/1.55 var(--mono);color:var(--soft);white-space:pre-wrap;overflow-wrap:anywhere;max-height:22em;overflow:auto}\n.warnings{margin:20px 0 0;padding:12px 16px 12px 32px;border:1px solid var(--red);color:var(--red)}\n.note{margin:20px 0 0;color:var(--soft)}\n.act{margin-top:28px;border-top:1px solid var(--line);padding-top:20px}\n.field{margin:0 0 16px;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}\n.picker{position:relative;flex:0 1 20em;min-width:0}\n.picker-button{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--paper);color:var(--ink);border:1px solid var(--ink);padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);user-select:none}\n.picker-button::after{content:\"\";width:0;height:0;border:5px solid transparent;border-top-color:var(--ink);border-bottom:0;flex:none}\n.picker-button[aria-expanded=\"true\"]::after{border-top:0;border-bottom:5px solid var(--ink)}\n.picker-button[aria-disabled=\"true\"]{opacity:.4;cursor:not-allowed}\n.picker-list{position:absolute;z-index:2;left:0;right:0;top:100%;margin:0;padding:0;list-style:none;background:var(--paper);border:1px solid var(--ink);border-top:0}\n.option{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);color:var(--soft)}\n.option+.option{border-top:1px solid var(--line)}\n.option.active{background:var(--alt);color:var(--ink)}\n.option[aria-selected=\"true\"]{color:var(--ink);font-weight:600}\n.option[aria-selected=\"true\"]::before{content:\"\";width:6px;height:6px;background:var(--ink);flex:none;align-self:center}\n.option[aria-disabled=\"true\"]{color:var(--muted);cursor:not-allowed;opacity:.6}\n.option-note{margin-left:auto;font-size:11px;font-weight:400;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}\n.buttons{display:flex;flex-wrap:wrap;gap:12px}\nbutton{padding:14px 22px;border:1px solid var(--ink);cursor:pointer}\n.primary{background:var(--ink);color:var(--paper)}\n.secondary{background:transparent;color:var(--ink)}\nbutton:disabled{cursor:not-allowed;opacity:.4}\nbutton:focus-visible,.picker-button:focus-visible,summary:focus-visible{outline:2px solid var(--red);outline-offset:2px}\n.status{min-height:1.5em;margin:16px 0 0;color:var(--soft)}\n.status.wait{color:var(--muted)}\n.status.ok{color:var(--ink);border-left:3px solid var(--ink);padding-left:12px}\n.status.error{color:var(--red);border-left:3px solid var(--red);padding-left:12px}\n.details{margin-top:32px;border-top:1px solid var(--line);padding-top:12px}\n.details summary{cursor:pointer;color:var(--muted);padding:6px 0}\n.details pre{margin-top:10px}\n[hidden]{display:none!important}\n@media (max-width:480px){.row{grid-template-columns:1fr}.card{padding-top:28px}button{flex:1 1 100%}.picker{flex-basis:100%}}\n";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval page shell and its stylesheet. Both are static: the page script fills in every
|
|
3
|
+
* request value as text. The stylesheet is served from the page's own origin (`style-src 'self'`),
|
|
4
|
+
* with no inline styles, web fonts or other resources.
|
|
5
|
+
*/
|
|
6
|
+
/** A small arch: the Annon mark. Inline SVG, so it needs no image source. */
|
|
7
|
+
const MARK = '<svg class="mark" viewBox="0 0 24 28" width="22" height="26" aria-hidden="true"><path d="M2 27V12a10 10 0 0 1 20 0v15h-5V12a5 5 0 0 0-10 0v15z"/></svg>';
|
|
8
|
+
export const LOGIN_PAGE_HTML = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light"><title>Annon wallet approval</title><link rel="stylesheet" href="/app.css"><script src="/app.js" defer></script></head><body><main class="card">
|
|
9
|
+
<header class="brand">${MARK}<p class="eyebrow">ANNON CLI</p></header>
|
|
10
|
+
<h1 id="title">Waiting for the CLI</h1>
|
|
11
|
+
<section class="pair" aria-labelledby="pair-label"><p class="label" id="pair-label">Pairing code</p><p id="pair" class="code">------</p><p class="hint">Check that your terminal shows the same code.</p></section>
|
|
12
|
+
<section id="summary" class="summary" hidden><p id="amount" class="amount" hidden></p><dl id="rows" class="rows"></dl><div id="message-box" class="message" hidden><p id="message-label" class="label"></p><pre id="message"></pre></div><ul id="warnings" class="warnings" hidden></ul><div id="terms-box" class="terms" hidden><p class="label">Terms from the CLI</p><p class="hint">For review. Your wallet signs only the request above.</p><dl id="terms" class="rows"></dl></div><p id="note" class="note"></p></section>
|
|
13
|
+
<section class="act"><div id="wallet-field" class="field" hidden><span class="label" id="near-wallet-label">NEAR wallet</span><div class="picker"><div id="near-wallet" class="picker-button" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="near-wallet-list" aria-labelledby="near-wallet-label near-wallet-value"><span id="near-wallet-value">Meteor Wallet</span></div><ul id="near-wallet-list" class="picker-list" role="listbox" aria-labelledby="near-wallet-label" hidden><li id="near-wallet-meteor" class="option" role="option" aria-selected="true">Meteor Wallet<span class="option-note">Extension or web popup</span></li><li id="near-wallet-hot" class="option" role="option" aria-selected="false">HOT Wallet<span class="option-note">Mainnet only</span></li></ul></div></div><div class="buttons"><button id="approve" class="primary" disabled>Review in wallet</button><button id="cancel" class="secondary" hidden>Cancel</button></div><p id="status" class="status" role="status" aria-live="polite"></p></section>
|
|
14
|
+
<details class="details"><summary>Technical details</summary><pre id="action"></pre></details>
|
|
15
|
+
</main></body></html>`;
|
|
16
|
+
export const LOGIN_PAGE_CSS = `:root{--paper:#FAF8F2;--alt:#F1EDE3;--ink:#111111;--soft:#2E2C28;--muted:#4A4741;--line:#D9D3C7;--red:#B3261E;--serif:Newsreader,"Iowan Old Style",Georgia,serif;--mono:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace}
|
|
17
|
+
*{box-sizing:border-box;border-radius:0}
|
|
18
|
+
html{background:var(--paper)}
|
|
19
|
+
body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.5 var(--serif);-webkit-text-size-adjust:100%}
|
|
20
|
+
.card{max-width:640px;margin:0 auto;padding:40px 16px 56px}
|
|
21
|
+
.brand{display:flex;align-items:center;gap:10px;margin-bottom:20px}
|
|
22
|
+
.mark{fill:var(--ink);flex:none}
|
|
23
|
+
.eyebrow,.label,dt,.details summary,button{font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;font-size:12px}
|
|
24
|
+
.eyebrow{margin:0;color:var(--soft)}
|
|
25
|
+
h1{font-weight:500;font-size:clamp(28px,6vw,36px);line-height:1.15;margin:0 0 24px;letter-spacing:-.01em}
|
|
26
|
+
.label{margin:0 0 6px;color:var(--muted)}
|
|
27
|
+
.hint{margin:6px 0 0;color:var(--muted);font-size:15px}
|
|
28
|
+
.pair{border:1px solid var(--ink);background:var(--alt);padding:16px 20px;margin-bottom:28px}
|
|
29
|
+
.code{margin:0;font:600 clamp(30px,9vw,40px)/1.1 var(--mono);letter-spacing:.18em;word-break:break-all}
|
|
30
|
+
.summary{border-top:1px solid var(--line);padding-top:20px}
|
|
31
|
+
.amount{margin:0 0 12px;font-size:clamp(30px,8vw,42px);line-height:1.1}
|
|
32
|
+
.rows{margin:0}
|
|
33
|
+
.row{display:grid;grid-template-columns:minmax(0,11em) minmax(0,1fr);gap:4px 16px;padding:9px 0;border-bottom:1px solid var(--line)}
|
|
34
|
+
dt{color:var(--muted);padding-top:3px}
|
|
35
|
+
dd{margin:0;color:var(--soft);overflow-wrap:anywhere}
|
|
36
|
+
dd.mono{font:14px/1.5 var(--mono)}
|
|
37
|
+
.message,.terms{margin-top:20px}
|
|
38
|
+
pre{margin:0;padding:14px 16px;background:var(--alt);border:1px solid var(--line);font:13px/1.55 var(--mono);color:var(--soft);white-space:pre-wrap;overflow-wrap:anywhere;max-height:22em;overflow:auto}
|
|
39
|
+
.warnings{margin:20px 0 0;padding:12px 16px 12px 32px;border:1px solid var(--red);color:var(--red)}
|
|
40
|
+
.note{margin:20px 0 0;color:var(--soft)}
|
|
41
|
+
.act{margin-top:28px;border-top:1px solid var(--line);padding-top:20px}
|
|
42
|
+
.field{margin:0 0 16px;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}
|
|
43
|
+
.picker{position:relative;flex:0 1 20em;min-width:0}
|
|
44
|
+
.picker-button{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--paper);color:var(--ink);border:1px solid var(--ink);padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);user-select:none}
|
|
45
|
+
.picker-button::after{content:"";width:0;height:0;border:5px solid transparent;border-top-color:var(--ink);border-bottom:0;flex:none}
|
|
46
|
+
.picker-button[aria-expanded="true"]::after{border-top:0;border-bottom:5px solid var(--ink)}
|
|
47
|
+
.picker-button[aria-disabled="true"]{opacity:.4;cursor:not-allowed}
|
|
48
|
+
.picker-list{position:absolute;z-index:2;left:0;right:0;top:100%;margin:0;padding:0;list-style:none;background:var(--paper);border:1px solid var(--ink);border-top:0}
|
|
49
|
+
.option{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);color:var(--soft)}
|
|
50
|
+
.option+.option{border-top:1px solid var(--line)}
|
|
51
|
+
.option.active{background:var(--alt);color:var(--ink)}
|
|
52
|
+
.option[aria-selected="true"]{color:var(--ink);font-weight:600}
|
|
53
|
+
.option[aria-selected="true"]::before{content:"";width:6px;height:6px;background:var(--ink);flex:none;align-self:center}
|
|
54
|
+
.option[aria-disabled="true"]{color:var(--muted);cursor:not-allowed;opacity:.6}
|
|
55
|
+
.option-note{margin-left:auto;font-size:11px;font-weight:400;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
56
|
+
.buttons{display:flex;flex-wrap:wrap;gap:12px}
|
|
57
|
+
button{padding:14px 22px;border:1px solid var(--ink);cursor:pointer}
|
|
58
|
+
.primary{background:var(--ink);color:var(--paper)}
|
|
59
|
+
.secondary{background:transparent;color:var(--ink)}
|
|
60
|
+
button:disabled{cursor:not-allowed;opacity:.4}
|
|
61
|
+
button:focus-visible,.picker-button:focus-visible,summary:focus-visible{outline:2px solid var(--red);outline-offset:2px}
|
|
62
|
+
.status{min-height:1.5em;margin:16px 0 0;color:var(--soft)}
|
|
63
|
+
.status.wait{color:var(--muted)}
|
|
64
|
+
.status.ok{color:var(--ink);border-left:3px solid var(--ink);padding-left:12px}
|
|
65
|
+
.status.error{color:var(--red);border-left:3px solid var(--red);padding-left:12px}
|
|
66
|
+
.details{margin-top:32px;border-top:1px solid var(--line);padding-top:12px}
|
|
67
|
+
.details summary{cursor:pointer;color:var(--muted);padding:6px 0}
|
|
68
|
+
.details pre{margin-top:10px}
|
|
69
|
+
[hidden]{display:none!important}
|
|
70
|
+
@media (max-width:480px){.row{grid-template-columns:1fr}.card{padding-top:28px}button{flex:1 1 100%}.picker{flex-basis:100%}}
|
|
71
|
+
`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CommandSession, RequestContext } from './command-context.js';
|
|
2
2
|
/**
|
|
3
|
-
* `status`, first step: a purchase already accepted on chain
|
|
4
|
-
*
|
|
3
|
+
* `status`, first step: a purchase already accepted on chain. Returns nothing when the saved
|
|
4
|
+
* record has to be checked; a missing backup secret then reports the pending setup answer.
|
|
5
5
|
*/
|
|
6
6
|
declare function statusOnChain(ctx: RequestContext): Promise<Record<string, unknown> | undefined>;
|
|
7
7
|
/** `status` from the saved purchase record: accepted, or still pending with a retry delay. */
|
|
@@ -16,11 +16,11 @@ import { resourceAllowed } from './resources.js';
|
|
|
16
16
|
import { retryGate } from './retry-gate.js';
|
|
17
17
|
import { readBackupSecret } from './wallets.js';
|
|
18
18
|
/**
|
|
19
|
-
* `status`, first step: a purchase already accepted on chain
|
|
20
|
-
*
|
|
19
|
+
* `status`, first step: a purchase already accepted on chain. Returns nothing when the saved
|
|
20
|
+
* record has to be checked; a missing backup secret then reports the pending setup answer.
|
|
21
21
|
*/
|
|
22
22
|
async function statusOnChain(ctx) {
|
|
23
|
-
const { c, d, env, o, paymentDetails
|
|
23
|
+
const { c, d, env, o, paymentDetails } = ctx;
|
|
24
24
|
if (!o.target || !isId(o.target))
|
|
25
25
|
throw usage();
|
|
26
26
|
if (d.agent)
|
|
@@ -35,8 +35,6 @@ async function statusOnChain(ctx) {
|
|
|
35
35
|
next: 'open',
|
|
36
36
|
...(await paymentDetails(async () => encryptedPurchaseStore(fileStore(join(c.stateDir, 'records')), await readBackupSecret(o, env)).load(purchaseKey(o.target)))),
|
|
37
37
|
};
|
|
38
|
-
if (!o.backupSecretFile && !env.ANNON_BACKUP_SECRET_FILE && !env.ANNON_BACKUP_SECRET)
|
|
39
|
-
return pendingSetup(o.target);
|
|
40
38
|
}
|
|
41
39
|
/** `status` from the saved purchase record: accepted, or still pending with a retry delay. */
|
|
42
40
|
async function status(ctx) {
|
|
@@ -66,14 +64,20 @@ async function status(ctx) {
|
|
|
66
64
|
}
|
|
67
65
|
if (d.agent && !accepted)
|
|
68
66
|
await retryGate({ ...c, purchaseId: o.target }).defer('settlement_pending', 30);
|
|
67
|
+
// A NEAR send that may be on chain: pending is not proof that it failed.
|
|
68
|
+
const uncertain = !accepted && record.nearSubmission?.state === 'uncertain';
|
|
69
69
|
return {
|
|
70
70
|
purchaseId: o.target,
|
|
71
71
|
state: accepted ? 'accepted' : 'pending',
|
|
72
|
+
...(uncertain ? { submission: 'uncertain' } : {}),
|
|
73
|
+
...(uncertain && record.nearSubmission?.txHash ? { txHash: record.nearSubmission.txHash } : {}),
|
|
72
74
|
...(await paymentDetails(async () => record)),
|
|
73
75
|
...(accepted ? {} : { retryAfterSeconds: 30 }),
|
|
74
76
|
next: accepted
|
|
75
77
|
? 'open'
|
|
76
|
-
:
|
|
78
|
+
: uncertain
|
|
79
|
+
? 'The wallet may have sent this payment. Do not pay again. Retry status until it is accepted or the purchase expires.'
|
|
80
|
+
: 'Check the payment and retry status. 1Click SUCCESS alone is not acceptance.',
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
/** `recover`: purchases found for the recovery wallet, with per-purchase public errors. */
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Input } from './cli.js';
|
|
2
|
+
import type { Environment } from './wallets.js';
|
|
3
|
+
/** Directory entries only: this cannot see unrelated custom directories or offline backups. */
|
|
4
|
+
export declare function assertNoSavedState(o: Partial<Input>, env: Environment): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Refuses a new backup secret where saved state already depends on an older one. */
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { configPath } from './agent.js';
|
|
5
|
+
import { defaultStateDir } from './config.js';
|
|
6
|
+
import { AdapterError } from './errors.js';
|
|
7
|
+
const savedState = () => new AdapterError('backup_secret_required', 'Saved records or backups already exist in the state directory or in ~/.local/share/annon; a new secret needs both empty. Restore their matching backup secret or select its existing file, because a new secret cannot decrypt them. To recover with a new secret instead, first move the whole old state, including ~/.local/share/annon, to a separate private archive and keep it.', 2);
|
|
8
|
+
const uncheckable = () => new AdapterError('backup_secret_required', 'Annon could not check the state directory for saved records or backups (unreadable config or state). Fix it before creating a backup secret.', 2);
|
|
9
|
+
async function hasEntries(dir) {
|
|
10
|
+
try {
|
|
11
|
+
return (await readdir(dir)).length > 0;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === 'ENOENT')
|
|
15
|
+
return false;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Human state precedence (flag, environment, config, default), read locally without the
|
|
21
|
+
* deployment checks of loadConfig. The legacy default is checked too when it differs.
|
|
22
|
+
*/
|
|
23
|
+
async function stateDirs(o, env) {
|
|
24
|
+
let file = {};
|
|
25
|
+
try {
|
|
26
|
+
file = JSON.parse(await readFile(configPath(o, env), 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT')
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
const stateDir = o.stateDir ?? env.ANNON_STATE_DIR ?? file.stateDir ?? defaultStateDir();
|
|
33
|
+
if (typeof stateDir !== 'string' || !stateDir)
|
|
34
|
+
throw new Error('invalid state directory');
|
|
35
|
+
return new Set([resolve(stateDir), resolve(defaultStateDir())]);
|
|
36
|
+
}
|
|
37
|
+
/** Directory entries only: this cannot see unrelated custom directories or offline backups. */
|
|
38
|
+
export async function assertNoSavedState(o, env) {
|
|
39
|
+
let found = false;
|
|
40
|
+
try {
|
|
41
|
+
for (const dir of await stateDirs(o, env))
|
|
42
|
+
for (const name of ['records', 'backups'])
|
|
43
|
+
found ||= await hasEntries(join(dir, name));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw uncheckable();
|
|
47
|
+
}
|
|
48
|
+
if (found)
|
|
49
|
+
throw savedState();
|
|
50
|
+
}
|
|
@@ -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),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const version: string;
|
package/dist/node/wallets.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import type { SplitCall } from '../pay/split.js';
|
|
|
3
3
|
import { type IdentityWallet } from '../wallet/wallet.js';
|
|
4
4
|
import type { Input } from './cli.js';
|
|
5
5
|
import { type Keychain } from './keychain.js';
|
|
6
|
+
export { readBackupSecret } from './backup-secret.js';
|
|
6
7
|
export type Environment = Record<string, string | undefined>;
|
|
7
8
|
export type LocalWallet = IdentityWallet & {
|
|
8
9
|
signUpload?: (intent: UploadIntent) => Promise<UploadAuth>;
|
|
9
10
|
sendBase?: (call: SplitCall, rpc: string) => Promise<void>;
|
|
10
11
|
};
|
|
11
12
|
export declare function identity(value: string): import("../settle-near/identity.js").Identity;
|
|
12
|
-
export declare function readBackupSecret(o: Partial<Input>, env: Environment): Promise<Uint8Array>;
|
|
13
13
|
export declare function loadWallet(o: Partial<Input>, env: Environment, nearRpc: string, keychain?: Keychain): Promise<LocalWallet>;
|
package/dist/node/wallets.js
CHANGED
|
@@ -7,11 +7,12 @@ import { privateKeyToAccount } from 'viem/accounts';
|
|
|
7
7
|
import { base } from 'viem/chains';
|
|
8
8
|
import { signNearUpload } from '../browser/wallets.js';
|
|
9
9
|
import { nep413Hash } from '../near/nep413.js';
|
|
10
|
-
import {
|
|
10
|
+
import { hex } from '../settle-near/codec.js';
|
|
11
11
|
import { identityJson, parseIdentity } from '../settle-near/identity.js';
|
|
12
12
|
import { nearKeyPairWallet } from '../wallet/wallet.js';
|
|
13
13
|
import { AdapterError } from './errors.js';
|
|
14
14
|
import { decodeKeychain, osKeychain } from './keychain.js';
|
|
15
|
+
export { readBackupSecret } from './backup-secret.js';
|
|
15
16
|
const credentialsError = () => new AdapterError('credentials_required', 'Select a wallet and provide its key through the documented environment variable or a named key file. Check the recovery identity matches.', 2);
|
|
16
17
|
export function identity(value) {
|
|
17
18
|
const at = value.indexOf(':');
|
|
@@ -24,15 +25,6 @@ async function source(file, env) {
|
|
|
24
25
|
return env.trim();
|
|
25
26
|
throw credentialsError();
|
|
26
27
|
}
|
|
27
|
-
export async function readBackupSecret(o, env) {
|
|
28
|
-
try {
|
|
29
|
-
const text = await source(o.backupSecretFile ?? env.ANNON_BACKUP_SECRET_FILE, env.ANNON_BACKUP_SECRET);
|
|
30
|
-
return bytes(text.startsWith('0x') ? text : `0x${text}`, 32);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
throw new AdapterError('backup_secret_required', 'Provide a 32-byte random backup secret as 64 hex characters in ANNON_BACKUP_SECRET or a named backup secret file.', 2);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
28
|
export async function loadWallet(o, env, nearRpc, keychain = osKeychain) {
|
|
37
29
|
try {
|
|
38
30
|
const selected = o.wallet ?? (o.recovery ? identity(o.recovery).kind : env.ANNON_WALLET);
|