@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @defdone/annon
2
2
 
3
+ ## 0.1.0-alpha.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Add `BrowserPurchaseController.reconcile()` so a purchase that 1Click reported as failed can be checked again. Under the purchase lease it keeps the same purchase, deposit and recovery identity, checks NEAR acceptance and reads the existing deposit's 1Click status once. Accepted moves to `accepted`, `SUCCESS` to `accepting` and `REFUNDED` to `refunded`; anything else stays `failed` with the new status saved. If the saved deposit address or memo, the restored purchase's deposit and the saved polling checkpoint disagree, it refuses before any read or write. It never creates, starts or pays for a purchase.
8
+ - Bare `annon init` creates the CLI backup secret at `~/.config/annon/backup-secret`: 32 random bytes as hex, mode 0600 from the first write, published with a hard link so it never appears half-written and never replaces an existing file. Running it again only validates the file. It refuses to create a new secret while saved records or backups exist, never prints the secret, and sends its one-time notice to stderr. `--backup-secret-file`, `ANNON_BACKUP_SECRET_FILE` and `ANNON_BACKUP_SECRET` keep their precedence; the default file is read only when none is set, and a selected source that fails never falls back. Other commands, agent mode and MCP read the default but never create it. Reading the default checks that `~/.config/annon` is a private directory you own, refuses symlinks, FIFOs and other non-regular files without blocking, and never repairs anything. Bare `init` reports an unreadable config as a setup error (exit 2). A pending `status` now uses the default file too. `annon --version` (or `-v`) prints the installed version. The quickstart replaces the manual secret recipe with `annon init` and explains backup and restore.
9
+ - The CLI wallet approval page shows a readable summary of exactly what the wallet signs: a title per action, the pairing code to compare, amounts in USDC or NEAR, receiver, account, expiry and what signing does, with the CLI's terms shown apart and the raw request under Technical details (the NEP-413 nonce as hex). Each click opens at most one wallet window, so Meteor's web popup is no longer blocked: NEP-413 messages are signed without a sign-in, and transactions connect first and then send. After a closed, blocked or declined wallet window the page explains it and allows another try until the request expires. The terminal prints a waiting line with the expiry, the reported wallet failure and a specific final error (`cancelled`, `rejected` or `wallet_error`) instead of `operation_failed`. Once the page has called the wallet's send for a transaction, the request is never attempted again: the CLI records the send start before the wallet is called, the request ends as `transaction_uncertain` with a warning not to pay again, the saved purchase keeps that state without inventing a transaction hash, and it blocks a plain rerun of `annon buy` until it is accepted or `--force` is used. A result the CLI cannot verify after the send start also ends as uncertain, keeping a valid transaction hash when there is one. An uncertain `annon buy` exits 4, and `annon status` reports `submission: "uncertain"` with a warning not to pay again. The NEAR wallet is chosen in a styled, keyboard-accessible list instead of the browser's native menu.
10
+
3
11
  ## 0.1.0-alpha.1
4
12
 
5
13
  ### Patch Changes
package/CLI.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Build with `pnpm build`. The `@defdone/annon` package ships the `annon` bin;
4
4
  in this checkout run `node packages/annon/dist/node/main.js` in its place.
5
- Run `annon --help` for all flags. All commands return JSON; `--json` uses one compact
5
+ Run `annon --help` for all flags and `annon --version` (or `-v`) for the installed version; with
6
+ `--json` the version is `{ "ok": true, "result": { "version": ... } }`. It loads no config, wallet
7
+ or secret and also works in agent mode. All commands return JSON; `--json` uses one compact
6
8
  JSON line and disables interactive prompts. Success is `{ "ok": true, "result": ... }`;
7
9
  errors are `{ "ok": false, "error": { "code": ..., "message": ... } }`.
8
10
 
@@ -53,10 +55,9 @@ On macOS, with no NEAR file or credential env value, explicitly select both
53
55
  service `near-mainnet-alice.near`, account `alice.near:ed25519:PUBLIC_KEY`.
54
56
  Other platforms use the credential JSON file or environment. No keychain enumeration occurs.
55
57
 
