@quillmark/quiver 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +16 -0
- package/README.md +5 -18
- package/dist/build.d.ts +9 -9
- package/dist/build.js +31 -38
- package/dist/built-loader.d.ts +12 -26
- package/dist/built-loader.js +176 -189
- package/dist/bundle.d.ts +0 -16
- package/dist/bundle.js +4 -7
- package/dist/format.d.ts +11 -17
- package/dist/format.js +11 -17
- package/dist/node.d.ts +1 -1
- package/dist/node.js +14 -3
- package/dist/quiver.d.ts +3 -16
- package/dist/quiver.js +7 -24
- package/dist/signature.d.ts +6 -0
- package/dist/signature.js +13 -0
- package/package.json +2 -2
- package/dist/digest.d.ts +0 -23
- package/dist/digest.js +0 -29
- package/dist/transports/fs-built-transport.d.ts +0 -14
- package/dist/transports/fs-built-transport.js +0 -36
- package/dist/transports/http-transport.d.ts +0 -12
- package/dist/transports/http-transport.js +0 -87
- package/dist/transports/memory-transport.d.ts +0 -16
- package/dist/transports/memory-transport.js +0 -35
package/dist/built-loader.js
CHANGED
|
@@ -1,113 +1,147 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-quiver loader: reads
|
|
3
|
-
*
|
|
4
|
-
* bundles and fonts on demand, each checked against the digest in its name.
|
|
2
|
+
* Built-quiver loader: reads `quiver.json` through a reader, validates it, and hands back
|
|
3
|
+
* a Quiver whose loader reads bundles and fonts on demand.
|
|
5
4
|
*
|
|
6
5
|
* Package-internal, and browser-safe: no static `node:` imports at any level.
|
|
7
6
|
*/
|
|
8
7
|
import { QuiverError } from './errors.js';
|
|
9
|
-
import {
|
|
8
|
+
import { unpackFiles } from './bundle.js';
|
|
10
9
|
import { isQuillName } from './ref.js';
|
|
11
10
|
import { isCanonicalSemver, compareSemver } from './semver.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { FORMAT, INDEX } from './format.js';
|
|
12
|
+
import { isFont, isZip } from './signature.js';
|
|
14
13
|
import { createQuiver } from './quiver.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
async function fetchVerified(transport, path, expected, maxBytes) {
|
|
38
|
-
const bytes = await transport.fetchBytes(path, { maxBytes });
|
|
39
|
-
const actual = await sha256Hex(bytes);
|
|
40
|
-
if (actual !== undefined && !actual.startsWith(expected)) {
|
|
41
|
-
throw new QuiverError('transport_error', `Digest mismatch for "${path}": the bytes hash to ${actual.slice(0, expected.length)}, not ${expected}`);
|
|
42
|
-
}
|
|
43
|
-
return bytes;
|
|
14
|
+
export function httpReader(base) {
|
|
15
|
+
const root = base.endsWith('/') ? base : `${base}/`;
|
|
16
|
+
return async (path, mode) => {
|
|
17
|
+
const url = `${root}${path}`;
|
|
18
|
+
let response;
|
|
19
|
+
try {
|
|
20
|
+
response = await globalThis.fetch(url, { cache: mode });
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
throw new QuiverError('transport_error', `Network error fetching "${url}": ${err.message}`, { cause: err });
|
|
24
|
+
}
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new QuiverError('transport_error', `HTTP ${response.status} fetching "${url}"`);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new QuiverError('transport_error', `Network error reading "${url}": ${err.message}`, { cause: err });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
44
35
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
/** Over bytes the caller holds; a leading `./` or `/` on a key is dropped. */
|
|
37
|
+
export function filesReader(files) {
|
|
38
|
+
const held = new Map([...files].map(([key, bytes]) => [key.replace(/^\.?\//, ''), bytes]));
|
|
39
|
+
return async (path) => {
|
|
40
|
+
const bytes = held.get(path);
|
|
41
|
+
if (bytes === undefined) {
|
|
42
|
+
throw new QuiverError('transport_error', `No bytes held for "${path}": Quiver.fromBuiltFiles needs the whole artifact`);
|
|
43
|
+
}
|
|
44
|
+
return bytes;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// The shapes `build` writes. Neither admits a separator, so a validated name read off
|
|
48
|
+
// `quiver.json` stays inside the artifact on every reader.
|
|
49
|
+
const BUNDLE_RE = /^[A-Za-z0-9_-]+@[0-9]+\.[0-9]+\.[0-9]+\.[0-9a-f]+\.zip$/;
|
|
55
50
|
const FONT_HASH_RE = /^[0-9a-f]{64}$/;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
throw new QuiverError('quiver_invalid', `${what} is invalid: "${name}"`);
|
|
61
|
-
}
|
|
62
|
-
return match[1];
|
|
51
|
+
/** Bytes a host answered with that are not the file named: a `transport_error`, so the
|
|
52
|
+
* reread below gets its turn. */
|
|
53
|
+
function refused(path, what) {
|
|
54
|
+
return new QuiverError('transport_error', `"${path}" arrived as something other than ${what}`);
|
|
63
55
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
/** A cached read that fails, by transport or by `opens`, is read once more past the cache,
|
|
57
|
+
* which replaces what the cache held. */
|
|
58
|
+
async function readChecked(read, path, opens, what) {
|
|
59
|
+
try {
|
|
60
|
+
const cached = await read(path, 'force-cache');
|
|
61
|
+
if (opens(cached))
|
|
62
|
+
return cached;
|
|
67
63
|
}
|
|
64
|
+
catch {
|
|
65
|
+
// The reload's answer is the one that stands.
|
|
66
|
+
}
|
|
67
|
+
const bytes = await read(path, 'reload');
|
|
68
|
+
if (!opens(bytes))
|
|
69
|
+
throw refused(path, what);
|
|
70
|
+
return bytes;
|
|
68
71
|
}
|
|
69
|
-
// ─── BuiltLoader implementation ─────────────────────────────────────────────
|
|
70
72
|
class BuiltLoader {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
constructor(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.transport = transport;
|
|
79
|
-
this.index = index;
|
|
73
|
+
read;
|
|
74
|
+
entries;
|
|
75
|
+
#fonts = new Map();
|
|
76
|
+
#rereading;
|
|
77
|
+
constructor(read, entries) {
|
|
78
|
+
this.read = read;
|
|
79
|
+
this.entries = entries;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A tab holding the previous generation's index asks for names the next one deleted,
|
|
83
|
+
* so a failed read rereads `quiver.json` once and retries under that entry where it
|
|
84
|
+
* names other files. A reread refusing the index is the error that explains the
|
|
85
|
+
* failure, and is the one thrown.
|
|
86
|
+
*/
|
|
81
87
|
async loadTree(name, version) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const key = `${name}@${version}`;
|
|
89
|
+
const entry = this.entries.get(key);
|
|
90
|
+
try {
|
|
91
|
+
return await this.#treeOf(entry);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
let index;
|
|
95
|
+
try {
|
|
96
|
+
index = await this.#reread();
|
|
97
|
+
}
|
|
98
|
+
catch (reread) {
|
|
99
|
+
throw reread instanceof QuiverError && reread.code === 'quiver_invalid' ? reread : err;
|
|
100
|
+
}
|
|
101
|
+
const fresh = index.entries.get(key);
|
|
102
|
+
if (fresh === undefined || sameFiles(fresh, entry))
|
|
103
|
+
throw err;
|
|
104
|
+
return this.#treeOf(fresh);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** One reread in flight, taking every entry both generations carry. */
|
|
108
|
+
#reread() {
|
|
109
|
+
return (this.#rereading ??= readIndex(this.read)
|
|
110
|
+
.then((index) => {
|
|
111
|
+
for (const [key, entry] of index.entries) {
|
|
112
|
+
if (this.entries.has(key))
|
|
113
|
+
this.entries.set(key, entry);
|
|
114
|
+
}
|
|
115
|
+
return index;
|
|
116
|
+
})
|
|
117
|
+
.finally(() => {
|
|
118
|
+
this.#rereading = undefined;
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
async #treeOf(entry) {
|
|
122
|
+
const zip = await readChecked(this.read, entry.bundle, isZip, 'a zip');
|
|
123
|
+
const files = unpackFiles(zip);
|
|
124
|
+
await Promise.all(Object.entries(entry.fonts).map(async ([path, hash]) => {
|
|
125
|
+
files[path] = await this.#font(hash, path);
|
|
90
126
|
}));
|
|
91
127
|
return new Map(Object.entries(files));
|
|
92
128
|
}
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let promise = this.fontCache.get(hash);
|
|
100
|
-
if (!promise) {
|
|
101
|
-
promise = fetchVerified(this.transport, `store/${hash}`, hash, MAX_FONT_BYTES).catch((err) => {
|
|
102
|
-
this.fontCache.delete(hash);
|
|
129
|
+
/** One read per hash, coalesced; a failure is evicted so a retry reads again. */
|
|
130
|
+
#font(hash, rel) {
|
|
131
|
+
let bytes = this.#fonts.get(hash);
|
|
132
|
+
if (bytes === undefined) {
|
|
133
|
+
bytes = readChecked(this.read, `fonts/${hash}`, (font) => isFont(font, rel), 'a font').catch((err) => {
|
|
134
|
+
this.#fonts.delete(hash);
|
|
103
135
|
throw err;
|
|
104
136
|
});
|
|
105
|
-
this.
|
|
137
|
+
this.#fonts.set(hash, bytes);
|
|
106
138
|
}
|
|
107
|
-
return
|
|
139
|
+
return bytes;
|
|
108
140
|
}
|
|
109
141
|
}
|
|
110
|
-
|
|
142
|
+
function sameFiles(a, b) {
|
|
143
|
+
return a.bundle === b.bundle && JSON.stringify(a.fonts) === JSON.stringify(b.fonts);
|
|
144
|
+
}
|
|
111
145
|
function assertNoUnknownKeys(obj, allowed, context) {
|
|
112
146
|
for (const key of Object.keys(obj)) {
|
|
113
147
|
if (!allowed.includes(key)) {
|
|
@@ -115,118 +149,106 @@ function assertNoUnknownKeys(obj, allowed, context) {
|
|
|
115
149
|
}
|
|
116
150
|
}
|
|
117
151
|
}
|
|
118
|
-
function
|
|
119
|
-
let
|
|
152
|
+
async function readIndex(read) {
|
|
153
|
+
let bytes;
|
|
120
154
|
try {
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
throw new QuiverError('quiver_invalid', 'latest.json contains invalid JSON');
|
|
155
|
+
bytes = await read(INDEX, 'no-cache');
|
|
125
156
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// reader rejecting what it does not recognise refuses the announcement too. Absent
|
|
133
|
-
// means a build from before the marker (`format.ts`).
|
|
134
|
-
const format = obj['format'] ?? POINTER_FORMAT;
|
|
135
|
-
if (typeof format !== 'number' || !Number.isInteger(format) || format < 1) {
|
|
136
|
-
throw new QuiverError('quiver_invalid', `latest.json: "format" must be a positive integer, got ${JSON.stringify(obj['format'])}`);
|
|
137
|
-
}
|
|
138
|
-
if (format > POINTER_FORMAT) {
|
|
139
|
-
throw new QuiverError('quiver_invalid', `This quiver is built in format ${format} and this loader reads ${POINTER_FORMAT}. ` +
|
|
140
|
-
`Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
|
|
141
|
-
}
|
|
142
|
-
const manifest = obj['manifest'];
|
|
143
|
-
if (typeof manifest !== 'string' || manifest.length === 0) {
|
|
144
|
-
throw new QuiverError('quiver_invalid', 'latest.json must have a non-empty string "manifest" field');
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (err instanceof QuiverError)
|
|
159
|
+
throw err;
|
|
160
|
+
throw new QuiverError('transport_error', `Failed to read ${INDEX}: ${err.message}`, {
|
|
161
|
+
cause: err
|
|
162
|
+
});
|
|
145
163
|
}
|
|
146
|
-
return
|
|
164
|
+
return parseIndex(new TextDecoder().decode(bytes));
|
|
147
165
|
}
|
|
148
|
-
function
|
|
166
|
+
function parseIndex(raw) {
|
|
149
167
|
let parsed;
|
|
150
168
|
try {
|
|
151
169
|
parsed = JSON.parse(raw);
|
|
152
170
|
}
|
|
153
171
|
catch {
|
|
154
|
-
throw new QuiverError('quiver_invalid',
|
|
172
|
+
throw new QuiverError('quiver_invalid', `${INDEX} contains invalid JSON`);
|
|
155
173
|
}
|
|
156
174
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
157
|
-
throw new QuiverError('quiver_invalid',
|
|
175
|
+
throw new QuiverError('quiver_invalid', `${INDEX} must be a JSON object`);
|
|
158
176
|
}
|
|
159
177
|
const obj = parsed;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
// Read before the key check, so a newer document is refused as newer rather than as
|
|
179
|
+
// carrying a field this reader does not know.
|
|
180
|
+
const format = obj['format'];
|
|
181
|
+
if (typeof format !== 'number' || !Number.isInteger(format) || format < 1) {
|
|
182
|
+
throw new QuiverError('quiver_invalid', `${INDEX}: "format" must be a positive integer, got ${JSON.stringify(format)}`);
|
|
164
183
|
}
|
|
165
|
-
if (
|
|
166
|
-
throw new QuiverError('quiver_invalid', `This quiver
|
|
184
|
+
if (format > FORMAT) {
|
|
185
|
+
throw new QuiverError('quiver_invalid', `This quiver is built in format ${format} and this loader reads ${FORMAT}. ` +
|
|
167
186
|
`Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
|
|
168
187
|
}
|
|
188
|
+
assertNoUnknownKeys(obj, ['format', 'name', 'description', 'quills'], INDEX);
|
|
169
189
|
const name = obj['name'];
|
|
170
190
|
if (typeof name !== 'string' || name.length === 0) {
|
|
171
|
-
throw new QuiverError('quiver_invalid',
|
|
191
|
+
throw new QuiverError('quiver_invalid', `${INDEX} must have a non-empty string "name" field`);
|
|
172
192
|
}
|
|
173
193
|
const description = obj['description'];
|
|
174
194
|
if (description !== undefined && typeof description !== 'string') {
|
|
175
|
-
throw new QuiverError('quiver_invalid',
|
|
195
|
+
throw new QuiverError('quiver_invalid', `${INDEX} "description" must be a string if present, got ${typeof description}`);
|
|
176
196
|
}
|
|
177
|
-
const
|
|
178
|
-
if (!Array.isArray(
|
|
179
|
-
throw new QuiverError('quiver_invalid',
|
|
197
|
+
const quills = obj['quills'];
|
|
198
|
+
if (!Array.isArray(quills)) {
|
|
199
|
+
throw new QuiverError('quiver_invalid', `${INDEX} must have a "quills" array`);
|
|
180
200
|
}
|
|
181
|
-
const
|
|
182
|
-
for (let i = 0; i <
|
|
183
|
-
const
|
|
201
|
+
const entries = new Map();
|
|
202
|
+
for (let i = 0; i < quills.length; i++) {
|
|
203
|
+
const at = `${INDEX} quills[${i}]`;
|
|
204
|
+
const entry = quills[i];
|
|
184
205
|
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
185
|
-
throw new QuiverError('quiver_invalid',
|
|
206
|
+
throw new QuiverError('quiver_invalid', `${at} must be an object`);
|
|
186
207
|
}
|
|
187
208
|
const e = entry;
|
|
188
|
-
assertNoUnknownKeys(e, ['name', 'version', 'bundle', 'fonts'],
|
|
209
|
+
assertNoUnknownKeys(e, ['name', 'version', 'bundle', 'fonts'], at);
|
|
189
210
|
const quillName = e['name'];
|
|
190
211
|
if (typeof quillName !== 'string' || quillName.length === 0) {
|
|
191
|
-
throw new QuiverError('quiver_invalid',
|
|
212
|
+
throw new QuiverError('quiver_invalid', `${at}.name must be a non-empty string`);
|
|
192
213
|
}
|
|
193
214
|
if (!isQuillName(quillName)) {
|
|
194
|
-
throw new QuiverError('quiver_invalid',
|
|
215
|
+
throw new QuiverError('quiver_invalid', `${at}.name "${quillName}" is not a name a ref can spell — only [A-Za-z0-9_-] are allowed`);
|
|
195
216
|
}
|
|
196
217
|
const version = e['version'];
|
|
197
218
|
if (typeof version !== 'string' || !isCanonicalSemver(version)) {
|
|
198
|
-
throw new QuiverError('quiver_invalid',
|
|
219
|
+
throw new QuiverError('quiver_invalid', `${at}.version must be canonical semver (x.y.z), got "${String(version)}"`);
|
|
199
220
|
}
|
|
200
221
|
const bundle = e['bundle'];
|
|
201
|
-
if (typeof bundle !== 'string' || bundle
|
|
202
|
-
throw new QuiverError('quiver_invalid',
|
|
222
|
+
if (typeof bundle !== 'string' || !BUNDLE_RE.test(bundle)) {
|
|
223
|
+
throw new QuiverError('quiver_invalid', `${at}.bundle is invalid: "${String(bundle)}"`);
|
|
203
224
|
}
|
|
204
|
-
digestOfName(BUNDLE_FILENAME_RE, bundle, `manifest.quills[${i}].bundle filename`);
|
|
205
225
|
const declared = e['fonts'];
|
|
206
226
|
if (typeof declared !== 'object' || declared === null || Array.isArray(declared)) {
|
|
207
|
-
throw new QuiverError('quiver_invalid',
|
|
227
|
+
throw new QuiverError('quiver_invalid', `${at}.fonts must be an object`);
|
|
208
228
|
}
|
|
209
229
|
const fonts = {};
|
|
210
|
-
for (const [
|
|
211
|
-
if (typeof
|
|
212
|
-
throw new QuiverError('quiver_invalid',
|
|
230
|
+
for (const [path, hash] of Object.entries(declared)) {
|
|
231
|
+
if (typeof hash !== 'string' || !FONT_HASH_RE.test(hash)) {
|
|
232
|
+
throw new QuiverError('quiver_invalid', `${at}.fonts["${path}"] is not a font hash: ${JSON.stringify(hash)}`);
|
|
213
233
|
}
|
|
214
|
-
|
|
215
|
-
fonts[k] = v;
|
|
234
|
+
fonts[path] = hash;
|
|
216
235
|
}
|
|
217
|
-
|
|
236
|
+
const key = `${quillName}@${version}`;
|
|
237
|
+
if (entries.has(key)) {
|
|
238
|
+
throw new QuiverError('quiver_invalid', `Duplicate quill entry in ${INDEX}: "${key}"`);
|
|
239
|
+
}
|
|
240
|
+
entries.set(key, { name: quillName, version, bundle, fonts });
|
|
218
241
|
}
|
|
219
|
-
return { name, description,
|
|
242
|
+
return { name, description, entries };
|
|
220
243
|
}
|
|
221
|
-
// ─── Catalog assembly ─────────────────────────────────────────────────────────
|
|
222
244
|
/**
|
|
223
|
-
* Name → versions (descending), derived from the
|
|
224
|
-
*
|
|
225
|
-
*
|
|
245
|
+
* Name → versions (descending), derived from the entries. One structure backs both the
|
|
246
|
+
* loader's lookups and the Quiver's catalog, so the two cannot disagree about what
|
|
247
|
+
* `quiver.json` holds.
|
|
226
248
|
*/
|
|
227
|
-
function catalogOf(
|
|
249
|
+
function catalogOf(entries) {
|
|
228
250
|
const catalog = new Map();
|
|
229
|
-
for (const entry of
|
|
251
|
+
for (const entry of entries.values()) {
|
|
230
252
|
const versions = catalog.get(entry.name) ?? [];
|
|
231
253
|
versions.push(entry.version);
|
|
232
254
|
catalog.set(entry.name, versions);
|
|
@@ -236,42 +258,7 @@ function catalogOf(index) {
|
|
|
236
258
|
}
|
|
237
259
|
return catalog;
|
|
238
260
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// The pointer is the one name that is not content-addressed, so it is also the
|
|
243
|
-
// one fetch that must revalidate.
|
|
244
|
-
let pointerBytes;
|
|
245
|
-
try {
|
|
246
|
-
pointerBytes = await transport.fetchBytes('latest.json', {
|
|
247
|
-
revalidate: true,
|
|
248
|
-
maxBytes: MAX_DOCUMENT_BYTES
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
catch (err) {
|
|
252
|
-
if (err instanceof QuiverError)
|
|
253
|
-
throw err;
|
|
254
|
-
throw new QuiverError('transport_error', `Failed to fetch latest.json: ${err.message}`, { cause: err });
|
|
255
|
-
}
|
|
256
|
-
const manifestFileName = parsePointer(new TextDecoder().decode(pointerBytes));
|
|
257
|
-
const manifestDigest = digestOfName(MANIFEST_FILENAME_RE, manifestFileName, 'Pointer manifest filename');
|
|
258
|
-
let manifestBytes;
|
|
259
|
-
try {
|
|
260
|
-
manifestBytes = await fetchVerified(transport, manifestFileName, manifestDigest, MAX_DOCUMENT_BYTES);
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
if (err instanceof QuiverError)
|
|
264
|
-
throw err;
|
|
265
|
-
throw new QuiverError('transport_error', `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
|
|
266
|
-
}
|
|
267
|
-
const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
|
|
268
|
-
const index = new Map();
|
|
269
|
-
for (const entry of manifest.quills) {
|
|
270
|
-
const key = `${entry.name}@${entry.version}`;
|
|
271
|
-
if (index.has(key)) {
|
|
272
|
-
throw new QuiverError('quiver_invalid', `Duplicate quill entry in manifest: "${key}"`);
|
|
273
|
-
}
|
|
274
|
-
index.set(key, entry);
|
|
275
|
-
}
|
|
276
|
-
return createQuiver(manifest.name, manifest.description, catalogOf(index), new BuiltLoader(transport, index));
|
|
261
|
+
export async function loadBuiltQuiver(read) {
|
|
262
|
+
const { name, description, entries } = await readIndex(read);
|
|
263
|
+
return createQuiver(name, description, catalogOf(entries), new BuiltLoader(read, entries));
|
|
277
264
|
}
|
package/dist/bundle.d.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zip utilities — browser-safe (uses fflate only).
|
|
3
3
|
*/
|
|
4
|
-
/**
|
|
5
|
-
* What a bundle may hold. Deflate tops out near 1032:1, so a megabyte of zip is a
|
|
6
|
-
* gigabyte resident and the bundle is what a reader inflates to find that out.
|
|
7
|
-
*
|
|
8
|
-
* The ceiling is a constant rather than something a consumer raises, because a quiver
|
|
9
|
-
* is a dependency pinned like any other (README, "What a quiver is trusted to be") and
|
|
10
|
-
* what a budget bounds there is a build that packed something enormous. Both ends spend
|
|
11
|
-
* it, so an artifact no loader would read is refused where it is written.
|
|
12
|
-
*
|
|
13
|
-
* Fonts are not in here: `build` writes them to the store and the manifest names them,
|
|
14
|
-
* leaving a bundle its markdown, its Typst, and what those inline.
|
|
15
|
-
*
|
|
16
|
-
* `built-loader.ts` spends it on the wire too. A deflate of what fits inside it is within
|
|
17
|
-
* zip framing of that, so one number bounds both ends and the fetch needs no second.
|
|
18
|
-
*/
|
|
19
|
-
export declare const MAX_BUNDLE_BYTES: number;
|
|
20
4
|
/**
|
|
21
5
|
* Pack a flat file map into a deterministic zip.
|
|
22
6
|
* Keys are sorted before zipping so insertion order doesn't affect output.
|
package/dist/bundle.js
CHANGED
|
@@ -23,13 +23,10 @@ const ZIP_EPOCH = new Date(1980, 0, 1, 0, 0, 0, 0);
|
|
|
23
23
|
* what a budget bounds there is a build that packed something enormous. Both ends spend
|
|
24
24
|
* it, so an artifact no loader would read is refused where it is written.
|
|
25
25
|
*
|
|
26
|
-
* Fonts are not in here: `build` writes them
|
|
26
|
+
* Fonts are not in here: `build` writes them under `fonts/` and `quiver.json` names them,
|
|
27
27
|
* leaving a bundle its markdown, its Typst, and what those inline.
|
|
28
|
-
*
|
|
29
|
-
* `built-loader.ts` spends it on the wire too. A deflate of what fits inside it is within
|
|
30
|
-
* zip framing of that, so one number bounds both ends and the fetch needs no second.
|
|
31
28
|
*/
|
|
32
|
-
|
|
29
|
+
const MAX_BUNDLE_BYTES = 64 * 1024 * 1024;
|
|
33
30
|
const MAX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
34
31
|
const MAX_BUNDLE_ENTRIES = 2048;
|
|
35
32
|
/**
|
|
@@ -38,8 +35,8 @@ const MAX_BUNDLE_ENTRIES = 2048;
|
|
|
38
35
|
* Unpacking spends the sizes the central directory declares, and those are the sizes
|
|
39
36
|
* fflate allocates: each entry inflates into a buffer of exactly its declared size,
|
|
40
37
|
* which never grows. So a header cannot buy more than it declares — it can only
|
|
41
|
-
* understate itself and truncate its own entry, which is
|
|
42
|
-
*
|
|
38
|
+
* understate itself and truncate its own entry, which is a corrupt bundle rather than an
|
|
39
|
+
* allocation.
|
|
43
40
|
*/
|
|
44
41
|
function bundleBudget() {
|
|
45
42
|
let total = 0;
|
package/dist/format.d.ts
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
+
/** The one name a reader fetches that carries no digest: the catalog, and where every
|
|
2
|
+
* other name is read off. */
|
|
3
|
+
export declare const INDEX = "quiver.json";
|
|
1
4
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* It moves when a reader assuming it would misread a tree rather than fail on it: a
|
|
7
|
-
* renamed pointer field, a second pointer, a store keyed by something other than the
|
|
8
|
-
* full hash. A change confined to the manifest's own shape moves the manifest's
|
|
9
|
-
* `version` instead.
|
|
5
|
+
* The artifact's format, stamped into `quiver.json` and refused to be read past (QUIVER
|
|
6
|
+
* §"The index"). Any change a reader assuming the previous shape would misread or reject
|
|
7
|
+
* moves it: `quiver.json` is closed, so an added field is one of those.
|
|
10
8
|
*/
|
|
11
|
-
export declare const
|
|
9
|
+
export declare const FORMAT = 2;
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* The manifest is closed, so a field added to it is a field an older reader rejects as an
|
|
17
|
-
* unknown key, naming neither what happened nor what to do. The version is what that
|
|
18
|
-
* reader reads first instead, and a manifest above its own is refused with the upgrade
|
|
19
|
-
* named.
|
|
11
|
+
* What a reader of format 1 fetches first. It reads `format` off it before anything else,
|
|
12
|
+
* so a file carrying this format and nothing more is refused there with the upgrade named,
|
|
13
|
+
* rather than as a missing file. No reader of this format reads it.
|
|
20
14
|
*/
|
|
21
|
-
export declare const
|
|
15
|
+
export declare const LEGACY_POINTER = "latest.json";
|
package/dist/format.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
+
/** The one name a reader fetches that carries no digest: the catalog, and where every
|
|
2
|
+
* other name is read off. */
|
|
3
|
+
export const INDEX = 'quiver.json';
|
|
1
4
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* It moves when a reader assuming it would misread a tree rather than fail on it: a
|
|
7
|
-
* renamed pointer field, a second pointer, a store keyed by something other than the
|
|
8
|
-
* full hash. A change confined to the manifest's own shape moves the manifest's
|
|
9
|
-
* `version` instead.
|
|
5
|
+
* The artifact's format, stamped into `quiver.json` and refused to be read past (QUIVER
|
|
6
|
+
* §"The index"). Any change a reader assuming the previous shape would misread or reject
|
|
7
|
+
* moves it: `quiver.json` is closed, so an added field is one of those.
|
|
10
8
|
*/
|
|
11
|
-
export const
|
|
9
|
+
export const FORMAT = 2;
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* The manifest is closed, so a field added to it is a field an older reader rejects as an
|
|
17
|
-
* unknown key, naming neither what happened nor what to do. The version is what that
|
|
18
|
-
* reader reads first instead, and a manifest above its own is refused with the upgrade
|
|
19
|
-
* named.
|
|
11
|
+
* What a reader of format 1 fetches first. It reads `format` off it before anything else,
|
|
12
|
+
* so a file carrying this format and nothing more is refused there with the upgrade named,
|
|
13
|
+
* rather than as a missing file. No reader of this format reads it.
|
|
20
14
|
*/
|
|
21
|
-
export const
|
|
15
|
+
export const LEGACY_POINTER = 'latest.json';
|
package/dist/node.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type { BuildOptions } from './build.js';
|
|
|
28
28
|
export declare function fromDir(pathOrFileUrl: string): Promise<Quiver>;
|
|
29
29
|
/**
|
|
30
30
|
* Loads a packed (build-output) quiver from a local directory containing
|
|
31
|
-
* `
|
|
31
|
+
* `quiver.json` and the bundles and fonts written by `build`.
|
|
32
32
|
* Symmetric to `Quiver.fromBuiltUrl(url)` but reads from disk instead of HTTP:
|
|
33
33
|
* no network required.
|
|
34
34
|
*
|
package/dist/node.js
CHANGED
|
@@ -12,7 +12,9 @@ import { createQuiver } from './quiver.js';
|
|
|
12
12
|
import { scanSourceQuiver, SourceLoader } from './source-loader.js';
|
|
13
13
|
import { buildQuiver } from './build.js';
|
|
14
14
|
import { loadBuiltQuiver } from './built-loader.js';
|
|
15
|
-
import {
|
|
15
|
+
import { QuiverError } from './errors.js';
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
16
18
|
import { fileURLToPath } from 'node:url';
|
|
17
19
|
/**
|
|
18
20
|
* Reads a Source Quiver from a local directory containing `Quiver.yaml` and
|
|
@@ -38,7 +40,7 @@ export async function fromDir(pathOrFileUrl) {
|
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Loads a packed (build-output) quiver from a local directory containing
|
|
41
|
-
* `
|
|
43
|
+
* `quiver.json` and the bundles and fonts written by `build`.
|
|
42
44
|
* Symmetric to `Quiver.fromBuiltUrl(url)` but reads from disk instead of HTTP:
|
|
43
45
|
* no network required.
|
|
44
46
|
*
|
|
@@ -49,7 +51,16 @@ export async function fromDir(pathOrFileUrl) {
|
|
|
49
51
|
* Throws `quiver_invalid` on format errors, `transport_error` on I/O failure.
|
|
50
52
|
*/
|
|
51
53
|
export async function fromBuiltDir(dirPath) {
|
|
52
|
-
return loadBuiltQuiver(
|
|
54
|
+
return loadBuiltQuiver(async (path) => {
|
|
55
|
+
const file = join(dirPath, path);
|
|
56
|
+
try {
|
|
57
|
+
const buf = await readFile(file);
|
|
58
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
throw new QuiverError('transport_error', `Failed to read "${file}": ${err.message}`, { cause: err });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
55
66
|
* Reads the Source Quiver at sourceDir, validates it, and writes the runtime
|