@defdone/annon 0.1.0-alpha.1 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CLI.md +116 -10
  3. package/README.md +41 -9
  4. package/dist/api/annon-types.d.ts +2 -1
  5. package/dist/api/annon.js +9 -2
  6. package/dist/browser/index.d.ts +1 -1
  7. package/dist/browser/login-bundle.js +61 -60
  8. package/dist/browser/login-dom.d.ts +13 -0
  9. package/dist/browser/login-dom.js +67 -0
  10. package/dist/browser/login-dropdown.d.ts +32 -0
  11. package/dist/browser/login-dropdown.js +129 -0
  12. package/dist/browser/login-format.d.ts +16 -0
  13. package/dist/browser/login-format.js +52 -0
  14. package/dist/browser/login-status.d.ts +70 -0
  15. package/dist/browser/login-status.js +85 -0
  16. package/dist/browser/login-terms.d.ts +10 -0
  17. package/dist/browser/login-terms.js +69 -0
  18. package/dist/browser/login-view.d.ts +34 -0
  19. package/dist/browser/login-view.js +227 -0
  20. package/dist/browser/login-wallet.d.ts +28 -0
  21. package/dist/browser/login-wallet.js +179 -0
  22. package/dist/browser/login.js +115 -140
  23. package/dist/browser/purchase-bundle.js +29 -29
  24. package/dist/browser/purchases.d.ts +4 -0
  25. package/dist/browser/purchases.js +14 -0
  26. package/dist/browser/reconcile.d.ts +41 -0
  27. package/dist/browser/reconcile.js +77 -0
  28. package/dist/node/adapter.js +5 -2
  29. package/dist/node/approval-status.d.ts +34 -0
  30. package/dist/node/approval-status.js +71 -0
  31. package/dist/node/backup-secret-init.d.ts +9 -0
  32. package/dist/node/backup-secret-init.js +103 -0
  33. package/dist/node/backup-secret.d.ts +35 -0
  34. package/dist/node/backup-secret.js +104 -0
  35. package/dist/node/browser-login.d.ts +2 -0
  36. package/dist/node/browser-login.js +67 -13
  37. package/dist/node/buy-commands.js +9 -2
  38. package/dist/node/cli.js +23 -7
  39. package/dist/node/command-context.js +1 -1
  40. package/dist/node/config.d.ts +2 -0
  41. package/dist/node/config.js +3 -1
  42. package/dist/node/login-page.d.ts +7 -0
  43. package/dist/node/login-page.js +71 -0
  44. package/dist/node/purchase-commands.d.ts +2 -2
  45. package/dist/node/purchase-commands.js +10 -6
  46. package/dist/node/saved-state.d.ts +4 -0
  47. package/dist/node/saved-state.js +50 -0
  48. package/dist/node/version.d.ts +1 -0
  49. package/dist/node/version.js +3 -0
  50. package/dist/node/wallets.d.ts +1 -1
  51. package/dist/node/wallets.js +2 -10
  52. package/dist/pay/purchase.d.ts +2 -1
  53. package/dist/wallet/uncertain.d.ts +6 -3
  54. package/dist/wallet/uncertain.js +4 -1
  55. package/package.json +1 -1
@@ -3,6 +3,8 @@ 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));
7
9
  /**
8
10
  * The approval page talks only to this loopback server, the NEAR RPC its Wallet Selector uses
@@ -12,6 +14,7 @@ const equal = (a, b) => Buffer.byteLength(a) === Buffer.byteLength(b) && timingS
12
14
  */
13
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'";
14
16
  export async function startBrowserSession(o) {
17
+ const notify = o.notify ?? ((line) => void process.stderr.write(`${line}\n`));
15
18
  let asset = o.asset;
16
19
  if (asset === undefined) {
17
20
  try {
@@ -41,6 +44,15 @@ export async function startBrowserSession(o) {
41
44
  .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
42
45
  .join(',')}}`
43
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';
44
56
  let pending;
45
57
  const server = createServer((req, res) => {
46
58
  void (async () => {
@@ -67,8 +79,12 @@ export async function startBrowserSession(o) {
67
79
  !equal(req.headers.authorization ?? '', `Bearer ${capability}`))
68
80
  return void send(403, '{}');
69
81
  if (path === '/api/action' && ['GET', 'POST'].includes(req.method ?? ''))
70
- return void send(200, JSON.stringify({ pairingCode, action: pending?.action ?? null }));
71
- if (path !== '/api/result' || req.method !== 'POST')
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')
72
88
  return void send(404, '{}');
73
89
  let size = 0;
74
90
  const parts = [];
@@ -79,6 +95,31 @@ export async function startBrowserSession(o) {
79
95
  parts.push(chunk);
80
96
  }
81
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
+ }
82
123
  const key = canonical(b.result);
83
124
  const previous = results.get(b.id);
84
125
  if (previous && Date.now() < previous.expiresAt) {
@@ -104,6 +145,7 @@ export async function startBrowserSession(o) {
104
145
  pending = undefined;
105
146
  armIdle();
106
147
  }
148
+ notify(APPROVED_LINE);
107
149
  p.resolve(b.result);
108
150
  complete(200);
109
151
  send(200, '{}');
@@ -114,7 +156,13 @@ export async function startBrowserSession(o) {
114
156
  pending = undefined;
115
157
  armIdle();
116
158
  }
117
- p.reject(error instanceof TransactionUncertain ? error : new Error('wallet result rejected'));
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'));
118
166
  complete(400);
119
167
  send(400, '{}');
120
168
  }
@@ -124,8 +172,10 @@ export async function startBrowserSession(o) {
124
172
  return void send(405, '{}');
125
173
  if (path === '/app.js')
126
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');
127
177
  if (path === '/' || path === '/callback')
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');
178
+ return void send(200, LOGIN_PAGE_HTML, 'text/html; charset=utf-8');
129
179
  send(404, '{}');
130
180
  })().catch(() => {
131
181
  if (!res.headersSent)
@@ -161,6 +211,9 @@ export async function startBrowserSession(o) {
161
211
  };
162
212
  armIdle();
163
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}/`));
164
217
  if (!o.noOpen)
