@decdn/client 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1409 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DecdnClient: () => DecdnClient,
24
+ DecdnError: () => DecdnError,
25
+ DecdnResponse: () => DecdnResponse,
26
+ LoopbackApi: () => LoopbackApi,
27
+ MAX_MANIFEST_BYTES: () => MAX_MANIFEST_BYTES,
28
+ PointerApi: () => PointerApi,
29
+ SCHEME: () => SCHEME,
30
+ base32ToHash: () => base32ToHash,
31
+ blake3Hex: () => blake3Hex,
32
+ createBlake3: () => createBlake3,
33
+ formatUri: () => formatUri,
34
+ hashToBase32: () => hashToBase32,
35
+ parseCapability: () => parseCapability,
36
+ parseManifest: () => parseManifest,
37
+ parseUri: () => parseUri,
38
+ pointerSigningHash: () => pointerSigningHash,
39
+ pointerTypedData: () => pointerTypedData,
40
+ recoverPointerSigner: () => recoverPointerSigner,
41
+ signPointer: () => signPointer
42
+ });
43
+ module.exports = __toCommonJS(src_exports);
44
+
45
+ // src/errors.ts
46
+ var DecdnError = class extends Error {
47
+ name = "DecdnError";
48
+ code;
49
+ status;
50
+ retryAfterSecs;
51
+ fallbackUri;
52
+ size;
53
+ seq;
54
+ body;
55
+ constructor(code, message, details = {}) {
56
+ super(
57
+ message,
58
+ details.cause === void 0 ? void 0 : { cause: details.cause }
59
+ );
60
+ this.code = code;
61
+ if (details.status !== void 0) this.status = details.status;
62
+ if (details.retryAfterSecs !== void 0)
63
+ this.retryAfterSecs = details.retryAfterSecs;
64
+ if (details.fallbackUri !== void 0)
65
+ this.fallbackUri = details.fallbackUri;
66
+ if (details.size !== void 0) this.size = details.size;
67
+ if (details.seq !== void 0) this.seq = details.seq;
68
+ if (details.body !== void 0) this.body = details.body;
69
+ }
70
+ };
71
+
72
+ // src/hex.ts
73
+ function toHex(bytes) {
74
+ let out = "";
75
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
76
+ return out;
77
+ }
78
+ function fromHex(hex) {
79
+ if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) return void 0;
80
+ const out = new Uint8Array(hex.length / 2);
81
+ for (let i = 0; i < out.length; i++)
82
+ out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
83
+ return out;
84
+ }
85
+ function isHash(s) {
86
+ return /^[0-9a-fA-F]{64}$/.test(s);
87
+ }
88
+
89
+ // src/uri.ts
90
+ var SCHEME = "decdn:";
91
+ var U64_MAX = (1n << 64n) - 1n;
92
+ function bad(message) {
93
+ return new DecdnError("BAD_URI", message);
94
+ }
95
+ function isPchar(b) {
96
+ return b >= 48 && b <= 57 || b >= 65 && b <= 90 || b >= 97 && b <= 122 || "-._~!$&'()*+,;=:@".includes(String.fromCharCode(b));
97
+ }
98
+ function hexValue(c) {
99
+ return c !== void 0 && /^[0-9a-fA-F]$/.test(c) ? parseInt(c, 16) : void 0;
100
+ }
101
+ var utf8 = new TextDecoder("utf-8", { fatal: true });
102
+ function decodeSegment(segment) {
103
+ const out = [];
104
+ for (let i = 0; i < segment.length; i++) {
105
+ const c = segment.charCodeAt(i);
106
+ if (c === 37) {
107
+ const hi = hexValue(segment[i + 1]);
108
+ const lo = hexValue(segment[i + 2]);
109
+ if (hi === void 0 || lo === void 0) {
110
+ throw bad("invalid bundle entry path in decdn URI: bad `%` escape");
111
+ }
112
+ out.push(hi << 4 | lo);
113
+ i += 2;
114
+ } else if (c < 128 && isPchar(c)) {
115
+ out.push(c);
116
+ } else {
117
+ throw bad(
118
+ `invalid bundle entry path in decdn URI: character ${JSON.stringify(segment[i])} must be percent-encoded`
119
+ );
120
+ }
121
+ }
122
+ try {
123
+ return utf8.decode(new Uint8Array(out));
124
+ } catch {
125
+ throw bad("invalid bundle entry path in decdn URI: not UTF-8 once decoded");
126
+ }
127
+ }
128
+ function decodePath(raw) {
129
+ const segments = raw.split("/").map((segment) => {
130
+ const decoded = decodeSegment(segment);
131
+ let why;
132
+ if (decoded === "")
133
+ why = "empty segment (a leading, trailing or doubled `/` inside the path)";
134
+ else if (decoded === "." || decoded === "..")
135
+ why = `\`${decoded}\` segment`;
136
+ else if (decoded.includes("/")) why = "encoded `/` (`%2F`) in a segment";
137
+ else if (decoded.includes("\0")) why = "NUL byte";
138
+ if (why !== void 0)
139
+ throw bad(`invalid bundle entry path in decdn URI: ${why}`);
140
+ return decoded;
141
+ });
142
+ return segments.join("/");
143
+ }
144
+ function badNamespace(why) {
145
+ return bad(
146
+ `invalid namespace in decdn URI (${why}): expected \`ns=<id>\` with a decimal id of at least 1; omit \`ns\` for no namespace`
147
+ );
148
+ }
149
+ function badQuery(why) {
150
+ return bad(`invalid query in decdn URI (${why}): the only key is \`ns\``);
151
+ }
152
+ function parseQuery(query) {
153
+ let namespace;
154
+ for (const pair of query.split("&")) {
155
+ const eq = pair.indexOf("=");
156
+ if (eq < 0) throw badQuery(`\`${pair}\` is not \`key=value\``);
157
+ const key = pair.slice(0, eq);
158
+ const value = pair.slice(eq + 1);
159
+ if (key !== "ns") throw badQuery(`unknown key \`${key}\``);
160
+ if (namespace !== void 0) throw badQuery("`ns` given twice");
161
+ if (!/^[0-9]+$/.test(value))
162
+ throw badNamespace(`\`${value}\` is not a decimal id`);
163
+ const id = BigInt(value);
164
+ if (id > U64_MAX) throw badNamespace(`\`${value}\` is out of range`);
165
+ if (id === 0n) throw badNamespace("0 means no namespace");
166
+ namespace = id;
167
+ }
168
+ if (namespace === void 0) throw badQuery("empty query");
169
+ return namespace;
170
+ }
171
+ function parseUri(s) {
172
+ if (!s.startsWith(SCHEME))
173
+ throw bad("not a decdn URI: expected `decdn:<hash>`");
174
+ const rest = s.slice(SCHEME.length);
175
+ if (rest.startsWith("//")) {
176
+ throw bad(
177
+ "a decdn URI has no `//`: write `decdn:<hash>`, not `decdn://<hash>`"
178
+ );
179
+ }
180
+ const q = rest.indexOf("?");
181
+ const body = q < 0 ? rest : rest.slice(0, q);
182
+ const query = q < 0 ? void 0 : rest.slice(q + 1);
183
+ const slash = body.indexOf("/");
184
+ const hash = slash < 0 ? body : body.slice(0, slash);
185
+ const path = slash < 0 ? void 0 : body.slice(slash + 1);
186
+ if (!isHash(hash)) {
187
+ const why = hash.length !== 64 ? `${hash.length} characters` : "not hex";
188
+ throw bad(
189
+ `invalid hash in decdn URI (${why}): expected 64 hex characters with no \`b3:\` or \`0x\` prefix`
190
+ );
191
+ }
192
+ const uri = { hash: hash.toLowerCase(), kind: "blob" };
193
+ if (path === "") uri.kind = "bundle";
194
+ else if (path !== void 0) {
195
+ uri.kind = "entry";
196
+ uri.path = decodePath(path);
197
+ }
198
+ if (query !== void 0) uri.namespace = parseQuery(query);
199
+ return uri;
200
+ }
201
+ function encodeSegment(segment) {
202
+ let out = "";
203
+ for (const b of new TextEncoder().encode(segment)) {
204
+ out += isPchar(b) ? String.fromCharCode(b) : `%${b.toString(16).toUpperCase().padStart(2, "0")}`;
205
+ }
206
+ return out;
207
+ }
208
+ function formatUri(uri) {
209
+ let out = SCHEME + uri.hash.toLowerCase();
210
+ if (uri.kind === "bundle") out += "/";
211
+ else if (uri.kind === "entry") {
212
+ for (const segment of (uri.path ?? "").split("/"))
213
+ out += `/${encodeSegment(segment)}`;
214
+ }
215
+ if (uri.namespace !== void 0) out += `?ns=${uri.namespace}`;
216
+ return out;
217
+ }
218
+
219
+ // src/base32.ts
220
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
221
+ var LABEL_LEN = 52;
222
+ function hashToBase32(hash) {
223
+ const bytes = isHash(hash) ? fromHex(hash) : void 0;
224
+ if (bytes === void 0)
225
+ throw new DecdnError("BAD_URI", `not a 64-hex-character hash: ${hash}`);
226
+ let out = "";
227
+ let buffer = 0;
228
+ let bits = 0;
229
+ for (const b of bytes) {
230
+ buffer = buffer << 8 | b;
231
+ bits += 8;
232
+ while (bits >= 5) {
233
+ bits -= 5;
234
+ out += ALPHABET[buffer >> bits & 31];
235
+ }
236
+ }
237
+ if (bits > 0) out += ALPHABET[buffer << 5 - bits & 31];
238
+ return out;
239
+ }
240
+ function base32ToHash(label) {
241
+ if (label.length !== LABEL_LEN || !/^[a-z2-7]+$/.test(label))
242
+ return void 0;
243
+ const out = new Uint8Array(32);
244
+ let buffer = 0;
245
+ let bits = 0;
246
+ let n = 0;
247
+ for (const c of label) {
248
+ buffer = (buffer << 5 | ALPHABET.indexOf(c)) & 4095;
249
+ bits += 5;
250
+ if (bits >= 8) {
251
+ bits -= 8;
252
+ out[n++] = buffer >> bits & 255;
253
+ }
254
+ }
255
+ if ((buffer & (1 << bits) - 1) !== 0) return void 0;
256
+ return toHex(out);
257
+ }
258
+
259
+ // src/hash.ts
260
+ var import_blake3 = require("@noble/hashes/blake3.js");
261
+ function blake3Hex(bytes) {
262
+ return toHex((0, import_blake3.blake3)(bytes));
263
+ }
264
+ function createBlake3() {
265
+ const state = import_blake3.blake3.create();
266
+ return {
267
+ update(chunk) {
268
+ state.update(chunk);
269
+ },
270
+ hex() {
271
+ return toHex(state.digest());
272
+ }
273
+ };
274
+ }
275
+
276
+ // src/manifest.ts
277
+ var MAX_MANIFEST_BYTES = 64 * 1024 * 1024;
278
+ function malformed(message) {
279
+ return new DecdnError("MALFORMED_MANIFEST", message);
280
+ }
281
+ function isRecord(v) {
282
+ return typeof v === "object" && v !== null && !Array.isArray(v);
283
+ }
284
+ function isSize(v) {
285
+ return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
286
+ }
287
+ function parseHash(s) {
288
+ if (typeof s !== "string") return void 0;
289
+ const hex = s.startsWith("0x") ? s.slice(2) : s.startsWith("b3:") ? s.slice(3) : s;
290
+ return isHash(hex) ? hex.toLowerCase() : void 0;
291
+ }
292
+ function parseEntry(raw, i) {
293
+ if (!isRecord(raw) || typeof raw.path !== "string") {
294
+ throw malformed(`bundle entry ${i} has no string \`path\``);
295
+ }
296
+ const path = raw.path;
297
+ const named = JSON.stringify(path);
298
+ const hash = parseHash(raw.hash);
299
+ if (hash === void 0) {
300
+ throw malformed(
301
+ `bundle entry ${named} has a malformed hash: ${JSON.stringify(raw.hash)}`
302
+ );
303
+ }
304
+ const entry = { path, hash };
305
+ if (raw.size !== void 0 && raw.size !== null) {
306
+ if (!isSize(raw.size))
307
+ throw malformed(`bundle entry ${named} has a malformed size`);
308
+ entry.size = raw.size;
309
+ }
310
+ if (raw.chunks !== void 0 && raw.chunks !== null) {
311
+ if (!Array.isArray(raw.chunks))
312
+ throw malformed(`bundle entry ${named} has malformed chunks`);
313
+ entry.chunks = raw.chunks.map((c, n) => {
314
+ const chunkHash = isRecord(c) ? parseHash(c.hash) : void 0;
315
+ if (!isRecord(c) || chunkHash === void 0) {
316
+ throw malformed(
317
+ `bundle entry ${named} has a malformed chunk ${n} hash`
318
+ );
319
+ }
320
+ if (!isSize(c.size))
321
+ throw malformed(
322
+ `bundle entry ${named} has a malformed chunk ${n} size`
323
+ );
324
+ return { hash: chunkHash, size: c.size };
325
+ });
326
+ }
327
+ return entry;
328
+ }
329
+ function parseManifest(bytes, expectedHash) {
330
+ const hash = blake3Hex(bytes);
331
+ if (expectedHash !== void 0 && hash !== expectedHash.toLowerCase()) {
332
+ throw new DecdnError(
333
+ "HASH_MISMATCH",
334
+ `bundle manifest hashes to ${hash}, not ${expectedHash.toLowerCase()}`
335
+ );
336
+ }
337
+ let json;
338
+ try {
339
+ json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
340
+ } catch (e) {
341
+ throw new DecdnError(
342
+ "MALFORMED_MANIFEST",
343
+ "parse bundle JSON (expected v1 manifest)",
344
+ {
345
+ cause: e
346
+ }
347
+ );
348
+ }
349
+ if (!isRecord(json) || typeof json.version !== "number" || !Array.isArray(json.entries)) {
350
+ throw malformed(
351
+ "parse bundle JSON (expected v1 manifest): missing `version` or `entries`"
352
+ );
353
+ }
354
+ if (json.version !== 1) {
355
+ throw malformed(
356
+ `unsupported bundle version ${json.version} (this SDK supports v1)`
357
+ );
358
+ }
359
+ return { hash, entries: json.entries.map(parseEntry) };
360
+ }
361
+
362
+ // src/capability.ts
363
+ var PREFIX = "dcap1:";
364
+ var B64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
365
+ function bad2(message) {
366
+ return new DecdnError("BAD_CAPABILITY", message);
367
+ }
368
+ function fromBase64Url(body) {
369
+ const out = [];
370
+ let buffer = 0;
371
+ let bits = 0;
372
+ for (const c of body) {
373
+ const v = B64URL.indexOf(c);
374
+ if (v < 0) throw bad2("capability token body is not valid base64url");
375
+ buffer = (buffer << 6 | v) & 65535;
376
+ bits += 6;
377
+ if (bits >= 8) {
378
+ bits -= 8;
379
+ out.push(buffer >> bits & 255);
380
+ }
381
+ }
382
+ if (body.length % 4 === 1 || (buffer & (1 << bits) - 1) !== 0) {
383
+ throw bad2("capability token body is not valid base64url");
384
+ }
385
+ return new Uint8Array(out);
386
+ }
387
+ var Reader = class {
388
+ constructor(bytes) {
389
+ this.bytes = bytes;
390
+ }
391
+ bytes;
392
+ at = 0;
393
+ varint() {
394
+ let value = 0n;
395
+ for (let shift = 0n; shift < 70n; shift += 7n) {
396
+ const b = this.bytes[this.at++];
397
+ if (b === void 0) throw truncated();
398
+ value |= BigInt(b & 127) << shift;
399
+ if ((b & 128) === 0) {
400
+ if (value >= 1n << 64n) throw truncated();
401
+ return value;
402
+ }
403
+ }
404
+ throw truncated();
405
+ }
406
+ bytesOf(len) {
407
+ const n = Number(this.varint());
408
+ if (len !== void 0 && n !== len) throw truncated();
409
+ if (this.at + n > this.bytes.length) throw truncated();
410
+ const out = this.bytes.subarray(this.at, this.at + n);
411
+ this.at += n;
412
+ return out;
413
+ }
414
+ done() {
415
+ return this.at === this.bytes.length;
416
+ }
417
+ };
418
+ function truncated() {
419
+ return bad2("capability token payload is malformed (truncated or corrupt)");
420
+ }
421
+ function parseCapability(token) {
422
+ if (!token.startsWith(PREFIX)) {
423
+ throw bad2("not a dcap1 capability token (missing the `dcap1:` prefix)");
424
+ }
425
+ const r = new Reader(fromBase64Url(token.slice(PREFIX.length)));
426
+ const capability = {
427
+ poolId: `0x${toHex(r.bytesOf(32))}`,
428
+ signer: `0x${toHex(r.bytesOf(20))}`,
429
+ spendingCap: r.varint(),
430
+ expiry: r.varint(),
431
+ ownerSignature: `0x${toHex(r.bytesOf())}`
432
+ };
433
+ if (!r.done()) throw truncated();
434
+ return capability;
435
+ }
436
+
437
+ // src/http.ts
438
+ var CODES = {
439
+ 400: "BAD_URI",
440
+ 401: "UNAUTHORIZED",
441
+ 402: "PAYMENT_REQUIRED",
442
+ 403: "FORBIDDEN",
443
+ 404: "NOT_FOUND",
444
+ 409: "CONFLICT",
445
+ 413: "PAYLOAD_TOO_LARGE",
446
+ 416: "RANGE_NOT_SATISFIABLE",
447
+ 422: "UNPROCESSABLE",
448
+ 429: "RATE_LIMITED",
449
+ 502: "UPSTREAM",
450
+ 503: "POOL_UNFUNDED"
451
+ };
452
+ async function send(fetch, url, init) {
453
+ try {
454
+ return await fetch(url, init);
455
+ } catch (e) {
456
+ if (init.signal?.aborted) throw init.signal.reason ?? e;
457
+ throw new DecdnError(
458
+ "NETWORK",
459
+ `cannot reach ${new URL(url).origin}: ${e.message}`,
460
+ {
461
+ cause: e
462
+ }
463
+ );
464
+ }
465
+ }
466
+ function linkUri(link) {
467
+ if (link === null) return void 0;
468
+ const m = /<(decdn:[^>]+)>\s*;\s*rel="?alternate"?/.exec(link);
469
+ return m?.[1];
470
+ }
471
+ async function errorOf(response) {
472
+ const status = response.status;
473
+ const code = CODES[status] ?? "HTTP";
474
+ let body = "";
475
+ try {
476
+ body = await response.text();
477
+ } catch {
478
+ }
479
+ const details = { status };
480
+ let message = body.trim();
481
+ if (message !== "") details.body = message;
482
+ const retryAfter = response.headers.get("retry-after");
483
+ if (retryAfter !== null && /^\d+$/.test(retryAfter.trim()))
484
+ details.retryAfterSecs = Number(retryAfter.trim());
485
+ const link = linkUri(response.headers.get("link"));
486
+ if (link !== void 0) details.fallbackUri = link;
487
+ if ((response.headers.get("content-type") ?? "").startsWith("application/json")) {
488
+ try {
489
+ const parsed = JSON.parse(body);
490
+ if (typeof parsed === "object" && parsed !== null) {
491
+ const o = parsed;
492
+ if (typeof o.error === "string") message = o.error;
493
+ if (typeof o.uri === "string") details.fallbackUri = o.uri;
494
+ if (typeof o.retry_after_secs === "number")
495
+ details.retryAfterSecs = o.retry_after_secs;
496
+ }
497
+ const seq = /"seq"\s*:\s*(\d+)/.exec(body)?.[1];
498
+ if (seq !== void 0) details.seq = BigInt(seq);
499
+ } catch {
500
+ }
501
+ }
502
+ if (status === 416) {
503
+ const size = /^bytes \*\/(\d+)$/.exec(
504
+ response.headers.get("content-range") ?? ""
505
+ )?.[1];
506
+ if (size !== void 0) details.size = Number(size);
507
+ }
508
+ return new DecdnError(
509
+ code,
510
+ message === "" ? `gateway answered ${status}` : message,
511
+ details
512
+ );
513
+ }
514
+ function parseWithBigInts(text, fields) {
515
+ const names = fields.join("|");
516
+ const quoted = text.replace(
517
+ new RegExp(`("(?:${names})"\\s*:\\s*)(\\d+)`, "g"),
518
+ '$1"$2"'
519
+ );
520
+ return JSON.parse(
521
+ quoted,
522
+ (key, value) => fields.includes(key) && typeof value === "string" && /^\d+$/.test(value) ? BigInt(value) : value
523
+ );
524
+ }
525
+
526
+ // src/loopback.ts
527
+ var LoopbackApi = class {
528
+ /** @internal */
529
+ constructor(client) {
530
+ this.client = client;
531
+ }
532
+ client;
533
+ async call(method, path, body) {
534
+ if (this.client.context !== "loopback") {
535
+ throw new DecdnError(
536
+ "CONFIG",
537
+ "the delegate and capability API is a loopback gateway's alone"
538
+ );
539
+ }
540
+ const init = {
541
+ method,
542
+ headers: { ...this.client.authHeaders() }
543
+ };
544
+ if (body !== void 0) {
545
+ init.body = JSON.stringify(body);
546
+ init.headers["content-type"] = "application/json";
547
+ }
548
+ const response = await send(
549
+ this.client.fetch,
550
+ `${this.client.origin}${path}`,
551
+ init
552
+ );
553
+ if (response.status < 200 || response.status > 299)
554
+ throw await errorOf(response);
555
+ return response.text();
556
+ }
557
+ /** A fresh delegate key the gateway holds for 15 minutes unless a capability claims it. */
558
+ async delegate() {
559
+ const parsed = JSON.parse(await this.call("GET", "/_delegate"));
560
+ return { address: parsed.address };
561
+ }
562
+ /** Register `capability` (a `dcap1:` issued to a delegate) for `scope`: `example.com`, `*.example.com` or `*`. */
563
+ async registerCapability(args) {
564
+ const text = await this.call("PUT", "/_capability", {
565
+ capability: args.capability,
566
+ scope: args.scope
567
+ });
568
+ return parseWithBigInts(text, ["expiry"]);
569
+ }
570
+ /** The capabilities the gateway holds. Needs the token. */
571
+ async capabilities() {
572
+ const text = await this.call("GET", "/_capability");
573
+ return parseWithBigInts(text, ["cap", "expiry"]);
574
+ }
575
+ };
576
+
577
+ // src/mime.ts
578
+ var REWRITTEN = /\.(html?|hxt|shtml|css)$/i;
579
+ function rewritten(path) {
580
+ return REWRITTEN.test(path);
581
+ }
582
+ var TYPES = {
583
+ html: "text/html",
584
+ htm: "text/html",
585
+ css: "text/css",
586
+ js: "text/javascript",
587
+ mjs: "text/javascript",
588
+ json: "application/json",
589
+ txt: "text/plain",
590
+ md: "text/markdown",
591
+ csv: "text/csv",
592
+ xml: "text/xml",
593
+ svg: "image/svg+xml",
594
+ png: "image/png",
595
+ jpg: "image/jpeg",
596
+ jpeg: "image/jpeg",
597
+ gif: "image/gif",
598
+ webp: "image/webp",
599
+ avif: "image/avif",
600
+ ico: "image/x-icon",
601
+ wasm: "application/wasm",
602
+ pdf: "application/pdf",
603
+ zip: "application/zip",
604
+ gz: "application/gzip",
605
+ tar: "application/x-tar",
606
+ mp4: "video/mp4",
607
+ webm: "video/webm",
608
+ mp3: "audio/mpeg",
609
+ ogg: "audio/ogg",
610
+ woff: "font/woff",
611
+ woff2: "font/woff2"
612
+ };
613
+ function siteType(path) {
614
+ const ext = /\.([^./]+)$/.exec(path)?.[1]?.toLowerCase() ?? "";
615
+ const t = TYPES[ext] ?? "application/octet-stream";
616
+ return t.startsWith("text/") ? `${t}; charset=utf-8` : t;
617
+ }
618
+
619
+ // src/pointer.ts
620
+ var import_secp256k1 = require("@noble/curves/secp256k1.js");
621
+ var import_sha3 = require("@noble/hashes/sha3.js");
622
+ var N = import_secp256k1.secp256k1.Point.CURVE().n;
623
+ var U64_MAX2 = (1n << 64n) - 1n;
624
+ var U256_MAX = (1n << 256n) - 1n;
625
+ var enc = new TextEncoder();
626
+ function integer(value, what) {
627
+ if (typeof value === "number" && !Number.isSafeInteger(value))
628
+ throw bad3(`${what} is an integer below 2^53, or a bigint: got ${value}`);
629
+ return BigInt(value);
630
+ }
631
+ function chainIdOf(domain) {
632
+ const id = integer(domain.chainId, "a chain id");
633
+ if (id < 1n || id > U256_MAX)
634
+ throw bad3("a chain id is a uint256 of at least 1");
635
+ return id;
636
+ }
637
+ function word(n) {
638
+ const bytes = n >= 0n && n <= U256_MAX ? fromHex(n.toString(16).padStart(64, "0")) : void 0;
639
+ if (bytes === void 0) throw bad3(`${n} does not fit a uint256`);
640
+ return bytes;
641
+ }
642
+ function concat(...parts) {
643
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
644
+ let at = 0;
645
+ for (const p of parts) {
646
+ out.set(p, at);
647
+ at += p.length;
648
+ }
649
+ return out;
650
+ }
651
+ var DOMAIN_TYPE = (0, import_sha3.keccak_256)(
652
+ enc.encode(
653
+ "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
654
+ )
655
+ );
656
+ var POINTER_TYPE = (0, import_sha3.keccak_256)(
657
+ enc.encode("Pointer(uint256 namespaceId,bytes32 root,uint64 seq)")
658
+ );
659
+ function bad3(message) {
660
+ return new DecdnError("CONFIG", message);
661
+ }
662
+ function address(s) {
663
+ const bytes = /^0x[0-9a-fA-F]{40}$/.test(s) ? fromHex(s.slice(2)) : void 0;
664
+ if (bytes === void 0) throw bad3(`not an address: ${s}`);
665
+ return bytes;
666
+ }
667
+ function checked(p) {
668
+ if (p.namespace < 1n || p.namespace > U64_MAX2)
669
+ throw bad3("a namespace id is a u64 of at least 1");
670
+ if (p.seq < 0n || p.seq > U64_MAX2) throw bad3("a pointer seq is a u64");
671
+ const root = p.root.startsWith("0x") ? p.root.slice(2) : p.root;
672
+ const bytes = isHash(root) ? fromHex(root) : void 0;
673
+ if (bytes === void 0) throw bad3(`not a manifest hash: ${p.root}`);
674
+ return bytes;
675
+ }
676
+ function pointerTypedData(domain, pointer) {
677
+ const root = checked(pointer);
678
+ address(domain.publisherRegistry);
679
+ return {
680
+ domain: {
681
+ name: "decdn-pointer",
682
+ version: "1",
683
+ chainId: chainIdOf(domain),
684
+ verifyingContract: domain.publisherRegistry
685
+ },
686
+ types: {
687
+ Pointer: [
688
+ { name: "namespaceId", type: "uint256" },
689
+ { name: "root", type: "bytes32" },
690
+ { name: "seq", type: "uint64" }
691
+ ]
692
+ },
693
+ primaryType: "Pointer",
694
+ message: {
695
+ namespaceId: pointer.namespace,
696
+ root: `0x${toHex(root)}`,
697
+ seq: pointer.seq
698
+ }
699
+ };
700
+ }
701
+ function digest(domain, pointer) {
702
+ const root = checked(pointer);
703
+ const separator = (0, import_sha3.keccak_256)(
704
+ concat(
705
+ DOMAIN_TYPE,
706
+ (0, import_sha3.keccak_256)(enc.encode("decdn-pointer")),
707
+ (0, import_sha3.keccak_256)(enc.encode("1")),
708
+ word(chainIdOf(domain)),
709
+ concat(new Uint8Array(12), address(domain.publisherRegistry))
710
+ )
711
+ );
712
+ const struct = (0, import_sha3.keccak_256)(
713
+ concat(POINTER_TYPE, word(pointer.namespace), root, word(pointer.seq))
714
+ );
715
+ return (0, import_sha3.keccak_256)(concat(Uint8Array.of(25, 1), separator, struct));
716
+ }
717
+ function pointerSigningHash(domain, pointer) {
718
+ return `0x${toHex(digest(domain, pointer))}`;
719
+ }
720
+ function normalised(signature) {
721
+ const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
722
+ if (!/^[0-9a-fA-F]{130}$/.test(hex))
723
+ throw new DecdnError("CONFIG", "the signer returned no 65-byte signature");
724
+ const r = hex.slice(0, 64);
725
+ let s = BigInt(`0x${hex.slice(64, 128)}`);
726
+ let parity = parseInt(hex.slice(128), 16);
727
+ parity = parity >= 27 ? parity - 27 : parity;
728
+ if (parity !== 0 && parity !== 1)
729
+ throw new DecdnError("CONFIG", "the signature's v is not 0, 1, 27 or 28");
730
+ if (s > N / 2n) {
731
+ s = N - s;
732
+ parity ^= 1;
733
+ }
734
+ return `0x${r.toLowerCase()}${s.toString(16).padStart(64, "0")}${(27 + parity).toString(16)}`;
735
+ }
736
+ async function signPointer(domain, pointer, signer) {
737
+ if ("privateKey" in signer) {
738
+ const key = typeof signer.privateKey === "string" ? fromHex(
739
+ signer.privateKey.startsWith("0x") ? signer.privateKey.slice(2) : signer.privateKey
740
+ ) : signer.privateKey;
741
+ if (key === void 0 || key.length !== 32)
742
+ throw bad3("a private key is 32 bytes");
743
+ const sig = import_secp256k1.secp256k1.sign(digest(domain, pointer), key, {
744
+ prehash: false,
745
+ lowS: true,
746
+ format: "recovered"
747
+ });
748
+ return normalised(`${toHex(sig.subarray(1))}${toHex(sig.subarray(0, 1))}`);
749
+ }
750
+ return normalised(
751
+ await signer.signTypedData(pointerTypedData(domain, pointer))
752
+ );
753
+ }
754
+ function recoverPointerSigner(domain, pointer, signature) {
755
+ const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
756
+ const bytes = /^[0-9a-fA-F]{130}$/.test(hex) ? fromHex(hex) : void 0;
757
+ const v = bytes?.[64];
758
+ const malformed2 = () => new DecdnError("UNPROCESSABLE", "the pointer signature is malformed");
759
+ if (bytes === void 0 || v === void 0 || v !== 27 && v !== 28)
760
+ throw malformed2();
761
+ if (BigInt(`0x${hex.slice(64, 128)}`) > N / 2n) throw malformed2();
762
+ try {
763
+ const recovered = concat(Uint8Array.of(v - 27), bytes.subarray(0, 64));
764
+ const key = import_secp256k1.secp256k1.recoverPublicKey(recovered, digest(domain, pointer), {
765
+ prehash: false
766
+ });
767
+ const point = import_secp256k1.secp256k1.Point.fromBytes(key).toBytes(false);
768
+ return `0x${toHex((0, import_sha3.keccak_256)(point.subarray(1)).subarray(12))}`;
769
+ } catch {
770
+ throw malformed2();
771
+ }
772
+ }
773
+ var PointerApi = class {
774
+ /** @internal */
775
+ constructor(client) {
776
+ this.client = client;
777
+ }
778
+ client;
779
+ /**
780
+ * Sign a pointer from `namespace` to the bundle `root` (`decdn:<hash>/` or
781
+ * its manifest hash) and `PUT` it. `seq` defaults to the current Unix
782
+ * second; the gateway takes only a `seq` above the one it holds from the
783
+ * same signer. The signer must own the namespace on the `PublisherRegistry`
784
+ * of `open({ pointer })`.
785
+ */
786
+ async set(args) {
787
+ const domain = this.client.pointerDomain;
788
+ if (this.client.context !== "public")
789
+ throw bad3("namespace pointers live on a public gateway");
790
+ if (domain === void 0) {
791
+ throw bad3(
792
+ "pointer.set needs open({ pointer: { chainId, publisherRegistry } })"
793
+ );
794
+ }
795
+ let root = args.root.startsWith("0x") ? args.root.slice(2) : args.root;
796
+ if (root.startsWith(SCHEME)) {
797
+ const uri = parseUri(root);
798
+ if (uri.kind !== "bundle") {
799
+ throw new DecdnError(
800
+ "BAD_URI",
801
+ "a pointer names a bundle: pass `decdn:<hash>/`, the manifest hash with its trailing slash"
802
+ );
803
+ }
804
+ root = uri.hash;
805
+ } else if (!isHash(root)) {
806
+ throw new DecdnError("BAD_URI", `not a manifest hash: ${args.root}`);
807
+ }
808
+ const pointer = {
809
+ namespace: integer(args.namespace, "a namespace id"),
810
+ root: root.toLowerCase(),
811
+ seq: args.seq === void 0 ? BigInt(Math.floor(Date.now() / 1e3)) : integer(args.seq, "a pointer seq")
812
+ };
813
+ const signature = await signPointer(domain, pointer, args.signer);
814
+ const init = {
815
+ method: "PUT",
816
+ headers: { "content-type": "application/json" },
817
+ body: `{"namespace":${pointer.namespace},"root":"0x${pointer.root}","seq":${pointer.seq},"signature":"${signature}"}`
818
+ };
819
+ if (args.signal !== void 0) init.signal = args.signal;
820
+ const response = await send(
821
+ this.client.fetch,
822
+ `${this.client.origin}/_pointer`,
823
+ init
824
+ );
825
+ if (response.status < 200 || response.status > 299)
826
+ throw await errorOf(response);
827
+ return parseWithBigInts(await response.text(), [
828
+ "namespace",
829
+ "seq"
830
+ ]);
831
+ }
832
+ };
833
+
834
+ // src/verify.ts
835
+ function guard(body, opts) {
836
+ const reader = body?.getReader();
837
+ const hasher = opts.hash === void 0 ? void 0 : createBlake3();
838
+ let delivered = 0;
839
+ return new ReadableStream({
840
+ async pull(controller) {
841
+ let chunk;
842
+ try {
843
+ chunk = reader === void 0 ? { done: true, value: void 0 } : await reader.read();
844
+ } catch (e) {
845
+ controller.error(
846
+ e instanceof DecdnError ? e : opts.signal?.aborted === true ? opts.signal.reason : new DecdnError("TRUNCATED", "the body ended early", {
847
+ cause: e
848
+ })
849
+ );
850
+ return;
851
+ }
852
+ if (!chunk.done) {
853
+ delivered += chunk.value.length;
854
+ if (opts.max !== void 0 && delivered > opts.max) {
855
+ controller.error(
856
+ new DecdnError(
857
+ "TRUNCATED",
858
+ `the body ran past the ${opts.max} bytes it may hold`
859
+ )
860
+ );
861
+ reader?.cancel().catch(() => void 0);
862
+ return;
863
+ }
864
+ hasher?.update(chunk.value);
865
+ controller.enqueue(chunk.value);
866
+ return;
867
+ }
868
+ if (opts.length !== void 0 && delivered < opts.length) {
869
+ controller.error(
870
+ new DecdnError(
871
+ "TRUNCATED",
872
+ `the body ended after ${delivered} of ${opts.length} bytes`
873
+ )
874
+ );
875
+ return;
876
+ }
877
+ if (hasher !== void 0 && opts.hash !== void 0) {
878
+ const got = hasher.hex();
879
+ if (got !== opts.hash) {
880
+ controller.error(
881
+ new DecdnError(
882
+ "HASH_MISMATCH",
883
+ `the body hashes to ${got}, not ${opts.hash}`
884
+ )
885
+ );
886
+ return;
887
+ }
888
+ }
889
+ controller.close();
890
+ },
891
+ cancel(reason) {
892
+ return reader?.cancel(reason);
893
+ }
894
+ });
895
+ }
896
+ async function collect(stream, limit) {
897
+ const reader = stream.getReader();
898
+ const parts = [];
899
+ let total = 0;
900
+ for (; ; ) {
901
+ const { done, value } = await reader.read();
902
+ if (done) break;
903
+ total += value.length;
904
+ if (limit !== void 0 && total > limit) {
905
+ await reader.cancel();
906
+ throw new DecdnError(
907
+ "MALFORMED_MANIFEST",
908
+ `the body is larger than ${limit} bytes`
909
+ );
910
+ }
911
+ parts.push(value);
912
+ }
913
+ const out = new Uint8Array(total);
914
+ let at = 0;
915
+ for (const p of parts) {
916
+ out.set(p, at);
917
+ at += p.length;
918
+ }
919
+ return out;
920
+ }
921
+
922
+ // src/client.ts
923
+ var DecdnResponse = class {
924
+ status;
925
+ headers;
926
+ url;
927
+ contentType;
928
+ contentLength;
929
+ contentRange;
930
+ /**
931
+ * `true`: the body is hashed as it streams, and ends with `HASH_MISMATCH`
932
+ * instead of `done` unless it hashes to the hash that names it.
933
+ * `"skipped"`: the bytes cannot be verified (a range, a rewritten page, a
934
+ * bundle listing). `false`: verification was turned off.
935
+ */
936
+ verified;
937
+ body;
938
+ /** @internal */
939
+ constructor(response, body, verified, contentType) {
940
+ this.status = response.status;
941
+ this.headers = response.headers;
942
+ this.url = response.url;
943
+ this.contentType = contentType ?? response.headers.get("content-type") ?? "application/octet-stream";
944
+ const length = response.headers.get("content-length");
945
+ if (length !== null && /^\d+$/.test(length))
946
+ this.contentLength = Number(length);
947
+ const range = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(
948
+ response.headers.get("content-range") ?? ""
949
+ );
950
+ if (range !== null) {
951
+ const [, a = "0", b = "0", size = "0"] = range;
952
+ this.contentRange = {
953
+ offset: Number(a),
954
+ length: Number(b) - Number(a) + 1,
955
+ size: Number(size)
956
+ };
957
+ }
958
+ this.verified = verified;
959
+ this.body = body;
960
+ }
961
+ /** The whole body. Rejects with `HASH_MISMATCH` or `TRUNCATED`. */
962
+ bytes() {
963
+ return collect(this.body);
964
+ }
965
+ async text() {
966
+ return new TextDecoder().decode(await this.bytes());
967
+ }
968
+ async json() {
969
+ return JSON.parse(await this.text());
970
+ }
971
+ };
972
+ var NAMESPACE_LABEL = /^[1-9][0-9]*$/;
973
+ function siteOf(protocol, host, gatewayHost) {
974
+ const labels = host.split(".");
975
+ const origin = `${protocol}//${host}`;
976
+ const first = labels[0] ?? "";
977
+ const hash = base32ToHash(first);
978
+ if (hash !== void 0 && labels.length > 1) {
979
+ return { origin, protocol, suffix: labels.slice(1).join("."), hash };
980
+ }
981
+ const second = base32ToHash(labels[1] ?? "");
982
+ if (second !== void 0 && NAMESPACE_LABEL.test(first) && labels.length > 2) {
983
+ return {
984
+ origin,
985
+ protocol,
986
+ suffix: labels.slice(2).join("."),
987
+ hash: second,
988
+ namespace: BigInt(first)
989
+ };
990
+ }
991
+ if (gatewayHost !== void 0 && NAMESPACE_LABEL.test(first) && labels.slice(1).join(".") === gatewayHost) {
992
+ return { origin, protocol, suffix: gatewayHost, namespace: BigInt(first) };
993
+ }
994
+ return void 0;
995
+ }
996
+ function isLoopbackHost(hostname) {
997
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
998
+ }
999
+ function config(message) {
1000
+ return new DecdnError("CONFIG", message);
1001
+ }
1002
+ function sleep(ms, signal) {
1003
+ if (signal?.aborted === true) return Promise.reject(signal.reason);
1004
+ return new Promise((resolve, reject) => {
1005
+ const abort = () => {
1006
+ clearTimeout(timer);
1007
+ reject(signal?.reason);
1008
+ };
1009
+ const timer = setTimeout(() => {
1010
+ signal?.removeEventListener("abort", abort);
1011
+ resolve();
1012
+ }, ms);
1013
+ signal?.addEventListener("abort", abort, { once: true });
1014
+ });
1015
+ }
1016
+ function untilAborted(promise, signal) {
1017
+ if (signal === void 0) return promise;
1018
+ if (signal.aborted) return Promise.reject(signal.reason);
1019
+ return new Promise((resolve, reject) => {
1020
+ const abort = () => reject(signal.reason);
1021
+ signal.addEventListener("abort", abort, { once: true });
1022
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
1023
+ });
1024
+ }
1025
+ function tail(uri) {
1026
+ return formatUri(uri).slice(SCHEME.length + 64);
1027
+ }
1028
+ function isRewrittenType(contentType) {
1029
+ const essence = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
1030
+ return essence === "text/html" || essence === "text/css";
1031
+ }
1032
+ var DecdnClient = class _DecdnClient {
1033
+ context;
1034
+ /** Namespace pointers (public gateway). */
1035
+ pointer = new PointerApi(this);
1036
+ /** The delegate and capability API (loopback gateway). */
1037
+ loopback = new LoopbackApi(this);
1038
+ /** @internal */
1039
+ fetch;
1040
+ /** @internal The bare origin: the gateway URL's, or the site's own. */
1041
+ origin;
1042
+ /** @internal */
1043
+ pointerDomain;
1044
+ gateway;
1045
+ site;
1046
+ token;
1047
+ host;
1048
+ verify;
1049
+ cacheSize;
1050
+ manifests = /* @__PURE__ */ new Map();
1051
+ constructor(context, opts, gateway, site) {
1052
+ this.context = context;
1053
+ this.fetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
1054
+ this.verify = opts.verify ?? "auto";
1055
+ this.cacheSize = opts.manifestCacheSize ?? 64;
1056
+ if (gateway !== void 0) this.gateway = gateway;
1057
+ if (site !== void 0) this.site = site;
1058
+ if (opts.token !== void 0) this.token = opts.token;
1059
+ if (opts.host !== void 0) this.host = opts.host;
1060
+ if (opts.pointer !== void 0) this.pointerDomain = opts.pointer;
1061
+ this.origin = site?.origin ?? gateway?.origin ?? "";
1062
+ }
1063
+ /**
1064
+ * A client for the gateway at `gatewayUrl`, or, in a page served from a
1065
+ * site origin (`[<ns>.]<hash32>.<host>`, or `<ns>.<host>` under the
1066
+ * configured gateway), for that site. A loopback `gatewayUrl` opens the
1067
+ * loopback gateway from any page; a page, which cannot send the token,
1068
+ * reaches its delegate and capability API alone.
1069
+ */
1070
+ static async open(opts = {}) {
1071
+ let gateway;
1072
+ if (opts.gatewayUrl !== void 0) {
1073
+ try {
1074
+ gateway = new URL(opts.gatewayUrl);
1075
+ } catch (e) {
1076
+ throw new DecdnError(
1077
+ "CONFIG",
1078
+ `not a gateway URL: ${opts.gatewayUrl}`,
1079
+ { cause: e }
1080
+ );
1081
+ }
1082
+ }
1083
+ const loopback = gateway !== void 0 && (opts.token !== void 0 || isLoopbackHost(gateway.hostname));
1084
+ const loc = globalThis.location;
1085
+ if (!loopback && loc?.host !== void 0 && loc.host !== "" && loc.protocol !== void 0) {
1086
+ const site = siteOf(loc.protocol, loc.host, gateway?.host);
1087
+ if (site !== void 0)
1088
+ return new _DecdnClient("site", opts, gateway, site);
1089
+ }
1090
+ if (gateway === void 0) {
1091
+ throw config("no gatewayUrl, and the page is not on a site origin");
1092
+ }
1093
+ if (opts.token !== void 0 && (opts.host === void 0 || opts.host === "")) {
1094
+ throw config(
1095
+ "a loopback token needs `host`: the host whose capability pays, sent as x-decdn-host"
1096
+ );
1097
+ }
1098
+ return new _DecdnClient(loopback ? "loopback" : "public", opts, gateway);
1099
+ }
1100
+ /** @internal Headers every bare-host request carries. */
1101
+ authHeaders() {
1102
+ if (this.token === void 0 || this.host === void 0) return {};
1103
+ return { "x-decdn-token": this.token, "x-decdn-host": this.host };
1104
+ }
1105
+ parse(uri) {
1106
+ return typeof uri === "string" ? parseUri(uri) : uri;
1107
+ }
1108
+ /** The URI on the bare host, or under `/_decdn/` on a site. */
1109
+ bareUrl(uri) {
1110
+ const rest = `${uri.hash}${tail(uri)}`;
1111
+ return this.site !== void 0 ? `${this.site.origin}/_decdn/${rest}` : `${this.origin}/${rest}`;
1112
+ }
1113
+ /** The site URL of a bundle or an entry. */
1114
+ siteUrl(uri) {
1115
+ const u = this.parse(uri);
1116
+ if (u.kind === "blob")
1117
+ throw config("a blob has no site: only a bundle or an entry does");
1118
+ const label = hashToBase32(u.hash);
1119
+ const host = u.namespace === void 0 ? label : `${u.namespace}.${label}`;
1120
+ const path = tail({
1121
+ hash: u.hash,
1122
+ kind: u.kind,
1123
+ ...u.path === void 0 ? {} : { path: u.path }
1124
+ });
1125
+ if (this.site !== void 0)
1126
+ return `${this.site.protocol}//${host}.${this.site.suffix}${path}`;
1127
+ const gw = this.gateway;
1128
+ if (this.context === "loopback") {
1129
+ const port = gw.port !== "" ? gw.port : gw.protocol === "https:" ? "443" : "80";
1130
+ return `${gw.protocol}//${host}.localhost:${port}${path}`;
1131
+ }
1132
+ return `${gw.protocol}//${host}.${gw.host}${path}`;
1133
+ }
1134
+ /** The host a namespace's pointer serves at on a public gateway: `https://<ns>.<host>/`. */
1135
+ namespaceUrl(namespace) {
1136
+ const ns = integer(namespace, "a namespace id");
1137
+ if (ns <= 0n) throw config("a namespace id is at least 1");
1138
+ if (this.site !== void 0)
1139
+ return `${this.site.protocol}//${ns}.${this.site.suffix}/`;
1140
+ if (this.context === "loopback")
1141
+ throw config("a loopback gateway holds no namespace pointers");
1142
+ const gw = this.gateway;
1143
+ return `${gw.protocol}//${ns}.${gw.host}/`;
1144
+ }
1145
+ /** The hash a manifest call names, or the site's own. */
1146
+ manifestHash(hashOrUri) {
1147
+ if (hashOrUri === void 0) {
1148
+ if (this.site?.hash !== void 0) return this.site.hash;
1149
+ throw config(
1150
+ this.site === void 0 ? "manifest needs a hash outside a site page" : "the manifest hash is unknown on a namespace host"
1151
+ );
1152
+ }
1153
+ if (typeof hashOrUri === "string" && isHash(hashOrUri))
1154
+ return hashOrUri.toLowerCase();
1155
+ return this.parse(hashOrUri).hash;
1156
+ }
1157
+ /** The bundle manifest a hash or URI names, parsed and verified; cached per client by hash. */
1158
+ async manifest(hashOrUri, signal) {
1159
+ const hash = this.manifestHash(hashOrUri);
1160
+ const cached = this.manifests.get(hash);
1161
+ if (cached !== void 0) {
1162
+ this.manifests.delete(hash);
1163
+ this.manifests.set(hash, cached);
1164
+ return untilAborted(cached, signal);
1165
+ }
1166
+ const pending = (async () => {
1167
+ const response = await this.request(
1168
+ this.bareUrl({ hash, kind: "blob" }),
1169
+ "GET",
1170
+ {}
1171
+ );
1172
+ return parseManifest(
1173
+ await collect(guard(response.body, {}), MAX_MANIFEST_BYTES),
1174
+ hash
1175
+ );
1176
+ })();
1177
+ this.manifests.set(hash, pending);
1178
+ pending.catch(() => {
1179
+ if (this.manifests.get(hash) === pending) this.manifests.delete(hash);
1180
+ });
1181
+ while (this.manifests.size > this.cacheSize) {
1182
+ const oldest = this.manifests.keys().next().value;
1183
+ if (oldest === void 0) break;
1184
+ this.manifests.delete(oldest);
1185
+ }
1186
+ return untilAborted(pending, signal);
1187
+ }
1188
+ /** The manifest entry `uri` names; `NOT_FOUND` when the bundle has none. */
1189
+ async entryOf(uri, signal) {
1190
+ const manifest = await this.manifest(uri.hash, signal);
1191
+ const entry = manifest.entries.find((e) => e.path === uri.path);
1192
+ if (entry === void 0) {
1193
+ throw new DecdnError(
1194
+ "NOT_FOUND",
1195
+ `bundle ${uri.hash} has no entry ${JSON.stringify(uri.path)}`
1196
+ );
1197
+ }
1198
+ return entry;
1199
+ }
1200
+ /** What `uri` names: for a bundle or an entry, after reading its manifest. */
1201
+ async resolve(uri) {
1202
+ const u = this.parse(uri);
1203
+ const out = {
1204
+ kind: u.kind,
1205
+ hash: u.hash,
1206
+ bareUrl: this.bareUrl(u)
1207
+ };
1208
+ if (u.kind === "blob") return out;
1209
+ out.siteUrl = this.siteUrl(u);
1210
+ if (u.kind === "bundle") await this.manifest(u.hash);
1211
+ else out.entry = await this.entryOf(u);
1212
+ return out;
1213
+ }
1214
+ /** A request to the gateway; a non-2xx answer is thrown as a `DecdnError`. */
1215
+ async request(url, method, headers, signal) {
1216
+ const bare = this.site === void 0 && url.startsWith(`${this.origin}/`);
1217
+ const init = {
1218
+ method,
1219
+ headers: bare ? { ...this.authHeaders(), ...headers } : headers
1220
+ };
1221
+ if (signal !== void 0) init.signal = signal;
1222
+ const response = await send(this.fetch, url, init);
1223
+ if (response.status < 200 || response.status > 299)
1224
+ throw await errorOf(response);
1225
+ return response;
1226
+ }
1227
+ /** An entry by its own hash, as a blob: the bytes the manifest names, unrewritten. */
1228
+ entryBlobUrl(uri, entry) {
1229
+ return this.bareUrl({
1230
+ hash: entry.hash,
1231
+ kind: "blob",
1232
+ ...uri.namespace === void 0 ? {} : { namespace: uri.namespace }
1233
+ });
1234
+ }
1235
+ /**
1236
+ * Where an entry comes from, given its manifest entry (when verifying).
1237
+ * `own` holds the query of a path on the site's own bundle: the site serves
1238
+ * by path alone and leaves the query to the page, and its host already
1239
+ * names the namespace.
1240
+ */
1241
+ entryUrl(uri, own, entry) {
1242
+ if (entry !== void 0 && rewritten(uri.path ?? "") && this.context !== "loopback") {
1243
+ return this.entryBlobUrl(uri, entry);
1244
+ }
1245
+ if (this.site !== void 0)
1246
+ return own !== void 0 ? `${this.site.origin}${tail({
1247
+ hash: uri.hash,
1248
+ kind: "entry",
1249
+ ...uri.path === void 0 ? {} : { path: uri.path }
1250
+ })}${own}` : this.bareUrl(uri);
1251
+ return this.context === "loopback" ? this.bareUrl(uri) : this.siteUrl(uri);
1252
+ }
1253
+ async plan(target, mode, ranged, signal) {
1254
+ const off = mode === false ? false : "skipped";
1255
+ const unverifiable = (why) => {
1256
+ if (mode === true) throw config(`verify: true, but ${why}`);
1257
+ };
1258
+ if (typeof target === "string" && target.startsWith("/")) {
1259
+ if (this.site === void 0)
1260
+ throw config(
1261
+ "a path is relative to a site: open the client on a site page"
1262
+ );
1263
+ const path = target.split(/[?#]/)[0] ?? "/";
1264
+ const query = target.slice(path.length).split("#")[0] ?? "";
1265
+ if (this.site.hash === void 0 || path === "/") {
1266
+ unverifiable(
1267
+ this.site.hash === void 0 ? "the bundle is unknown on a namespace host" : "a bundle listing is not content"
1268
+ );
1269
+ return { url: `${this.site.origin}${target}`, verified: off };
1270
+ }
1271
+ const entry = path.endsWith("/") ? `${path}index.html` : path;
1272
+ const uri2 = parseUri(`${SCHEME}${this.site.hash}${entry}`);
1273
+ if (this.site.namespace !== void 0)
1274
+ uri2.namespace = this.site.namespace;
1275
+ return this.planEntry(uri2, query, mode, ranged, signal);
1276
+ }
1277
+ const uri = this.parse(target);
1278
+ if (uri.kind === "blob") {
1279
+ if (ranged)
1280
+ throw config(
1281
+ "a blob has no declared size to range: fetch it whole, or range a bundle entry"
1282
+ );
1283
+ return mode === false ? { url: this.bareUrl(uri), verified: false } : { url: this.bareUrl(uri), hash: uri.hash, verified: true };
1284
+ }
1285
+ if (uri.kind === "bundle") {
1286
+ unverifiable("a bundle's index is a page the gateway rewrites");
1287
+ const own2 = this.site !== void 0 && this.site.hash === uri.hash;
1288
+ const url = this.site !== void 0 ? own2 ? `${this.site.origin}/` : this.bareUrl(uri) : this.context === "loopback" ? this.bareUrl(uri) : this.siteUrl(uri);
1289
+ return { url, verified: off };
1290
+ }
1291
+ const own = this.site !== void 0 && this.site.hash === uri.hash;
1292
+ return this.planEntry(uri, own ? "" : void 0, mode, ranged, signal);
1293
+ }
1294
+ async planEntry(uri, own, mode, ranged, signal) {
1295
+ if (mode === false || ranged) {
1296
+ if (ranged && mode === true)
1297
+ throw config("verify: true, but a range cannot be verified");
1298
+ return {
1299
+ url: this.entryUrl(uri, own),
1300
+ verified: mode === false ? false : "skipped"
1301
+ };
1302
+ }
1303
+ const entry = await this.entryOf(uri, signal);
1304
+ const plan = {
1305
+ url: this.entryUrl(uri, own, entry),
1306
+ hash: entry.hash,
1307
+ verified: true
1308
+ };
1309
+ if (entry.size !== void 0) plan.length = entry.size;
1310
+ const blobUrl = this.entryBlobUrl(uri, entry);
1311
+ if (plan.url !== blobUrl && this.context !== "loopback")
1312
+ plan.blobUrl = blobUrl;
1313
+ return plan;
1314
+ }
1315
+ /**
1316
+ * Fetch what `uri` names: a `decdn:` URI, or on a site page a path of its
1317
+ * own bundle. Verification follows `verify` (see {@link DecdnResponse.verified}).
1318
+ */
1319
+ async get(uri, opts = {}) {
1320
+ const mode = opts.verify ?? this.verify;
1321
+ const plan = await this.plan(
1322
+ uri,
1323
+ mode,
1324
+ opts.range !== void 0,
1325
+ opts.signal
1326
+ );
1327
+ const headers = {};
1328
+ if (opts.range !== void 0) {
1329
+ const { offset, length: length2 } = opts.range;
1330
+ if (!Number.isSafeInteger(offset) || offset < 0 || length2 !== void 0 && (!Number.isSafeInteger(length2) || length2 < 1)) {
1331
+ throw config("a range is a non-negative offset and a positive length");
1332
+ }
1333
+ headers.range = `bytes=${offset}-${length2 === void 0 ? "" : offset + length2 - 1}`;
1334
+ }
1335
+ let response = await this.fetchRetrying(plan.url, headers, opts);
1336
+ let contentType;
1337
+ if (plan.blobUrl !== void 0 && isRewrittenType(response.headers.get("content-type"))) {
1338
+ contentType = response.headers.get("content-type") ?? void 0;
1339
+ response.body?.cancel().catch(() => void 0);
1340
+ response = await this.fetchRetrying(plan.blobUrl, headers, opts);
1341
+ }
1342
+ const whole = opts.range === void 0 || response.status === 200;
1343
+ const verified = plan.verified === true && !whole ? "skipped" : plan.verified;
1344
+ const length = response.headers.get("content-length");
1345
+ const guardOpts = {};
1346
+ if (verified === true && plan.hash !== void 0)
1347
+ guardOpts.hash = plan.hash;
1348
+ if (whole && plan.length !== void 0) {
1349
+ guardOpts.length = plan.length;
1350
+ guardOpts.max = plan.length;
1351
+ } else {
1352
+ if (length !== null && /^\d+$/.test(length))
1353
+ guardOpts.length = Number(length);
1354
+ if (!whole && opts.range?.length !== void 0)
1355
+ guardOpts.max = opts.range.length;
1356
+ }
1357
+ if (opts.signal !== void 0) guardOpts.signal = opts.signal;
1358
+ return new DecdnResponse(
1359
+ response,
1360
+ guard(response.body, guardOpts),
1361
+ verified,
1362
+ contentType
1363
+ );
1364
+ }
1365
+ /** A `GET` of `url`, once more after one `Retry-After` when `opts.retry` asks. */
1366
+ async fetchRetrying(url, headers, opts) {
1367
+ try {
1368
+ return await this.request(url, "GET", headers, opts.signal);
1369
+ } catch (e) {
1370
+ const retryable = e instanceof DecdnError && ["PAYMENT_REQUIRED", "POOL_UNFUNDED", "RATE_LIMITED"].includes(e.code);
1371
+ if (!opts.retry || !retryable || e.retryAfterSecs === void 0) throw e;
1372
+ await sleep(e.retryAfterSecs * 1e3, opts.signal);
1373
+ return this.request(url, "GET", headers, opts.signal);
1374
+ }
1375
+ }
1376
+ /**
1377
+ * What is known about `uri` without its bytes: on a public gateway from
1378
+ * `HEAD`, elsewhere (where `HEAD` would cost a pull) from the manifest. A
1379
+ * public `HEAD` of a raw blob whose size the gateway does not know yet
1380
+ * pays for one chunk group, the proof of its size, and is refused or fails
1381
+ * as its `GET` would be.
1382
+ */
1383
+ async head(uri) {
1384
+ const u = this.parse(uri);
1385
+ if (this.context === "public") {
1386
+ const url = u.kind === "blob" ? this.bareUrl(u) : this.siteUrl(u);
1387
+ const response = await this.request(url, "HEAD", {});
1388
+ const info2 = {};
1389
+ const length = response.headers.get("content-length");
1390
+ if (length !== null && /^\d+$/.test(length)) info2.size = Number(length);
1391
+ const etag = /^"?([0-9a-fA-F]{64})"?$/.exec(
1392
+ response.headers.get("etag") ?? ""
1393
+ )?.[1];
1394
+ if (etag !== void 0) info2.hash = etag.toLowerCase();
1395
+ const type = response.headers.get("content-type");
1396
+ if (type !== null) info2.contentType = type;
1397
+ return info2;
1398
+ }
1399
+ if (u.kind !== "entry") return { hash: u.hash };
1400
+ const entry = await this.entryOf(u);
1401
+ const info = {
1402
+ hash: entry.hash,
1403
+ contentType: siteType(entry.path)
1404
+ };
1405
+ if (entry.size !== void 0) info.size = entry.size;
1406
+ return info;
1407
+ }
1408
+ };
1409
+ //# sourceMappingURL=index.cjs.map