@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,541 @@
|
|
|
1
|
+
import { concatParts } from '../bytes.js';
|
|
2
|
+
import { AnnonError } from '../errors.js';
|
|
3
|
+
import { account } from '../settle-near/codec.js';
|
|
4
|
+
/**
|
|
5
|
+
* PAY2, the Annon file format: `specs/pay2.md`. WebCrypto only, so the same code runs in browsers
|
|
6
|
+
* and Node. `open` runs the checks of spec section 6 in its order and fails with its error codes.
|
|
7
|
+
*
|
|
8
|
+
* These convenience functions buffer input and output. For bounded-memory file operations,
|
|
9
|
+
* use stream.ts and replay.ts, which share this module's format and crypto primitives.
|
|
10
|
+
*/
|
|
11
|
+
import { hex as canonicalHex } from '../settle-near/codec.js';
|
|
12
|
+
import { identityKind, parseIdentity, solanaWalletAuth, verifyAuthorization, } from '../settle-near/identity.js';
|
|
13
|
+
import { publishTextV2 } from '../settle-near/terms.js';
|
|
14
|
+
import { signedBy } from './evm.js';
|
|
15
|
+
export class Pay2Error extends AnnonError {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message = code) {
|
|
18
|
+
super(code, message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = 'Pay2Error';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const fail = (code, message) => {
|
|
24
|
+
throw new Pay2Error(code, message);
|
|
25
|
+
};
|
|
26
|
+
export const MAGIC = [0x50, 0x41, 0x59, 0x32]; // "PAY2"
|
|
27
|
+
export const FIXED = 54;
|
|
28
|
+
export const TAG = 16;
|
|
29
|
+
export const MAX_HEADER = 65536;
|
|
30
|
+
export const MAX_CHUNKS = 1024;
|
|
31
|
+
const MAX_PREVIEW = 65536;
|
|
32
|
+
export const MAX_META = 4096;
|
|
33
|
+
export const MAC = 32;
|
|
34
|
+
export const SIG = 65;
|
|
35
|
+
export const NEAR_CKD = 'near-ckd/v1';
|
|
36
|
+
const subtle = globalThis.crypto.subtle;
|
|
37
|
+
const utf8 = new TextEncoder();
|
|
38
|
+
export function concat(...parts) {
|
|
39
|
+
return concatParts(parts);
|
|
40
|
+
}
|
|
41
|
+
export function int(value, bytes) {
|
|
42
|
+
const out = new Uint8Array(bytes);
|
|
43
|
+
const view = new DataView(out.buffer);
|
|
44
|
+
if (bytes === 1)
|
|
45
|
+
view.setUint8(0, value);
|
|
46
|
+
else if (bytes === 2)
|
|
47
|
+
view.setUint16(0, value);
|
|
48
|
+
else if (bytes === 4)
|
|
49
|
+
view.setUint32(0, value);
|
|
50
|
+
else
|
|
51
|
+
view.setBigUint64(0, BigInt(value));
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export const equal = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
55
|
+
export const copy = (b) => new Uint8Array(b);
|
|
56
|
+
/** The input itself, never a copy: a hostile file must not cost memory before the limits pass. */
|
|
57
|
+
function view(b) {
|
|
58
|
+
if (!(b.buffer instanceof ArrayBuffer))
|
|
59
|
+
throw new TypeError('pass bytes over an ArrayBuffer');
|
|
60
|
+
return b;
|
|
61
|
+
}
|
|
62
|
+
export async function sha256(...parts) {
|
|
63
|
+
const data = parts.length === 1 ? view(parts[0]) : concat(...parts);
|
|
64
|
+
return new Uint8Array(await subtle.digest('SHA-256', data));
|
|
65
|
+
}
|
|
66
|
+
/** Derives 32 bytes. `ikm` is raw bytes or an HKDF key, so an unwrapped `K` never leaves WebCrypto. */
|
|
67
|
+
async function hkdf(ikm, salt, info) {
|
|
68
|
+
const key = ikm instanceof Uint8Array
|
|
69
|
+
? await subtle.importKey('raw', copy(ikm), 'HKDF', false, ['deriveBits'])
|
|
70
|
+
: ikm;
|
|
71
|
+
const bits = await subtle.deriveBits({
|
|
72
|
+
name: 'HKDF',
|
|
73
|
+
hash: 'SHA-256',
|
|
74
|
+
salt: copy(salt),
|
|
75
|
+
info: typeof info === 'string' ? utf8.encode(info) : copy(info),
|
|
76
|
+
}, key, 256);
|
|
77
|
+
return new Uint8Array(bits);
|
|
78
|
+
}
|
|
79
|
+
export const chunkKey = (K, fileId, i) => hkdf(K, fileId, concat(utf8.encode('annon/pay2/chunk'), int(i, 4)));
|
|
80
|
+
/** Spec 2: binds a revealable chunk key, since AES-GCM alone doesn't commit to its key. */
|
|
81
|
+
export const commitment = (k) => sha256(utf8.encode('annon/pay2/key'), k);
|
|
82
|
+
function nonce(last) {
|
|
83
|
+
const n = new Uint8Array(12);
|
|
84
|
+
n[11] = last ? 1 : 0;
|
|
85
|
+
return n;
|
|
86
|
+
}
|
|
87
|
+
async function gcm(mode, key, last, data) {
|
|
88
|
+
const k = await subtle.importKey('raw', copy(key), 'AES-GCM', false, [mode]);
|
|
89
|
+
const out = await subtle[mode]({ name: 'AES-GCM', iv: nonce(last) }, k, view(data));
|
|
90
|
+
return new Uint8Array(out);
|
|
91
|
+
}
|
|
92
|
+
export { gcm, hkdf };
|
|
93
|
+
export const wrapKeyFor = async (ckdSecret, fileId, usage) => {
|
|
94
|
+
if (ckdSecret.length !== 48)
|
|
95
|
+
throw new TypeError('ckdSecret must be the 48-byte G1 point');
|
|
96
|
+
const raw = await hkdf(ckdSecret, fileId, 'annon/pay2/near-ckd/v1');
|
|
97
|
+
return subtle.importKey('raw', raw, 'AES-KW', false, [usage]);
|
|
98
|
+
};
|
|
99
|
+
/** Spec 3: binds the layout fields known before `register`, the commitments and the hash list. */
|
|
100
|
+
export const bodyHash = (h) => sha256(utf8.encode('annon/pay2/body'), new Uint8Array([h.version ?? 2, h.chunkLog2]), h.fileId, int(h.metaLen, 2), int(h.previewLen, 4), int(h.n, 4), ...h.commits, ...h.hashes);
|
|
101
|
+
function toBase64(b) {
|
|
102
|
+
let s = '';
|
|
103
|
+
for (const x of b)
|
|
104
|
+
s += String.fromCharCode(x);
|
|
105
|
+
return btoa(s);
|
|
106
|
+
}
|
|
107
|
+
function hexBytes(hex, bytes = 32) {
|
|
108
|
+
const h = typeof hex === 'string' ? hex.replace(/^0x/, '') : '';
|
|
109
|
+
if (typeof hex !== 'string'
|
|
110
|
+
? hex.length !== bytes
|
|
111
|
+
: !new RegExp(`^[0-9a-f]{${2 * bytes}}$`, 'i').test(h)) {
|
|
112
|
+
throw new TypeError(`expected ${bytes} bytes`);
|
|
113
|
+
}
|
|
114
|
+
return typeof hex === 'string'
|
|
115
|
+
? Uint8Array.from(h.match(/../g) ?? [], (x) => Number.parseInt(x, 16))
|
|
116
|
+
: hex;
|
|
117
|
+
}
|
|
118
|
+
export { signedBy } from './evm.js';
|
|
119
|
+
function sellerAddress(seller) {
|
|
120
|
+
if (!/^0x[0-9a-f]{40}$/i.test(seller))
|
|
121
|
+
throw new TypeError('seller must be a 0x EVM address');
|
|
122
|
+
return seller.toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
/** Spec 6, step 1. With `prefixOnly`, stops after the hash list (a preview package). */
|
|
125
|
+
export function parseHeader(b, prefixOnly = false) {
|
|
126
|
+
if (b.length < 4 || !MAGIC.every((x, i) => b[i] === x))
|
|
127
|
+
fail('bad_magic');
|
|
128
|
+
if (b.length < FIXED)
|
|
129
|
+
fail('bad_header', 'shorter than the fixed header');
|
|
130
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
131
|
+
if (b[4] !== 2)
|
|
132
|
+
fail('unsupported_version');
|
|
133
|
+
const version = b[4];
|
|
134
|
+
const authKind = b[7] ?? 0;
|
|
135
|
+
const chunkLog2 = b[5] ?? 0;
|
|
136
|
+
if (chunkLog2 < 16 || chunkLog2 > 24)
|
|
137
|
+
fail('bad_chunk_size');
|
|
138
|
+
const headerLen = dv.getUint32(8);
|
|
139
|
+
if (headerLen > MAX_HEADER)
|
|
140
|
+
fail('header_too_large');
|
|
141
|
+
const stanzaCount = b[6] ?? 0;
|
|
142
|
+
const metaLen = dv.getUint16(44);
|
|
143
|
+
const previewLen = dv.getUint32(46);
|
|
144
|
+
const n = dv.getUint32(50);
|
|
145
|
+
const kc = previewLen > 0 ? 2 : 1;
|
|
146
|
+
const prefixLen = FIXED + 32 * (kc + n);
|
|
147
|
+
// A preview package has no stanzas, so it carries 0 in both stanza fields (spec 7).
|
|
148
|
+
const stanzasOk = prefixOnly
|
|
149
|
+
? stanzaCount === 0 && headerLen === 0
|
|
150
|
+
: stanzaCount >= 1 && stanzaCount <= 8 && headerLen >= prefixLen + MAC + SIG;
|
|
151
|
+
if (!stanzasOk ||
|
|
152
|
+
authKind > 2 ||
|
|
153
|
+
metaLen < 256 ||
|
|
154
|
+
metaLen > MAX_META ||
|
|
155
|
+
metaLen % 256 !== 0 ||
|
|
156
|
+
previewLen > MAX_PREVIEW ||
|
|
157
|
+
n < 2 ||
|
|
158
|
+
n > MAX_CHUNKS ||
|
|
159
|
+
(previewLen > 0 && n < 3) ||
|
|
160
|
+
b.length < (prefixOnly ? prefixLen : headerLen)) {
|
|
161
|
+
fail('bad_header');
|
|
162
|
+
}
|
|
163
|
+
const at32 = (i) => b.slice(FIXED + 32 * i, FIXED + 32 * (i + 1));
|
|
164
|
+
const header = {
|
|
165
|
+
version,
|
|
166
|
+
authKind: authKind,
|
|
167
|
+
chunkLog2,
|
|
168
|
+
fileId: b.slice(12, 44),
|
|
169
|
+
metaLen,
|
|
170
|
+
previewLen,
|
|
171
|
+
n,
|
|
172
|
+
commits: Array.from({ length: kc }, (_, i) => at32(i)),
|
|
173
|
+
hashes: Array.from({ length: n }, (_, i) => at32(kc + i)),
|
|
174
|
+
prefixLen,
|
|
175
|
+
headerLen,
|
|
176
|
+
stanzas: [],
|
|
177
|
+
};
|
|
178
|
+
if (prefixOnly)
|
|
179
|
+
return header;
|
|
180
|
+
const end = headerLen - MAC - SIG;
|
|
181
|
+
let at = prefixLen;
|
|
182
|
+
for (let s = 0; s < stanzaCount; s++) {
|
|
183
|
+
const typeLen = b[at] ?? 0;
|
|
184
|
+
if (typeLen < 1 || typeLen > 32 || at + 3 + typeLen > end)
|
|
185
|
+
fail('bad_stanza');
|
|
186
|
+
const type = String.fromCharCode(...b.subarray(at + 1, at + 1 + typeLen));
|
|
187
|
+
const bodyLen = dv.getUint16(at + 1 + typeLen);
|
|
188
|
+
const bodyAt = at + 3 + typeLen;
|
|
189
|
+
if (!/^[a-z0-9/.-]+$/.test(type) ||
|
|
190
|
+
bodyLen > 1024 ||
|
|
191
|
+
bodyAt + bodyLen > end ||
|
|
192
|
+
header.stanzas.some((x) => x.type === type)) {
|
|
193
|
+
fail('bad_stanza');
|
|
194
|
+
}
|
|
195
|
+
const body = b.slice(bodyAt, bodyAt + bodyLen);
|
|
196
|
+
if (type === NEAR_CKD && !nearCkd(body))
|
|
197
|
+
fail('bad_stanza');
|
|
198
|
+
header.stanzas.push({ type, body });
|
|
199
|
+
at = bodyAt + bodyLen;
|
|
200
|
+
}
|
|
201
|
+
if (at !== end)
|
|
202
|
+
fail('bad_header', 'stanzas do not end at the MAC');
|
|
203
|
+
return header;
|
|
204
|
+
}
|
|
205
|
+
/** Spec 5.1: `u8 len ‖ contractId ‖ wrapped K (40)`. */
|
|
206
|
+
export function nearCkd(body) {
|
|
207
|
+
const len = body[0] ?? 0;
|
|
208
|
+
if (len < 2 || len > 64 || body.length !== 1 + len + 40)
|
|
209
|
+
return undefined;
|
|
210
|
+
const contractId = String.fromCharCode(...body.subarray(1, 1 + len));
|
|
211
|
+
if (!/^[a-z0-9._-]+$/.test(contractId))
|
|
212
|
+
return undefined;
|
|
213
|
+
return { contractId, wrapped: body.slice(1 + len) };
|
|
214
|
+
}
|
|
215
|
+
/** Wire lengths of chunks 0 to n - 2 (spec 6, step 3); the last one is the remainder. */
|
|
216
|
+
function fixedLengths(h) {
|
|
217
|
+
return Array.from({ length: h.n - 1 }, (_, i) => {
|
|
218
|
+
if (i === 0)
|
|
219
|
+
return h.metaLen + TAG;
|
|
220
|
+
if (i === 1 && h.previewLen > 0)
|
|
221
|
+
return h.previewLen + TAG;
|
|
222
|
+
return 2 ** h.chunkLog2 + TAG;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** Spec 3: the chunk count a file of `size` bytes must have. */
|
|
226
|
+
export const chunkCount = (size, previewLen, chunkLog2) => 1 + (previewLen > 0 ? 1 : 0) + Math.max(1, Math.ceil((size - previewLen) / 2 ** chunkLog2));
|
|
227
|
+
/** Spec 3.1, and the checks against the header that need only the metadata. */
|
|
228
|
+
export function parseMeta(piece, h) {
|
|
229
|
+
const bad = (why) => fail('meta_invalid', why);
|
|
230
|
+
const dv = new DataView(piece.buffer, piece.byteOffset, piece.byteLength);
|
|
231
|
+
const nameLen = dv.getUint16(0);
|
|
232
|
+
const mimeAt = 2 + nameLen;
|
|
233
|
+
if (mimeAt + 1 > piece.length)
|
|
234
|
+
bad('name runs past the metadata');
|
|
235
|
+
const mimeLen = piece[mimeAt] ?? 0;
|
|
236
|
+
const sizeAt = mimeAt + 1 + mimeLen;
|
|
237
|
+
const end = sizeAt + 8;
|
|
238
|
+
if (end > piece.length)
|
|
239
|
+
bad('mime runs past the metadata');
|
|
240
|
+
if (Math.ceil(end / 256) * 256 !== piece.length)
|
|
241
|
+
bad('padding is not minimal');
|
|
242
|
+
if (piece.subarray(end).some((x) => x !== 0))
|
|
243
|
+
bad('padding is not zero');
|
|
244
|
+
let name = '';
|
|
245
|
+
try {
|
|
246
|
+
name = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(piece.subarray(2, mimeAt));
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
bad('name is not UTF-8');
|
|
250
|
+
}
|
|
251
|
+
const mimeBytes = piece.subarray(mimeAt + 1, sizeAt);
|
|
252
|
+
if (mimeBytes.some((x) => x < 0x20 || x > 0x7e))
|
|
253
|
+
bad('mime is not printable ASCII');
|
|
254
|
+
const big = dv.getBigUint64(sizeAt);
|
|
255
|
+
if (big > BigInt(Number.MAX_SAFE_INTEGER))
|
|
256
|
+
bad('size too large');
|
|
257
|
+
const size = Number(big);
|
|
258
|
+
if (h.previewLen > previewLimit(size))
|
|
259
|
+
bad('preview over the limit');
|
|
260
|
+
if (h.n !== chunkCount(size, h.previewLen, h.chunkLog2))
|
|
261
|
+
bad('chunk count does not fit the size');
|
|
262
|
+
return { name, mime: String.fromCharCode(...mimeBytes), size };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Spec 6, steps 1 to 4: needs no key, so a client can run it before paying when the bytes are
|
|
266
|
+
* public. Throws `Pay2Error`.
|
|
267
|
+
*/
|
|
268
|
+
export async function inspect(file, listing, contractId) {
|
|
269
|
+
return inspected(await checkPublic(view(file), listing, contractId), view(file), contractId);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Spec 6, steps 1 to 3, on the header alone: what a client checks before paying when only the
|
|
273
|
+
* header is public (x402-annon.md section 7). `header` must be exactly the header, with no body bytes.
|
|
274
|
+
* It proves which bytes the buyer pays for, not that a host holds them. Throws `Pay2Error`.
|
|
275
|
+
*/
|
|
276
|
+
export async function inspectHeader(header, listing, contractId) {
|
|
277
|
+
const b = view(header);
|
|
278
|
+
const h = await checkSigned(b, listing, contractId);
|
|
279
|
+
if (b.length !== h.headerLen)
|
|
280
|
+
fail('bad_header', 'expected the header alone');
|
|
281
|
+
return inspected(h, b, contractId);
|
|
282
|
+
}
|
|
283
|
+
const inspected = async (h, b, contractId) => {
|
|
284
|
+
const header = copy(b.subarray(0, h.headerLen));
|
|
285
|
+
return {
|
|
286
|
+
fileId: h.fileId,
|
|
287
|
+
usable: h.stanzas.some((s) => s.type === NEAR_CKD && nearCkd(s.body)?.contractId === contractId),
|
|
288
|
+
header,
|
|
289
|
+
headerHash: await sha256(header),
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
/** Spec 6, steps 1 to 3. */
|
|
293
|
+
export async function checkSigned(b, listing, contractId) {
|
|
294
|
+
const parsed = parseHeader(b);
|
|
295
|
+
const version = listing.pay2Version ?? 2;
|
|
296
|
+
if (parsed.version !== version)
|
|
297
|
+
fail('unsupported_version');
|
|
298
|
+
const h = await checkRegistration(parsed, listing);
|
|
299
|
+
const seller = typeof listing.seller === 'string'
|
|
300
|
+
? parseIdentity({ kind: 'evm', id: sellerAddress(listing.seller) })
|
|
301
|
+
: parseIdentity(listing.seller);
|
|
302
|
+
const hash = await sha256(b.subarray(0, h.headerLen - SIG));
|
|
303
|
+
const trailer = b.subarray(h.headerLen - SIG, h.headerLen);
|
|
304
|
+
if (identityKind(seller) !== h.authKind)
|
|
305
|
+
fail('seller_signature_invalid');
|
|
306
|
+
if (seller.kind === 'near') {
|
|
307
|
+
if (trailer.some((x) => x !== 0) ||
|
|
308
|
+
listing.publishedHeaderHash === undefined ||
|
|
309
|
+
!equal(hash, hexBytes(listing.publishedHeaderHash)))
|
|
310
|
+
fail('seller_signature_invalid');
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
if (seller.kind === 'solana' && trailer[64] !== 0)
|
|
314
|
+
fail('seller_signature_invalid');
|
|
315
|
+
const sig = seller.kind === 'solana' ? trailer.subarray(0, 64) : trailer;
|
|
316
|
+
if (!verifyAuthorization(seller, publishTextV2(contractId, canonicalHex(h.fileId), canonicalHex(hash)), { kind: seller.kind, sig: canonicalHex(sig) }))
|
|
317
|
+
fail('seller_signature_invalid');
|
|
318
|
+
}
|
|
319
|
+
return h;
|
|
320
|
+
}
|
|
321
|
+
async function checkPublic(b, listing, contractId) {
|
|
322
|
+
const h = await checkSigned(b, listing, contractId);
|
|
323
|
+
const lengths = fixedLengths(h);
|
|
324
|
+
const body = b.subarray(h.headerLen);
|
|
325
|
+
const last = body.length - lengths.reduce((s, x) => s + x, 0);
|
|
326
|
+
if (last < TAG || last > 2 ** h.chunkLog2 + TAG)
|
|
327
|
+
fail('bad_body_length');
|
|
328
|
+
let at = 0;
|
|
329
|
+
for (const [i, len] of [...lengths, last].entries()) {
|
|
330
|
+
if (!equal(await sha256(body.subarray(at, at + len)), h.hashes[i])) {
|
|
331
|
+
fail('chunk_hash_mismatch', `chunk ${i}`);
|
|
332
|
+
}
|
|
333
|
+
at += len;
|
|
334
|
+
}
|
|
335
|
+
return h;
|
|
336
|
+
}
|
|
337
|
+
/** Spec 6, step 2. */
|
|
338
|
+
async function checkRegistration(h, listing) {
|
|
339
|
+
if (!equal(h.fileId, hexBytes(listing.fileId)))
|
|
340
|
+
fail('file_id_mismatch');
|
|
341
|
+
if (!equal(await bodyHash(h), hexBytes(listing.cipherHash)))
|
|
342
|
+
fail('cipher_hash_mismatch');
|
|
343
|
+
return h;
|
|
344
|
+
}
|
|
345
|
+
/** Spec 6, all steps. Returns nothing unless every check passes. */
|
|
346
|
+
export async function open(file, opts) {
|
|
347
|
+
const b = view(file);
|
|
348
|
+
const h = await checkPublic(b, opts.listing, opts.contractId);
|
|
349
|
+
const stanza = h.stanzas
|
|
350
|
+
.map((s) => (s.type === NEAR_CKD ? nearCkd(s.body) : undefined))
|
|
351
|
+
.find((s) => s?.contractId === opts.contractId);
|
|
352
|
+
if (!stanza)
|
|
353
|
+
return fail('no_usable_stanza');
|
|
354
|
+
const wrapKey = await wrapKeyFor(await opts.ckdSecret(h.fileId), h.fileId, 'unwrapKey');
|
|
355
|
+
let K;
|
|
356
|
+
try {
|
|
357
|
+
K = await subtle.unwrapKey('raw', stanza.wrapped, wrapKey, 'AES-KW', 'HKDF', false, [
|
|
358
|
+
'deriveBits',
|
|
359
|
+
]);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return fail('key_unwrap_failed');
|
|
363
|
+
}
|
|
364
|
+
const macKey = await subtle.importKey('raw', await hkdf(K, h.fileId, 'annon/pay2/header'), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
365
|
+
const macAt = h.headerLen - MAC - SIG;
|
|
366
|
+
const mac = b.subarray(macAt, macAt + MAC);
|
|
367
|
+
if (!(await subtle.verify('HMAC', macKey, mac, b.subarray(0, macAt)))) {
|
|
368
|
+
fail('header_mac_invalid');
|
|
369
|
+
}
|
|
370
|
+
const lengths = fixedLengths(h);
|
|
371
|
+
const body = b.subarray(h.headerLen);
|
|
372
|
+
lengths.push(body.length - lengths.reduce((s, x) => s + x, 0));
|
|
373
|
+
const wire = [];
|
|
374
|
+
let at = 0;
|
|
375
|
+
for (const len of lengths) {
|
|
376
|
+
wire.push(body.subarray(at, at + len));
|
|
377
|
+
at += len;
|
|
378
|
+
}
|
|
379
|
+
const decrypt = async (i) => {
|
|
380
|
+
const k = await chunkKey(K, h.fileId, i);
|
|
381
|
+
const c = h.commits[i];
|
|
382
|
+
if (c && !equal(await commitment(k), c))
|
|
383
|
+
fail('chunk_invalid', `chunk ${i} key commitment`);
|
|
384
|
+
try {
|
|
385
|
+
return await gcm('decrypt', k, i === h.n - 1, wire[i]);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return fail('chunk_invalid', `chunk ${i}`);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const meta = parseMeta(await decrypt(0), h);
|
|
392
|
+
if (lengths.slice(1).reduce((s, x) => s + x - TAG, 0) !== meta.size) {
|
|
393
|
+
fail('meta_invalid', 'chunk lengths do not add up to the size');
|
|
394
|
+
}
|
|
395
|
+
const pieces = [];
|
|
396
|
+
for (let i = 1; i < h.n; i++)
|
|
397
|
+
pieces.push(await decrypt(i));
|
|
398
|
+
return { meta, data: concat(...pieces) };
|
|
399
|
+
}
|
|
400
|
+
/** @internal Shared by buffered and streaming encoders. */
|
|
401
|
+
export function sealAuth(opts) {
|
|
402
|
+
const version = opts.version ?? 2;
|
|
403
|
+
const authKind = opts.authKind ?? 0;
|
|
404
|
+
if (version !== 2 || ![0, 1, 2].includes(authKind))
|
|
405
|
+
throw new TypeError('invalid PAY2 version/auth kind');
|
|
406
|
+
return { version, authKind };
|
|
407
|
+
}
|
|
408
|
+
/** @internal Build and check the fixed-width authentication trailer. */
|
|
409
|
+
export async function finishTrailer(version, authKind, opts, fileId, headerHash) {
|
|
410
|
+
const seller = typeof opts.seller === 'string'
|
|
411
|
+
? parseIdentity({ kind: 'evm', id: sellerAddress(opts.seller) })
|
|
412
|
+
: parseIdentity(opts.seller);
|
|
413
|
+
if (identityKind(seller) !== authKind)
|
|
414
|
+
throw new TypeError('seller identity does not match auth kind');
|
|
415
|
+
if (seller.kind === 'near')
|
|
416
|
+
return new Uint8Array(SIG);
|
|
417
|
+
if (!opts.sign)
|
|
418
|
+
throw new TypeError('seller signature required');
|
|
419
|
+
if (version !== 2)
|
|
420
|
+
throw new TypeError('unsupported PAY2 version');
|
|
421
|
+
const text = publishTextV2(opts.contractId, canonicalHex(fileId), canonicalHex(headerHash));
|
|
422
|
+
const result = await opts.sign(text);
|
|
423
|
+
if (seller.kind === 'solana') {
|
|
424
|
+
if (typeof result === 'string' || result instanceof Uint8Array)
|
|
425
|
+
throw new TypeError('Solana wallet must return signed bytes');
|
|
426
|
+
const auth = solanaWalletAuth(seller, text, result);
|
|
427
|
+
return concat(hexBytes(auth.sig, 64), new Uint8Array([0]));
|
|
428
|
+
}
|
|
429
|
+
if (typeof result !== 'string' && !(result instanceof Uint8Array))
|
|
430
|
+
throw new TypeError('expected EVM signature bytes');
|
|
431
|
+
const sig = hexBytes(result, SIG);
|
|
432
|
+
if (!signedBy(text, sig, seller.id))
|
|
433
|
+
throw new Error('the signature is not from the seller');
|
|
434
|
+
return sig;
|
|
435
|
+
}
|
|
436
|
+
/** Encrypts a file under a fresh random key. */
|
|
437
|
+
export function seal(data, opts) {
|
|
438
|
+
return sealWithKey(globalThis.crypto.getRandomValues(new Uint8Array(32)), data, opts);
|
|
439
|
+
}
|
|
440
|
+
/** Encrypts with a caller-supplied fresh key, including deterministic replay. Never reuse it for another file. */
|
|
441
|
+
export async function sealWithKey(K, data, opts) {
|
|
442
|
+
if (opts.fileId.length !== 32 || K.length !== 32)
|
|
443
|
+
throw new TypeError('fileId and K are 32 bytes');
|
|
444
|
+
const { version, authKind } = sealAuth(opts);
|
|
445
|
+
const fileId = copy(opts.fileId);
|
|
446
|
+
const previewLen = opts.previewLen ?? 0;
|
|
447
|
+
if (!Number.isSafeInteger(previewLen) ||
|
|
448
|
+
previewLen < 0 ||
|
|
449
|
+
previewLen > previewLimit(data.length)) {
|
|
450
|
+
throw new RangeError(`previewLen must be 0 to ${previewLimit(data.length)}`);
|
|
451
|
+
}
|
|
452
|
+
let chunkLog2 = opts.chunkLog2 ?? 22;
|
|
453
|
+
while (opts.chunkLog2 === undefined &&
|
|
454
|
+
chunkLog2 < 24 &&
|
|
455
|
+
chunkCount(data.length, previewLen, chunkLog2) > MAX_CHUNKS) {
|
|
456
|
+
chunkLog2++;
|
|
457
|
+
}
|
|
458
|
+
if (!Number.isInteger(chunkLog2) || chunkLog2 < 16 || chunkLog2 > 24) {
|
|
459
|
+
throw new RangeError('chunkLog2 must be an integer from 16 to 24');
|
|
460
|
+
}
|
|
461
|
+
if (chunkCount(data.length, previewLen, chunkLog2) > MAX_CHUNKS) {
|
|
462
|
+
throw new RangeError('file too large for PAY2');
|
|
463
|
+
}
|
|
464
|
+
const C = 2 ** chunkLog2;
|
|
465
|
+
const name = utf8.encode(opts.name);
|
|
466
|
+
if (!/^[\x20-\x7e]{0,255}$/.test(opts.mime))
|
|
467
|
+
throw new RangeError('mime must be printable ASCII');
|
|
468
|
+
const fields = concat(int(name.length, 2), name, int(opts.mime.length, 1), utf8.encode(opts.mime), int(data.length, 8));
|
|
469
|
+
const metaLen = Math.ceil(fields.length / 256) * 256;
|
|
470
|
+
if (metaLen > MAX_META)
|
|
471
|
+
throw new RangeError('name too long');
|
|
472
|
+
const pieces = [concat(fields, new Uint8Array(metaLen - fields.length))];
|
|
473
|
+
if (previewLen > 0)
|
|
474
|
+
pieces.push(data.subarray(0, previewLen));
|
|
475
|
+
const rest = data.subarray(previewLen);
|
|
476
|
+
for (let at = 0; at < rest.length || at === 0; at += C)
|
|
477
|
+
pieces.push(rest.subarray(at, at + C));
|
|
478
|
+
const n = pieces.length;
|
|
479
|
+
const keys = await Promise.all(pieces.map((_, i) => chunkKey(K, fileId, i)));
|
|
480
|
+
const commits = await Promise.all(keys.slice(0, previewLen > 0 ? 2 : 1).map((k) => commitment(k)));
|
|
481
|
+
const wire = await Promise.all(pieces.map((p, i) => gcm('encrypt', keys[i], i === n - 1, p)));
|
|
482
|
+
const hashes = await Promise.all(wire.map((w) => sha256(w)));
|
|
483
|
+
const prefix = concat(new Uint8Array(MAGIC), new Uint8Array([version, chunkLog2, 1, authKind]), new Uint8Array(4), // headerLen, set in finish
|
|
484
|
+
fileId, int(metaLen, 2), int(previewLen, 4), int(n, 4), ...commits, ...hashes);
|
|
485
|
+
let finished;
|
|
486
|
+
return {
|
|
487
|
+
fileId,
|
|
488
|
+
cipherHash: await bodyHash({
|
|
489
|
+
version,
|
|
490
|
+
chunkLog2,
|
|
491
|
+
fileId,
|
|
492
|
+
metaLen,
|
|
493
|
+
previewLen,
|
|
494
|
+
n,
|
|
495
|
+
commits,
|
|
496
|
+
hashes,
|
|
497
|
+
}),
|
|
498
|
+
async finish(finishOpts) {
|
|
499
|
+
const { contractId, ckdSecret } = finishOpts;
|
|
500
|
+
account(contractId);
|
|
501
|
+
const kw = await wrapKeyFor(ckdSecret, fileId, 'wrapKey');
|
|
502
|
+
const fileKey = await subtle.importKey('raw', copy(K), 'AES-GCM', true, ['encrypt']);
|
|
503
|
+
const wrapped = new Uint8Array(await subtle.wrapKey('raw', fileKey, kw, 'AES-KW'));
|
|
504
|
+
const type = utf8.encode(NEAR_CKD);
|
|
505
|
+
const body = concat(new Uint8Array([contractId.length]), utf8.encode(contractId), wrapped);
|
|
506
|
+
const stanza = concat(new Uint8Array([type.length]), type, int(body.length, 2), body);
|
|
507
|
+
const header = concat(prefix, stanza);
|
|
508
|
+
new DataView(header.buffer).setUint32(8, header.length + MAC + SIG);
|
|
509
|
+
const macKey = await subtle.importKey('raw', await hkdf(K, fileId, 'annon/pay2/header'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
510
|
+
const mac = new Uint8Array(await subtle.sign('HMAC', macKey, header));
|
|
511
|
+
const sig = await finishTrailer(version, authKind, finishOpts, fileId, await sha256(header, mac));
|
|
512
|
+
finished = header;
|
|
513
|
+
return concat(header, mac, sig, ...wire);
|
|
514
|
+
},
|
|
515
|
+
async preview(show) {
|
|
516
|
+
if (!finished)
|
|
517
|
+
throw new Error('call finish first: the preview carries the final header');
|
|
518
|
+
if (show.data && previewLen === 0)
|
|
519
|
+
throw new RangeError('sealed without a preview');
|
|
520
|
+
const indexes = [...(show.meta ? [0] : []), ...(show.data ? [1] : [])];
|
|
521
|
+
return {
|
|
522
|
+
v: 1,
|
|
523
|
+
header: toBase64(previewPrefix(finished.subarray(0, prefix.length))),
|
|
524
|
+
chunks: indexes.map((i) => ({
|
|
525
|
+
index: i,
|
|
526
|
+
key: toBase64(keys[i]),
|
|
527
|
+
data: toBase64(wire[i]),
|
|
528
|
+
})),
|
|
529
|
+
};
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/** Spec 7: the header up to the hash list, with stanza count and `headerLen` set to 0. */
|
|
534
|
+
function previewPrefix(header) {
|
|
535
|
+
const p = copy(header);
|
|
536
|
+
p[6] = 0;
|
|
537
|
+
p.fill(0, 8, 12);
|
|
538
|
+
return p;
|
|
539
|
+
}
|
|
540
|
+
/** Spec 3: at most 64 KiB and a tenth of the file. */
|
|
541
|
+
export const previewLimit = (size) => Math.min(MAX_PREVIEW, Math.floor(size / 10));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Replayable file adapters shared by the browser SDK and Node file I/O. */
|
|
2
|
+
import type { UploadManifest } from '../download/upload.js';
|
|
3
|
+
import { type Bytes, type FileMeta, type FinishOptions, type OpenOptions, type SealOptions } from './pay2.js';
|
|
4
|
+
/** Each read starts a new bounded stream of the same immutable bytes. Never use tee(). */
|
|
5
|
+
export interface ReplayableSource {
|
|
6
|
+
size: number;
|
|
7
|
+
read(): ReadableStream<Uint8Array>;
|
|
8
|
+
}
|
|
9
|
+
/** Store encrypted bytes until use() completes, then remove the spool even on failure. */
|
|
10
|
+
export type Spool = <T>(stream: ReadableStream<Uint8Array>, use: (source: ReplayableSource) => Promise<T>) => Promise<T>;
|
|
11
|
+
/** @internal A single-use stream needs external storage to preserve spec 6 validation order. */
|
|
12
|
+
export declare function withReplay<T>(source: ReplayableSource | ReadableStream<Uint8Array>, use: (source: ReplayableSource) => Promise<T>, spool?: Spool): Promise<T>;
|
|
13
|
+
export declare function blobSource(blob: Blob): ReplayableSource;
|
|
14
|
+
export type SellData = Uint8Array | ReplayableSource;
|
|
15
|
+
export type SellFile<T extends SellData> = T extends Uint8Array ? Bytes : ReadableStream<Uint8Array>;
|
|
16
|
+
export interface PreparedSeal<T extends SellData> {
|
|
17
|
+
manifest: UploadManifest;
|
|
18
|
+
headerSize(contractId: string): number;
|
|
19
|
+
body(): ReadableStream<Uint8Array>;
|
|
20
|
+
/** Stage ciphertext in caller-owned immutable storage for reservation and restart. */
|
|
21
|
+
stage<R>(spool: Spool, use: (source: ReplayableSource) => Promise<R>): Promise<R>;
|
|
22
|
+
cipherHash: Bytes;
|
|
23
|
+
preview: import('./pay2.js').Sealed['preview'];
|
|
24
|
+
finish(opts: FinishOptions): Promise<{
|
|
25
|
+
file: SellFile<T>;
|
|
26
|
+
header: Bytes;
|
|
27
|
+
consumed(): boolean;
|
|
28
|
+
cancel(reason: unknown): Promise<void>;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/** @internal Keeps registration and sell recovery independent of the file representation. */
|
|
32
|
+
export declare function prepareSeal<T extends SellData>(key: Uint8Array, data: T, opts: SealOptions): Promise<PreparedSeal<T>>;
|
|
33
|
+
/**
|
|
34
|
+
* Spec 6 in buffered-open order with bounded memory. Validate ALL body lengths and hashes
|
|
35
|
+
* before selecting a stanza or obtaining a key. Replay only immutable ciphertext. A download
|
|
36
|
+
* must first be spooled to caller-owned storage. Sink contents remain provisional until success.
|
|
37
|
+
*/
|
|
38
|
+
export declare function prepareOpen(source: ReplayableSource, opts: Omit<OpenOptions, 'ckdSecret'>): Promise<{
|
|
39
|
+
header: Uint8Array<ArrayBuffer>;
|
|
40
|
+
writeTo(sink: WritableStream<Uint8Array>, ckdSecret: OpenOptions["ckdSecret"]): Promise<FileMeta>;
|
|
41
|
+
}>;
|
|
42
|
+
/** Bounded-memory open to a provisional sink, with exactly the checks and order of open(). */
|
|
43
|
+
export declare function openTo(source: ReplayableSource | ReadableStream<Uint8Array>, sink: WritableStream<Uint8Array>, opts: OpenOptions & {
|
|
44
|
+
spool?: Spool;
|
|
45
|
+
}): Promise<FileMeta>;
|