165
218
  (o.open ??
166
219
  ((target) => {
@@ -183,21 +236,22 @@ export async function startBrowserSession(o) {
183
236
  return;
184
237
  }
185
238
  clearTimeout(timeout);
186
- const timer = setTimeout(() => {
187
- if (pending?.timer === timer) {
188
- pending = undefined;
189
- armIdle();
190
- }
191
- reject(new Error('action expired'));
192
- }, action.expiresAt - Date.now());
193
- pending = {
239
+ const entry = {
194
240
  action: JSON.parse(JSON.stringify(action)),
195
241
  busy: false,
196
242
  resolve: resolve,
197
243
  reject,
198
244
  verify: verify,
199
- 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()),
200
252
  };
253
+ pending = entry;
254
+ notify(waitingLine(action.expiresAt - Date.now()));
201
255
  }),
202
256
  };
203
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
- ...(bought.uncertain ? { txHash: bought.uncertain.txHash } : {}),
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: 'Fund the deposit if instructed, then check status and open this purchase.',
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
- if (cmd === 'init' && values.agent !== true)
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
- Secrets: env ANNON_EVM_PRIVATE_KEY, ANNON_SOLANA_KEYPAIR, ANNON_NEAR_CREDENTIALS,
162
- ANNON_BACKUP_SECRET (64 hex); or explicitly named files. See packages/annon/CLI.md.
163
- Exit: 0 success (buy may need deposit), 1 operation failed, 2 arguments/config, 3 trust required, 4 purchase pending.
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
- return input.command === 'status' &&
189
- result &&
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; configure readable saved state and the matching backup secret to resume Base unlock. 1Click SUCCESS alone is not acceptance.',
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. */
@@ -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>;
@@ -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
- join(homedir(), '.local', 'share', 'annon'),
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, or the pending answer when no
4
- * backup secret is configured. Returns nothing when the saved record has to be checked.
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, or the pending answer when no
20
- * backup secret is configured. Returns nothing when the saved record has to be checked.
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, pendingSetup } = ctx;
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
- : 'Check the payment and retry status. 1Click SUCCESS alone is not acceptance.',
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
+ }
@@ -0,0 +1 @@
1
+ export declare const version: string;
@@ -0,0 +1,3 @@
1
+ /** The installed package version; package.json sits two levels above both src/node and dist/node. */
2
+ import { createRequire } from 'node:module';
3
+ export const { version } = createRequire(import.meta.url)('../../package.json');
@@ -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>;
@@ -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 { bytes, hex } from '../settle-near/codec.js';
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);
@@ -76,9 +76,10 @@ export interface PurchaseRecord extends Versioned {
76
76
  recoveryProved: boolean;
77
77
  /** Source details are the reviewed 1Click quote. */
78
78
  payment: PaymentSummary;
79
+ /** A NEAR send that may be on chain; `txHash` only when the wallet returned one. */
79
80
  nearSubmission?: {
80
81
  state: 'uncertain';
81
- txHash: string;
82
+ txHash?: string;
82
83
  };
83
84
  paymentState?: 'expired' | 'canceled';
84
85
  purchase: Purchase;
@@ -1,8 +1,11 @@
1
1
  import { AnnonError } from '../errors.js';
2
- /** Public evidence to retain whenever a submitted NEAR transaction cannot be confirmed. */
2
+ /**
3
+ * Public evidence to retain whenever a submitted NEAR transaction cannot be confirmed. `txHash` is
4
+ * absent when a wallet may have broadcast but never returned a hash: unknown, never invented.
5
+ */
3
6
  export declare class TransactionUncertain extends AnnonError {
4
- readonly txHash: string;
7
+ readonly txHash: string | undefined;
5
8
  readonly purchaseId?: string | undefined;
6
9
  readonly code = "transaction_uncertain";
7
- constructor(txHash: string, purchaseId?: string | undefined);
10
+ constructor(txHash: string | undefined, purchaseId?: string | undefined);
8
11
  }
@@ -1,5 +1,8 @@
1
1
  import { AnnonError } from '../errors.js';
2
- /** Public evidence to retain whenever a submitted NEAR transaction cannot be confirmed. */
2
+ /**
3
+ * Public evidence to retain whenever a submitted NEAR transaction cannot be confirmed. `txHash` is
4
+ * absent when a wallet may have broadcast but never returned a hash: unknown, never invented.
5
+ */
3
6
  export class TransactionUncertain extends AnnonError {
4
7
  txHash;
5
8
  purchaseId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defdone/annon",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
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",