@defdone/annon 0.0.0-stage → 0.1.0-alpha.0
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 +56 -0
- package/CLI.md +378 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +581 -2
- package/dist/api/annon-types.d.ts +108 -0
- package/dist/api/annon-types.js +2 -0
- package/dist/api/annon.d.ts +117 -0
- package/dist/api/annon.js +1152 -0
- package/dist/api/hosted-record.d.ts +13 -0
- package/dist/api/hosted-record.js +149 -0
- package/dist/api/sharing-types.d.ts +109 -0
- package/dist/api/sharing-types.js +1 -0
- package/dist/api/sharing.d.ts +36 -0
- package/dist/api/sharing.js +879 -0
- package/dist/browser/download.d.ts +19 -0
- package/dist/browser/download.js +52 -0
- package/dist/browser/index.d.ts +23 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/login-bundle.js +116 -0
- package/dist/browser/login.d.ts +1 -0
- package/dist/browser/login.js +177 -0
- package/dist/browser/purchase-bundle.js +84 -0
- package/dist/browser/purchases.d.ts +98 -0
- package/dist/browser/purchases.js +735 -0
- package/dist/browser/record.d.ts +55 -0
- package/dist/browser/record.js +16 -0
- package/dist/browser/selling.d.ts +146 -0
- package/dist/browser/selling.js +418 -0
- package/dist/browser/wallets.d.ts +53 -0
- package/dist/browser/wallets.js +286 -0
- package/dist/bytes.d.ts +4 -0
- package/dist/bytes.js +16 -0
- package/dist/download/download.d.ts +59 -0
- package/dist/download/download.js +384 -0
- package/dist/download/host.d.ts +56 -0
- package/dist/download/host.js +161 -0
- package/dist/download/http.d.ts +23 -0
- package/dist/download/http.js +87 -0
- package/dist/download/proof.d.ts +20 -0
- package/dist/download/proof.js +79 -0
- package/dist/download/upload.d.ts +130 -0
- package/dist/download/upload.js +408 -0
- package/dist/envelope/evm.d.ts +4 -0
- package/dist/envelope/evm.js +32 -0
- package/dist/envelope/pay2.d.ts +176 -0
- package/dist/envelope/pay2.js +541 -0
- package/dist/envelope/replay.d.ts +45 -0
- package/dist/envelope/replay.js +186 -0
- package/dist/envelope/stream.d.ts +78 -0
- package/dist/envelope/stream.js +372 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +300 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +23 -0
- package/dist/keys/near-ckd.d.ts +47 -0
- package/dist/keys/near-ckd.js +96 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.js +4 -0
- package/dist/near/nep413.d.ts +22 -0
- package/dist/near/nep413.js +39 -0
- package/dist/near/rpc.d.ts +170 -0
- package/dist/near/rpc.js +423 -0
- package/dist/near/texts.d.ts +39 -0
- package/dist/near/texts.js +60 -0
- package/dist/net/ip.d.ts +7 -0
- package/dist/net/ip.js +62 -0
- package/dist/net/retry.d.ts +3 -0
- package/dist/net/retry.js +31 -0
- package/dist/node/adapter-checks.d.ts +6 -0
- package/dist/node/adapter-checks.js +13 -0
- package/dist/node/adapter.d.ts +8 -0
- package/dist/node/adapter.js +99 -0
- package/dist/node/agent.d.ts +49 -0
- package/dist/node/agent.js +266 -0
- package/dist/node/amounts.d.ts +2 -0
- package/dist/node/amounts.js +20 -0
- package/dist/node/browser-login.d.ts +18 -0
- package/dist/node/browser-login.js +212 -0
- package/dist/node/budget.d.ts +24 -0
- package/dist/node/budget.js +59 -0
- package/dist/node/buy-commands.d.ts +24 -0
- package/dist/node/buy-commands.js +301 -0
- package/dist/node/cli.d.ts +60 -0
- package/dist/node/cli.js +201 -0
- package/dist/node/command-context.d.ts +82 -0
- package/dist/node/command-context.js +77 -0
- package/dist/node/config.d.ts +37 -0
- package/dist/node/config.js +189 -0
- package/dist/node/core-adapter.d.ts +43 -0
- package/dist/node/core-adapter.js +122 -0
- package/dist/node/errors.d.ts +8 -0
- package/dist/node/errors.js +12 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +5 -0
- package/dist/node/io.d.ts +13 -0
- package/dist/node/io.js +112 -0
- package/dist/node/keychain.d.ts +10 -0
- package/dist/node/keychain.js +67 -0
- package/dist/node/main.d.ts +2 -0
- package/dist/node/main.js +21 -0
- package/dist/node/offers.d.ts +16 -0
- package/dist/node/offers.js +91 -0
- package/dist/node/origin.d.ts +11 -0
- package/dist/node/origin.js +80 -0
- package/dist/node/paths.d.ts +3 -0
- package/dist/node/paths.js +71 -0
- package/dist/node/payment-terms.d.ts +26 -0
- package/dist/node/payment-terms.js +127 -0
- package/dist/node/purchase-commands.d.ts +16 -0
- package/dist/node/purchase-commands.js +237 -0
- package/dist/node/purchase-guard.d.ts +19 -0
- package/dist/node/purchase-guard.js +53 -0
- package/dist/node/relay-transport.d.ts +6 -0
- package/dist/node/relay-transport.js +173 -0
- package/dist/node/resources.d.ts +2 -0
- package/dist/node/resources.js +26 -0
- package/dist/node/retry-gate.d.ts +15 -0
- package/dist/node/retry-gate.js +53 -0
- package/dist/node/seller-commands.d.ts +12 -0
- package/dist/node/seller-commands.js +207 -0
- package/dist/node/sharing.d.ts +3 -0
- package/dist/node/sharing.js +28 -0
- package/dist/node/shops.d.ts +17 -0
- package/dist/node/shops.js +74 -0
- package/dist/node/signer-mock.d.ts +11 -0
- package/dist/node/signer-mock.js +79 -0
- package/dist/node/signer-wallet.d.ts +8 -0
- package/dist/node/signer-wallet.js +147 -0
- package/dist/node/signing.d.ts +29 -0
- package/dist/node/signing.js +200 -0
- package/dist/node/storage-commands.d.ts +11 -0
- package/dist/node/storage-commands.js +50 -0
- package/dist/node/sync-dir.d.ts +1 -0
- package/dist/node/sync-dir.js +11 -0
- package/dist/node/wallets.d.ts +13 -0
- package/dist/node/wallets.js +146 -0
- package/dist/oneclick/oneclick.d.ts +132 -0
- package/dist/oneclick/oneclick.js +342 -0
- package/dist/pay/backup.d.ts +70 -0
- package/dist/pay/backup.js +402 -0
- package/dist/pay/base.d.ts +135 -0
- package/dist/pay/base.js +393 -0
- package/dist/pay/eip3009.d.ts +59 -0
- package/dist/pay/eip3009.js +72 -0
- package/dist/pay/pay.d.ts +86 -0
- package/dist/pay/pay.js +297 -0
- package/dist/pay/purchase.d.ts +103 -0
- package/dist/pay/purchase.js +20 -0
- package/dist/pay/recover.d.ts +31 -0
- package/dist/pay/recover.js +34 -0
- package/dist/pay/seller.d.ts +130 -0
- package/dist/pay/seller.js +444 -0
- package/dist/pay/settle.d.ts +96 -0
- package/dist/pay/settle.js +308 -0
- package/dist/pay/split.d.ts +115 -0
- package/dist/pay/split.js +256 -0
- package/dist/pay/store-node.d.ts +8 -0
- package/dist/pay/store-node.js +224 -0
- package/dist/pay/store.d.ts +54 -0
- package/dist/pay/store.js +135 -0
- package/dist/pay/x402.d.ts +95 -0
- package/dist/pay/x402.js +136 -0
- package/dist/relay/admission.d.ts +8 -0
- package/dist/relay/admission.js +58 -0
- package/dist/relay/capabilities.d.ts +36 -0
- package/dist/relay/capabilities.js +232 -0
- package/dist/relay/fees.d.ts +17 -0
- package/dist/relay/fees.js +105 -0
- package/dist/relay/index.d.ts +10 -0
- package/dist/relay/index.js +8 -0
- package/dist/relay/keeper-balance.d.ts +17 -0
- package/dist/relay/keeper-balance.js +46 -0
- package/dist/relay/keeper-runner.d.ts +114 -0
- package/dist/relay/keeper-runner.js +703 -0
- package/dist/relay/keeper.d.ts +38 -0
- package/dist/relay/keeper.js +73 -0
- package/dist/relay/ledger.d.ts +155 -0
- package/dist/relay/ledger.js +392 -0
- package/dist/relay/relay-checks.d.ts +3 -0
- package/dist/relay/relay-checks.js +244 -0
- package/dist/relay/relay-reconciliation.d.ts +15 -0
- package/dist/relay/relay-reconciliation.js +293 -0
- package/dist/relay/relay-request.d.ts +6 -0
- package/dist/relay/relay-request.js +139 -0
- package/dist/relay/relay-transactions.d.ts +23 -0
- package/dist/relay/relay-transactions.js +182 -0
- package/dist/relay/relay-types.d.ts +128 -0
- package/dist/relay/relay-types.js +17 -0
- package/dist/relay/relay.d.ts +102 -0
- package/dist/relay/relay.js +426 -0
- package/dist/relay/reservation.d.ts +29 -0
- package/dist/relay/reservation.js +132 -0
- package/dist/relay/sponsorship.d.ts +42 -0
- package/dist/relay/sponsorship.js +184 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +1 -0
- package/dist/settle-near/codec.d.ts +17 -0
- package/dist/settle-near/codec.js +86 -0
- package/dist/settle-near/identity.d.ts +27 -0
- package/dist/settle-near/identity.js +80 -0
- package/dist/settle-near/purchase.d.ts +40 -0
- package/dist/settle-near/purchase.js +153 -0
- package/dist/settle-near/terms.d.ts +37 -0
- package/dist/settle-near/terms.js +89 -0
- package/dist/shops/discovery.d.ts +95 -0
- package/dist/shops/discovery.js +384 -0
- package/dist/wallet/account-only.d.ts +11 -0
- package/dist/wallet/account-only.js +6 -0
- package/dist/wallet/uncertain.d.ts +8 -0
- package/dist/wallet/uncertain.js +13 -0
- package/dist/wallet/wallet.d.ts +100 -0
- package/dist/wallet/wallet.js +204 -0
- package/package.json +135 -3
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/** Durable SDK orchestration. Storage belongs to the embedding origin; no UI or wallet connector. */
|
|
2
|
+
import { Annon } from '../api/annon.js';
|
|
3
|
+
import { DownloadError } from '../download/download.js';
|
|
4
|
+
import { sha256 } from '../envelope/pay2.js';
|
|
5
|
+
import { AnnonError } from '../errors.js';
|
|
6
|
+
import { OneClick, } from '../oneclick/oneclick.js';
|
|
7
|
+
import { encryptedBrowserIndexStore, encryptedBrowserPurchaseStore, encryptedPurchaseStore, encryptedRecoveryArchiveIndexStore, encryptedRecoveryStore, exportBrowserReceipt, exportPurchase, exportRecovery, importBrowserReceipt, importPurchase, importRecovery, newBackupSecret, sealBrowserRecord, } from '../pay/backup.js';
|
|
8
|
+
import { purchaseKey } from '../pay/purchase.js';
|
|
9
|
+
import { checkNext, indexedDbStore, } from '../pay/store.js';
|
|
10
|
+
import { bytes, hex } from '../settle-near/codec.js';
|
|
11
|
+
import { BROWSER_DOWNLOAD_LIMIT, browserDownload, downloadProgress, } from './download.js';
|
|
12
|
+
const INDEX = 'annon/browser/index';
|
|
13
|
+
const conflict = () => new AnnonError('store_conflict', 'browser purchase store conflict; reload the saved purchase');
|
|
14
|
+
const entryKey = (id) => `annon/browser/entry/${id}`;
|
|
15
|
+
const backupKey = (name) => `annon/browser/backup/${name}`;
|
|
16
|
+
const MAX_CAS_ATTEMPTS = 32;
|
|
17
|
+
/** Local secret is random, persisted once with CAS. Clearing site storage removes local recovery. */
|
|
18
|
+
export async function browserPurchaseStorage(options = {}) {
|
|
19
|
+
const local = indexedDbStore(options);
|
|
20
|
+
const secretName = 'annon/browser/secret';
|
|
21
|
+
let saved = await local.load(secretName);
|
|
22
|
+
if (!saved) {
|
|
23
|
+
const next = { id: 'browser-secret', revision: 0, secret: hex(newBackupSecret()) };
|
|
24
|
+
if (await local.compareAndSet(secretName, null, next))
|
|
25
|
+
saved = next;
|
|
26
|
+
else
|
|
27
|
+
saved = await local.load(secretName);
|
|
28
|
+
}
|
|
29
|
+
if (saved?.id !== 'browser-secret')
|
|
30
|
+
throw new Error('local backup secret missing');
|
|
31
|
+
const backupSecret = bytes(saved.secret, 32);
|
|
32
|
+
const rawStore = indexedDbStore(options);
|
|
33
|
+
const indexStore = encryptedBrowserIndexStore(rawStore, backupSecret);
|
|
34
|
+
const entries = encryptedBrowserPurchaseStore(rawStore, backupSecret);
|
|
35
|
+
const read = () => indexStore.load(INDEX);
|
|
36
|
+
const empty = () => ({
|
|
37
|
+
kind: 'browser-purchase-index',
|
|
38
|
+
id: 'browser-index',
|
|
39
|
+
revision: 0,
|
|
40
|
+
entries: [],
|
|
41
|
+
});
|
|
42
|
+
async function update(fn) {
|
|
43
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
44
|
+
const before = await read();
|
|
45
|
+
const next = before ? structuredClone({ ...before, revision: before.revision + 1 }) : empty();
|
|
46
|
+
// Re-run the per-entry comparison on every fresh index. Only index contention is retried.
|
|
47
|
+
const writes = await fn(next);
|
|
48
|
+
const sealed = await sealBrowserRecord(INDEX, next, backupSecret);
|
|
49
|
+
if (await rawStore.compareAndSetMany([
|
|
50
|
+
{ key: INDEX, expected: before, next: sealed },
|
|
51
|
+
...writes,
|
|
52
|
+
]))
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw conflict();
|
|
56
|
+
}
|
|
57
|
+
const load = async (id) => {
|
|
58
|
+
const stored = await entries.load(entryKey(id));
|
|
59
|
+
if (!stored)
|
|
60
|
+
return null;
|
|
61
|
+
const { kind: _kind, ...record } = stored;
|
|
62
|
+
return record;
|
|
63
|
+
};
|
|
64
|
+
const save = async (expected, next, uniquePurchase = false) => {
|
|
65
|
+
checkNext(expected, next);
|
|
66
|
+
await update(async (index) => {
|
|
67
|
+
if (uniquePurchase &&
|
|
68
|
+
next.purchase &&
|
|
69
|
+
index.entries.some((e) => e.purchaseId === next.purchase?.id))
|
|
70
|
+
throw conflict();
|
|
71
|
+
const current = index.entries.find((e) => e.id === next.id);
|
|
72
|
+
if (expected ? !current || current.revision !== expected.revision : current)
|
|
73
|
+
throw conflict();
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const metadata = {
|
|
76
|
+
id: next.id,
|
|
77
|
+
revision: next.revision,
|
|
78
|
+
fileId: next.fileId,
|
|
79
|
+
route: next.route,
|
|
80
|
+
state: next.state,
|
|
81
|
+
resourceUrl: next.resourceUrl,
|
|
82
|
+
createdAt: current?.createdAt ?? now,
|
|
83
|
+
updatedAt: Math.max(current?.updatedAt ?? now, now),
|
|
84
|
+
...(next.purchase ? { purchaseId: next.purchase.id } : {}),
|
|
85
|
+
};
|
|
86
|
+
if (current)
|
|
87
|
+
index.entries[index.entries.indexOf(current)] = metadata;
|
|
88
|
+
else
|
|
89
|
+
index.entries.push(metadata);
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
key: entryKey(next.id),
|
|
93
|
+
expected,
|
|
94
|
+
next: await sealBrowserRecord(entryKey(next.id), { ...next, kind: 'browser-purchase' }, backupSecret),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const archiveIndex = encryptedRecoveryArchiveIndexStore(rawStore, backupSecret);
|
|
100
|
+
const archiveIndexKey = (purchaseId) => `annon/browser/archives/${purchaseId}`;
|
|
101
|
+
const recoveryArchives = async (purchaseId) => {
|
|
102
|
+
const index = await archiveIndex.load(archiveIndexKey(purchaseId));
|
|
103
|
+
return Promise.all((index?.names ?? []).map(async (name) => {
|
|
104
|
+
const archived = await rawStore.load(backupKey(name));
|
|
105
|
+
if (!archived)
|
|
106
|
+
throw new Error('recovery archive missing');
|
|
107
|
+
const record = await importRecovery(archived.backup, backupSecret);
|
|
108
|
+
if (record.purchase_id !== purchaseId ||
|
|
109
|
+
name !== `${record.id}:revision:${record.revision}`)
|
|
110
|
+
throw new Error('recovery archive mismatch');
|
|
111
|
+
return record;
|
|
112
|
+
}));
|
|
113
|
+
};
|
|
114
|
+
const writeBackup = async (name, encryptedFile) => {
|
|
115
|
+
const key = backupKey(name);
|
|
116
|
+
const canonical = name.replace(/:revision:[0-9]+$/, '');
|
|
117
|
+
const recovery = canonical.startsWith('recovery:')
|
|
118
|
+
? await importRecovery(encryptedFile, backupSecret)
|
|
119
|
+
: undefined;
|
|
120
|
+
const sourceRevision = recovery?.revision ??
|
|
121
|
+
(/^0x[0-9a-f]{64}$/.test(canonical)
|
|
122
|
+
? (await importPurchase(encryptedFile, backupSecret)).revision
|
|
123
|
+
: undefined);
|
|
124
|
+
const archived = name !== canonical;
|
|
125
|
+
if (archived && (!recovery || name !== `${recovery.id}:revision:${recovery.revision}`))
|
|
126
|
+
throw new TypeError('invalid recovery archive name');
|
|
127
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
128
|
+
const before = (await rawStore.load(key));
|
|
129
|
+
if (sourceRevision !== undefined &&
|
|
130
|
+
before?.sourceRevision !== undefined &&
|
|
131
|
+
before.sourceRevision >= sourceRevision)
|
|
132
|
+
return;
|
|
133
|
+
const next = {
|
|
134
|
+
...(sourceRevision === undefined ? {} : { sourceRevision }),
|
|
135
|
+
id: name,
|
|
136
|
+
revision: before ? before.revision + 1 : 0,
|
|
137
|
+
backup: encryptedFile,
|
|
138
|
+
};
|
|
139
|
+
if (archived && recovery) {
|
|
140
|
+
const indexKey = archiveIndexKey(recovery.purchase_id);
|
|
141
|
+
const current = await archiveIndex.load(indexKey);
|
|
142
|
+
const names = current?.names ?? [];
|
|
143
|
+
const index = {
|
|
144
|
+
kind: 'browser-recovery-archives',
|
|
145
|
+
id: `recovery-archives:${recovery.purchase_id}`,
|
|
146
|
+
revision: current ? current.revision + 1 : 0,
|
|
147
|
+
purchaseId: recovery.purchase_id,
|
|
148
|
+
names: names.includes(name) ? names : [...names, name],
|
|
149
|
+
};
|
|
150
|
+
if (await rawStore.compareAndSetMany([
|
|
151
|
+
{ key, expected: before, next },
|
|
152
|
+
{
|
|
153
|
+
key: indexKey,
|
|
154
|
+
expected: current,
|
|
155
|
+
next: await sealBrowserRecord(indexKey, index, backupSecret),
|
|
156
|
+
},
|
|
157
|
+
]))
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
else if (await rawStore.compareAndSet(key, before, next))
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
throw conflict();
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
rawStore,
|
|
167
|
+
backupSecret,
|
|
168
|
+
load,
|
|
169
|
+
save,
|
|
170
|
+
writeBackup,
|
|
171
|
+
recoveryArchives,
|
|
172
|
+
async restorePurchase(record) {
|
|
173
|
+
const purchases = encryptedPurchaseStore(rawStore, backupSecret);
|
|
174
|
+
const name = purchaseKey(record.id);
|
|
175
|
+
const current = await purchases.load(name);
|
|
176
|
+
if (current &&
|
|
177
|
+
(current.message !== record.message || current.buyerSecret !== record.buyerSecret))
|
|
178
|
+
throw conflict();
|
|
179
|
+
if (current && current.revision >= record.revision)
|
|
180
|
+
return current;
|
|
181
|
+
const restored = { ...record, revision: current ? current.revision + 1 : 0 };
|
|
182
|
+
if (!(await purchases.compareAndSet(name, current, restored)))
|
|
183
|
+
throw conflict();
|
|
184
|
+
await writeBackup(restored.id, await exportPurchase(restored, backupSecret));
|
|
185
|
+
return restored;
|
|
186
|
+
},
|
|
187
|
+
async list() {
|
|
188
|
+
return ((await read())?.entries ?? []).map(({ id, fileId, route, state }) => ({
|
|
189
|
+
id,
|
|
190
|
+
fileId,
|
|
191
|
+
route,
|
|
192
|
+
state,
|
|
193
|
+
}));
|
|
194
|
+
},
|
|
195
|
+
async exportReceipt(id, secret) {
|
|
196
|
+
const entry = await load(id);
|
|
197
|
+
if (!entry)
|
|
198
|
+
throw new Error('purchase missing');
|
|
199
|
+
const portable = structuredClone(entry);
|
|
200
|
+
if (entry.purchase)
|
|
201
|
+
portable.recoveryArchives = await recoveryArchives(entry.purchase.id);
|
|
202
|
+
if (entry.purchase) {
|
|
203
|
+
const current = await encryptedPurchaseStore(rawStore, backupSecret).load(purchaseKey(entry.purchase.id));
|
|
204
|
+
if (current && current.revision >= entry.purchase.revision)
|
|
205
|
+
portable.purchase = current;
|
|
206
|
+
const backed = await rawStore.load(backupKey(entry.purchase.id));
|
|
207
|
+
if (backed) {
|
|
208
|
+
const purchase = await importPurchase(backed.backup, backupSecret);
|
|
209
|
+
if (purchase.revision > (portable.purchase?.revision ?? -1))
|
|
210
|
+
portable.purchase = purchase;
|
|
211
|
+
}
|
|
212
|
+
const recovery = await encryptedRecoveryStore(rawStore, backupSecret).load(`annon/recovery/${entry.purchase.id}`);
|
|
213
|
+
const pending = await rawStore.load(backupKey(`recovery:${entry.purchase.id}`));
|
|
214
|
+
if (recovery)
|
|
215
|
+
portable.recovery = recovery;
|
|
216
|
+
else if (pending)
|
|
217
|
+
portable.recovery = await importRecovery(pending.backup, backupSecret);
|
|
218
|
+
}
|
|
219
|
+
return exportBrowserReceipt({ kind: 'browser-receipt', entry: portable }, secret);
|
|
220
|
+
},
|
|
221
|
+
async importReceipt(file, secret) {
|
|
222
|
+
const { entry } = await importBrowserReceipt(file, secret);
|
|
223
|
+
if (await load(entry.id))
|
|
224
|
+
throw conflict();
|
|
225
|
+
if (entry.purchase) {
|
|
226
|
+
const index = await read();
|
|
227
|
+
if (index?.entries.some((e) => e.purchaseId === entry.purchase?.id))
|
|
228
|
+
throw conflict();
|
|
229
|
+
const purchases = encryptedPurchaseStore(rawStore, backupSecret);
|
|
230
|
+
const next = { ...entry.purchase, revision: 0 };
|
|
231
|
+
const current = await purchases.load(purchaseKey(next.id));
|
|
232
|
+
if (current &&
|
|
233
|
+
(current.message !== next.message || current.buyerSecret !== next.buyerSecret))
|
|
234
|
+
throw conflict();
|
|
235
|
+
if (!current && !(await purchases.compareAndSet(purchaseKey(next.id), null, next)))
|
|
236
|
+
throw conflict();
|
|
237
|
+
entry.purchase = current ?? next;
|
|
238
|
+
}
|
|
239
|
+
if (entry.recovery) {
|
|
240
|
+
const recoveries = encryptedRecoveryStore(rawStore, backupSecret);
|
|
241
|
+
const name = `annon/recovery/${entry.recovery.purchase_id}`;
|
|
242
|
+
const current = await recoveries.load(name);
|
|
243
|
+
if (current && current.buyerSecret !== entry.recovery.buyerSecret)
|
|
244
|
+
throw conflict();
|
|
245
|
+
const next = { ...entry.recovery, revision: 0 };
|
|
246
|
+
if (!current && !(await recoveries.compareAndSet(name, null, next)))
|
|
247
|
+
throw conflict();
|
|
248
|
+
entry.recovery = current ?? next;
|
|
249
|
+
}
|
|
250
|
+
for (const archive of entry.recoveryArchives ?? [])
|
|
251
|
+
await writeBackup(`${archive.id}:revision:${archive.revision}`, await exportRecovery(archive, backupSecret));
|
|
252
|
+
await save(null, {
|
|
253
|
+
...entry,
|
|
254
|
+
revision: 0,
|
|
255
|
+
needsProof: true,
|
|
256
|
+
lease: undefined,
|
|
257
|
+
state: [
|
|
258
|
+
'terms_review',
|
|
259
|
+
'refunded',
|
|
260
|
+
'failed',
|
|
261
|
+
'expired',
|
|
262
|
+
'recovering',
|
|
263
|
+
'accepted',
|
|
264
|
+
'file_ready',
|
|
265
|
+
'abandoned',
|
|
266
|
+
].includes(entry.state)
|
|
267
|
+
? entry.state
|
|
268
|
+
: 'payment_uncertain',
|
|
269
|
+
}, true);
|
|
270
|
+
return entry.id;
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Build the real SDK using the same encrypted storage and durable internal backups. */
|
|
275
|
+
export function browserAnnon(storage, options) {
|
|
276
|
+
return new Annon({
|
|
277
|
+
...options,
|
|
278
|
+
rawStore: storage.rawStore,
|
|
279
|
+
backupSecret: storage.backupSecret,
|
|
280
|
+
writeBackup: storage.writeBackup,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const LEASE_SECONDS = 120;
|
|
284
|
+
const HEARTBEAT_MS = 30000;
|
|
285
|
+
export class PurchaseLeaseBusy extends AnnonError {
|
|
286
|
+
waitUntil;
|
|
287
|
+
code = 'lease_busy';
|
|
288
|
+
constructor(waitUntil) {
|
|
289
|
+
super('lease_busy', 'Another tab holds this purchase; wait for its lease to expire.');
|
|
290
|
+
this.waitUntil = waitUntil;
|
|
291
|
+
this.name = 'PurchaseLeaseBusy';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export class BrowserPurchaseController {
|
|
295
|
+
options;
|
|
296
|
+
storage;
|
|
297
|
+
owner = crypto.randomUUID();
|
|
298
|
+
listeners = new Set();
|
|
299
|
+
constructor(options) {
|
|
300
|
+
this.options = options;
|
|
301
|
+
this.storage = options.storage;
|
|
302
|
+
}
|
|
303
|
+
static async create(options, terms) {
|
|
304
|
+
const record = {
|
|
305
|
+
id: terms.id,
|
|
306
|
+
revision: 0,
|
|
307
|
+
fileId: terms.fileId,
|
|
308
|
+
header: hex(terms.header),
|
|
309
|
+
resourceUrl: terms.resourceUrl,
|
|
310
|
+
route: terms.route,
|
|
311
|
+
maxPrice: String(terms.maxPrice),
|
|
312
|
+
state: 'terms_review',
|
|
313
|
+
submission: 'not_submitted',
|
|
314
|
+
};
|
|
315
|
+
await options.storage.save(null, record);
|
|
316
|
+
return new BrowserPurchaseController({ ...options, id: terms.id });
|
|
317
|
+
}
|
|
318
|
+
subscribe(listener) {
|
|
319
|
+
this.listeners.add(listener);
|
|
320
|
+
return () => {
|
|
321
|
+
this.listeners.delete(listener);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async read() {
|
|
325
|
+
const record = await this.storage.load(this.options.id);
|
|
326
|
+
if (!record)
|
|
327
|
+
throw new Error('purchase missing');
|
|
328
|
+
return record;
|
|
329
|
+
}
|
|
330
|
+
/** Safe to render. Full records are available only through the persistence adapter. */
|
|
331
|
+
async snapshot() {
|
|
332
|
+
const record = await this.read();
|
|
333
|
+
return {
|
|
334
|
+
id: record.id,
|
|
335
|
+
state: record.state,
|
|
336
|
+
revision: record.revision,
|
|
337
|
+
fileId: record.fileId,
|
|
338
|
+
route: record.route,
|
|
339
|
+
resourceUrl: record.resourceUrl,
|
|
340
|
+
...this.waiting(record),
|
|
341
|
+
...(record.checkpoint?.lastStatus ? { swapStatus: record.checkpoint.lastStatus.status } : {}),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
now() {
|
|
345
|
+
return (this.options.now ?? (() => Math.floor(Date.now() / 1000)))();
|
|
346
|
+
}
|
|
347
|
+
waiting(record) {
|
|
348
|
+
return record.lease && record.lease.expiresAt > this.now()
|
|
349
|
+
? { leaseWaitUntil: record.lease.expiresAt }
|
|
350
|
+
: {};
|
|
351
|
+
}
|
|
352
|
+
emit(event) {
|
|
353
|
+
for (const listener of this.listeners) {
|
|
354
|
+
try {
|
|
355
|
+
listener(event);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
/* Observers cannot interrupt the workflow. */
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
busy(record) {
|
|
363
|
+
if (!record.lease || record.lease.expiresAt <= this.now())
|
|
364
|
+
return;
|
|
365
|
+
this.emit({
|
|
366
|
+
id: record.id,
|
|
367
|
+
state: record.state,
|
|
368
|
+
revision: record.revision,
|
|
369
|
+
...this.waiting(record),
|
|
370
|
+
});
|
|
371
|
+
return new PurchaseLeaseBusy(record.lease.expiresAt);
|
|
372
|
+
}
|
|
373
|
+
/** Every asynchronous operation owns a renewable lease, with revision CAS on every write. */
|
|
374
|
+
async withLease(run, options = {}) {
|
|
375
|
+
const before = await this.read();
|
|
376
|
+
const busy = this.busy(before);
|
|
377
|
+
if (busy)
|
|
378
|
+
throw busy;
|
|
379
|
+
if (options.initial?.state === 'preparing' && before.state !== 'terms_review')
|
|
380
|
+
throw new Error('purchase already started; resume the saved purchase');
|
|
381
|
+
const lease = () => ({ owner: this.owner, expiresAt: this.now() + LEASE_SECONDS });
|
|
382
|
+
const session = {
|
|
383
|
+
record: await this.change(before, { ...options.initial, lease: lease() }),
|
|
384
|
+
change: (patch) => enqueue(async () => {
|
|
385
|
+
if (heartbeatError)
|
|
386
|
+
throw heartbeatError;
|
|
387
|
+
if (session.record.lease?.owner !== this.owner ||
|
|
388
|
+
session.record.lease.expiresAt <= this.now())
|
|
389
|
+
throw new Error('purchase lease expired; resume the saved purchase');
|
|
390
|
+
session.record = await this.change(session.record, { ...patch, lease: lease() });
|
|
391
|
+
return session.record;
|
|
392
|
+
}),
|
|
393
|
+
};
|
|
394
|
+
let queue = Promise.resolve();
|
|
395
|
+
let heartbeatError;
|
|
396
|
+
function enqueue(fn) {
|
|
397
|
+
const next = queue.then(fn);
|
|
398
|
+
queue = next.catch(() => { });
|
|
399
|
+
return next;
|
|
400
|
+
}
|
|
401
|
+
const heartbeat = setInterval(() => {
|
|
402
|
+
void session.change({}).catch((error) => {
|
|
403
|
+
heartbeatError = error;
|
|
404
|
+
});
|
|
405
|
+
}, HEARTBEAT_MS);
|
|
406
|
+
try {
|
|
407
|
+
const result = await run(session);
|
|
408
|
+
if (options.finalCheckpoint !== false)
|
|
409
|
+
await session.change({});
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
clearInterval(heartbeat);
|
|
414
|
+
await queue;
|
|
415
|
+
// A stale owner must never release a newer owner's lease.
|
|
416
|
+
await this.change(session.record, { lease: undefined }).catch(() => { });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async change(before, patch) {
|
|
420
|
+
const next = { ...before, ...patch, id: before.id, revision: before.revision + 1 };
|
|
421
|
+
await this.storage.save(before, next);
|
|
422
|
+
const event = {
|
|
423
|
+
id: next.id,
|
|
424
|
+
state: next.state,
|
|
425
|
+
revision: next.revision,
|
|
426
|
+
...(next.checkpoint?.lastStatus ? { swapStatus: next.checkpoint.lastStatus.status } : {}),
|
|
427
|
+
};
|
|
428
|
+
this.emit(event);
|
|
429
|
+
return next;
|
|
430
|
+
}
|
|
431
|
+
async start(options) {
|
|
432
|
+
const before = await this.read();
|
|
433
|
+
if (before.state !== 'terms_review')
|
|
434
|
+
throw new Error('purchase already started; resume the saved purchase');
|
|
435
|
+
if (before.route === 'base' && typeof options.submitBase !== 'function')
|
|
436
|
+
throw new TypeError('Base requires submitBase');
|
|
437
|
+
await this.withLease(async (session) => {
|
|
438
|
+
const persist = async (patch) => {
|
|
439
|
+
await session.change(patch);
|
|
440
|
+
};
|
|
441
|
+
try {
|
|
442
|
+
const result = await this.options.sdk.buy({
|
|
443
|
+
...options,
|
|
444
|
+
fileId: session.record.fileId,
|
|
445
|
+
header: bytes(session.record.header, (session.record.header.length - 2) / 2),
|
|
446
|
+
url: session.record.resourceUrl,
|
|
447
|
+
route: session.record.route,
|
|
448
|
+
maxPrice: BigInt(session.record.maxPrice),
|
|
449
|
+
beforeSave: async (purchase) => {
|
|
450
|
+
await persist({
|
|
451
|
+
purchase,
|
|
452
|
+
...(session.record.route === 'base' &&
|
|
453
|
+
purchase.attempts.some((attempt) => !!attempt.payload)
|
|
454
|
+
? { submission: 'uncertain' }
|
|
455
|
+
: {}),
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
onSaved: async (purchase) => {
|
|
459
|
+
if (session.record.purchase?.revision !== purchase.revision)
|
|
460
|
+
await persist({
|
|
461
|
+
purchase,
|
|
462
|
+
...(session.record.route === 'base' &&
|
|
463
|
+
purchase.attempts.some((attempt) => !!attempt.payload)
|
|
464
|
+
? { submission: 'uncertain' }
|
|
465
|
+
: {}),
|
|
466
|
+
});
|
|
467
|
+
},
|
|
468
|
+
beforePayment: async (purchase) => {
|
|
469
|
+
if (!purchase.recoveryProved || session.record.purchase?.id !== purchase.id)
|
|
470
|
+
throw new Error('durable recovery proof required');
|
|
471
|
+
if (session.record.lease?.owner !== this.owner ||
|
|
472
|
+
session.record.lease.expiresAt <= this.now())
|
|
473
|
+
throw new Error('purchase lease expired');
|
|
474
|
+
if (session.record.route !== '1click')
|
|
475
|
+
await persist({
|
|
476
|
+
state: 'payment_uncertain',
|
|
477
|
+
purchase,
|
|
478
|
+
submission: 'uncertain',
|
|
479
|
+
paymentStarted: true,
|
|
480
|
+
});
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
await persist({
|
|
484
|
+
purchase: result.record,
|
|
485
|
+
state: session.record.route === '1click' ? 'awaiting_deposit' : 'payment_uncertain',
|
|
486
|
+
submission: session.record.route === '1click'
|
|
487
|
+
? 'not_submitted'
|
|
488
|
+
: result.uncertain
|
|
489
|
+
? 'uncertain'
|
|
490
|
+
: session.record.submission === 'not_submitted'
|
|
491
|
+
? 'not_submitted'
|
|
492
|
+
: 'submitted',
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
// Only an unstaged failure can return to review. A staged purchase must resume its original commitment.
|
|
497
|
+
await session
|
|
498
|
+
.change({
|
|
499
|
+
state: session.record.purchase ? 'payment_uncertain' : 'terms_review',
|
|
500
|
+
})
|
|
501
|
+
.catch(() => { });
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}, { initial: { state: 'preparing', submission: 'not_submitted' }, finalCheckpoint: false });
|
|
505
|
+
}
|
|
506
|
+
deposit(record) {
|
|
507
|
+
return record.purchase?.quote?.deposit;
|
|
508
|
+
}
|
|
509
|
+
async instructions() {
|
|
510
|
+
const record = await this.read();
|
|
511
|
+
const deposit = this.deposit(record);
|
|
512
|
+
if (!['awaiting_deposit', 'incomplete_deposit'].includes(record.state) ||
|
|
513
|
+
record.needsProof ||
|
|
514
|
+
!record.purchase?.recoveryProved ||
|
|
515
|
+
!deposit ||
|
|
516
|
+
!Number.isFinite(Date.parse(deposit.depositBefore)) ||
|
|
517
|
+
Date.parse(deposit.depositBefore) - this.now() * 1000 < 600000)
|
|
518
|
+
return null;
|
|
519
|
+
return structuredClone(deposit);
|
|
520
|
+
}
|
|
521
|
+
async resume(options = {}) {
|
|
522
|
+
let saved = await this.read();
|
|
523
|
+
if (this.busy(saved))
|
|
524
|
+
return;
|
|
525
|
+
if (['terms_review', 'refunded', 'failed', 'file_ready', 'accepted', 'abandoned'].includes(saved.state))
|
|
526
|
+
return;
|
|
527
|
+
if (saved.state === 'recovering') {
|
|
528
|
+
if (options.recovery)
|
|
529
|
+
await this.recover(options.recovery);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const interrupted = saved.lease !== undefined;
|
|
533
|
+
return this.withLease(async (session) => {
|
|
534
|
+
saved = session.record;
|
|
535
|
+
if (!saved.purchase) {
|
|
536
|
+
await session.change({ state: 'terms_review' });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const purchase = saved.purchase;
|
|
540
|
+
const signed = purchase.attempts.some((attempt) => !!attempt.payload);
|
|
541
|
+
if (saved.route !== '1click' &&
|
|
542
|
+
saved.submission === 'not_submitted' &&
|
|
543
|
+
!signed &&
|
|
544
|
+
!saved.paymentStarted &&
|
|
545
|
+
!saved.purchase.nearSubmission) {
|
|
546
|
+
await session.change({ state: 'abandoned' });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if ((interrupted || signed) && saved.submission === 'not_submitted')
|
|
550
|
+
saved = await session.change({
|
|
551
|
+
...(signed ? { submission: 'uncertain' } : {}),
|
|
552
|
+
});
|
|
553
|
+
let restored = await this.storage.restorePurchase(purchase);
|
|
554
|
+
if (JSON.stringify(restored) !== JSON.stringify(saved.purchase))
|
|
555
|
+
saved = await session.change({ purchase: restored });
|
|
556
|
+
const pendingBase = () => saved.route === 'base' &&
|
|
557
|
+
restored.paymentState !== 'expired' &&
|
|
558
|
+
(signed || saved.submission !== 'not_submitted' || saved.state === 'accepting');
|
|
559
|
+
if (pendingBase() && saved.state !== 'accepting')
|
|
560
|
+
saved = await session.change({ state: 'accepting' });
|
|
561
|
+
if (await this.options.sdk.accept(restored)) {
|
|
562
|
+
await session.change({ state: 'accepted' });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
restored = await this.storage.restorePurchase(restored);
|
|
566
|
+
if (JSON.stringify(restored) !== JSON.stringify(saved.purchase))
|
|
567
|
+
saved = await session.change({ purchase: restored });
|
|
568
|
+
const status = saved.checkpoint?.lastStatus?.status;
|
|
569
|
+
const progress = status === 'SUCCESS' || saved.state === 'accepting'
|
|
570
|
+
? 'accepting'
|
|
571
|
+
: status === 'PROCESSING' || status === 'KNOWN_DEPOSIT_TX' || saved.state === 'processing'
|
|
572
|
+
? 'processing'
|
|
573
|
+
: null;
|
|
574
|
+
if (saved.route === '1click' && progress) {
|
|
575
|
+
await session.change({ state: progress });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (saved.state === 'expired' && saved.route !== 'base')
|
|
579
|
+
return;
|
|
580
|
+
if (saved.route !== '1click') {
|
|
581
|
+
await session.change({
|
|
582
|
+
state: pendingBase()
|
|
583
|
+
? 'accepting'
|
|
584
|
+
: this.now() >= Number(restored.purchase.expires_at)
|
|
585
|
+
? 'expired'
|
|
586
|
+
: saved.submission === 'submitted'
|
|
587
|
+
? 'accepting'
|
|
588
|
+
: 'payment_uncertain',
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const deposit = this.deposit(saved);
|
|
593
|
+
if (!deposit ||
|
|
594
|
+
!Number.isFinite(Date.parse(deposit.depositBefore)) ||
|
|
595
|
+
Date.parse(deposit.depositBefore) - this.now() * 1000 < 600000) {
|
|
596
|
+
await session.change({ state: 'expired' });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (saved.needsProof || !saved.purchase?.recoveryProved) {
|
|
600
|
+
if (!options.recovery)
|
|
601
|
+
return;
|
|
602
|
+
const purchase = await this.options.sdk.proveRecovery(restored, options.recovery);
|
|
603
|
+
saved = await session.change({ purchase, needsProof: false });
|
|
604
|
+
if (Date.parse(deposit.depositBefore) - this.now() * 1000 < 600000) {
|
|
605
|
+
await session.change({ state: 'expired' });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
await session.change({
|
|
610
|
+
state: status === 'SUCCESS'
|
|
611
|
+
? 'accepting'
|
|
612
|
+
: status === 'INCOMPLETE_DEPOSIT'
|
|
613
|
+
? 'incomplete_deposit'
|
|
614
|
+
: status === 'PROCESSING'
|
|
615
|
+
? 'processing'
|
|
616
|
+
: 'awaiting_deposit',
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/** Polling remains available after the deposit cutoff to track late deposits and refunds. */
|
|
621
|
+
async poll(options = {}) {
|
|
622
|
+
return this.withLease(async (session) => {
|
|
623
|
+
let saved = session.record;
|
|
624
|
+
const deposit = this.deposit(saved);
|
|
625
|
+
if (saved.route !== '1click' ||
|
|
626
|
+
!deposit ||
|
|
627
|
+
[
|
|
628
|
+
'terms_review',
|
|
629
|
+
'accepted',
|
|
630
|
+
'recovering',
|
|
631
|
+
'file_ready',
|
|
632
|
+
'refunded',
|
|
633
|
+
'failed',
|
|
634
|
+
'abandoned',
|
|
635
|
+
].includes(saved.state))
|
|
636
|
+
throw new Error('no pollable 1Click deposit in this state');
|
|
637
|
+
const { oneClick = new OneClick(), ...pollOptions } = options;
|
|
638
|
+
await oneClick.poll(deposit.address, {
|
|
639
|
+
...pollOptions,
|
|
640
|
+
...(deposit.memo === undefined ? {} : { memo: deposit.memo }),
|
|
641
|
+
...(saved.checkpoint ? { checkpoint: saved.checkpoint } : {}),
|
|
642
|
+
onProgress: async (checkpoint) => {
|
|
643
|
+
const status = checkpoint.lastStatus?.status;
|
|
644
|
+
const states = {
|
|
645
|
+
PENDING_DEPOSIT: 'awaiting_deposit',
|
|
646
|
+
KNOWN_DEPOSIT_TX: 'processing',
|
|
647
|
+
PROCESSING: 'processing',
|
|
648
|
+
INCOMPLETE_DEPOSIT: 'incomplete_deposit',
|
|
649
|
+
SUCCESS: 'accepting',
|
|
650
|
+
REFUNDED: 'refunded',
|
|
651
|
+
FAILED: 'failed',
|
|
652
|
+
};
|
|
653
|
+
let state = status ? (states[status] ?? saved.state) : saved.state;
|
|
654
|
+
if (['awaiting_deposit', 'incomplete_deposit'].includes(state) &&
|
|
655
|
+
(saved.state === 'expired' || Date.parse(deposit.depositBefore) <= this.now() * 1000))
|
|
656
|
+
state = 'expired';
|
|
657
|
+
saved = await session.change({ checkpoint, state });
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
if (saved.state === 'accepting' &&
|
|
661
|
+
saved.purchase &&
|
|
662
|
+
(await this.options.sdk.accept(saved.purchase)))
|
|
663
|
+
await session.change({ state: 'accepted' });
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
async recover(wallet) {
|
|
667
|
+
return this.withLease(async (session) => {
|
|
668
|
+
let saved = session.record;
|
|
669
|
+
if (!saved.purchase)
|
|
670
|
+
throw new Error('saved purchase required');
|
|
671
|
+
const purchase = await this.storage.restorePurchase(saved.purchase);
|
|
672
|
+
saved = await session.change({ state: 'recovering', purchase });
|
|
673
|
+
const recovered = await this.options.sdk.recover({ wallet, purchaseId: purchase.id });
|
|
674
|
+
const recovery = recovered[0];
|
|
675
|
+
if (!recovery || recovery.grant === null)
|
|
676
|
+
throw new Error('recovery not final; resume recovery');
|
|
677
|
+
await session.change({ recovery, state: 'accepted' });
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
/** Open supplied ciphertext, or download with the saved grant. Returns plaintext only in memory. */
|
|
681
|
+
async open(input = {}) {
|
|
682
|
+
return this.withLease(async (session) => {
|
|
683
|
+
let saved = session.record;
|
|
684
|
+
if (!['accepted', 'file_ready'].includes(saved.state) || !saved.purchase)
|
|
685
|
+
throw new Error('purchase is not accepted');
|
|
686
|
+
const restored = await this.storage.restorePurchase(saved.purchase);
|
|
687
|
+
if (JSON.stringify(restored) !== JSON.stringify(saved.purchase))
|
|
688
|
+
saved = await session.change({ purchase: restored });
|
|
689
|
+
const options = input instanceof Uint8Array ? {} : input;
|
|
690
|
+
if (input instanceof Uint8Array && input.length > BROWSER_DOWNLOAD_LIMIT)
|
|
691
|
+
throw new DownloadError('too_large', 'File exceeds the 50 MB size limit.');
|
|
692
|
+
const header = bytes(saved.header, (saved.header.length - 2) / 2);
|
|
693
|
+
if (saved.recovery?.grant === null)
|
|
694
|
+
throw new Error('recovery grant is not final');
|
|
695
|
+
const file = input instanceof Uint8Array
|
|
696
|
+
? input
|
|
697
|
+
: await browserDownload({
|
|
698
|
+
...options,
|
|
699
|
+
url: saved.resourceUrl,
|
|
700
|
+
contractId: restored.terms.contractId,
|
|
701
|
+
fileId: saved.fileId,
|
|
702
|
+
header,
|
|
703
|
+
checkedHeaderHash: restored.checkedHeaderHash,
|
|
704
|
+
listing: {
|
|
705
|
+
fileId: saved.fileId,
|
|
706
|
+
cipherHash: restored.terms.cipherHash,
|
|
707
|
+
seller: restored.terms.seller,
|
|
708
|
+
// The complete header digest was pinned before payment. Annon.open also checks final terms.
|
|
709
|
+
publishedHeaderHash: hex(await sha256(header.subarray(0, -65))),
|
|
710
|
+
},
|
|
711
|
+
proof: {
|
|
712
|
+
secret: bytes(saved.recovery?.buyerSecret ?? restored.buyerSecret, 32),
|
|
713
|
+
purchase_id: restored.id,
|
|
714
|
+
grant: saved.recovery?.grant ?? 0,
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
if (options.signal?.aborted)
|
|
718
|
+
throw new DownloadError('cancelled');
|
|
719
|
+
downloadProgress(options.onProgress, { phase: 'decrypting', received: file.length });
|
|
720
|
+
const result = await this.options.sdk.open({
|
|
721
|
+
file,
|
|
722
|
+
record: restored,
|
|
723
|
+
...(saved.recovery ? { recovery: saved.recovery } : {}),
|
|
724
|
+
});
|
|
725
|
+
if (options.signal?.aborted)
|
|
726
|
+
throw new DownloadError('cancelled');
|
|
727
|
+
await session.change({ state: 'file_ready' });
|
|
728
|
+
downloadProgress(options.onProgress, { phase: 'ready', received: file.length });
|
|
729
|
+
return result;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
exportReceipt(secret) {
|
|
733
|
+
return this.storage.exportReceipt(this.options.id, secret);
|
|
734
|
+
}
|
|
735
|
+
}
|