56
- Sell, buy, open, recover and stop also require a user-held random 32-byte backup secret,
57
- encoded as 64 lowercase hex characters in `ANNON_BACKUP_SECRET`, or in the file named by
58
- `--backup-secret-file` / `ANNON_BACKUP_SECRET_FILE`. Create it with a cryptographic random
59
- generator and keep it outside the repository. It is never derived from a wallet signature.
58
+ Sell, buy, open, recover and stop also require a random 32-byte backup secret. It encrypts the
59
+ local purchase, recovery and sell records and the portable backups. It is never derived from a
60
+ wallet signature or key. See [Backup secret](#backup-secret).
60
61
  `verify` and accepted/read-only `status` need neither a wallet nor this secret. A pending Base `status` with saved state and a backup secret may send `unlock` through the configured relayer or NEAR wallet. Status can update encrypted local state and backups: expired/canceled Base payments are marked with `paymentState`, preserving the purchase, attempts and recovery key. It never deletes the purchase record.
61
62
 
62
63
  State directories are created with mode 0700 and records/outputs with 0600. Keep existing
@@ -64,8 +65,99 @@ state directories private. Purchases and recovery grants are encrypted by the SD
64
65
  encrypted backups are under `stateDir/backups`. Public headers and resource URLs are saved
65
66
  separately for later downloading. Preserve state after timeouts. Losing local state does not
66
67
  lose recovery rights: the same recovery wallet can create a fresh grant on a new device with
67
- a new backup secret. Keys, signed payment payloads and decrypted file contents are never
68
- returned in CLI/MCP results. Plaintext is written only to the explicitly named output.
68
+ a new backup secret ([Loss](#backup-secret) explains when `init` creates one). Keys, signed payment
69
+ payloads and decrypted file contents are never returned in CLI/MCP results. Plaintext is written only to the explicitly named output.
70
+
71
+ ### Backup secret
72
+
73
+ Run `annon init` once, before buying, selling or enabling agent mode. It creates
74
+ `~/.config/annon/backup-secret`: 32 random bytes as 64 lowercase hex characters and a newline,
75
+ mode 0600, in a directory created with mode 0700. The file is written to an exclusive temporary
76
+ file and published with a hard link, so it never appears half-written and an existing file is
77
+ never replaced. Running `init` again only validates the file and reports `"created": false`. An
78
+ unsafe default is refused and never repaired: a symlink, FIFO or other non-regular file, a file not
79
+ owned by you or with group or other permission bits, malformed content, or an `~/.config/annon`
80
+ that is a symlink, is not owned by you, or is writable by group or others. An existing 0755
81
+ directory is fine. Every command that reads the default applies the same checks.
82
+
83
+ The location is fixed. `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `--config`,
84
+ `ANNON_CONFIG` and `--state-dir` do not move it. For another private location, name an existing file with the explicit inputs below.
85
+
86
+ Sources, in order, as before this file existed:
87
+
88
+ 1. `--backup-secret-file` (or, when the flag is absent, `ANNON_BACKUP_SECRET_FILE`), a file with the
89
+ 64 hex characters.
90
+ 2. `ANNON_BACKUP_SECRET`, the hex value itself. Prefer a file: environment values are inherited by
91
+ child processes.
92
+ 3. The default file, only when neither is set.
93
+
94
+ An empty value counts as unset, as before: an empty `--backup-secret-file` selects no file even when
95
+ `ANNON_BACKUP_SECRET_FILE` is set, and moves on to `ANNON_BACKUP_SECRET`. Hex must be lowercase in
96
+ every source; an optional `0x` prefix and surrounding whitespace are accepted. A selected source that is missing,
97
+ unreadable or malformed fails with `backup_secret_required` (exit 2); there is no fallback to a
98
+ later source, the default file or a new secret. `annon init` with an explicit source only validates
99
+ it and never writes, creates or changes that file. Commands other than `init` read the secret but
100
+ never create it, and agent mode and MCP never create it. Bare `init` accepts only `--json`,
101
+ `--config`, `--state-dir` and `--backup-secret-file`; signer options need `--agent`.
102
+
103
+ Output: stdout carries the usual single JSON result, for example
104
+ `{"ok":true,"result":{"configured":true,"backupSecret":{"source":"default-file","path":"/home/me/.config/annon/backup-secret","created":true}}}`.
105
+ `source` is `default-file`, `flag-file`, `env-file` or `env`; `path` is shown for files only. The
106
+ one-time creation notice goes to stderr, also with `--json`. The secret itself is never printed, in
107
+ results, errors or notices.
108
+
109
+ **Back it up.** The secret is not part of the encrypted backups in `stateDir/backups`; copy the file
110
+ itself, separately, to private encrypted storage. Never `cat` it or paste it into a terminal. This
111
+ copies without displaying it and refuses an existing destination:
112
+
113
+ ```sh
114
+ node --input-type=module -e '
115
+ import { constants } from "node:fs";
116
+ import { copyFile } from "node:fs/promises";
117
+ await copyFile(process.argv[1], process.argv[2], constants.COPYFILE_EXCL);
118
+ ' "$HOME/.config/annon/backup-secret" /mounted-private-encrypted-backup/annon-backup-secret
119
+ ```
120
+
121
+ The copy keeps mode 0600 when the destination supports POSIX permissions; check it. A secret manager
122
+ that imports files is also fine.
123
+
124
+ **Restore.** On a new machine, put the saved file in a private directory (directory mode 0700, file
125
+ mode 0600), copy the encrypted record backups separately, and select the file. Do not run `init`
126
+ first:
127
+
128
+ ```sh
129
+ export ANNON_BACKUP_SECRET_FILE=/absolute/private/restored-annon-backup-secret
130
+ annon open /absolute/private/purchase-backup.json --url https://host.example/file --output restored-file
131
+ ```
132
+
133
+ Opening one known backup proves the restore. To restore to the default location instead, create
134
+ `~/.config/annon` with mode 0700 and copy in the other direction, still refusing an existing file:
135
+
136
+ ```sh
137
+ mkdir -p -m 700 "$HOME/.config/annon"
138
+ node --input-type=module -e '
139
+ import { constants } from "node:fs";
140
+ import { copyFile } from "node:fs/promises";
141
+ await copyFile(process.argv[1], process.argv[2], constants.COPYFILE_EXCL);
142
+ ' /mounted-private-encrypted-backup/annon-backup-secret "$HOME/.config/annon/backup-secret"
143
+ ```
144
+
145
+ **Loss.** If the secret disappears, commands fail with `backup_secret_required` and a pending
146
+ `status` stays pending; nothing recreates it. Restore the same bytes. Bare `init` creates a new
147
+ secret only when both checked state locations are empty: the effective state directory and the
148
+ default `~/.local/share/annon`, each in `records` and `backups`. If every copy is lost and you want
149
+ to recover with a new secret, first move the whole old state, including `~/.local/share/annon`, to
150
+ a separate private archive and keep it. Then use an empty state directory, run `init`, and run
151
+ `recover` with the same recovery wallet. That creates a fresh grant and new records; it cannot
152
+ decrypt the archived records or resume pending sell jobs. An operator can instead provision a
153
+ separate secret file and select it with `ANNON_BACKUP_SECRET_FILE`. Use one secret per state
154
+ directory.
155
+
156
+ **Agents.** `init --agent` does not create or reference a backup secret. Run bare `init` before it,
157
+ or give the agent an operator-managed file with an absolute `ANNON_BACKUP_SECRET_FILE` in the
158
+ launcher. A launcher that sets `ANNON_BACKUP_SECRET_FILE` must point it at a file that exists; a
159
+ missing file fails without falling back to the default. Once agent mode is active, `init` is
160
+ refused, so provision or restore the secret outside the running agent.
69
161
 
70
162
  ## Commands
71
163
 
@@ -161,9 +253,23 @@ blindly retried. Raw upstream errors are intentionally suppressed to avoid leaki
161
253
  | --- | --- |
162
254
  | 0 | Command completed; buy may still require a deposit or settlement |
163
255
  | 1 | Operation failed; inspect final status and retained encrypted state before retrying |
164
- | 2 | Invalid arguments, configuration, wallet source or missing local input |
165
- | 3 | First-use contract trust required or declined |
166
- | 4 | `status` reports pending; acceptance has not been observed (not proof of payment failure) |
256
+ | 2 | Invalid arguments, configuration, wallet source or missing local input; `purchase_open` when a saved purchase of the file, including an uncertain NEAR send, still blocks a new one |
257
+ | 3 | First-use contract trust required or declined; browser approval cancelled (`cancelled`), expired (`cancelled`, or `rejected` or `wallet_error` after the last wallet failure the page reported) or declined by the wallet |
258
+ | 4 | Not settled, and not proof of failure: `status` reports pending; `buy` returned `state: "uncertain"` (its payment may be on chain; the JSON result is unchanged and names the saved purchase); or `transaction_uncertain`: a transaction may have been sent and is not confirmed |
259
+
260
+ In browser approval, once the CLI has acknowledged the page's send start, the request can only end
261
+ as a verified transaction or as `transaction_uncertain`: a wallet failure, a cancel, the expiry, or a
262
+ result the CLI cannot verify (an RPC rejection, malformed evidence) never counts as "not sent". The
263
+ purchase is saved with that uncertainty, with the transaction hash only when the wallet returned a
264
+ valid one. `annon buy` then exits 4 with `state: "uncertain"`, and `annon status <purchase id>`
265
+ answers `submission: "uncertain"` until the purchase is accepted. Do not pay again.
266
+
267
+ A plain `annon buy` of the same file is refused with `purchase_open` while that uncertainty is
268
+ saved, also after the purchase expired: a late transfer still moves tokens, and the contract then
269
+ owes a refund that is paid out later rather than returned at once. Neither a pending status, an
270
+ expired purchase nor an empty wallet history proves that nothing was sent. `--force` overrides the
271
+ guard; use it only after you have reconciled the original attempt, for example by finding its
272
+ final transaction or its refund, because otherwise you may pay twice.
167
273
 
168
274
  ## SDK seller resume and CKD configuration
169
275
 
package/README.md CHANGED
@@ -203,6 +203,7 @@ const { meta, data } = await controller.open({ onProgress: renderDownload });
203
203
  before an expired key is replaced. Portable receipts carry those archives too.
204
204
  - The local random backup secret is persisted in IndexedDB for reload recovery. It protects
205
205
  encrypted storage from accidental disclosure and shares the embedding page's trust boundary.
206
+ This is the browser's secret; the CLI keeps its own in a file ([CLI.md](CLI.md#backup-secret)).
206
207
  - Export with `controller.exportReceipt(portableSecret)`, using a separate random 32-byte
207
208
  `newBackupSecret()` that you keep separately. Import with
208
209
  `storage.importReceipt(file, portableSecret)`. Import restores the header, resource URL and
@@ -295,6 +296,9 @@ shop-provided data: on each shop's `info` from `listShops`, and as `rules` with
295
296
 
296
297
  ## CLI browser wallet login
297
298
 
299
+ Run `annon init` once first. It creates the CLI backup secret file that encrypts local records
300
+ ([CLI.md](CLI.md#backup-secret)); login neither creates nor exports it.
301
+
298
302
  ```sh
299
303
  annon login --wallet evm
300
304
  annon login --wallet near --network near:testnet
@@ -308,21 +312,44 @@ in the wallet.
308
312
  | Wallet | How | Signing supported |
309
313
  | --- | --- | --- |
310
314
  | EVM | Injected EIP-1193 wallet on Base | `personal_sign` for the SDK's authorization texts, `eth_signTypedData_v4` for the exact EIP-3009/x402 payload |
311
- | NEAR | Meteor Wallet (extension or web popup; mainnet and testnet) or HOT Wallet (mainnet only) through Wallet Selector, picked on the page; account-only sign-in (no `contractId`, no method list) | NEP-413 and `signAndSendTransaction`, including the required deposit `"1"` on `ft_transfer_call` |
315
+ | NEAR | Meteor Wallet (extension or web popup; mainnet and testnet) or HOT Wallet (mainnet only) through Wallet Selector, picked on the page. NEP-413 messages are signed directly, with the account chosen in the wallet and no sign-in. Transactions take two clicks: an account-only sign-in (no `contractId`, no method list), then the transaction | NEP-413 and `signAndSendTransaction`, including the required deposit `"1"` on `ft_transfer_call` |
312
316
 
313
317
  Browser and remote EVM split-deployment transactions and browser and remote Solana signing are not
314
318
  included; the local-key adapters remain available for those workflows.
315
319
 
316
320
  **The local page.** The listener binds `127.0.0.1` on an OS-assigned port before opening the browser.
317
321
  Match the short pairing code with the terminal, and review the action, amount, recipients, file and
318
- chain on the page before approving in the wallet.
322
+ chain on the page before approving in the wallet. The page shows a summary built only from the
323
+ request the wallet signs (amounts in USDC or NEAR, receiver, account, expiry), the CLI's payment or
324
+ listing terms apart from it with a warning if their price differs, and the raw request under
325
+ Technical details. While a request waits, the terminal prints its expiry on stderr.
326
+
327
+ - Each click opens at most one wallet window, inside that click, so the browser's pop-up blocker
328
+ allows it. If the wallet window closes, is blocked or declines before anything is sent, the page
329
+ says so and you can try again, with the same or another wallet, until the request expires.
330
+ Nothing is retried for you.
331
+ - Once the page has called the wallet's send for a transaction, that request is never attempted
332
+ again, whatever the wallet answers, because it may already have broadcast. The request ends as
333
+ `transaction_uncertain` and the terminal says: "The wallet may have sent the transaction. Do not
334
+ pay again. Run annon status <purchase id> to check it." The page records the send start with
335
+ the CLI before it calls the wallet, so a reloaded page or the request's expiry cannot turn it
336
+ into a retryable failure, and a result the CLI cannot verify after that point also ends as
337
+ uncertain. `annon buy` exits 4, `annon status` reports `submission: "uncertain"`, and the saved
338
+ purchase blocks a plain `annon buy` of the same file (`purchase_open`). `--force` overrides it
339
+ only after you have reconciled the first attempt (see CLI.md).
340
+ - The page reports only a fixed reason name (`window_closed`, `popup_blocked`, `rejected`,
341
+ `wallet_error`, `refused`, `send_uncertain`, `send_started` or `cancelled`) to the CLI, which
342
+ prints its own line for it. Cancel ends the request at once; an expired request fails with the
343
+ last reported reason, for example `rejected` or `wallet_error`, instead of a generic error. A
344
+ report never completes a request.
319
345
 
320
346
  - Host (including port), Origin and an unpredictable RAM-only capability are checked on action and
321
347
  result API calls.
322
348
  - In open mode, a same-origin POST bootstraps the capability once into the opened page, then the
323
349
  bootstrap route closes. With `--no-open`, bootstrap is disabled and the capability exists only in
324
350
  the printed URL fragment. It is never passed to the browser launcher in argv.
325
- - Bundled scripts, a restrictive CSP, no framing, `no-store` and `no-referrer` are enforced. The page
351
+ - Bundled scripts and a stylesheet from the page's own origin (no inline styles, fonts or other
352
+ resources), a restrictive CSP, no framing, `no-store` and `no-referrer` are enforced. The page
326
353
  connects only to itself, the Wallet Selector's NEAR RPC (`free.rpc.fastnear.com` on mainnet,
327
354
  `test.rpc.fastnear.com` on testnet, fixed in the page and independent of your configured RPC) and
328
355
  HOT's relay `h4n.app`, and frames only `hot-labs.org`.
@@ -405,8 +432,11 @@ Order matters, because agent mode refuses `init`, `login` and per-call trust or
405
432
  2. Write the deployment, `maxPrice`, `workDir` and, after reviewing the contract,
406
433
  `trustContract: true` (or trust it once with a human command and `--trust-contract`). Do not set
407
434
  `agent: true` yourself.
408
- 3. Run one `annon init --agent ...` from the examples above. It saves the signer, the daily budget
409
- and `agent: true`.
435
+ 3. Run bare `annon init` to create the backup secret, or give the launcher an absolute
436
+ `ANNON_BACKUP_SECRET_FILE` you manage ([CLI.md](CLI.md#backup-secret)). Agent mode never creates
437
+ one.
438
+ 4. Run one `annon init --agent ...` from the examples above. It saves the signer, the daily budget
439
+ and `agent: true`. It does not create or reference a backup secret.
410
440
 
411
441
  - Agents never prompt. Missing trust, price cap or daily budget causes a refusal.
412
442
  - `allowedResources` is optional. Without it, any secure resource URL is allowed within the
@@ -447,7 +477,8 @@ Before a human payment the CLI confirms the current price, split recipients and
447
477
  and file, token and network. Browser mode shows these exact terms with each signing request for
448
478
  approval in the page and the wallet. Local and remote human signing require an explicit terminal
449
479
  confirmation. Purchase and recovery secrets still need the independent backup secret described in
450
- CLI.md; login does not derive one from a signature.
480
+ [CLI.md](CLI.md#backup-secret) and created by `annon init`; login does not derive one from a
481
+ signature.
451
482
 
452
483
  ## Remote signer, HTTP JSON v1
453
484
 
@@ -501,9 +532,10 @@ RPC. A returned transaction hash alone never proves success.
501
532
  - Definite mismatches and failed finalized transactions are rejected without saving the hash as
502
533
  `nearSubmission` evidence. Only RPC unavailability or a non-final status keeps the outcome
503
534
  uncertain.
504
- - If submission or finalized verification is ambiguous, the CLI returns `state: "uncertain"`,
505
- `purchaseId` and `txHash`, and keeps `nearSubmission: {state:"uncertain",txHash}` in the encrypted
506
- purchase backup. Check status for that saved purchase before buying again.
535
+ - If submission or finalized verification is ambiguous, the CLI returns `state: "uncertain"` and
536
+ `purchaseId`, and keeps `nearSubmission: {state:"uncertain"}` in the encrypted purchase backup.
537
+ Both carry `txHash` only when the wallet returned a valid one. `annon buy` exits 4. Check status
538
+ for that saved purchase before buying again.
507
539
  - Signing errors with a known hash also expose safe hash and purchase evidence, with exit code 4.
508
540
 
509
541
  **Reference handler.** `node/signer-mock` exports `signerMock`, an authenticated HTTP
@@ -100,9 +100,10 @@ export interface Bought {
100
100
  route: BuyRoute;
101
101
  record: PurchaseRecord;
102
102
  instructions?: Deposit;
103
+ /** `txHash` is absent when the wallet may have sent the transaction without returning one. */
103
104
  uncertain?: {
104
105
  purchaseId: string;
105
- txHash: string;
106
+ txHash?: string;
106
107
  };
107
108
  payment?: string;
108
109
  }
package/dist/api/annon.js CHANGED
@@ -869,11 +869,18 @@ export class Annon {
869
869
  const next = {
870
870
  ...record,
871
871
  revision: record.revision + 1,
872
- nearSubmission: { state: 'uncertain', txHash: error.txHash },
872
+ nearSubmission: {
873
+ state: 'uncertain',
874
+ ...(error.txHash ? { txHash: error.txHash } : {}),
875
+ },
873
876
  };
874
877
  if (!(await store.compareAndSet(purchaseKey(id), record, next)))
875
878
  throw new TransactionUncertain(error.txHash, id);
876
- return { route, record: next, uncertain: { purchaseId: id, txHash: error.txHash } };
879
+ return {
880
+ route,
881
+ record: next,
882
+ uncertain: { purchaseId: id, ...(error.txHash ? { txHash: error.txHash } : {}) },
883
+ };
877
884
  }
878
885
  return { route, record };
879
886
  }
@@ -18,6 +18,6 @@ export { type CatalogEntry, type CatalogQuery, type ConfiguredShop, canonicalMan
18
18
  export { TransactionUncertain } from '../wallet/uncertain.js';
19
19
  export { type EvmWallet, FileFundingRefused, type IdentityWallet, type NearWallet, } from '../wallet/wallet.js';
20
20
  export { BROWSER_DOWNLOAD_LIMIT, type BrowserDownloadOptions, type BrowserDownloadProgress, browserDownload, } from './download.js';
21
- export { type BrowserOpenOptions, BrowserPurchaseController, type BrowserPurchaseRecord, type BrowserPurchaseState, type BrowserPurchaseStorage, browserAnnon, browserPurchaseStorage, type ControllerOptions, PurchaseLeaseBusy, type PurchaseProgress, type PurchaseSdk, type PurchaseTermsReview, } from './purchases.js';
21
+ export { type BrowserOpenOptions, BrowserPurchaseController, type BrowserPurchaseRecord, type BrowserPurchaseState, type BrowserPurchaseStorage, browserAnnon, browserPurchaseStorage, type ControllerOptions, PurchaseLeaseBusy, type PurchaseProgress, type PurchaseSdk, type PurchaseTermsReview, type ReconcileOptions, type ReconcileResult, } from './purchases.js';
22
22
  export { type BrowserSaleIndex, type BrowserSellerOptions, browserSellerSpool, browserSellerStorage, createBrowserSeller, type SaleHandle, type SaleView, type SellDraft, type SellerConnection, type SellerFacade, type SellProgress, type SellQuote, type SellStep, type SellTerms, } from './selling.js';
23
23
  export { type BrowserNearProvider, type BrowserNearTransaction, BrowserWalletError, type BrowserWalletErrorCode, browserEvmWallet, browserNearWallet, browserSolanaWallet, type InjectedEvmProvider, type InjectedSolanaKey, type InjectedSolanaProvider, signNearUpload, } from './wallets.js';