@teepin/sdk 0.2.1 → 0.3.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.
Files changed (134) hide show
  1. package/README.md +41 -36
  2. package/dist/cjs/client.js +42 -4
  3. package/dist/cjs/client.js.map +1 -1
  4. package/dist/cjs/direct/bytes.js +50 -0
  5. package/dist/cjs/direct/bytes.js.map +1 -0
  6. package/dist/cjs/direct/common.js +167 -0
  7. package/dist/cjs/direct/common.js.map +1 -0
  8. package/dist/cjs/direct/downloads-plan.js +127 -0
  9. package/dist/cjs/direct/downloads-plan.js.map +1 -0
  10. package/dist/cjs/direct/engine.js +405 -0
  11. package/dist/cjs/direct/engine.js.map +1 -0
  12. package/dist/cjs/direct/formats.js +44 -0
  13. package/dist/cjs/direct/formats.js.map +1 -0
  14. package/dist/cjs/direct/hpke.js +275 -0
  15. package/dist/cjs/direct/hpke.js.map +1 -0
  16. package/dist/cjs/direct/journal.js +80 -0
  17. package/dist/cjs/direct/journal.js.map +1 -0
  18. package/dist/cjs/direct/large.js +850 -0
  19. package/dist/cjs/direct/large.js.map +1 -0
  20. package/dist/cjs/direct/reads.js +1314 -0
  21. package/dist/cjs/direct/reads.js.map +1 -0
  22. package/dist/cjs/direct/sha256.js +122 -0
  23. package/dist/cjs/direct/sha256.js.map +1 -0
  24. package/dist/cjs/direct/tws1.js +480 -0
  25. package/dist/cjs/direct/tws1.js.map +1 -0
  26. package/dist/cjs/direct/uploads.js +1706 -0
  27. package/dist/cjs/direct/uploads.js.map +1 -0
  28. package/dist/cjs/errors.js +70 -13
  29. package/dist/cjs/errors.js.map +1 -1
  30. package/dist/cjs/index.js +5 -1
  31. package/dist/cjs/index.js.map +1 -1
  32. package/dist/cjs/links.js +195 -0
  33. package/dist/cjs/links.js.map +1 -0
  34. package/dist/cjs/node/downloads.js +835 -0
  35. package/dist/cjs/node/downloads.js.map +1 -0
  36. package/dist/cjs/node/journal.js +841 -0
  37. package/dist/cjs/node/journal.js.map +1 -0
  38. package/dist/cjs/node/uploads.js +219 -0
  39. package/dist/cjs/node/uploads.js.map +1 -0
  40. package/dist/cjs/node.js +26 -39
  41. package/dist/cjs/node.js.map +1 -1
  42. package/dist/cjs/storage.js +214 -184
  43. package/dist/cjs/storage.js.map +1 -1
  44. package/dist/cjs/transfer.js +18 -351
  45. package/dist/cjs/transfer.js.map +1 -1
  46. package/dist/esm/client.js +42 -4
  47. package/dist/esm/client.js.map +1 -1
  48. package/dist/esm/direct/bytes.js +42 -0
  49. package/dist/esm/direct/bytes.js.map +1 -0
  50. package/dist/esm/direct/common.js +155 -0
  51. package/dist/esm/direct/common.js.map +1 -0
  52. package/dist/esm/direct/downloads-plan.js +117 -0
  53. package/dist/esm/direct/downloads-plan.js.map +1 -0
  54. package/dist/esm/direct/engine.js +385 -0
  55. package/dist/esm/direct/engine.js.map +1 -0
  56. package/dist/esm/direct/formats.js +40 -0
  57. package/dist/esm/direct/formats.js.map +1 -0
  58. package/dist/esm/direct/hpke.js +262 -0
  59. package/dist/esm/direct/hpke.js.map +1 -0
  60. package/dist/esm/direct/journal.js +75 -0
  61. package/dist/esm/direct/journal.js.map +1 -0
  62. package/dist/esm/direct/large.js +816 -0
  63. package/dist/esm/direct/large.js.map +1 -0
  64. package/dist/esm/direct/reads.js +1275 -0
  65. package/dist/esm/direct/reads.js.map +1 -0
  66. package/dist/esm/direct/sha256.js +118 -0
  67. package/dist/esm/direct/sha256.js.map +1 -0
  68. package/dist/esm/direct/tws1.js +460 -0
  69. package/dist/esm/direct/tws1.js.map +1 -0
  70. package/dist/esm/direct/uploads.js +1660 -0
  71. package/dist/esm/direct/uploads.js.map +1 -0
  72. package/dist/esm/errors.js +64 -11
  73. package/dist/esm/errors.js.map +1 -1
  74. package/dist/esm/index.js +1 -0
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/esm/links.js +189 -0
  77. package/dist/esm/links.js.map +1 -0
  78. package/dist/esm/node/downloads.js +829 -0
  79. package/dist/esm/node/downloads.js.map +1 -0
  80. package/dist/esm/node/journal.js +824 -0
  81. package/dist/esm/node/journal.js.map +1 -0
  82. package/dist/esm/node/uploads.js +187 -0
  83. package/dist/esm/node/uploads.js.map +1 -0
  84. package/dist/esm/node.js +17 -14
  85. package/dist/esm/node.js.map +1 -1
  86. package/dist/esm/storage.js +214 -184
  87. package/dist/esm/storage.js.map +1 -1
  88. package/dist/esm/transfer.js +19 -349
  89. package/dist/esm/transfer.js.map +1 -1
  90. package/dist/types/client.d.ts +45 -4
  91. package/dist/types/client.d.ts.map +1 -1
  92. package/dist/types/direct/bytes.d.ts +11 -0
  93. package/dist/types/direct/bytes.d.ts.map +1 -0
  94. package/dist/types/direct/common.d.ts +47 -0
  95. package/dist/types/direct/common.d.ts.map +1 -0
  96. package/dist/types/direct/downloads-plan.d.ts +75 -0
  97. package/dist/types/direct/downloads-plan.d.ts.map +1 -0
  98. package/dist/types/direct/engine.d.ts +191 -0
  99. package/dist/types/direct/engine.d.ts.map +1 -0
  100. package/dist/types/direct/formats.d.ts +9 -0
  101. package/dist/types/direct/formats.d.ts.map +1 -0
  102. package/dist/types/direct/hpke.d.ts +55 -0
  103. package/dist/types/direct/hpke.d.ts.map +1 -0
  104. package/dist/types/direct/journal.d.ts +55 -0
  105. package/dist/types/direct/journal.d.ts.map +1 -0
  106. package/dist/types/direct/large.d.ts +170 -0
  107. package/dist/types/direct/large.d.ts.map +1 -0
  108. package/dist/types/direct/reads.d.ts +306 -0
  109. package/dist/types/direct/reads.d.ts.map +1 -0
  110. package/dist/types/direct/sha256.d.ts +16 -0
  111. package/dist/types/direct/sha256.d.ts.map +1 -0
  112. package/dist/types/direct/tws1.d.ts +157 -0
  113. package/dist/types/direct/tws1.d.ts.map +1 -0
  114. package/dist/types/direct/uploads.d.ts +461 -0
  115. package/dist/types/direct/uploads.d.ts.map +1 -0
  116. package/dist/types/errors.d.ts +47 -8
  117. package/dist/types/errors.d.ts.map +1 -1
  118. package/dist/types/index.d.ts +1 -0
  119. package/dist/types/index.d.ts.map +1 -1
  120. package/dist/types/links.d.ts +86 -0
  121. package/dist/types/links.d.ts.map +1 -0
  122. package/dist/types/node/downloads.d.ts +73 -0
  123. package/dist/types/node/downloads.d.ts.map +1 -0
  124. package/dist/types/node/journal.d.ts +162 -0
  125. package/dist/types/node/journal.d.ts.map +1 -0
  126. package/dist/types/node/uploads.d.ts +86 -0
  127. package/dist/types/node/uploads.d.ts.map +1 -0
  128. package/dist/types/node.d.ts +4 -2
  129. package/dist/types/node.d.ts.map +1 -1
  130. package/dist/types/storage.d.ts +107 -64
  131. package/dist/types/storage.d.ts.map +1 -1
  132. package/dist/types/transfer.d.ts +14 -23
  133. package/dist/types/transfer.d.ts.map +1 -1
  134. package/package.json +2 -2
