@quillmark/quiver 0.16.0 → 0.18.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/LICENSE +201 -21
- package/NOTICE +2 -0
- package/README.md +84 -245
- package/dist/bin/quiver.d.ts +6 -6
- package/dist/bin/quiver.js +31 -63
- package/dist/build.d.ts +12 -12
- package/dist/build.js +78 -33
- package/dist/built-loader.d.ts +16 -16
- package/dist/built-loader.js +126 -112
- package/dist/bundle.js +1 -1
- package/dist/digest.d.ts +19 -0
- package/dist/digest.js +25 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/format.d.ts +11 -0
- package/dist/format.js +11 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -5
- package/dist/node.d.ts +47 -76
- package/dist/node.js +59 -64
- package/dist/quiver-yaml.js +19 -19
- package/dist/quiver.d.ts +25 -37
- package/dist/quiver.js +46 -113
- package/dist/ref.d.ts +3 -3
- package/dist/ref.js +15 -13
- package/dist/semver.d.ts +5 -1
- package/dist/semver.js +9 -10
- package/dist/source-loader.d.ts +3 -3
- package/dist/source-loader.js +26 -26
- package/dist/transports/fs-built-transport.d.ts +2 -2
- package/dist/transports/fs-built-transport.js +10 -8
- package/dist/transports/http-transport.d.ts +2 -2
- package/dist/transports/http-transport.js +11 -9
- package/package.json +68 -72
- package/dist/preview.d.ts +0 -81
- package/dist/preview.js +0 -208
- package/dist/testing.d.ts +0 -28
- package/dist/testing.js +0 -66
package/dist/built-loader.js
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-quiver loader
|
|
3
|
-
*
|
|
2
|
+
* Built-quiver loader: reads a packed artifact through a transport, validates
|
|
3
|
+
* its pointer and manifest, and hands back a Quiver whose loader fetches
|
|
4
|
+
* bundles and fonts on demand, each checked against the digest in its name.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
* Package-internal, and browser-safe: no static `node:` imports at any level.
|
|
7
|
+
*/
|
|
8
|
+
import { QuiverError } from './errors.js';
|
|
9
|
+
import { unpackFiles } from './bundle.js';
|
|
10
|
+
import { isCanonicalSemver, compareSemver } from './semver.js';
|
|
11
|
+
import { NAME_DIGEST_LENGTH, sha256Hex } from './digest.js';
|
|
12
|
+
import { POINTER_FORMAT } from './format.js';
|
|
13
|
+
import { createQuiver } from './quiver.js';
|
|
14
|
+
/**
|
|
15
|
+
* Fetch, then check the bytes against the digest their name carries. That
|
|
16
|
+
* check is what makes "content-addressed, safe to cache forever" a property
|
|
17
|
+
* rather than a hope: it catches a corrupted CDN object, a partial sync, and a
|
|
18
|
+
* name reused across releases. A mismatch is a transport failure (the bytes
|
|
19
|
+
* that arrived are not the bytes asked for), so the caches that evict on error
|
|
20
|
+
* let a retry succeed.
|
|
8
21
|
*
|
|
9
|
-
*
|
|
22
|
+
* Where no digest primitive exists (a page served over plain http, which is
|
|
23
|
+
* not a secure context) the fetch passes through unchecked.
|
|
10
24
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const MANIFEST_FILENAME_RE = /^manifest\.[0-9a-f]+\.json$/;
|
|
17
|
-
const BUNDLE_FILENAME_RE = /^[A-Za-z0-9_.-]+@[0-9]+\.[0-9]+\.[0-9]+\.[0-9a-f]+\.zip$/;
|
|
18
|
-
const FONT_HASH_RE = /^[0-9a-f]{32}$/;
|
|
19
|
-
function validateManifestFileName(name) {
|
|
20
|
-
if (!MANIFEST_FILENAME_RE.test(name)) {
|
|
21
|
-
throw new QuiverError("quiver_invalid", `Pointer manifest filename is invalid: "${name}"`);
|
|
25
|
+
async function fetchVerified(transport, path, expected) {
|
|
26
|
+
const bytes = await transport.fetchBytes(path);
|
|
27
|
+
const actual = await sha256Hex(bytes);
|
|
28
|
+
if (actual !== undefined && !actual.startsWith(expected)) {
|
|
29
|
+
throw new QuiverError('transport_error', `Digest mismatch for "${path}": the bytes hash to ${actual.slice(0, expected.length)}, not ${expected}`);
|
|
22
30
|
}
|
|
31
|
+
return bytes;
|
|
23
32
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
// ─── Path validation ──────────────────────────────────────────────────────────
|
|
34
|
+
// Each name carries the digest of what it names; the capture group is the
|
|
35
|
+
// digest the fetch is checked against, so path validation and verification
|
|
36
|
+
// read the same character span. `build` writes exactly NAME_DIGEST_LENGTH
|
|
37
|
+
// chars for bundles and manifests and the full 64 for store entries.
|
|
38
|
+
const DIGEST = `[0-9a-f]{${NAME_DIGEST_LENGTH},}`;
|
|
39
|
+
const MANIFEST_FILENAME_RE = new RegExp(`^manifest\\.(${DIGEST})\\.json$`);
|
|
40
|
+
const BUNDLE_FILENAME_RE = new RegExp(`^[A-Za-z0-9_.-]+@[0-9]+\\.[0-9]+\\.[0-9]+\\.(${DIGEST})\\.zip$`);
|
|
41
|
+
const FONT_HASH_RE = /^[0-9a-f]{64}$/;
|
|
42
|
+
/** The digest a content-addressed filename carries. Throws `quiver_invalid`. */
|
|
43
|
+
function digestOfName(re, name, what) {
|
|
44
|
+
const match = re.exec(name);
|
|
45
|
+
if (match === null) {
|
|
46
|
+
throw new QuiverError('quiver_invalid', `${what} is invalid: "${name}"`);
|
|
27
47
|
}
|
|
48
|
+
return match[1];
|
|
28
49
|
}
|
|
29
50
|
function validateFontHash(hash, context) {
|
|
30
51
|
if (!FONT_HASH_RE.test(hash)) {
|
|
31
|
-
throw new QuiverError(
|
|
52
|
+
throw new QuiverError('quiver_invalid', `${context}: font hash is invalid: "${hash}"`);
|
|
32
53
|
}
|
|
33
54
|
}
|
|
34
55
|
// ─── BuiltLoader implementation ─────────────────────────────────────────────
|
|
@@ -44,16 +65,11 @@ class BuiltLoader {
|
|
|
44
65
|
this.index = index;
|
|
45
66
|
}
|
|
46
67
|
async loadTree(name, version) {
|
|
68
|
+
// The catalog the outer Quiver resolves against derives from this index,
|
|
69
|
+
// so every ref reaching here has an entry.
|
|
47
70
|
const entry = this.index.get(`${name}@${version}`);
|
|
48
|
-
// If entry is missing, the outer Quiver.loadTree gate already validated
|
|
49
|
-
// that this name/version is in the catalog; trust that gate and fall
|
|
50
|
-
// through. The transport will surface a transport_error naturally.
|
|
51
|
-
// (Defensive: this should not be reachable in normal operation.)
|
|
52
|
-
if (!entry) {
|
|
53
|
-
throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in built-quiver manifest`, { version, ref: `${name}@${version}` });
|
|
54
|
-
}
|
|
55
71
|
// 1. Fetch + unpack bundle zip.
|
|
56
|
-
const zipBytes = await this.transport.
|
|
72
|
+
const zipBytes = await fetchVerified(this.transport, entry.bundle, digestOfName(BUNDLE_FILENAME_RE, entry.bundle, 'bundle filename'));
|
|
57
73
|
const files = unpackFiles(zipBytes);
|
|
58
74
|
// 2. Rehydrate fonts from store (coalesced).
|
|
59
75
|
const fontEntries = Object.entries(entry.fonts);
|
|
@@ -71,9 +87,7 @@ class BuiltLoader {
|
|
|
71
87
|
fetchFont(hash) {
|
|
72
88
|
let promise = this.fontCache.get(hash);
|
|
73
89
|
if (!promise) {
|
|
74
|
-
promise = this.transport
|
|
75
|
-
.fetchBytes(`store/${hash}`)
|
|
76
|
-
.catch((err) => {
|
|
90
|
+
promise = fetchVerified(this.transport, `store/${hash}`, hash).catch((err) => {
|
|
77
91
|
this.fontCache.delete(hash);
|
|
78
92
|
throw err;
|
|
79
93
|
});
|
|
@@ -86,7 +100,7 @@ class BuiltLoader {
|
|
|
86
100
|
function assertNoUnknownKeys(obj, allowed, context) {
|
|
87
101
|
for (const key of Object.keys(obj)) {
|
|
88
102
|
if (!allowed.includes(key)) {
|
|
89
|
-
throw new QuiverError(
|
|
103
|
+
throw new QuiverError('quiver_invalid', `${context}: unknown field "${key}"`);
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
}
|
|
@@ -96,17 +110,28 @@ function parsePointer(raw) {
|
|
|
96
110
|
parsed = JSON.parse(raw);
|
|
97
111
|
}
|
|
98
112
|
catch {
|
|
99
|
-
throw new QuiverError(
|
|
113
|
+
throw new QuiverError('quiver_invalid', 'latest.json contains invalid JSON');
|
|
100
114
|
}
|
|
101
|
-
if (typeof parsed !==
|
|
102
|
-
throw new QuiverError(
|
|
115
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
116
|
+
throw new QuiverError('quiver_invalid', 'latest.json must be a JSON object');
|
|
103
117
|
}
|
|
104
118
|
const obj = parsed;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
// The format, read before anything is believed about the rest. An unknown key is NOT
|
|
120
|
+
// a failure here: this is the document a newer format announces itself on, and a
|
|
121
|
+
// reader rejecting what it does not recognise refuses the announcement too. Absent
|
|
122
|
+
// means a build from before the marker (`format.ts`).
|
|
123
|
+
const format = obj['format'] ?? POINTER_FORMAT;
|
|
124
|
+
if (typeof format !== 'number' || !Number.isInteger(format) || format < 1) {
|
|
125
|
+
throw new QuiverError('quiver_invalid', `latest.json: "format" must be a positive integer, got ${JSON.stringify(obj['format'])}`);
|
|
126
|
+
}
|
|
127
|
+
if (format > POINTER_FORMAT) {
|
|
128
|
+
throw new QuiverError('quiver_invalid', `This quiver is built in format ${format} and this loader reads ${POINTER_FORMAT}. ` +
|
|
129
|
+
`Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
|
|
130
|
+
}
|
|
131
|
+
if (typeof obj['manifest'] !== 'string' || obj['manifest'].length === 0) {
|
|
132
|
+
throw new QuiverError('quiver_invalid', 'latest.json must have a non-empty string "manifest" field');
|
|
108
133
|
}
|
|
109
|
-
return obj[
|
|
134
|
+
return obj['manifest'];
|
|
110
135
|
}
|
|
111
136
|
function parseManifest(raw) {
|
|
112
137
|
let parsed;
|
|
@@ -114,95 +139,82 @@ function parseManifest(raw) {
|
|
|
114
139
|
parsed = JSON.parse(raw);
|
|
115
140
|
}
|
|
116
141
|
catch {
|
|
117
|
-
throw new QuiverError(
|
|
142
|
+
throw new QuiverError('quiver_invalid', 'Manifest file contains invalid JSON');
|
|
118
143
|
}
|
|
119
|
-
if (typeof parsed !==
|
|
120
|
-
throw new QuiverError(
|
|
144
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
145
|
+
throw new QuiverError('quiver_invalid', 'Manifest must be a JSON object');
|
|
121
146
|
}
|
|
122
147
|
const obj = parsed;
|
|
123
|
-
assertNoUnknownKeys(obj, [
|
|
124
|
-
if (obj[
|
|
125
|
-
throw new QuiverError(
|
|
148
|
+
assertNoUnknownKeys(obj, ['version', 'name', 'quills'], 'manifest');
|
|
149
|
+
if (obj['version'] !== 1) {
|
|
150
|
+
throw new QuiverError('quiver_invalid', `Manifest version must be 1, got ${String(obj['version'])}`);
|
|
126
151
|
}
|
|
127
|
-
if (typeof obj[
|
|
128
|
-
throw new QuiverError(
|
|
152
|
+
if (typeof obj['name'] !== 'string' || obj['name'].length === 0) {
|
|
153
|
+
throw new QuiverError('quiver_invalid', 'Manifest must have a non-empty string "name" field');
|
|
129
154
|
}
|
|
130
|
-
if (!Array.isArray(obj[
|
|
131
|
-
throw new QuiverError(
|
|
155
|
+
if (!Array.isArray(obj['quills'])) {
|
|
156
|
+
throw new QuiverError('quiver_invalid', 'Manifest must have a "quills" array');
|
|
132
157
|
}
|
|
133
158
|
const quills = [];
|
|
134
|
-
for (let i = 0; i < obj[
|
|
135
|
-
const entry = obj[
|
|
136
|
-
if (typeof entry !==
|
|
137
|
-
throw new QuiverError(
|
|
159
|
+
for (let i = 0; i < obj['quills'].length; i++) {
|
|
160
|
+
const entry = obj['quills'][i];
|
|
161
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
162
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}] must be an object`);
|
|
138
163
|
}
|
|
139
164
|
const e = entry;
|
|
140
|
-
assertNoUnknownKeys(e, [
|
|
141
|
-
if (typeof e[
|
|
142
|
-
throw new QuiverError(
|
|
165
|
+
assertNoUnknownKeys(e, ['name', 'version', 'bundle', 'fonts'], `manifest.quills[${i}]`);
|
|
166
|
+
if (typeof e['name'] !== 'string' || e['name'].length === 0) {
|
|
167
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].name must be a non-empty string`);
|
|
143
168
|
}
|
|
144
|
-
if (typeof e[
|
|
145
|
-
|
|
146
|
-
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].version must be canonical semver (x.y.z), got "${String(e["version"])}"`);
|
|
169
|
+
if (typeof e['version'] !== 'string' || !isCanonicalSemver(e['version'])) {
|
|
170
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].version must be canonical semver (x.y.z), got "${String(e['version'])}"`);
|
|
147
171
|
}
|
|
148
|
-
if (typeof e[
|
|
149
|
-
|
|
150
|
-
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].bundle must be a non-empty string`);
|
|
172
|
+
if (typeof e['bundle'] !== 'string' || e['bundle'].length === 0) {
|
|
173
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].bundle must be a non-empty string`);
|
|
151
174
|
}
|
|
152
|
-
|
|
153
|
-
if (typeof e[
|
|
154
|
-
|
|
155
|
-
Array.isArray(e["fonts"])) {
|
|
156
|
-
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].fonts must be an object`);
|
|
175
|
+
digestOfName(BUNDLE_FILENAME_RE, e['bundle'], `manifest.quills[${i}].bundle filename`);
|
|
176
|
+
if (typeof e['fonts'] !== 'object' || e['fonts'] === null || Array.isArray(e['fonts'])) {
|
|
177
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].fonts must be an object`);
|
|
157
178
|
}
|
|
158
|
-
const fonts = e[
|
|
179
|
+
const fonts = e['fonts'];
|
|
159
180
|
for (const [k, v] of Object.entries(fonts)) {
|
|
160
|
-
if (typeof v !==
|
|
161
|
-
throw new QuiverError(
|
|
181
|
+
if (typeof v !== 'string') {
|
|
182
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].fonts["${k}"] must be a string`);
|
|
162
183
|
}
|
|
163
184
|
validateFontHash(v, `manifest.quills[${i}].fonts["${k}"]`);
|
|
164
185
|
}
|
|
165
186
|
quills.push({
|
|
166
|
-
name: e[
|
|
167
|
-
version: e[
|
|
168
|
-
bundle: e[
|
|
169
|
-
fonts: fonts
|
|
187
|
+
name: e['name'],
|
|
188
|
+
version: e['version'],
|
|
189
|
+
bundle: e['bundle'],
|
|
190
|
+
fonts: fonts
|
|
170
191
|
});
|
|
171
192
|
}
|
|
172
193
|
return {
|
|
173
194
|
version: 1,
|
|
174
|
-
name: obj[
|
|
175
|
-
quills
|
|
195
|
+
name: obj['name'],
|
|
196
|
+
quills
|
|
176
197
|
};
|
|
177
198
|
}
|
|
178
199
|
// ─── Catalog assembly ─────────────────────────────────────────────────────────
|
|
179
200
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
201
|
+
* Name → versions (descending), derived from the index. One structure backs
|
|
202
|
+
* both the loader's lookups and the Quiver's catalog, so the two cannot
|
|
203
|
+
* disagree about what the manifest holds.
|
|
183
204
|
*/
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const index = new Map();
|
|
189
|
-
for (const entry of manifest.quills) {
|
|
190
|
-
const key = `${entry.name}@${entry.version}`;
|
|
191
|
-
if (index.has(key)) {
|
|
192
|
-
throw new QuiverError("quiver_invalid", `Duplicate quill entry in manifest: "${key}"`);
|
|
193
|
-
}
|
|
194
|
-
index.set(key, entry);
|
|
195
|
-
const versions = catalogRaw.get(entry.name) ?? [];
|
|
205
|
+
function catalogOf(index) {
|
|
206
|
+
const catalog = new Map();
|
|
207
|
+
for (const entry of index.values()) {
|
|
208
|
+
const versions = catalog.get(entry.name) ?? [];
|
|
196
209
|
versions.push(entry.version);
|
|
197
|
-
|
|
210
|
+
catalog.set(entry.name, versions);
|
|
198
211
|
}
|
|
199
|
-
for (const
|
|
212
|
+
for (const versions of catalog.values()) {
|
|
200
213
|
versions.sort((a, b) => compareSemver(b, a));
|
|
201
214
|
}
|
|
202
|
-
|
|
203
|
-
return Quiver._fromLoader(manifest.name, catalogRaw, loader);
|
|
215
|
+
return catalog;
|
|
204
216
|
}
|
|
205
|
-
// ─── Main entry
|
|
217
|
+
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
206
218
|
/**
|
|
207
219
|
* Load a build-output quiver via the given transport.
|
|
208
220
|
*
|
|
@@ -212,36 +224,38 @@ function buildQuiverFromManifestBytes(transport, manifestBytes) {
|
|
|
212
224
|
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
213
225
|
*/
|
|
214
226
|
export async function loadBuiltQuiver(transport) {
|
|
215
|
-
// 1. Fetch and parse pointer.
|
|
227
|
+
// 1. Fetch and parse pointer. It is the one name that is not
|
|
228
|
+
// content-addressed, so it is also the one fetch that must revalidate.
|
|
216
229
|
let pointerBytes;
|
|
217
230
|
try {
|
|
218
|
-
pointerBytes = await transport.fetchBytes(
|
|
231
|
+
pointerBytes = await transport.fetchBytes('latest.json', { revalidate: true });
|
|
219
232
|
}
|
|
220
233
|
catch (err) {
|
|
221
234
|
if (err instanceof QuiverError)
|
|
222
235
|
throw err;
|
|
223
|
-
throw new QuiverError(
|
|
236
|
+
throw new QuiverError('transport_error', `Failed to fetch latest.json: ${err.message}`, { cause: err });
|
|
224
237
|
}
|
|
225
238
|
const manifestFileName = parsePointer(new TextDecoder().decode(pointerBytes));
|
|
226
|
-
|
|
239
|
+
const manifestDigest = digestOfName(MANIFEST_FILENAME_RE, manifestFileName, 'Pointer manifest filename');
|
|
227
240
|
// 2. Fetch and parse manifest.
|
|
228
241
|
let manifestBytes;
|
|
229
242
|
try {
|
|
230
|
-
manifestBytes = await transport
|
|
243
|
+
manifestBytes = await fetchVerified(transport, manifestFileName, manifestDigest);
|
|
231
244
|
}
|
|
232
245
|
catch (err) {
|
|
233
246
|
if (err instanceof QuiverError)
|
|
234
247
|
throw err;
|
|
235
|
-
throw new QuiverError(
|
|
248
|
+
throw new QuiverError('transport_error', `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
|
|
236
249
|
}
|
|
237
250
|
// 3–4. Validate manifest + assemble catalog/loader.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
251
|
+
const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
|
|
252
|
+
const index = new Map();
|
|
253
|
+
for (const entry of manifest.quills) {
|
|
254
|
+
const key = `${entry.name}@${entry.version}`;
|
|
255
|
+
if (index.has(key)) {
|
|
256
|
+
throw new QuiverError('quiver_invalid', `Duplicate quill entry in manifest: "${key}"`);
|
|
257
|
+
}
|
|
258
|
+
index.set(key, entry);
|
|
259
|
+
}
|
|
260
|
+
return createQuiver(manifest.name, catalogOf(index), new BuiltLoader(transport, index));
|
|
247
261
|
}
|
package/dist/bundle.js
CHANGED
package/dist/digest.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content addressing. Browser-safe.
|
|
3
|
+
*
|
|
4
|
+
* `crypto.subtle` is the one digest primitive a browser and Node both expose,
|
|
5
|
+
* and it exists only in a secure context: a page served over plain `http://`
|
|
6
|
+
* to something other than localhost has no digest at all. Verification
|
|
7
|
+
* degrades to a no-op there rather than failing every fetch, so `undefined`
|
|
8
|
+
* means "cannot check", never "check failed". Node and any https page always
|
|
9
|
+
* have one.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Digest width in the names `build` writes, in hex chars: 48 bits of SHA-256.
|
|
13
|
+
* Wide enough that a manifest name colliding with a prior release's, which
|
|
14
|
+
* under immutable CDN caching serves the old catalog forever, is not a birthday
|
|
15
|
+
* problem within any release count a quiver will see.
|
|
16
|
+
*/
|
|
17
|
+
export declare const NAME_DIGEST_LENGTH = 12;
|
|
18
|
+
/** SHA-256 over `bytes` as lowercase hex, or undefined where no digest exists. */
|
|
19
|
+
export declare function sha256Hex(bytes: Uint8Array): Promise<string | undefined>;
|
package/dist/digest.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content addressing. Browser-safe.
|
|
3
|
+
*
|
|
4
|
+
* `crypto.subtle` is the one digest primitive a browser and Node both expose,
|
|
5
|
+
* and it exists only in a secure context: a page served over plain `http://`
|
|
6
|
+
* to something other than localhost has no digest at all. Verification
|
|
7
|
+
* degrades to a no-op there rather than failing every fetch, so `undefined`
|
|
8
|
+
* means "cannot check", never "check failed". Node and any https page always
|
|
9
|
+
* have one.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Digest width in the names `build` writes, in hex chars: 48 bits of SHA-256.
|
|
13
|
+
* Wide enough that a manifest name colliding with a prior release's, which
|
|
14
|
+
* under immutable CDN caching serves the old catalog forever, is not a birthday
|
|
15
|
+
* problem within any release count a quiver will see.
|
|
16
|
+
*/
|
|
17
|
+
export const NAME_DIGEST_LENGTH = 12;
|
|
18
|
+
/** SHA-256 over `bytes` as lowercase hex, or undefined where no digest exists. */
|
|
19
|
+
export async function sha256Hex(bytes) {
|
|
20
|
+
const subtle = globalThis.crypto?.subtle;
|
|
21
|
+
if (subtle === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
const digest = await subtle.digest('SHA-256', bytes);
|
|
24
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
25
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type QuiverErrorCode =
|
|
1
|
+
export type QuiverErrorCode = 'invalid_ref' | 'quill_not_found' | 'quiver_invalid' | 'transport_error';
|
|
2
2
|
export declare class QuiverError extends Error {
|
|
3
3
|
readonly code: QuiverErrorCode;
|
|
4
4
|
/** Offending ref string, when available. */
|
package/dist/errors.js
CHANGED
|
@@ -8,7 +8,7 @@ export class QuiverError extends Error {
|
|
|
8
8
|
quiverName;
|
|
9
9
|
constructor(code, message, options) {
|
|
10
10
|
super(message, { cause: options?.cause });
|
|
11
|
-
this.name =
|
|
11
|
+
this.name = 'QuiverError';
|
|
12
12
|
this.code = code;
|
|
13
13
|
this.ref = options?.ref;
|
|
14
14
|
this.version = options?.version;
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built artifact's format, stamped into `latest.json` and refused to be read past
|
|
3
|
+
* (QUIVER §"The pointer" for why the pointer carries it and reads past keys it does not
|
|
4
|
+
* know).
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export declare const POINTER_FORMAT = 1;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built artifact's format, stamped into `latest.json` and refused to be read past
|
|
3
|
+
* (QUIVER §"The pointer" for why the pointer carries it and reads past keys it does not
|
|
4
|
+
* know).
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export const POINTER_FORMAT = 1;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { QuiverError } from
|
|
2
|
-
export type { QuiverErrorCode } from
|
|
3
|
-
export { Quiver } from
|
|
1
|
+
export { QuiverError } from './errors.js';
|
|
2
|
+
export type { QuiverErrorCode } from './errors.js';
|
|
3
|
+
export { Quiver } from './quiver.js';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Main browser-safe entrypoint.
|
|
2
2
|
//
|
|
3
|
-
// Exposes only browser-safe surface. Node-only factories
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
export { Quiver } from "./quiver.js";
|
|
3
|
+
// Exposes only browser-safe surface. The Node-only factories (`fromDir`,
|
|
4
|
+
// `fromBuiltDir`, `build`) live at `@quillmark/quiver/node`.
|
|
5
|
+
export { QuiverError } from './errors.js';
|
|
6
|
+
export { Quiver } from './quiver.js';
|
|
8
7
|
// The canonical API (`Quill`, `Document`, `Engine`, `RenderResult`, …) is not
|
|
9
8
|
// re-exported: import it straight from the `@quillmark/wasm` peer dependency,
|
|
10
9
|
// which is the single source of truth. `quiver.getQuill(ref)` returns a core
|
package/dist/node.d.ts
CHANGED
|
@@ -1,81 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node-only entrypoint
|
|
2
|
+
* Node-only entrypoint: the factories that read a filesystem, as free
|
|
3
|
+
* functions. Each returns a `Quiver`; the class itself is the browser-safe one
|
|
4
|
+
* from the main entry, unmodified. Importing this module installs nothing, so
|
|
5
|
+
* the import path is the whole contract and a bundler drops what is not called.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* Bundler note: this entry pulls in `./source-loader.js`, `./build.js`, and
|
|
8
|
+
* `./transports/fs-built-transport.js`, all of which statically import `node:*`
|
|
9
|
+
* builtins. Browser bundles must never reach it. The main entry (`./index.js`)
|
|
10
|
+
* makes no static or dynamic reference to it.
|
|
11
|
+
*/
|
|
12
|
+
import { Quiver } from './quiver.js';
|
|
13
|
+
/**
|
|
14
|
+
* Reads a Source Quiver from a local directory containing `Quiver.yaml` and
|
|
15
|
+
* `quills/<name>/<version>/Quill.yaml` entries.
|
|
16
|
+
*
|
|
17
|
+
* Also accepts an `import.meta.url`-style `file://` URL; the URL's parent
|
|
18
|
+
* directory is used as the source root.
|
|
19
|
+
*
|
|
20
|
+
* A quiver installed from npm is a directory like any other:
|
|
21
|
+
* `dirname(createRequire(import.meta.url).resolve('<pkg>/Quiver.yaml'))` is its
|
|
22
|
+
* root, resolved from the caller, whose dependencies are reachable from the
|
|
23
|
+
* caller alone under an isolated `node_modules` layout.
|
|
24
|
+
*
|
|
25
|
+
* Throws `quiver_invalid` on schema violations, `transport_error` on I/O
|
|
26
|
+
* failure.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fromDir(pathOrFileUrl: string): Promise<Quiver>;
|
|
29
|
+
/**
|
|
30
|
+
* Loads a packed (build-output) quiver from a local directory containing
|
|
31
|
+
* `latest.json` and the manifest/bundle/store files written by `build`.
|
|
32
|
+
* Symmetric to `Quiver.fromBuiltUrl(url)` but reads from disk instead of HTTP:
|
|
33
|
+
* no network required.
|
|
34
|
+
*
|
|
35
|
+
* Use this for server-side runtime when a packed artifact ships in the
|
|
36
|
+
* deployment image; consumers can keep source quivers as devDependencies and
|
|
37
|
+
* avoid self-fetching over their own load balancer.
|
|
8
38
|
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
39
|
+
* Throws `quiver_invalid` on format errors, `transport_error` on I/O failure.
|
|
40
|
+
*/
|
|
41
|
+
export declare function fromBuiltDir(dirPath: string): Promise<Quiver>;
|
|
42
|
+
/**
|
|
43
|
+
* Reads the Source Quiver at sourceDir, validates it, and writes the runtime
|
|
44
|
+
* build artifact to outDir, which the build clears first and therefore owns.
|
|
14
45
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* statically import `node:*` builtins. Browser bundles must never reach this
|
|
18
|
-
* module. The main entry (`./index.js`) makes no static or dynamic reference
|
|
19
|
-
* to it.
|
|
46
|
+
* Throws `quiver_invalid` on source validation failures, `transport_error` on
|
|
47
|
+
* I/O failures and on an outDir holding the source quiver or the cwd.
|
|
20
48
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* Resolves an npm specifier against `node_modules` and loads the source
|
|
26
|
-
* layout at the package root. The resolved package must have `Quiver.yaml`
|
|
27
|
-
* at its root.
|
|
28
|
-
*
|
|
29
|
-
* Throws `transport_error` on resolution/I/O failure, `quiver_invalid` on
|
|
30
|
-
* schema violations.
|
|
31
|
-
*/
|
|
32
|
-
fromPackage(specifier: string): Promise<Base>;
|
|
33
|
-
/**
|
|
34
|
-
* Reads a Source Quiver from a local directory containing `Quiver.yaml`
|
|
35
|
-
* and `quills/<name>/<version>/Quill.yaml` entries.
|
|
36
|
-
*
|
|
37
|
-
* Also accepts `import.meta.url`-style `file://` URLs as a convenience for
|
|
38
|
-
* tests; the URL's parent directory is used as the source root.
|
|
39
|
-
*
|
|
40
|
-
* Throws `quiver_invalid` on schema violations, `transport_error` on I/O
|
|
41
|
-
* failure.
|
|
42
|
-
*/
|
|
43
|
-
fromDir(pathOrFileUrl: string): Promise<Base>;
|
|
44
|
-
/**
|
|
45
|
-
* Loads a packed (build-output) quiver from a local directory containing
|
|
46
|
-
* `latest.json` and the manifest/bundle/store files written by
|
|
47
|
-
* `Quiver.build`. Symmetric to `fromBuiltUrl(url)` but reads from disk
|
|
48
|
-
* instead of HTTP — no network required.
|
|
49
|
-
*
|
|
50
|
-
* Use this for server-side runtime when a packed artifact ships in the
|
|
51
|
-
* deployment image; consumers can keep source quivers as devDependencies
|
|
52
|
-
* and avoid self-fetching over their own load balancer.
|
|
53
|
-
*
|
|
54
|
-
* Throws `quiver_invalid` on format errors, `transport_error` on I/O
|
|
55
|
-
* failure.
|
|
56
|
-
*/
|
|
57
|
-
fromBuiltDir(dirPath: string): Promise<Base>;
|
|
58
|
-
/**
|
|
59
|
-
* Reads the Source Quiver at sourceDir, validates it, and writes the
|
|
60
|
-
* runtime build artifact to outDir.
|
|
61
|
-
*
|
|
62
|
-
* Throws `quiver_invalid` on source validation failures, `transport_error`
|
|
63
|
-
* on I/O failures.
|
|
64
|
-
*/
|
|
65
|
-
build(sourceDir: string, outDir: string, opts?: BuildOptions): Promise<void>;
|
|
66
|
-
/**
|
|
67
|
-
* Resolves an npm specifier against `node_modules` and builds the source
|
|
68
|
-
* layout at the package root. The resolved package must have `Quiver.yaml`
|
|
69
|
-
* at its root. Symmetric to `fromPackage` but writes a runtime build
|
|
70
|
-
* artifact to outDir instead of loading.
|
|
71
|
-
*
|
|
72
|
-
* Throws `transport_error` on resolution/I/O failure, `quiver_invalid` on
|
|
73
|
-
* source validation failures.
|
|
74
|
-
*/
|
|
75
|
-
buildPackage(specifier: string, outDir: string, opts?: BuildOptions): Promise<void>;
|
|
76
|
-
};
|
|
77
|
-
export type Quiver = Base;
|
|
78
|
-
export declare const Quiver: typeof Base & NodeQuiverStatics;
|
|
79
|
-
export { QuiverError } from "./errors.js";
|
|
80
|
-
export type { QuiverErrorCode } from "./errors.js";
|
|
81
|
-
export type { BuildOptions } from "./build.js";
|
|
49
|
+
export declare function build(sourceDir: string, outDir: string): Promise<void>;
|
|
50
|
+
export { Quiver } from './quiver.js';
|
|
51
|
+
export { QuiverError } from './errors.js';
|
|
52
|
+
export type { QuiverErrorCode } from './errors.js';
|