@defdone/annon 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/CLI.md +378 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +591 -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 +151 -0
- package/dist/browser/login.d.ts +1 -0
- package/dist/browser/login.js +216 -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 +98 -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 +25 -0
- package/dist/node/browser-login.js +219 -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 +149 -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 +386 -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 +136 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { checkSigned, equal, NEAR_CKD, nearCkd, Pay2Error, sealWithKey, sha256, TAG, } from './pay2.js';
|
|
2
|
+
import { ByteReader, openStream, readHeader, sealStreamWithKey } from './stream.js';
|
|
3
|
+
/** @internal A single-use stream needs external storage to preserve spec 6 validation order. */
|
|
4
|
+
export function withReplay(source, use, spool) {
|
|
5
|
+
if (!(source instanceof ReadableStream))
|
|
6
|
+
return use(source);
|
|
7
|
+
if (!spool)
|
|
8
|
+
return Promise.reject(new TypeError('a ciphertext spool is required for a single-use stream'));
|
|
9
|
+
return spool(source, use);
|
|
10
|
+
}
|
|
11
|
+
export function blobSource(blob) {
|
|
12
|
+
return { size: blob.size, read: () => blob.stream() };
|
|
13
|
+
}
|
|
14
|
+
/** @internal Keeps registration and sell recovery independent of the file representation. */
|
|
15
|
+
export async function prepareSeal(key, data, opts) {
|
|
16
|
+
if (data instanceof Uint8Array) {
|
|
17
|
+
const immutable = data.slice();
|
|
18
|
+
const sealed = await sealWithKey(key, immutable, opts);
|
|
19
|
+
const stream = await sealStreamWithKey(key, new Blob([immutable]).stream(), immutable.length, opts);
|
|
20
|
+
const body = () => stream.body(new Blob([immutable]).stream());
|
|
21
|
+
return {
|
|
22
|
+
...sealed,
|
|
23
|
+
manifest: stream.manifest,
|
|
24
|
+
headerSize: stream.headerSize,
|
|
25
|
+
body,
|
|
26
|
+
stage: (spool, use) => spool(body(), use),
|
|
27
|
+
async finish(options) {
|
|
28
|
+
const file = await sealed.finish(options);
|
|
29
|
+
return {
|
|
30
|
+
consumed: () => true,
|
|
31
|
+
cancel: async () => { },
|
|
32
|
+
file: file,
|
|
33
|
+
header: file.subarray(0, new DataView(file.buffer).getUint32(8)),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const firstPass = data.read();
|
|
39
|
+
let sealed;
|
|
40
|
+
try {
|
|
41
|
+
sealed = await sealStreamWithKey(key, firstPass, data.size, opts);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
await firstPass.cancel(error).catch(() => { });
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...sealed,
|
|
49
|
+
body: () => sealed.body(data.read()),
|
|
50
|
+
stage: (spool, use) => spool(sealed.body(data.read()), use),
|
|
51
|
+
async finish(options) {
|
|
52
|
+
const replay = data.read();
|
|
53
|
+
let wire;
|
|
54
|
+
try {
|
|
55
|
+
wire = await sealed.finish(replay, options);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
await replay.cancel(error).catch(() => { });
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
const reader = wire.getReader();
|
|
62
|
+
let header;
|
|
63
|
+
try {
|
|
64
|
+
// sealStream always emits the bounded header as its first piece.
|
|
65
|
+
header = (await reader.read()).value;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
await reader.cancel(error);
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
let released = false;
|
|
73
|
+
const release = () => {
|
|
74
|
+
if (!released) {
|
|
75
|
+
released = true;
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
let first = true;
|
|
80
|
+
let consumed = false;
|
|
81
|
+
const file = new ReadableStream({
|
|
82
|
+
async pull(c) {
|
|
83
|
+
if (first) {
|
|
84
|
+
first = false;
|
|
85
|
+
c.enqueue(header);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const result = await reader.read();
|
|
90
|
+
if (result.done) {
|
|
91
|
+
consumed = true;
|
|
92
|
+
c.close();
|
|
93
|
+
release();
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
c.enqueue(result.value);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
c.error(error);
|
|
100
|
+
release();
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async cancel(reason) {
|
|
104
|
+
await reader.cancel(reason);
|
|
105
|
+
release();
|
|
106
|
+
},
|
|
107
|
+
}, { highWaterMark: 0 });
|
|
108
|
+
return {
|
|
109
|
+
file: file,
|
|
110
|
+
header,
|
|
111
|
+
consumed: () => consumed,
|
|
112
|
+
async cancel(reason) {
|
|
113
|
+
if (!released) {
|
|
114
|
+
try {
|
|
115
|
+
await reader.cancel(reason);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
release();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await file.cancel(reason).catch(() => { });
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Spec 6 in buffered-open order with bounded memory. Validate ALL body lengths and hashes
|
|
129
|
+
* before selecting a stanza or obtaining a key. Replay only immutable ciphertext. A download
|
|
130
|
+
* must first be spooled to caller-owned storage. Sink contents remain provisional until success.
|
|
131
|
+
*/
|
|
132
|
+
export async function prepareOpen(source, opts) {
|
|
133
|
+
const input = new ByteReader(source.read());
|
|
134
|
+
let header;
|
|
135
|
+
let plaintextSize = 0;
|
|
136
|
+
try {
|
|
137
|
+
header = await readHeader(input);
|
|
138
|
+
const h = await checkSigned(header, opts.listing, opts.contractId);
|
|
139
|
+
const C = 2 ** h.chunkLog2;
|
|
140
|
+
let mismatch = -1;
|
|
141
|
+
for (let i = 0; i < h.n; i++) {
|
|
142
|
+
const last = i === h.n - 1;
|
|
143
|
+
const length = i === 0 ? h.metaLen + TAG : i === 1 && h.previewLen > 0 ? h.previewLen + TAG : C + TAG;
|
|
144
|
+
const wire = await input.read(last ? C + TAG + 1 : length);
|
|
145
|
+
if (last ? wire.length < TAG || wire.length > C + TAG : wire.length !== length)
|
|
146
|
+
throw new Pay2Error('bad_body_length');
|
|
147
|
+
if (i > 0)
|
|
148
|
+
plaintextSize += wire.length - TAG;
|
|
149
|
+
if (!equal(await sha256(wire), h.hashes[i]) && mismatch < 0)
|
|
150
|
+
mismatch = i;
|
|
151
|
+
}
|
|
152
|
+
// Length errors take precedence over every hash error, as in checkPublic.
|
|
153
|
+
if (mismatch >= 0)
|
|
154
|
+
throw new Pay2Error('chunk_hash_mismatch', `chunk ${mismatch}`);
|
|
155
|
+
if (!h.stanzas.some((s) => s.type === NEAR_CKD && nearCkd(s.body)?.contractId === opts.contractId))
|
|
156
|
+
throw new Pay2Error('no_usable_stanza');
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await input.cancel();
|
|
160
|
+
input.release();
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
header: header.slice(),
|
|
164
|
+
async writeTo(sink, ckdSecret) {
|
|
165
|
+
let meta;
|
|
166
|
+
await openStream(source.read(), {
|
|
167
|
+
...opts,
|
|
168
|
+
expectedHeader: header,
|
|
169
|
+
ckdSecret,
|
|
170
|
+
onMeta: (value) => {
|
|
171
|
+
meta = value;
|
|
172
|
+
},
|
|
173
|
+
}, plaintextSize).pipeTo(sink);
|
|
174
|
+
if (!meta)
|
|
175
|
+
throw new Pay2Error('meta_invalid');
|
|
176
|
+
return meta;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Bounded-memory open to a provisional sink, with exactly the checks and order of open(). */
|
|
181
|
+
export async function openTo(source, sink, opts) {
|
|
182
|
+
return withReplay(source, async (replay) => {
|
|
183
|
+
const prepared = await prepareOpen(replay, opts);
|
|
184
|
+
return prepared.writeTo(sink, opts.ckdSecret);
|
|
185
|
+
}, opts.spool);
|
|
186
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { UploadManifest } from '../download/upload.js';
|
|
2
|
+
import { AnnonError } from '../errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bounded-memory PAY2 version 2 using Web Streams and the one-shot encoder's WebCrypto primitives.
|
|
5
|
+
*
|
|
6
|
+
* The format puts all hashes before the body, and registration precedes the final stanza/MAC/
|
|
7
|
+
* signature. Sealing therefore consumes TWO streams: sealStream's first pass computes the
|
|
8
|
+
* registration; finish replays the same immutable plaintext. Nothing spools or tees the file.
|
|
9
|
+
* The header/hash table is bounded by the format (64 KiB / 1024 hashes), independent of size.
|
|
10
|
+
* Only a few crypto chunks plus one caller-owned input buffer are retained. Upstream queues
|
|
11
|
+
* and a consumer that collects the output are outside this bound; use backpressure at both ends.
|
|
12
|
+
* A Blob/File can supply the two passes with separate calls to blob.stream(). A non-replayable
|
|
13
|
+
* source must be spooled externally by the caller; using tee() would buffer an entire branch.
|
|
14
|
+
*/
|
|
15
|
+
import { type Bytes, type FileMeta, type FinishOptions, type OpenOptions, type SealOptions } from './pay2.js';
|
|
16
|
+
/** A replay no longer matches the source authenticated in the first pass. */
|
|
17
|
+
export declare class SourceChangedError extends AnnonError<'source_changed'> {
|
|
18
|
+
constructor();
|
|
19
|
+
}
|
|
20
|
+
/** Copies into a bounded destination, never concatenates or queues source chunks. */
|
|
21
|
+
export declare class ByteReader {
|
|
22
|
+
private readonly reader;
|
|
23
|
+
private pending;
|
|
24
|
+
private at;
|
|
25
|
+
private ended;
|
|
26
|
+
private released;
|
|
27
|
+
private cancellation;
|
|
28
|
+
constructor(source: ReadableStream<Uint8Array>);
|
|
29
|
+
/** Read exactly length bytes, or fewer only at EOF. No allocation depends on source size. */
|
|
30
|
+
read(length: number): Promise<Bytes>;
|
|
31
|
+
cancel(reason?: unknown): Promise<void>;
|
|
32
|
+
release(): void;
|
|
33
|
+
}
|
|
34
|
+
interface StreamSealed {
|
|
35
|
+
manifest: UploadManifest;
|
|
36
|
+
headerSize(contractId: string): number;
|
|
37
|
+
/** Replays encrypted body chunks before registration without CKD or a final header. */
|
|
38
|
+
body(plaintext: ReadableStream<Uint8Array>): ReadableStream<Uint8Array>;
|
|
39
|
+
fileId: Bytes;
|
|
40
|
+
/** Register this value before calling finish, as with the one-shot seal. */
|
|
41
|
+
cipherHash: Bytes;
|
|
42
|
+
preview: import('./pay2.js').Sealed['preview'];
|
|
43
|
+
/**
|
|
44
|
+
* A NEW stream of the same immutable plaintext, from byte zero. Signs the final header and
|
|
45
|
+
* returns header then ciphertext under backpressure. Each replay chunk must match its
|
|
46
|
+
* first-pass hash before it is emitted. A changed replay errors; discard its entire output.
|
|
47
|
+
* First-pass length mismatches throw RangeError; replay changes throw SourceChangedError. Source errors propagate unchanged.
|
|
48
|
+
*/
|
|
49
|
+
finish(plaintext: ReadableStream<Uint8Array>, opts: FinishOptions): Promise<ReadableStream<Uint8Array>>;
|
|
50
|
+
}
|
|
51
|
+
/** Consumes plaintext once to prepare registration, using a fresh random file key. */
|
|
52
|
+
export declare function sealStream(plaintext: ReadableStream<Uint8Array>, size: number, opts: SealOptions): Promise<StreamSealed>;
|
|
53
|
+
/** Seals with a caller-chosen file key (the streaming sell and the vectors use it). Never reuse a key for different files. */
|
|
54
|
+
export declare function sealStreamWithKey(K: Uint8Array, plaintext: ReadableStream<Uint8Array>, size: number, opts: SealOptions): Promise<StreamSealed>;
|
|
55
|
+
interface OpenStreamOptions extends Omit<OpenOptions, 'ckdSecret'> {
|
|
56
|
+
/** Called after registration/signature/stanza checks, BEFORE the complete body is checked. */
|
|
57
|
+
ckdSecret: OpenOptions['ckdSecret'];
|
|
58
|
+
/** Authenticated metadata. Like plaintext, discard it if the stream later errors. */
|
|
59
|
+
onMeta?: (meta: FileMeta) => void;
|
|
60
|
+
/** @internal Bind a replay to the header validated before key release. */
|
|
61
|
+
expectedHeader?: Uint8Array;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks the fixed fields that decide how much to read (spec 6, step 1, up to `headerLen`), then
|
|
65
|
+
* reads the rest; `parseHeader` checks everything else on the whole header.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readHeader(input: ByteReader): Promise<Bytes>;
|
|
68
|
+
/**
|
|
69
|
+
* Authenticates each chunk's registered SHA-256 hash AND GCM tag before releasing plaintext.
|
|
70
|
+
* The final chunk waits for EOF, so trailing bytes cannot be silently ignored. On a late error,
|
|
71
|
+
* earlier authenticated chunks may already have reached the consumer: DISCARD ALL OUTPUT.
|
|
72
|
+
* Unlike buffered open, key acquisition precedes complete-body validation; error precedence
|
|
73
|
+
* across independent faults may differ. No unauthenticated plaintext is ever emitted.
|
|
74
|
+
*/
|
|
75
|
+
export declare function openStream(file: ReadableStream<Uint8Array>, opts: OpenStreamOptions,
|
|
76
|
+
/** @internal Full plaintext size established by the replayable validation pass. */
|
|
77
|
+
expectedSize?: number): ReadableStream<Uint8Array>;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { AnnonError } from '../errors.js';
|
|
2
|
+
import { account, hex } from '../settle-near/codec.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bounded-memory PAY2 version 2 using Web Streams and the one-shot encoder's WebCrypto primitives.
|
|
5
|
+
*
|
|
6
|
+
* The format puts all hashes before the body, and registration precedes the final stanza/MAC/
|
|
7
|
+
* signature. Sealing therefore consumes TWO streams: sealStream's first pass computes the
|
|
8
|
+
* registration; finish replays the same immutable plaintext. Nothing spools or tees the file.
|
|
9
|
+
* The header/hash table is bounded by the format (64 KiB / 1024 hashes), independent of size.
|
|
10
|
+
* Only a few crypto chunks plus one caller-owned input buffer are retained. Upstream queues
|
|
11
|
+
* and a consumer that collects the output are outside this bound; use backpressure at both ends.
|
|
12
|
+
* A Blob/File can supply the two passes with separate calls to blob.stream(). A non-replayable
|
|
13
|
+
* source must be spooled externally by the caller; using tee() would buffer an entire branch.
|
|
14
|
+
*/
|
|
15
|
+
import { bodyHash, checkSigned, chunkCount, chunkKey, commitment, concat, copy, equal, FIXED, finishTrailer, gcm, hkdf, int, MAC, MAGIC, MAX_CHUNKS, MAX_HEADER, MAX_META, NEAR_CKD, nearCkd, Pay2Error, parseMeta, previewLimit, SIG, sealAuth, sha256, TAG, wrapKeyFor, } from './pay2.js';
|
|
16
|
+
const subtle = globalThis.crypto.subtle;
|
|
17
|
+
const utf8 = new TextEncoder();
|
|
18
|
+
/** A replay no longer matches the source authenticated in the first pass. */
|
|
19
|
+
export class SourceChangedError extends AnnonError {
|
|
20
|
+
constructor() {
|
|
21
|
+
super('source_changed', 'source changed between passes');
|
|
22
|
+
this.name = 'SourceChangedError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Copies into a bounded destination, never concatenates or queues source chunks. */
|
|
26
|
+
export class ByteReader {
|
|
27
|
+
reader;
|
|
28
|
+
pending = new Uint8Array();
|
|
29
|
+
at = 0;
|
|
30
|
+
ended = false;
|
|
31
|
+
released = false;
|
|
32
|
+
cancellation;
|
|
33
|
+
constructor(source) {
|
|
34
|
+
this.reader = source.getReader();
|
|
35
|
+
}
|
|
36
|
+
/** Read exactly length bytes, or fewer only at EOF. No allocation depends on source size. */
|
|
37
|
+
async read(length) {
|
|
38
|
+
const bytes = new Uint8Array(length);
|
|
39
|
+
let at = 0;
|
|
40
|
+
while (at < length) {
|
|
41
|
+
if (this.at === this.pending.length) {
|
|
42
|
+
this.pending = new Uint8Array();
|
|
43
|
+
this.at = 0;
|
|
44
|
+
if (this.ended)
|
|
45
|
+
break;
|
|
46
|
+
const result = await this.reader.read();
|
|
47
|
+
if (result.done) {
|
|
48
|
+
this.ended = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (!(result.value instanceof Uint8Array))
|
|
52
|
+
throw new TypeError('expected Uint8Array');
|
|
53
|
+
this.pending = result.value;
|
|
54
|
+
}
|
|
55
|
+
const take = Math.min(length - at, this.pending.length - this.at);
|
|
56
|
+
bytes.set(this.pending.subarray(this.at, this.at + take), at);
|
|
57
|
+
this.at += take;
|
|
58
|
+
at += take;
|
|
59
|
+
if (this.at === this.pending.length) {
|
|
60
|
+
this.pending = new Uint8Array();
|
|
61
|
+
this.at = 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return bytes.subarray(0, at);
|
|
65
|
+
}
|
|
66
|
+
cancel(reason) {
|
|
67
|
+
this.cancellation ??= this.reader.cancel(reason).catch(() => { });
|
|
68
|
+
return this.cancellation;
|
|
69
|
+
}
|
|
70
|
+
release() {
|
|
71
|
+
if (this.released)
|
|
72
|
+
return;
|
|
73
|
+
this.released = true;
|
|
74
|
+
this.pending = new Uint8Array();
|
|
75
|
+
this.reader.releaseLock();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** One generator yield per consumer read, including cancellation during an outstanding read. */
|
|
79
|
+
function output(source, produce) {
|
|
80
|
+
const input = new ByteReader(source);
|
|
81
|
+
async function* managed() {
|
|
82
|
+
try {
|
|
83
|
+
yield* produce(input);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await input.cancel();
|
|
87
|
+
input.release();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const iterator = managed();
|
|
91
|
+
return new ReadableStream({
|
|
92
|
+
async pull(controller) {
|
|
93
|
+
try {
|
|
94
|
+
const result = await iterator.next();
|
|
95
|
+
if (result.done)
|
|
96
|
+
controller.close();
|
|
97
|
+
else
|
|
98
|
+
controller.enqueue(result.value);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
controller.error(error);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async cancel(reason) {
|
|
105
|
+
// Cancel first: return() alone cannot interrupt a generator waiting on reader.read().
|
|
106
|
+
await input.cancel(reason);
|
|
107
|
+
await iterator.return();
|
|
108
|
+
input.release(); // Also needed if the generator never started.
|
|
109
|
+
},
|
|
110
|
+
}, { highWaterMark: 0 });
|
|
111
|
+
}
|
|
112
|
+
/** Consumes plaintext once to prepare registration, using a fresh random file key. */
|
|
113
|
+
export function sealStream(plaintext, size, opts) {
|
|
114
|
+
return sealStreamWithKey(crypto.getRandomValues(new Uint8Array(32)), plaintext, size, opts);
|
|
115
|
+
}
|
|
116
|
+
/** Seals with a caller-chosen file key (the streaming sell and the vectors use it). Never reuse a key for different files. */
|
|
117
|
+
export async function sealStreamWithKey(K, plaintext, size, opts) {
|
|
118
|
+
if (opts.fileId.length !== 32 || K.length !== 32)
|
|
119
|
+
throw new TypeError('fileId and K are 32 bytes');
|
|
120
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
121
|
+
throw new RangeError('size must be a safe unsigned integer');
|
|
122
|
+
const { version, authKind } = sealAuth(opts);
|
|
123
|
+
const fileId = copy(opts.fileId);
|
|
124
|
+
const key = copy(K);
|
|
125
|
+
const previewLen = opts.previewLen ?? 0;
|
|
126
|
+
if (!Number.isSafeInteger(previewLen) || previewLen < 0 || previewLen > previewLimit(size)) {
|
|
127
|
+
throw new RangeError(`previewLen must be 0 to ${previewLimit(size)}`);
|
|
128
|
+
}
|
|
129
|
+
let chunkLog2 = opts.chunkLog2 ?? 22;
|
|
130
|
+
while (opts.chunkLog2 === undefined &&
|
|
131
|
+
chunkLog2 < 24 &&
|
|
132
|
+
chunkCount(size, previewLen, chunkLog2) > MAX_CHUNKS) {
|
|
133
|
+
chunkLog2++;
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isInteger(chunkLog2) || chunkLog2 < 16 || chunkLog2 > 24) {
|
|
136
|
+
throw new RangeError('chunkLog2 must be an integer from 16 to 24');
|
|
137
|
+
}
|
|
138
|
+
const n = chunkCount(size, previewLen, chunkLog2);
|
|
139
|
+
if (n > MAX_CHUNKS)
|
|
140
|
+
throw new RangeError('file too large for PAY2');
|
|
141
|
+
const C = 2 ** chunkLog2;
|
|
142
|
+
const name = utf8.encode(opts.name);
|
|
143
|
+
if (!/^[\x20-\x7e]{0,255}$/.test(opts.mime))
|
|
144
|
+
throw new RangeError('mime must be printable ASCII');
|
|
145
|
+
const fields = concat(int(name.length, 2), name, int(opts.mime.length, 1), utf8.encode(opts.mime), int(size, 8));
|
|
146
|
+
const metaLen = Math.ceil(fields.length / 256) * 256;
|
|
147
|
+
if (metaLen > MAX_META)
|
|
148
|
+
throw new RangeError('name too long');
|
|
149
|
+
const meta = concat(fields, new Uint8Array(metaLen - fields.length));
|
|
150
|
+
const kc = previewLen > 0 ? 2 : 1;
|
|
151
|
+
const commits = [];
|
|
152
|
+
for (let i = 0; i < kc; i++)
|
|
153
|
+
commits.push(await commitment(await chunkKey(key, fileId, i)));
|
|
154
|
+
// This is at most 32,886 bytes, even for the largest allowed file. Hashes live directly in it.
|
|
155
|
+
const prefix = concat(new Uint8Array(MAGIC), new Uint8Array([version, chunkLog2, 1, authKind]), new Uint8Array(4), fileId, int(metaLen, 2), int(previewLen, 4), int(n, 4), ...commits, new Uint8Array(32 * n));
|
|
156
|
+
const hashes = Array.from({ length: n }, (_, i) => prefix.subarray(FIXED + 32 * (kc + i), FIXED + 32 * (kc + i + 1)));
|
|
157
|
+
async function* encrypt(input, replay = false) {
|
|
158
|
+
let remaining = size;
|
|
159
|
+
for (let i = 0; i < n; i++) {
|
|
160
|
+
const length = i === 0 ? metaLen : i === 1 && previewLen > 0 ? previewLen : Math.min(C, remaining);
|
|
161
|
+
const piece = i === 0 ? meta : await input.read(length);
|
|
162
|
+
if (piece.length !== length)
|
|
163
|
+
throw replay ? new SourceChangedError() : new RangeError('plaintext shorter than size');
|
|
164
|
+
if (i > 0)
|
|
165
|
+
remaining -= length;
|
|
166
|
+
const last = i === n - 1;
|
|
167
|
+
if (last && (await input.read(1)).length !== 0)
|
|
168
|
+
throw replay ? new SourceChangedError() : new RangeError('plaintext longer than size');
|
|
169
|
+
yield await gcm('encrypt', await chunkKey(key, fileId, i), last, piece);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const input = new ByteReader(plaintext);
|
|
173
|
+
const previewWire = [];
|
|
174
|
+
try {
|
|
175
|
+
let i = 0;
|
|
176
|
+
for await (const wire of encrypt(input)) {
|
|
177
|
+
if (i < kc)
|
|
178
|
+
previewWire.push(wire);
|
|
179
|
+
hashes[i++].set(await sha256(wire));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await input.cancel();
|
|
184
|
+
input.release();
|
|
185
|
+
}
|
|
186
|
+
const cipherHash = await bodyHash({
|
|
187
|
+
version,
|
|
188
|
+
chunkLog2,
|
|
189
|
+
fileId,
|
|
190
|
+
metaLen,
|
|
191
|
+
previewLen,
|
|
192
|
+
n,
|
|
193
|
+
commits,
|
|
194
|
+
hashes,
|
|
195
|
+
});
|
|
196
|
+
let finished;
|
|
197
|
+
return {
|
|
198
|
+
manifest: {
|
|
199
|
+
version,
|
|
200
|
+
chunkLog2,
|
|
201
|
+
metaLen,
|
|
202
|
+
previewLen,
|
|
203
|
+
n,
|
|
204
|
+
keyCommitments: commits.map(hex),
|
|
205
|
+
chunkHashes: hashes.map(hex),
|
|
206
|
+
},
|
|
207
|
+
headerSize(contractId) {
|
|
208
|
+
account(contractId);
|
|
209
|
+
return 206 + utf8.encode(contractId).length + 32 * (kc + n);
|
|
210
|
+
},
|
|
211
|
+
body(replay) {
|
|
212
|
+
return output(replay, async function* (reader) {
|
|
213
|
+
let i = 0;
|
|
214
|
+
for await (const wire of encrypt(reader, true)) {
|
|
215
|
+
if (!equal(await sha256(wire), hashes[i++]))
|
|
216
|
+
throw new SourceChangedError();
|
|
217
|
+
yield wire;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
fileId: copy(fileId),
|
|
222
|
+
cipherHash,
|
|
223
|
+
async finish(replay, finishOpts) {
|
|
224
|
+
const { contractId, ckdSecret } = finishOpts;
|
|
225
|
+
account(contractId);
|
|
226
|
+
const kw = await wrapKeyFor(ckdSecret, fileId, 'wrapKey');
|
|
227
|
+
const fileKey = await subtle.importKey('raw', key, 'AES-GCM', true, ['encrypt']);
|
|
228
|
+
const wrapped = new Uint8Array(await subtle.wrapKey('raw', fileKey, kw, 'AES-KW'));
|
|
229
|
+
const type = utf8.encode(NEAR_CKD);
|
|
230
|
+
const body = concat(new Uint8Array([contractId.length]), utf8.encode(contractId), wrapped);
|
|
231
|
+
const stanza = concat(new Uint8Array([type.length]), type, int(body.length, 2), body);
|
|
232
|
+
const header = concat(prefix, stanza);
|
|
233
|
+
new DataView(header.buffer).setUint32(8, header.length + MAC + SIG);
|
|
234
|
+
const macKey = await subtle.importKey('raw', await hkdf(key, fileId, 'annon/pay2/header'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
235
|
+
const mac = new Uint8Array(await subtle.sign('HMAC', macKey, header));
|
|
236
|
+
const sig = await finishTrailer(version, authKind, finishOpts, fileId, await sha256(header, mac));
|
|
237
|
+
const finalHeader = concat(header, mac, sig);
|
|
238
|
+
finished = finalHeader;
|
|
239
|
+
return output(replay, async function* (reader) {
|
|
240
|
+
yield finalHeader;
|
|
241
|
+
let i = 0;
|
|
242
|
+
for await (const wire of encrypt(reader, true)) {
|
|
243
|
+
if (!equal(await sha256(wire), hashes[i++])) {
|
|
244
|
+
throw new SourceChangedError();
|
|
245
|
+
}
|
|
246
|
+
yield wire;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
async preview(show) {
|
|
251
|
+
if (!finished)
|
|
252
|
+
throw new Error('call finish first: the preview carries the final header');
|
|
253
|
+
if (show.data && previewLen === 0)
|
|
254
|
+
throw new RangeError('sealed without a preview');
|
|
255
|
+
const prefixOnly = finished.slice(0, prefix.length);
|
|
256
|
+
prefixOnly[6] = 0;
|
|
257
|
+
new DataView(prefixOnly.buffer).setUint32(8, 0);
|
|
258
|
+
const b64 = (b) => {
|
|
259
|
+
let text = '';
|
|
260
|
+
for (const byte of b)
|
|
261
|
+
text += String.fromCharCode(byte);
|
|
262
|
+
return btoa(text);
|
|
263
|
+
};
|
|
264
|
+
const chunks = [];
|
|
265
|
+
for (const i of [0, 1]) {
|
|
266
|
+
if (!(i === 0 ? show.meta : show.data))
|
|
267
|
+
continue;
|
|
268
|
+
chunks.push({
|
|
269
|
+
index: i,
|
|
270
|
+
key: b64(await chunkKey(key, fileId, i)),
|
|
271
|
+
data: b64(previewWire[i]),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return { v: 1, header: b64(prefixOnly), chunks };
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Checks the fixed fields that decide how much to read (spec 6, step 1, up to `headerLen`), then
|
|
280
|
+
* reads the rest; `parseHeader` checks everything else on the whole header.
|
|
281
|
+
*/
|
|
282
|
+
export async function readHeader(input) {
|
|
283
|
+
const fixed = await input.read(FIXED);
|
|
284
|
+
if (fixed.length < 4 || !MAGIC.every((byte, i) => fixed[i] === byte))
|
|
285
|
+
throw new Pay2Error('bad_magic');
|
|
286
|
+
if (fixed.length < FIXED)
|
|
287
|
+
throw new Pay2Error('bad_header');
|
|
288
|
+
if (fixed[4] !== 2)
|
|
289
|
+
throw new Pay2Error('unsupported_version');
|
|
290
|
+
const chunkLog2 = fixed[5] ?? 0;
|
|
291
|
+
if (chunkLog2 < 16 || chunkLog2 > 24)
|
|
292
|
+
throw new Pay2Error('bad_chunk_size');
|
|
293
|
+
const dv = new DataView(fixed.buffer);
|
|
294
|
+
const headerLen = dv.getUint32(8);
|
|
295
|
+
if (headerLen > MAX_HEADER)
|
|
296
|
+
throw new Pay2Error('header_too_large');
|
|
297
|
+
// A headerLen below the fixed part reads nothing more; parseHeader then refuses it.
|
|
298
|
+
return concat(fixed, await input.read(Math.max(0, headerLen - FIXED)));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Authenticates each chunk's registered SHA-256 hash AND GCM tag before releasing plaintext.
|
|
302
|
+
* The final chunk waits for EOF, so trailing bytes cannot be silently ignored. On a late error,
|
|
303
|
+
* earlier authenticated chunks may already have reached the consumer: DISCARD ALL OUTPUT.
|
|
304
|
+
* Unlike buffered open, key acquisition precedes complete-body validation; error precedence
|
|
305
|
+
* across independent faults may differ. No unauthenticated plaintext is ever emitted.
|
|
306
|
+
*/
|
|
307
|
+
export function openStream(file, opts,
|
|
308
|
+
/** @internal Full plaintext size established by the replayable validation pass. */
|
|
309
|
+
expectedSize) {
|
|
310
|
+
return output(file, async function* (input) {
|
|
311
|
+
const header = await readHeader(input);
|
|
312
|
+
if (opts.expectedHeader && !equal(header, opts.expectedHeader))
|
|
313
|
+
throw new SourceChangedError();
|
|
314
|
+
const h = await checkSigned(header, opts.listing, opts.contractId);
|
|
315
|
+
const stanza = h.stanzas
|
|
316
|
+
.map((s) => (s.type === NEAR_CKD ? nearCkd(s.body) : undefined))
|
|
317
|
+
.find((s) => s?.contractId === opts.contractId);
|
|
318
|
+
if (!stanza)
|
|
319
|
+
throw new Pay2Error('no_usable_stanza');
|
|
320
|
+
const wrapKey = await wrapKeyFor(await opts.ckdSecret(copy(h.fileId)), h.fileId, 'unwrapKey');
|
|
321
|
+
let key;
|
|
322
|
+
try {
|
|
323
|
+
key = await subtle.unwrapKey('raw', stanza.wrapped, wrapKey, 'AES-KW', 'HKDF', false, [
|
|
324
|
+
'deriveBits',
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
throw new Pay2Error('key_unwrap_failed');
|
|
329
|
+
}
|
|
330
|
+
const macKey = await subtle.importKey('raw', await hkdf(key, h.fileId, 'annon/pay2/header'), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
331
|
+
const macAt = h.headerLen - MAC - SIG;
|
|
332
|
+
if (!(await subtle.verify('HMAC', macKey, header.subarray(macAt, macAt + MAC), header.subarray(0, macAt))))
|
|
333
|
+
throw new Pay2Error('header_mac_invalid');
|
|
334
|
+
const C = 2 ** h.chunkLog2;
|
|
335
|
+
let meta;
|
|
336
|
+
let size = 0;
|
|
337
|
+
for (let i = 0; i < h.n; i++) {
|
|
338
|
+
const last = i === h.n - 1;
|
|
339
|
+
const length = i === 0 ? h.metaLen + TAG : i === 1 && h.previewLen > 0 ? h.previewLen + TAG : C + TAG;
|
|
340
|
+
// The last is the remainder, capped with one extra byte to detect extensions without
|
|
341
|
+
// reading an unbounded hostile tail. read() waits for EOF if the last fits the cap.
|
|
342
|
+
const wire = await input.read(last ? C + TAG + 1 : length);
|
|
343
|
+
if (last ? wire.length < TAG || wire.length > C + TAG : wire.length !== length)
|
|
344
|
+
throw new Pay2Error('bad_body_length');
|
|
345
|
+
if (!equal(await sha256(wire), h.hashes[i]))
|
|
346
|
+
throw new Pay2Error('chunk_hash_mismatch', `chunk ${i}`);
|
|
347
|
+
const k = await chunkKey(key, h.fileId, i);
|
|
348
|
+
const c = h.commits[i];
|
|
349
|
+
if (c && !equal(await commitment(k), c))
|
|
350
|
+
throw new Pay2Error('chunk_invalid', `chunk ${i} key commitment`);
|
|
351
|
+
let piece;
|
|
352
|
+
try {
|
|
353
|
+
piece = await gcm('decrypt', k, last, wire);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
throw new Pay2Error('chunk_invalid', `chunk ${i}`);
|
|
357
|
+
}
|
|
358
|
+
if (i === 0) {
|
|
359
|
+
meta = parseMeta(piece, h);
|
|
360
|
+
if (expectedSize !== undefined && meta.size !== expectedSize)
|
|
361
|
+
throw new Pay2Error('meta_invalid', 'chunk lengths do not add up to the size');
|
|
362
|
+
opts.onMeta?.({ ...meta });
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
size += piece.length;
|
|
366
|
+
if (last && size !== meta?.size)
|
|
367
|
+
throw new Pay2Error('meta_invalid', 'chunk lengths do not add up to the size');
|
|
368
|
+
yield piece;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Browser-safe public failures. Upstream diagnostics never cross a public boundary. */
|
|
2
|
+
export declare const annonErrorCodes: readonly ["invalid_arguments", "invalid_config", "invalid_record", "config_conflict", "agent_policy", "human_only", "confirmation_required", "sale_not_approved", "trust_required", "signer_required", "signer_init_only", "credentials_required", "backup_secret_required", "work_dir_required", "max_price_required", "budget_required", "budget_exceeded", "base_rpc_required", "transport_required", "near_wallet_required", "base_wallet_required", "base_unavailable", "route_unavailable", "file_required", "offer_required", "purchase_required", "origin_required", "origin_asset_required", "origin_not_allowed", "resource_not_allowed", "path_not_allowed", "output_exists", "unknown_file", "already_registered", "taken_down", "sales_stopped", "unknown_moderator", "storage_required", "funding_refused", "file_storage_low", "protocol_fee_above_max", "price_mismatch", "price_above_max", "recipient_mismatch", "file_mismatch", "network_mismatch", "token_mismatch", "method_mismatch", "untrusted_contract", "file_invalid", "bad_requirements", "insufficient_balance", "smart_wallet_unsupported", "bad_signature", "payment_rejected", "purchase_open", "payment_id_conflict", "payment_expired", "payment_not_found", "payment_used", "quote_refused", "quote_expired", "settlement_pending", "transaction_uncertain", "not_sponsored", "not_buyer", "bad_magic", "unsupported_version", "bad_chunk_size", "header_too_large", "bad_header", "bad_stanza", "file_id_mismatch", "cipher_hash_mismatch", "seller_signature_invalid", "bad_body_length", "chunk_hash_mismatch", "no_usable_stanza", "key_unwrap_failed", "header_mac_invalid", "chunk_invalid", "meta_invalid", "preview_invalid", "ckd_failed", "ckd_invalid", "rejected", "wrong_chain", "wrong_identity", "invalid_signature", "missing_method", "invalid_response", "wallet_error", "store_conflict", "lease_busy", "source_changed", "sell_incomplete", "recovery_not_final", "transport", "refused", "bad_request", "bad_deadline", "limited", "duplicate", "in_flight", "unavailable", "disabled", "rate_limited", "budget", "busy", "not_found", "internal", "relayer_unavailable", "relayer_rate_limited", "base_rpc_rate_limited", "near_rpc_rate_limited", "host_payment_failed", "facilitator_busy", "bad_response", "not_authorized", "cancelled", "too_large", "download_limited", "download_in_progress", "file_expired", "upload_expired", "upload_conflict", "upload_incomplete", "host_capacity", "operation_failed"];
|
|
3
|
+
export type AnnonErrorCode = (typeof annonErrorCodes)[number];
|
|
4
|
+
export interface PublicError {
|
|
5
|
+
code: AnnonErrorCode;
|
|
6
|
+
message: string;
|
|
7
|
+
exitCode: 1 | 2 | 3 | 4;
|
|
8
|
+
fileId?: string;
|
|
9
|
+
purchaseId?: string;
|
|
10
|
+
txHash?: string;
|
|
11
|
+
minimumAmountUsdc?: string;
|
|
12
|
+
retryAfterSeconds?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function isAnnonErrorCode(code: unknown): code is AnnonErrorCode;
|
|
15
|
+
export declare function isTransient(error: unknown): boolean;
|
|
16
|
+
export declare class AnnonError<C extends AnnonErrorCode = AnnonErrorCode> extends Error {
|
|
17
|
+
readonly code: C;
|
|
18
|
+
constructor(code: C, message?: `${string}.` | NonNullable<Partial<Record<"invalid_arguments" | "invalid_config" | "invalid_record" | "config_conflict" | "agent_policy" | "human_only" | "confirmation_required" | "sale_not_approved" | "trust_required" | "signer_required" | "signer_init_only" | "credentials_required" | "backup_secret_required" | "work_dir_required" | "max_price_required" | "budget_required" | "budget_exceeded" | "base_rpc_required" | "transport_required" | "near_wallet_required" | "base_wallet_required" | "base_unavailable" | "route_unavailable" | "file_required" | "offer_required" | "purchase_required" | "origin_required" | "origin_asset_required" | "origin_not_allowed" | "resource_not_allowed" | "path_not_allowed" | "output_exists" | "unknown_file" | "already_registered" | "taken_down" | "sales_stopped" | "unknown_moderator" | "storage_required" | "funding_refused" | "file_storage_low" | "protocol_fee_above_max" | "price_mismatch" | "price_above_max" | "recipient_mismatch" | "file_mismatch" | "network_mismatch" | "token_mismatch" | "method_mismatch" | "untrusted_contract" | "file_invalid" | "bad_requirements" | "insufficient_balance" | "smart_wallet_unsupported" | "bad_signature" | "payment_rejected" | "purchase_open" | "payment_id_conflict" | "payment_expired" | "payment_not_found" | "payment_used" | "quote_refused" | "quote_expired" | "settlement_pending" | "transaction_uncertain" | "not_sponsored" | "not_buyer" | "bad_magic" | "unsupported_version" | "bad_chunk_size" | "header_too_large" | "bad_header" | "bad_stanza" | "file_id_mismatch" | "cipher_hash_mismatch" | "seller_signature_invalid" | "bad_body_length" | "chunk_hash_mismatch" | "no_usable_stanza" | "key_unwrap_failed" | "header_mac_invalid" | "chunk_invalid" | "meta_invalid" | "preview_invalid" | "ckd_failed" | "ckd_invalid" | "rejected" | "wrong_chain" | "wrong_identity" | "invalid_signature" | "missing_method" | "invalid_response" | "wallet_error" | "store_conflict" | "lease_busy" | "source_changed" | "sell_incomplete" | "recovery_not_final" | "transport" | "refused" | "bad_request" | "bad_deadline" | "limited" | "duplicate" | "in_flight" | "unavailable" | "disabled" | "rate_limited" | "budget" | "busy" | "not_found" | "internal" | "relayer_unavailable" | "relayer_rate_limited" | "base_rpc_rate_limited" | "near_rpc_rate_limited" | "host_payment_failed" | "facilitator_busy" | "bad_response" | "not_authorized" | "cancelled" | "too_large" | "download_limited" | "download_in_progress" | "file_expired" | "upload_expired" | "upload_conflict" | "upload_incomplete" | "host_capacity" | "operation_failed", string>>[C]>, options?: ErrorOptions);
|
|
19
|
+
}
|
|
20
|
+
export declare function toPublicError(error: unknown): PublicError;
|
|
21
|
+
export declare function present<T>(value: T | null | undefined, code?: AnnonErrorCode): T;
|