@@ -0,0 +1,1314 @@
1
+ "use strict";
2
+ // Copyright 2026 TEEPIN Project
3
+ // Licensed under the Apache License, Version 2.0
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || function (mod) {
21
+ if (mod && mod.__esModule) return mod;
22
+ var result = {};
23
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24
+ __setModuleDefault(result, mod);
25
+ return result;
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.ObjectFile = exports.drain = exports.readable = exports.collect = exports.LinkReads = exports.LINK_READ_PATH = exports.DirectReads = exports.Body = exports.Tries = exports.KeyCache = exports.Session = exports.Signed = exports.linked = exports.stagedError = exports.DEFAULT_KEY_CACHE_TTL_MS = exports.DEFAULT_WINDOW = void 0;
29
+ // Reads straight from the storage network (TEEPIN-STORAGE-DESIGN.md sections 21.3, 22.5 and 24), a port of
30
+ // teepin-python's _reads.py with the same rules.
31
+ //
32
+ // A read session (object/read) gives an object's format, where its bytes are, its data key, and signed requests for
33
+ // them. Every byte is verified before it is used:
34
+ //
35
+ // - tws1 in its own blob: windows (the server's, 64 MiB), read segment by segment as they stream, each GCM tag checked
36
+ // (the header's key commitment first, the last segment told by the session's size). A window that breaks off goes
37
+ // on from its last verified segment, under a new signature.
38
+ // - anything packed: the needle first (TPK1, the object id, which is the session's version, the stored size, and the
39
+ // stored bytes' SHA-256), then the whole object decrypted (tws1, teepin-v1) or taken as stored (plain).
40
+ // - plain in its own blob (legacy objects only): as stored.
41
+ // - source "api": an object still in the API's old stage. It isn't read (the API's content route is retired): the read
42
+ // fails with stagedError until the object has been moved into storage.
43
+ //
44
+ // A whole-object read also checks the plaintext SHA-256 against the session's, as its last bytes arrive, except for a
45
+ // legacy object stored in parts, whose checksum is a composite. Streamed bytes are hashed with sha256.ts, since a
46
+ // browser's WebCrypto can only hash a whole buffer.
47
+ //
48
+ // Data keys are cached in memory for the client's keyCacheTtl (10 minutes), by bucket, key and version, so later reads
49
+ // sign without them. Each read takes its own copy and zeroes it when done; the cache zeroes its copies on expiry,
50
+ // eviction and close.
51
+ //
52
+ // Failures: a passing one (the network, a busy storage network) is retried with backoff; an expired or refused
53
+ // signature is signed again; a 404 is signed again once (a server pack may have been compacted), then NotFoundError;
54
+ // a failed check is retried once, then DataIntegrityError.
55
+ //
56
+ // A share link's reads (LinkReads, section 26.3) are the same, with their sessions from the link's token.
57
+ //
58
+ // Cancellation is an AbortSignal (section 22.13's rules). An abort ends a read at once and rejects with the signal's
59
+ // reason: open responses are closed, a wait is cut short, and a download's parallel windows are all stopped and waited
60
+ // for, so no request is left running. Cleanup never retries or sleeps.
61
+ //
62
+ // Nothing here imports a Node module: the same code runs in Node 20+ and browsers.
63
+ const client_js_1 = require("../client.js");
64
+ const errors_js_1 = require("../errors.js");
65
+ const bytes_js_1 = require("./bytes.js");
66
+ const engine_js_1 = require("./engine.js");
67
+ const formats_js_1 = require("./formats.js");
68
+ const sha256_js_1 = require("./sha256.js");
69
+ const tws1 = __importStar(require("./tws1.js"));
70
+ /** The server's read window until it shows its own: 64 MiB of stored bytes. */
71
+ exports.DEFAULT_WINDOW = 64 * 2 ** 20;
72
+ /** How long a data key stays cached, in ms, unless the client says. */
73
+ exports.DEFAULT_KEY_CACHE_TTL_MS = 600000;
74
+ /** The most a plain blob's read takes from its body at once. */
75
+ const READ_CHUNK = 2 ** 20;
76
+ /** Keys a batch read session names at most (the control plane's cap). */
77
+ const READ_BATCH = 1000;
78
+ const EMPTY = new Uint8Array(0);
79
+ const bucketPath = (bucket) => `/v1/storage/buckets/${encodeURIComponent(bucket)}`;
80
+ const unquote = (etag) => etag.trim().replace(/^W\//, '').replace(/^"|"$/g, '');
81
+ const str = (v) => (v === undefined || v === null ? '' : String(v));
82
+ const int = (v) => Math.trunc(Number(v || 0)) || 0;
83
+ /** A time from the API, or undefined when there is none or it doesn't parse. */
84
+ function when(v) {
85
+ if (!v)
86
+ return undefined;
87
+ const t = Date.parse(String(v));
88
+ return Number.isNaN(t) ? undefined : new Date(t);
89
+ }
90
+ async function sha256Hex(data) {
91
+ return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', data)), (b) => b.toString(16).padStart(2, '0')).join('');
92
+ }
93
+ /** The error for reading an object still in the API's old stage (a session with source "api"): it can't be read yet. */
94
+ function stagedError(key) {
95
+ return new errors_js_1.TeepinError(`${key}: this object is still being moved into storage and can't be read yet; try again later`);
96
+ }
97
+ exports.stagedError = stagedError;
98
+ /** A window that stopped short: go on from the last verified byte. A ConnectionError, as it is to a caller. */
99
+ class Broken extends errors_js_1.ConnectionError {
100
+ }
101
+ /** A signature too close to its expiry to use. */
102
+ class Expired extends errors_js_1.TeepinError {
103
+ constructor() {
104
+ super('the signature is about to expire');
105
+ this.name = 'Expired';
106
+ }
107
+ }
108
+ /** A controller that aborts with parent's reason, and a function that unlinks it once it is no longer needed. */
109
+ function linked(parent) {
110
+ const ctl = new AbortController();
111
+ if (!parent)
112
+ return { ctl, unlink: () => { } };
113
+ if (parent.aborted) {
114
+ ctl.abort(parent.reason);
115
+ return { ctl, unlink: () => { } };
116
+ }
117
+ const onAbort = () => ctl.abort(parent.reason);
118
+ parent.addEventListener('abort', onAbort, { once: true });
119
+ return { ctl, unlink: () => parent.removeEventListener('abort', onAbort) };
120
+ }
121
+ exports.linked = linked;
122
+ // --- sessions -----------------------------------------------------------------
123
+ /** One signed request to the storage network. */
124
+ class Signed {
125
+ url;
126
+ headers;
127
+ expires;
128
+ prefix;
129
+ first;
130
+ last;
131
+ constructor(url, headers,
132
+ /** When the signature expires (ms since the epoch); undefined when the session didn't say. */
133
+ expires,
134
+ /** Bytes before the object's own (a packed object's needle). */
135
+ prefix,
136
+ /** The object's stored bytes it covers, inclusive. */
137
+ first, last) {
138
+ this.url = url;
139
+ this.headers = headers;
140
+ this.expires = expires;
141
+ this.prefix = prefix;
142
+ this.first = first;
143
+ this.last = last;
144
+ }
145
+ static of(d) {
146
+ const headers = {};
147
+ for (const [k, v] of Object.entries(d.headers ?? {}))
148
+ headers[k] = str(v);
149
+ return new Signed(str(d.url), headers, when(d.expires_at)?.getTime(), int(d.prefix), int(d.stored_first), int(d.stored_last));
150
+ }
151
+ /** The bytes it answers: the prefix and the object's own. */
152
+ get length() {
153
+ return this.prefix + this.last - this.first + 1;
154
+ }
155
+ /** Whether it can still be used: not within a minute of its expiry. */
156
+ fresh() {
157
+ return this.expires === undefined || this.expires - Date.now() > engine_js_1.SIGN_MARGIN_MS;
158
+ }
159
+ }
160
+ exports.Signed = Signed;
161
+ /** One read session. dek is this session's own copy of the data key. */
162
+ class Session {
163
+ bucket;
164
+ key;
165
+ version;
166
+ size;
167
+ storedSize;
168
+ etag;
169
+ sha256;
170
+ contentType;
171
+ metadata;
172
+ format;
173
+ source;
174
+ uploadedAt;
175
+ dek;
176
+ ad;
177
+ requests;
178
+ constructor(bucket, d) {
179
+ this.bucket = bucket;
180
+ this.key = str(d.key);
181
+ this.version = str(d.version);
182
+ this.size = int(d.size);
183
+ this.storedSize = int(d.stored_size);
184
+ this.etag = unquote(str(d.etag));
185
+ this.sha256 = str(d.sha256);
186
+ this.contentType = str(d.content_type);
187
+ this.metadata = { ...(d.metadata ?? {}) };
188
+ this.format = str(d.format);
189
+ this.source = str(d.source) || formats_js_1.SOURCE_STORAGE;
190
+ this.uploadedAt = when(d.uploaded_at);
191
+ this.dek = d.key_material ? (0, bytes_js_1.fromBase64)(String(d.key_material)) : null;
192
+ this.ad = (0, bytes_js_1.fromBase64)(str(d.associated_data));
193
+ this.requests = (d.requests ?? []).map((r) => Signed.of(r));
194
+ if (this.source === formats_js_1.SOURCE_STORAGE && !formats_js_1.FORMATS.includes(this.format)) {
195
+ throw new errors_js_1.DataIntegrityError(`${this.key}: unknown storage format ${JSON.stringify(this.format)}`, 'format');
196
+ }
197
+ }
198
+ /** Still in the API's old stage: not readable yet (stagedError). */
199
+ get staged() {
200
+ return this.source === formats_js_1.SOURCE_API;
201
+ }
202
+ get packed() {
203
+ return this.requests.length > 0 && this.requests[this.requests.length - 1].prefix > 0;
204
+ }
205
+ /** The window it signs: its last request (a later window's first one is the header). */
206
+ get window() {
207
+ return this.requests[this.requests.length - 1];
208
+ }
209
+ get needsKey() {
210
+ return !this.staged && this.storedSize > 0 && (this.format === formats_js_1.FORMAT_TWS1 || this.format === formats_js_1.FORMAT_SERVER);
211
+ }
212
+ /** Whether the whole object's SHA-256 can be checked (not a legacy composite of parts). */
213
+ get checkable() {
214
+ return Boolean(this.sha256) && !this.etag.includes('-');
215
+ }
216
+ /** Zeroes this session's copy of the key. */
217
+ forget() {
218
+ this.dek?.fill(0);
219
+ this.dek = null;
220
+ }
221
+ /** The object's details, as statObject would give them. */
222
+ object() {
223
+ const parts = /-(\d+)$/.exec(this.etag);
224
+ const t = this.uploadedAt ?? new Date(NaN);
225
+ return {
226
+ id: this.version,
227
+ key: this.key,
228
+ size: this.size,
229
+ contentType: this.contentType || undefined,
230
+ metadata: this.metadata,
231
+ checksumSha256: this.sha256 || undefined,
232
+ etag: this.etag,
233
+ partCount: parts ? Number(parts[1]) : 0,
234
+ status: 'available',
235
+ backend: '',
236
+ createdAt: t,
237
+ updatedAt: t,
238
+ uploadedAt: this.uploadedAt,
239
+ };
240
+ }
241
+ }
242
+ exports.Session = Session;
243
+ /**
244
+ * Data keys by bucket and key, each for one version, for ttl ms (0: none are kept). It hands out copies, so zeroing one
245
+ * never breaks a read in progress, and zeroes its own on expiry, replacement, drop and clear.
246
+ */
247
+ class KeyCache {
248
+ ttl;
249
+ /** The clock, in ms (monotonic); a test may move it. */
250
+ now = () => performance.now();
251
+ entries = new Map();
252
+ constructor(ttl) {
253
+ this.ttl = ttl;
254
+ }
255
+ static id(bucket, key) {
256
+ return JSON.stringify([bucket, key]);
257
+ }
258
+ /** The cached version and a copy of its key, or undefined. */
259
+ get(bucket, key) {
260
+ const id = KeyCache.id(bucket, key);
261
+ const e = this.entries.get(id);
262
+ if (!e)
263
+ return undefined;
264
+ if (this.now() - e.at > this.ttl) {
265
+ this.entries.delete(id);
266
+ e.key.fill(0);
267
+ return undefined;
268
+ }
269
+ return { version: e.version, key: e.key.slice() };
270
+ }
271
+ /** Keeps a copy of dek for this version (the caller keeps its own). */
272
+ put(bucket, key, version, dek) {
273
+ if (this.ttl <= 0)
274
+ return; // the cache is off: the caller's copy is the only one
275
+ const id = KeyCache.id(bucket, key);
276
+ const old = this.entries.get(id);
277
+ if (old && old.version === version)
278
+ return;
279
+ this.entries.set(id, { version, key: dek.slice(), at: this.now() });
280
+ old?.key.fill(0);
281
+ }
282
+ drop(bucket, key) {
283
+ const id = KeyCache.id(bucket, key);
284
+ const e = this.entries.get(id);
285
+ this.entries.delete(id);
286
+ e?.key.fill(0);
287
+ }
288
+ clear() {
289
+ for (const e of this.entries.values())
290
+ e.key.fill(0);
291
+ this.entries.clear();
292
+ }
293
+ get size() {
294
+ return this.entries.size;
295
+ }
296
+ }
297
+ exports.KeyCache = KeyCache;
298
+ /** One read's failures since its last verified bytes, and what each means. */
299
+ class Tries {
300
+ reads;
301
+ s;
302
+ signal;
303
+ passing = 0;
304
+ bad = 0;
305
+ notFound = false;
306
+ constructor(reads, s, signal) {
307
+ this.reads = reads;
308
+ this.s = s;
309
+ this.signal = signal;
310
+ }
311
+ progressed() {
312
+ this.passing = 0;
313
+ this.bad = 0;
314
+ }
315
+ /** Resolves when the read should try again (after any wait); throws otherwise. An abort is never tried again. */
316
+ async failed(err) {
317
+ if (this.signal?.aborted)
318
+ throw this.signal.reason;
319
+ // A share link that expired (or was never good) stays so: signing again would only be refused again.
320
+ if (err instanceof errors_js_1.ObjectChangedError || err instanceof errors_js_1.InvalidLinkError)
321
+ throw err;
322
+ if (err instanceof errors_js_1.DataIntegrityError) {
323
+ this.bad++; // retried once: a flipped byte in transit, or for good
324
+ if (this.bad > 1)
325
+ throw err;
326
+ return;
327
+ }
328
+ if (err instanceof errors_js_1.APIError && err.statusCode === 404) {
329
+ // A server pack may have been compacted, so the bytes moved: sign again, once.
330
+ this.reads.keys.drop(this.s.bucket, this.s.key);
331
+ if (this.notFound) {
332
+ throw new errors_js_1.NotFoundError({ statusCode: 404, message: `${this.s.key}: the storage network doesn't have this object`, code: 'object_not_found' });
333
+ }
334
+ this.notFound = true;
335
+ return;
336
+ }
337
+ const resign = err instanceof Expired || (err instanceof errors_js_1.APIError && (err.statusCode === 401 || err.statusCode === 403));
338
+ const passing = err instanceof errors_js_1.ConnectionError || (err instanceof errors_js_1.APIError && (err.statusCode === 429 || err.statusCode >= 500));
339
+ if (!(resign || passing))
340
+ throw err;
341
+ this.passing++;
342
+ if (this.passing > this.reads.client.maxRetries) {
343
+ if (err instanceof Expired)
344
+ throw new errors_js_1.TeepinError(`${this.s.key}: every signature expired before it could be used; check this computer's clock`);
345
+ throw err;
346
+ }
347
+ if (passing)
348
+ await this.reads.client.sleep((0, client_js_1.backoff)(this.passing - 1, err), this.signal);
349
+ }
350
+ }
351
+ exports.Tries = Tries;
352
+ /** A signed GET's body as it arrives, read a piece at a time; each read waits at most idleMs for bytes. */
353
+ class Body {
354
+ ctl;
355
+ unlink;
356
+ idleMs;
357
+ signal;
358
+ reader;
359
+ rest = EMPTY;
360
+ ended;
361
+ closed = false;
362
+ constructor(response, ctl, unlink, idleMs, signal) {
363
+ this.ctl = ctl;
364
+ this.unlink = unlink;
365
+ this.idleMs = idleMs;
366
+ this.signal = signal;
367
+ this.reader = response.body?.getReader();
368
+ this.ended = !this.reader;
369
+ }
370
+ /** Up to max bytes (fewer is fine), empty at the end. A body that breaks off throws ConnectionError. */
371
+ async read(max) {
372
+ while (!this.rest.length) {
373
+ const c = await this.next();
374
+ if (!c)
375
+ return EMPTY;
376
+ this.rest = c;
377
+ }
378
+ const out = this.rest.length > max ? this.rest.subarray(0, max) : this.rest;
379
+ this.rest = this.rest.subarray(out.length);
380
+ return out;
381
+ }
382
+ /** n bytes, or fewer where the body ends. */
383
+ async readExact(n) {
384
+ const first = await this.read(n);
385
+ if (first.length === n || !first.length)
386
+ return first;
387
+ const parts = [first];
388
+ let got = first.length;
389
+ while (got < n) {
390
+ const c = await this.read(n - got);
391
+ if (!c.length)
392
+ break;
393
+ parts.push(c);
394
+ got += c.length;
395
+ }
396
+ return (0, bytes_js_1.concat)(parts);
397
+ }
398
+ async next() {
399
+ if (this.ended || !this.reader)
400
+ return null;
401
+ const ms = this.idleMs;
402
+ const timer = ms > 0 ? setTimeout(() => this.ctl.abort(new errors_js_1.ConnectionError(`the storage network sent nothing for ${ms} ms`)), ms) : undefined;
403
+ try {
404
+ const { done, value } = await this.reader.read();
405
+ if (done) {
406
+ this.ended = true;
407
+ return null;
408
+ }
409
+ return value;
410
+ }
411
+ catch (err) {
412
+ if (this.signal?.aborted)
413
+ throw this.signal.reason;
414
+ const why = this.ctl.signal.reason;
415
+ if (this.ctl.signal.aborted && why instanceof errors_js_1.ConnectionError)
416
+ throw why;
417
+ throw new errors_js_1.ConnectionError(`the read broke off: ${err instanceof Error ? err.message : String(err)}`, err);
418
+ }
419
+ finally {
420
+ clearTimeout(timer);
421
+ }
422
+ }
423
+ /** Ends the request: a body not read to its end is cancelled and its connection dropped. Never waits. */
424
+ close() {
425
+ if (this.closed)
426
+ return;
427
+ this.closed = true;
428
+ this.unlink();
429
+ if (!this.ended) {
430
+ this.ctl.abort(new errors_js_1.TeepinError('the read was closed'));
431
+ this.reader?.cancel().catch(() => undefined);
432
+ }
433
+ }
434
+ }
435
+ exports.Body = Body;
436
+ class Progress {
437
+ total;
438
+ fn;
439
+ done = 0;
440
+ constructor(total, fn) {
441
+ this.total = total;
442
+ this.fn = fn;
443
+ }
444
+ add(n) {
445
+ if (!this.fn || n === 0)
446
+ return;
447
+ this.done += n;
448
+ this.fn(this.done, this.total);
449
+ }
450
+ }
451
+ /**
452
+ * Reads through read sessions, for one client. The methods tests replace (as the Python tests patch _reads.py) are
453
+ * call, session, window, open and plaintext.
454
+ */
455
+ class DirectReads {
456
+ client;
457
+ keys;
458
+ constructor(client) {
459
+ this.client = client;
460
+ this.keys = new KeyCache(client.keyCacheTtl);
461
+ }
462
+ /** Zeroes the cached data keys. */
463
+ close() {
464
+ this.keys.clear();
465
+ }
466
+ // --- sessions ---------------------------------------------------------------
467
+ /** One read session (object/read), retried by the client as a safe call. */
468
+ async call(bucket, key, off, length, includeKey, signal) {
469
+ const json = { key, stored_offset: off, stored_length: length, include_key: includeKey };
470
+ const { data } = await this.client.call({
471
+ method: 'POST',
472
+ path: bucketPath(bucket) + '/object/read',
473
+ json,
474
+ mode: 'safe',
475
+ signal,
476
+ });
477
+ return new Session(bucket, data ?? {});
478
+ }
479
+ /**
480
+ * A read's first session, with the data key: from the cache when it holds this version's, so the server needn't send
481
+ * it again. An object still in the old stage throws stagedError.
482
+ */
483
+ async session(bucket, key, off = 0, length = 0, signal) {
484
+ let cached = this.keys.get(bucket, key);
485
+ let s;
486
+ try {
487
+ s = await this.call(bucket, key, off, length, cached === undefined, signal);
488
+ if (s.needsKey && !s.dek) {
489
+ if (cached && cached.version === s.version) {
490
+ s.dek = cached.key;
491
+ cached = undefined;
492
+ }
493
+ else {
494
+ s = await this.call(bucket, key, off, length, true, signal);
495
+ }
496
+ }
497
+ }
498
+ finally {
499
+ cached?.key.fill(0);
500
+ }
501
+ if (s.staged) {
502
+ s.forget();
503
+ throw stagedError(key);
504
+ }
505
+ if (s.dek)
506
+ this.keys.put(bucket, key, s.version, s.dek);
507
+ return s;
508
+ }
509
+ /**
510
+ * Read sessions for many objects, signed 1,000 to a request (objects/read): a Session, or the error for an object
511
+ * that can't be read (each on its own; stagedError for one still in the old stage). Each Session holds its own key;
512
+ * the caller forgets it.
513
+ */
514
+ async sessions(bucket, keys, signal) {
515
+ const out = new Map();
516
+ const unique = [...new Set(keys)];
517
+ for (let i = 0; i < unique.length; i += READ_BATCH) {
518
+ const { data } = await this.client.call({
519
+ method: 'POST',
520
+ path: bucketPath(bucket) + '/objects/read',
521
+ json: { keys: unique.slice(i, i + READ_BATCH), include_keys: true },
522
+ mode: 'safe',
523
+ signal,
524
+ });
525
+ for (const item of data?.objects ?? []) {
526
+ const k = str(item.key);
527
+ if (item.error) {
528
+ out.set(k, sessionError(k, item.error));
529
+ continue;
530
+ }
531
+ let s;
532
+ try {
533
+ s = new Session(bucket, item);
534
+ }
535
+ catch (err) {
536
+ if (!(err instanceof errors_js_1.DataIntegrityError))
537
+ throw err;
538
+ out.set(k, err);
539
+ continue;
540
+ }
541
+ if (s.staged) {
542
+ s.forget();
543
+ out.set(k, stagedError(k));
544
+ continue;
545
+ }
546
+ if (s.dek)
547
+ this.keys.put(bucket, k, s.version, s.dek);
548
+ out.set(k, s);
549
+ }
550
+ }
551
+ for (const k of unique) {
552
+ if (!out.has(k))
553
+ out.set(k, new errors_js_1.NotFoundError({ statusCode: 404, message: `${k}: missing from the read batch's answer`, code: 'object_not_found' }));
554
+ }
555
+ return out;
556
+ }
557
+ /** A later window of the same version, signed without the key. */
558
+ async window(s0, off, length, signal) {
559
+ const s = await this.call(s0.bucket, s0.key, off, length, false, signal);
560
+ if (s.version !== s0.version) {
561
+ s.forget();
562
+ throw new errors_js_1.ObjectChangedError();
563
+ }
564
+ s.forget(); // never used: the read has its own copy (a server that sent one anyway gets it zeroed)
565
+ return s;
566
+ }
567
+ /** A signed GET, its headers in (the client's timeout), its body to read; Expired for a signature about to expire. */
568
+ async open(r, signal) {
569
+ if (!r.fresh())
570
+ throw new Expired();
571
+ signal?.throwIfAborted();
572
+ const { ctl, unlink } = linked(signal);
573
+ const ms = this.client.timeout;
574
+ const timer = ms > 0 ? setTimeout(() => ctl.abort(new errors_js_1.ConnectionError(`no answer from the storage network in ${ms} ms`)), ms) : undefined;
575
+ try {
576
+ const response = await this.client.sendSigned({ method: 'GET', url: r.url, headers: r.headers, signal: ctl.signal });
577
+ return new Body(response, ctl, unlink, ms, signal);
578
+ }
579
+ catch (err) {
580
+ unlink();
581
+ if (signal?.aborted)
582
+ throw signal.reason;
583
+ throw err;
584
+ }
585
+ finally {
586
+ clearTimeout(timer);
587
+ }
588
+ }
589
+ // --- verified bytes ---------------------------------------------------------
590
+ /**
591
+ * The object's bytes [a, b), each verified before it is yielded; with check (a whole read), the object's SHA-256
592
+ * too, throwing ChecksumMismatchError after the last bytes.
593
+ */
594
+ async *plaintext(s, dek, a, b, check, signal) {
595
+ const checked = check && s.checkable;
596
+ if (!s.requests.length) {
597
+ if (s.size)
598
+ throw new errors_js_1.DataIntegrityError(`${s.key}: the read session signed nothing`, 'session');
599
+ return;
600
+ }
601
+ if (s.packed || s.format === formats_js_1.FORMAT_SERVER) {
602
+ const data = await this.whole(s, dek, signal);
603
+ if (checked) {
604
+ const got = await sha256Hex(data);
605
+ if (got !== s.sha256)
606
+ throw new errors_js_1.ChecksumMismatchError(`${s.key}: received ${got}, stored ${s.sha256}`);
607
+ }
608
+ if (b > a)
609
+ yield data.subarray(a, b);
610
+ return;
611
+ }
612
+ let chunks;
613
+ if (s.format === formats_js_1.FORMAT_TWS1) {
614
+ if (!dek)
615
+ throw new errors_js_1.DataIntegrityError(`${s.key}: the read session carried no key`, 'key');
616
+ chunks = this.tws1Blob(s, dek, a, b, signal);
617
+ }
618
+ else {
619
+ chunks = this.rawBlob(s, a, b, signal);
620
+ }
621
+ const h = checked ? new sha256_js_1.Sha256() : null;
622
+ for await (const c of chunks) {
623
+ h?.update(c);
624
+ yield c;
625
+ }
626
+ if (h) {
627
+ const got = h.hexDigest();
628
+ if (got !== s.sha256)
629
+ throw new errors_js_1.ChecksumMismatchError(`${s.key}: received ${got}, stored ${s.sha256}`);
630
+ }
631
+ }
632
+ /** A packed object, its needle checked and decrypted whole; or a teepin-v1 object in its own blob, read whole and opened. */
633
+ async whole(s, dek, signal) {
634
+ const stored = s.packed ? await this.packed(s, signal) : await collect(this.rawBlob(s, 0, s.storedSize, signal));
635
+ let data;
636
+ if (s.format === formats_js_1.FORMAT_TWS1 || s.format === formats_js_1.FORMAT_SERVER) {
637
+ if (!dek)
638
+ throw new errors_js_1.DataIntegrityError(`${s.key}: the read session carried no key`, 'key');
639
+ data = s.format === formats_js_1.FORMAT_TWS1 ? await tws1.decrypt(dek, stored) : await (0, formats_js_1.openServerV1)(dek, stored, s.ad);
640
+ }
641
+ else {
642
+ data = stored;
643
+ }
644
+ if (data.length !== s.size)
645
+ throw new errors_js_1.DataIntegrityError(`${s.key}: ${data.length} bytes, not the ${s.size} stored`, 'size');
646
+ return data;
647
+ }
648
+ async packed(s0, signal) {
649
+ const tries = new Tries(this, s0, signal);
650
+ let cur = s0;
651
+ for (;;) {
652
+ try {
653
+ const r = cur.window;
654
+ const body = await this.open(r, signal);
655
+ let data;
656
+ try {
657
+ data = await body.readExact(r.length);
658
+ }
659
+ finally {
660
+ body.close();
661
+ }
662
+ if (data.length !== r.length)
663
+ throw new Broken(`${data.length} of ${r.length} bytes`);
664
+ const stored = data.subarray(r.prefix);
665
+ await tws1.checkNeedle(data.subarray(0, r.prefix), tws1.objectIdBytes(s0.version), stored);
666
+ return stored;
667
+ }
668
+ catch (err) {
669
+ await tries.failed(err);
670
+ cur = await this.window(s0, 0, 0, signal);
671
+ }
672
+ }
673
+ }
674
+ async *tws1Blob(s0, dek, a, b, signal) {
675
+ const size = s0.size;
676
+ const n = tws1.segments(size);
677
+ const r = tws1.locate(size, a, b - a);
678
+ let seg = r.firstSegment;
679
+ const last = b <= a ? seg : Math.floor((b - 1) / tws1.SEGMENT_SIZE);
680
+ const [lastOff, lastLen] = tws1.segmentAt(size, last);
681
+ const end = lastOff + lastLen; // the stored end, exclusive
682
+ let skip = r.skip;
683
+ let left = r.length;
684
+ let keys = null;
685
+ let cap = exports.DEFAULT_WINDOW;
686
+ const tries = new Tries(this, s0, signal);
687
+ let start = seg === 0 ? 0 : tws1.segmentAt(size, seg)[0];
688
+ let cur = s0.requests.length && s0.window.first === start ? s0 : null;
689
+ while (seg <= last) {
690
+ start = seg === 0 ? 0 : tws1.segmentAt(size, seg)[0];
691
+ let asked = 0;
692
+ try {
693
+ if (!cur) {
694
+ asked = aligned(start, end, cap);
695
+ cur = await this.window(s0, start, asked, signal);
696
+ }
697
+ const win = cur.window;
698
+ if (win.first !== start)
699
+ throw new errors_js_1.DataIntegrityError(`${s0.key}: a window signed from ${win.first}, not ${start}`, 'window');
700
+ if (asked && win.last - win.first + 1 < asked)
701
+ cap = win.last - win.first + 1; // the server's window
702
+ if (!keys && start > 0)
703
+ keys = await this.header(cur, dek, signal);
704
+ const body = await this.open(win, signal);
705
+ try {
706
+ if (start === 0) {
707
+ const head = await body.readExact(tws1.HEADER_SIZE);
708
+ if (head.length < tws1.HEADER_SIZE)
709
+ throw new Broken('the header broke off');
710
+ if (!keys)
711
+ keys = await tws1.openHeader(dek, head);
712
+ else if (!(0, bytes_js_1.constantTimeEqual)(head, keys.header))
713
+ throw new errors_js_1.DataIntegrityError(`${s0.key}: the header changed`, 'header');
714
+ }
715
+ while (seg <= last) {
716
+ const [soff, slen] = tws1.segmentAt(size, seg);
717
+ if (soff + slen - 1 > win.last)
718
+ break; // the window ends inside this segment: the next one starts with it
719
+ const data = await body.readExact(slen);
720
+ if (data.length < slen)
721
+ throw new Broken(`segment ${seg} broke off`);
722
+ if (!keys)
723
+ throw new errors_js_1.DataIntegrityError(`${s0.key}: no header to open segment ${seg} with`, 'header');
724
+ const plain = await keys.open(seg, seg === n - 1, data);
725
+ seg++;
726
+ tries.progressed();
727
+ const out = skip || left < plain.length ? plain.subarray(skip, skip + left) : plain;
728
+ skip = 0;
729
+ left -= out.length;
730
+ if (out.length)
731
+ yield out;
732
+ }
733
+ }
734
+ finally {
735
+ body.close();
736
+ }
737
+ }
738
+ catch (err) {
739
+ await tries.failed(err);
740
+ }
741
+ cur = null;
742
+ }
743
+ }
744
+ /** The object's keys from the header a later window's session signs first; its commitment is checked. */
745
+ async header(cur, dek, signal) {
746
+ const hr = cur.requests.length > 1 ? cur.requests[0] : null;
747
+ if (!hr || hr.first !== 0)
748
+ throw new errors_js_1.DataIntegrityError(`${cur.key}: the read session didn't sign the header`, 'header');
749
+ const body = await this.open(hr, signal);
750
+ let head;
751
+ try {
752
+ head = await body.readExact(tws1.HEADER_SIZE);
753
+ }
754
+ finally {
755
+ body.close();
756
+ }
757
+ if (head.length < tws1.HEADER_SIZE)
758
+ throw new Broken('the header broke off');
759
+ return tws1.openHeader(dek, head);
760
+ }
761
+ /** Stored bytes [a, b) of an object in its own blob, as stored. */
762
+ async *rawBlob(s0, a, b, signal) {
763
+ let pos = a;
764
+ let cap = exports.DEFAULT_WINDOW;
765
+ const tries = new Tries(this, s0, signal);
766
+ let cur = s0.requests.length && s0.window.first === a ? s0 : null;
767
+ while (pos < b) {
768
+ let asked = 0;
769
+ try {
770
+ if (!cur) {
771
+ asked = Math.min(b - pos, cap);
772
+ cur = await this.window(s0, pos, asked, signal);
773
+ }
774
+ const win = cur.window;
775
+ if (win.first !== pos)
776
+ throw new errors_js_1.DataIntegrityError(`${s0.key}: a window signed from ${win.first}, not ${pos}`, 'window');
777
+ if (asked && win.last - win.first + 1 < asked)
778
+ cap = win.last - win.first + 1;
779
+ const body = await this.open(win, signal);
780
+ try {
781
+ let want = Math.min(b, win.last + 1) - pos;
782
+ while (want > 0) {
783
+ const chunk = await body.read(Math.min(READ_CHUNK, want));
784
+ if (!chunk.length)
785
+ throw new Broken(`${want} bytes short`);
786
+ pos += chunk.length;
787
+ want -= chunk.length;
788
+ tries.progressed();
789
+ yield chunk;
790
+ }
791
+ }
792
+ finally {
793
+ body.close();
794
+ }
795
+ }
796
+ catch (err) {
797
+ await tries.failed(err);
798
+ }
799
+ cur = null;
800
+ }
801
+ }
802
+ // --- whole operations ---------------------------------------------------------
803
+ /** How a download splits: one range per server window (partSize may split finer), or one range for an object read whole. */
804
+ ranges(s, partSize) {
805
+ const size = s.size;
806
+ if (s.packed || !s.requests.length || s.format === formats_js_1.FORMAT_SERVER || s.window.last + 1 >= s.storedSize)
807
+ return [[0, size]];
808
+ let stride = s.window.last - s.window.first + 1;
809
+ if (s.format === formats_js_1.FORMAT_TWS1) {
810
+ const seg = tws1.SEGMENT_SIZE;
811
+ stride = Math.max(1, Math.floor((stride - tws1.HEADER_SIZE) / tws1.STORED_SEGMENT_SIZE)) * seg;
812
+ if (partSize > 0)
813
+ stride = Math.min(stride, Math.max(seg, Math.floor(partSize / seg) * seg));
814
+ }
815
+ else if (partSize > 0) {
816
+ stride = Math.min(stride, partSize);
817
+ }
818
+ const out = [];
819
+ for (let a = 0; a < size; a += stride)
820
+ out.push([a, Math.min(a + stride, size)]);
821
+ return out;
822
+ }
823
+ /**
824
+ * Writes the whole object to the target open makes (once per attempt, given the object's details): windows in
825
+ * parallel, each pinned to the first session's version. A SHA-256 mismatch is tried again once.
826
+ */
827
+ async download(bucket, key, open, p) {
828
+ let prog;
829
+ for (let attempt = 0;; attempt++) {
830
+ const s = await this.session(bucket, key, 0, 0, p.signal);
831
+ const dek = s.dek ? s.dek.slice() : null;
832
+ s.forget();
833
+ const progress = (prog ??= new Progress(s.size, p.onProgress));
834
+ try {
835
+ const target = open(s.object());
836
+ const ranges = this.ranges(s, p.partSize);
837
+ if (ranges.length === 1 || p.concurrency === 1) {
838
+ await this.writeRange(s, dek, 0, s.size, p.verify, target, progress, p.signal);
839
+ return s.object();
840
+ }
841
+ await pool(p.concurrency, ranges, p.signal, (r, signal) => this.writeRange(s, dek, r[0], r[1], false, target, progress, signal));
842
+ if (p.verify && s.checkable && target.sha256) {
843
+ const got = await target.sha256(s.size);
844
+ if (got !== s.sha256)
845
+ throw new errors_js_1.ChecksumMismatchError(`${key}: received ${got}, stored ${s.sha256}`);
846
+ }
847
+ return s.object();
848
+ }
849
+ catch (err) {
850
+ if (!(err instanceof errors_js_1.ChecksumMismatchError) || attempt > 0)
851
+ throw err;
852
+ }
853
+ finally {
854
+ dek?.fill(0);
855
+ }
856
+ }
857
+ }
858
+ async writeRange(s, dek, a, b, check, target, prog, signal) {
859
+ let pos = a;
860
+ for await (const c of this.plaintext(s, dek, a, b, check, signal)) {
861
+ await target.write(pos, c);
862
+ pos += c.length;
863
+ prog.add(c.length);
864
+ }
865
+ }
866
+ /**
867
+ * The first part of getObject: the session, checked, and the range [a, b) to read (start and end inclusive, end
868
+ * undefined for to the end); the bytes come from body.
869
+ */
870
+ async get(bucket, key, start, end, ifMatch, signal) {
871
+ const ranged = start > 0 || end !== undefined;
872
+ // A range signs a little at first (its window comes later); a whole read signs a window.
873
+ const s = await this.session(bucket, key, 0, ranged ? tws1.HEADER_SIZE + tws1.STORED_SEGMENT_SIZE : 0, signal);
874
+ if (ifMatch && s.etag !== unquote(ifMatch)) {
875
+ s.forget();
876
+ throw new errors_js_1.ObjectChangedError();
877
+ }
878
+ const b = end === undefined ? s.size : Math.min(end + 1, s.size);
879
+ if (start < 0 || (ranged && start >= s.size) || start > b) {
880
+ s.forget();
881
+ throw (0, errors_js_1.apiError)({ statusCode: 416, message: `${key}: bytes ${start}- are past the object's ${s.size} bytes`, code: 'invalid_range' });
882
+ }
883
+ const dek = s.dek ? s.dek.slice() : null;
884
+ s.forget();
885
+ return { session: s, dek, a: start, b, ranged };
886
+ }
887
+ /** The verified bytes [a, b); the key copy is zeroed at the end, or when the reader stops early. */
888
+ async *body(s, dek, a, b, check, signal) {
889
+ try {
890
+ yield* this.plaintext(s, dek, a, b, check, signal);
891
+ }
892
+ finally {
893
+ dek?.fill(0);
894
+ }
895
+ }
896
+ /**
897
+ * The first part of openObject: the session, the key copy, the block (one window, or the whole object) and the
898
+ * alignment of a jump.
899
+ */
900
+ async openFile(bucket, key, signal) {
901
+ const s = await this.session(bucket, key, 0, 0, signal);
902
+ const dek = s.dek ? s.dek.slice() : null;
903
+ s.forget();
904
+ return { session: s, dek, ...this.layout(s) };
905
+ }
906
+ /** How an ObjectFile reads s: its block (one window, or the whole object) and the alignment of a jump. */
907
+ layout(s) {
908
+ const block = this.ranges(s, 0)[0][1] || 1;
909
+ const align = s.packed || s.format === formats_js_1.FORMAT_SERVER ? Math.max(1, s.size) : tws1.SEGMENT_SIZE;
910
+ return { block, align };
911
+ }
912
+ /** The verified bytes [a, b), joined; the whole object's SHA-256 is checked when that is what's asked for. */
913
+ fetch(s, dek, a, b, signal) {
914
+ return collect(this.plaintext(s, dek, a, b, a === 0 && b === s.size, signal));
915
+ }
916
+ }
917
+ exports.DirectReads = DirectReads;
918
+ // --- share links ------------------------------------------------------------------
919
+ /** The route that opens a share link: a read session for the token, with no sign-in (section 26.2). */
920
+ exports.LINK_READ_PATH = '/v1/storage/links/read';
921
+ /**
922
+ * Reads through a share link (section 26.3): the same engine and checks as DirectReads, with the link's read sessions
923
+ * (POST /v1/storage/links/read) in place of the authenticated ones. Every session carries the data key, so none is
924
+ * cached (the client's keyCacheTtl is 0), and every session after the first must be of the version the link first
925
+ * opened: ObjectChangedError otherwise. The bucket and key the engine passes around are unused: the token names the
926
+ * object.
927
+ *
928
+ * The first session takes the link's token. Each answer brings a continuation, and every later session takes the
929
+ * newest one instead: it reads only the version first opened (the server answers 412 for a replaced object, which is
930
+ * ObjectChangedError here), lasts 30 minutes from its issue, and never past the link's expiry plus 24 hours, so a
931
+ * download that began while the link was valid can finish after it expires. A continuation left idle too long is
932
+ * refused; the link's token is then tried once more, which opens only while the link itself is valid.
933
+ */
934
+ class LinkReads extends DirectReads {
935
+ token;
936
+ /** The version the link first opened (set by openLink); '' until then. */
937
+ version = '';
938
+ /** From the link's answers: how the link is meant to be offered, and the name to save the object as. */
939
+ disposition = '';
940
+ fileName = '';
941
+ /** The newest continuation, and when it stops working (ms); '' until the first answer. */
942
+ continuation = '';
943
+ continuationEnds = 0;
944
+ constructor(client, token) {
945
+ super(client);
946
+ this.token = token;
947
+ }
948
+ /** One read session for the link, retried by the client as a safe call; the version pinned once it is known. */
949
+ async call(bucket, _key, off, length, _includeKey, signal) {
950
+ const cont = this.continuation;
951
+ let d;
952
+ try {
953
+ d = await this.openWith(cont ? { continuation: cont } : { token: this.token }, off, length, signal);
954
+ }
955
+ catch (err) {
956
+ if (!cont)
957
+ throw err;
958
+ if (err instanceof errors_js_1.APIError && err.statusCode === 412 && err.code === 'precondition_failed')
959
+ throw new errors_js_1.ObjectChangedError();
960
+ if (!(err instanceof errors_js_1.InvalidLinkError))
961
+ throw err;
962
+ d = await this.openWith({ token: this.token }, off, length, signal);
963
+ }
964
+ this.disposition = str(d.disposition) || this.disposition;
965
+ this.fileName = str(d.file_name) || this.fileName;
966
+ this.keep(d.continuation);
967
+ const s = new Session(bucket, d);
968
+ if (this.version && s.version !== this.version) {
969
+ s.forget();
970
+ throw new errors_js_1.ObjectChangedError();
971
+ }
972
+ return s;
973
+ }
974
+ /** POST /v1/storage/links/read with the link's token or a continuation. */
975
+ async openWith(cred, off, length, signal) {
976
+ const { data } = await this.client.call({
977
+ method: 'POST',
978
+ path: exports.LINK_READ_PATH,
979
+ json: { ...cred, stored_offset: off, stored_length: length },
980
+ mode: 'safe',
981
+ signal,
982
+ });
983
+ return data ?? {};
984
+ }
985
+ /** Keeps an answer's continuation when it lasts at least as long as the one held (windows may answer out of order). */
986
+ keep(c) {
987
+ if (!c || typeof c !== 'object')
988
+ return;
989
+ const token = str(c.token);
990
+ const ends = Date.parse(str(c.expires_at));
991
+ if (!token || Number.isNaN(ends) || ends < this.continuationEnds)
992
+ return;
993
+ this.continuation = token;
994
+ this.continuationEnds = ends;
995
+ }
996
+ /** A read's first session, always with its key (nothing is cached); an object still in the old stage throws stagedError. */
997
+ async session(bucket, key, off = 0, length = 0, signal) {
998
+ const s = await this.call(bucket, key, off, length, true, signal);
999
+ if (s.staged) {
1000
+ s.forget();
1001
+ throw stagedError(this.fileName || s.key || 'the linked object');
1002
+ }
1003
+ return s;
1004
+ }
1005
+ }
1006
+ exports.LinkReads = LinkReads;
1007
+ /** Everything chunks yields, joined. */
1008
+ async function collect(chunks) {
1009
+ const parts = [];
1010
+ for await (const c of chunks)
1011
+ parts.push(c);
1012
+ return parts.length === 1 ? parts[0] : (0, bytes_js_1.concat)(parts);
1013
+ }
1014
+ exports.collect = collect;
1015
+ /**
1016
+ * Runs fn for every item, at most n at once. The first failure stops the others (their signal aborts), and every
1017
+ * worker is waited for before it is thrown, so nothing is left running; an abort of signal throws its reason.
1018
+ */
1019
+ async function pool(n, items, signal, fn) {
1020
+ const { ctl, unlink } = linked(signal);
1021
+ let next = 0;
1022
+ let failed = false;
1023
+ let failure;
1024
+ const worker = async () => {
1025
+ while (!failed && next < items.length && !ctl.signal.aborted) {
1026
+ const item = items[next++];
1027
+ try {
1028
+ await fn(item, ctl.signal);
1029
+ }
1030
+ catch (err) {
1031
+ if (!failed) {
1032
+ failed = true;
1033
+ failure = err;
1034
+ }
1035
+ ctl.abort(err);
1036
+ }
1037
+ }
1038
+ };
1039
+ try {
1040
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(n, items.length)) }, worker));
1041
+ }
1042
+ finally {
1043
+ unlink();
1044
+ }
1045
+ if (signal?.aborted)
1046
+ throw signal.reason;
1047
+ if (failed)
1048
+ throw failure;
1049
+ }
1050
+ /**
1051
+ * A ReadableStream of a read's verified chunks, made only as they are asked for. An abort of signal, or a cancel, ends
1052
+ * it at once: the stream errors with the reason, the read's requests stop, and release runs (it zeroes the read's key
1053
+ * copy), as it does when the stream ends.
1054
+ */
1055
+ function readable(chunks, signal, release) {
1056
+ const { ctl, unlink } = linked(signal);
1057
+ let it;
1058
+ let finished = false;
1059
+ const end = () => {
1060
+ if (finished)
1061
+ return;
1062
+ finished = true;
1063
+ unlink();
1064
+ release?.();
1065
+ void it?.return(undefined).catch(() => undefined);
1066
+ };
1067
+ return new ReadableStream({
1068
+ start(c) {
1069
+ const onAbort = () => {
1070
+ try {
1071
+ c.error(ctl.signal.reason);
1072
+ }
1073
+ catch {
1074
+ // already closed
1075
+ }
1076
+ end();
1077
+ };
1078
+ if (ctl.signal.aborted)
1079
+ onAbort();
1080
+ else
1081
+ ctl.signal.addEventListener('abort', onAbort, { once: true });
1082
+ },
1083
+ async pull(c) {
1084
+ it ??= chunks(ctl.signal);
1085
+ try {
1086
+ const r = await it.next();
1087
+ if (r.done) {
1088
+ end();
1089
+ c.close();
1090
+ }
1091
+ else {
1092
+ c.enqueue(r.value);
1093
+ }
1094
+ }
1095
+ catch (err) {
1096
+ end();
1097
+ c.error(ctl.signal.aborted ? ctl.signal.reason : err);
1098
+ }
1099
+ },
1100
+ cancel(reason) {
1101
+ ctl.abort(reason ?? new errors_js_1.TeepinError('the read was cancelled'));
1102
+ end();
1103
+ },
1104
+ }, { highWaterMark: 0 });
1105
+ }
1106
+ exports.readable = readable;
1107
+ /** A stream's bytes, joined; its error, as it is. */
1108
+ async function drain(body) {
1109
+ const reader = body.getReader();
1110
+ const parts = [];
1111
+ try {
1112
+ for (;;) {
1113
+ const { done, value } = await reader.read();
1114
+ if (done)
1115
+ break;
1116
+ parts.push(value);
1117
+ }
1118
+ }
1119
+ finally {
1120
+ reader.releaseLock();
1121
+ }
1122
+ return (0, bytes_js_1.concat)(parts);
1123
+ }
1124
+ exports.drain = drain;
1125
+ const closedError = () => new errors_js_1.TeepinError('the object file is closed');
1126
+ /**
1127
+ * An object open for reading at any offset (TEEPIN-STORAGE-DESIGN.md section 22.5): read(offset, length), size, info
1128
+ * and close().
1129
+ *
1130
+ * Reading straight on (each read starting where the last ended) fetches a block at a time (one window, 64 MiB on the
1131
+ * storage network) and the next prefetch blocks ahead, each signed as it is fetched; a read somewhere else fetches
1132
+ * only what it needs, in whole segments. Up to prefetch + 2 blocks are held. Every byte is verified before it is read,
1133
+ * and every block is read from the version first opened: ObjectChangedError if the object is replaced. Close it when
1134
+ * done: that stops its fetches and zeroes its copy of the data key.
1135
+ */
1136
+ class ObjectFile {
1137
+ /** The object's size in bytes. */
1138
+ size;
1139
+ /** The object's details, as headObject gives them. */
1140
+ info;
1141
+ fetch;
1142
+ block;
1143
+ align;
1144
+ prefetch;
1145
+ onClose;
1146
+ signal;
1147
+ ctl;
1148
+ unlink;
1149
+ blocks = new Map(); // oldest first
1150
+ end = 0; // where the last read ended: reading on from here is reading straight on
1151
+ isClosed = false;
1152
+ constructor(fetch, size, block, align, info, prefetch = 2, onClose, signal) {
1153
+ this.fetch = fetch;
1154
+ this.size = size;
1155
+ this.block = Math.max(1, block);
1156
+ this.align = Math.max(1, align);
1157
+ this.prefetch = Math.max(0, Math.floor(prefetch) || 0);
1158
+ this.info = info;
1159
+ this.onClose = onClose;
1160
+ this.signal = signal;
1161
+ const { ctl, unlink } = linked(signal);
1162
+ this.ctl = ctl;
1163
+ this.unlink = unlink;
1164
+ }
1165
+ get closed() {
1166
+ return this.isClosed;
1167
+ }
1168
+ /** Up to length bytes from offset: fewer only at the object's end, none past it. */
1169
+ async read(offset, length) {
1170
+ this.checkOpen();
1171
+ if (!(0, bytes_js_1.isInt)(offset) || !(0, bytes_js_1.isInt)(length) || offset < 0 || length < 0) {
1172
+ throw new errors_js_1.TeepinError(`read(${offset}, ${length}): the offset and length must be whole numbers, 0 or more`);
1173
+ }
1174
+ const n = Math.min(length, Math.max(0, this.size - offset));
1175
+ if (n === 0)
1176
+ return new Uint8Array(0);
1177
+ try {
1178
+ const straight = offset === this.end;
1179
+ // A jump fetches only what it reads, unless the object is read whole anyway (packed): then its one block is
1180
+ // cached like any other.
1181
+ const whole = this.align >= this.block;
1182
+ let data;
1183
+ if (!straight && !whole && n < this.block / 2 && !this.blocks.has(Math.floor(offset / this.block))) {
1184
+ data = await this.some(offset, n);
1185
+ }
1186
+ else {
1187
+ const out = [];
1188
+ let pos = offset;
1189
+ let left = n;
1190
+ while (left > 0) {
1191
+ const i = Math.floor(pos / this.block);
1192
+ const blk = await this.get(i);
1193
+ const off = pos - i * this.block;
1194
+ const piece = blk.subarray(off, off + left);
1195
+ if (!piece.length)
1196
+ throw new errors_js_1.DataIntegrityError('the object is shorter than its size', 'truncated');
1197
+ out.push(piece);
1198
+ pos += piece.length;
1199
+ left -= piece.length;
1200
+ }
1201
+ this.ahead(Math.floor(pos / this.block));
1202
+ data = out.length === 1 ? out[0].slice() : (0, bytes_js_1.concat)(out);
1203
+ }
1204
+ this.checkOpen();
1205
+ this.end = offset + data.length;
1206
+ return data;
1207
+ }
1208
+ catch (err) {
1209
+ if (this.signal?.aborted)
1210
+ throw this.signal.reason;
1211
+ if (this.isClosed)
1212
+ throw closedError();
1213
+ throw err;
1214
+ }
1215
+ }
1216
+ /** Stops every fetch and zeroes this file's copy of the data key. Reads after it fail. */
1217
+ close() {
1218
+ if (this.isClosed)
1219
+ return;
1220
+ this.isClosed = true;
1221
+ this.ctl.abort(closedError());
1222
+ this.blocks.clear();
1223
+ this.unlink();
1224
+ this.onClose?.();
1225
+ }
1226
+ checkOpen() {
1227
+ if (this.signal?.aborted)
1228
+ throw this.signal.reason;
1229
+ if (this.isClosed)
1230
+ throw closedError();
1231
+ }
1232
+ /** Block i's fetch, started if it isn't, and made the newest; the oldest past the bound are dropped. */
1233
+ submit(i) {
1234
+ let e = this.blocks.get(i);
1235
+ if (e) {
1236
+ this.blocks.delete(i);
1237
+ }
1238
+ else {
1239
+ const a = i * this.block;
1240
+ const b = Math.min(a + this.block, this.size);
1241
+ const { ctl, unlink } = linked(this.ctl.signal);
1242
+ const promise = (async () => {
1243
+ try {
1244
+ return await this.fetch(a, b, ctl.signal);
1245
+ }
1246
+ finally {
1247
+ unlink();
1248
+ }
1249
+ })();
1250
+ const entry = { promise, ctl, readers: 0 };
1251
+ // A failed block is fetched again by the next read that needs it (and a prefetch nobody waits for fails quietly).
1252
+ promise.catch(() => {
1253
+ if (this.blocks.get(i) === entry)
1254
+ this.blocks.delete(i);
1255
+ });
1256
+ e = entry;
1257
+ }
1258
+ this.blocks.set(i, e);
1259
+ while (this.blocks.size > this.prefetch + 2) {
1260
+ const [oldest, old] = this.blocks.entries().next().value;
1261
+ this.blocks.delete(oldest);
1262
+ if (old.readers === 0)
1263
+ old.ctl.abort(new errors_js_1.TeepinError('dropped from the read-ahead'));
1264
+ }
1265
+ return e;
1266
+ }
1267
+ async get(i) {
1268
+ const e = this.submit(i);
1269
+ e.readers++;
1270
+ try {
1271
+ return await e.promise;
1272
+ }
1273
+ finally {
1274
+ e.readers--;
1275
+ }
1276
+ }
1277
+ ahead(i) {
1278
+ const last = Math.floor((this.size - 1) / this.block);
1279
+ for (let j = i; j <= Math.min(i + this.prefetch, last); j++)
1280
+ if (j * this.block < this.size)
1281
+ this.submit(j);
1282
+ }
1283
+ /** n bytes from pos, fetched alone, whole segments only. */
1284
+ async some(pos, n) {
1285
+ const a = Math.floor(pos / this.align) * this.align;
1286
+ const b = Math.min(this.size, Math.ceil((pos + n) / this.align) * this.align);
1287
+ const data = await this.fetch(a, b, this.ctl.signal);
1288
+ return data.slice(pos - a, pos - a + n);
1289
+ }
1290
+ }
1291
+ exports.ObjectFile = ObjectFile;
1292
+ /** The error a batch read gives one object, as the single route would throw it. */
1293
+ function sessionError(key, err) {
1294
+ const code = str(err.code);
1295
+ const message = `${key}: ${str(err.error) || code.replaceAll('_', ' ')}`;
1296
+ if (code === 'key_erased')
1297
+ return new errors_js_1.KeyErasedError({ statusCode: 410, message, code });
1298
+ if (code === 'object_not_found')
1299
+ return new errors_js_1.NotFoundError({ statusCode: 404, message, code });
1300
+ return new errors_js_1.APIError({ statusCode: 500, message, code });
1301
+ }
1302
+ /** Stored bytes to ask for from start: whole segments within cap (the header too from 0), at most to end. */
1303
+ function aligned(start, end, cap) {
1304
+ let want;
1305
+ if (start === 0) {
1306
+ const n = Math.max(1, Math.floor((cap - tws1.HEADER_SIZE) / tws1.STORED_SEGMENT_SIZE));
1307
+ want = tws1.HEADER_SIZE + n * tws1.STORED_SEGMENT_SIZE;
1308
+ }
1309
+ else {
1310
+ want = Math.max(1, Math.floor(cap / tws1.STORED_SEGMENT_SIZE)) * tws1.STORED_SEGMENT_SIZE;
1311
+ }
1312
+ return Math.min(end - start, want);
1313
+ }
1314
+ //# sourceMappingURL=reads.js.map