@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.
@@ -1,34 +1,55 @@
1
1
  /**
2
- * Built-quiver loader — browser-safe at module level.
3
- * Internal; not exported from index.ts.
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
- * Exposes:
6
- * - BuiltTransport interface (implemented by HttpTransport)
7
- * - loadBuiltQuiver(transport) → Quiver
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
- * NO static node: imports — this module is safe to load in browser contexts.
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
- import { QuiverError } from "./errors.js";
12
- import { unpackFiles } from "./bundle.js";
13
- import { isCanonicalSemver, compareSemver } from "./semver.js";
14
- import { Quiver } from "./quiver.js";
15
- // ─── Path validation ──────────────────────────────────────────────────────────
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
- function validateBundleFileName(bundle, context) {
25
- if (!BUNDLE_FILENAME_RE.test(bundle)) {
26
- throw new QuiverError("quiver_invalid", `${context}: bundle filename is invalid: "${bundle}"`);
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("quiver_invalid", `${context}: font hash is invalid: "${hash}"`);
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.fetchBytes(entry.bundle);
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("quiver_invalid", `${context}: unknown field "${key}"`);
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("quiver_invalid", "latest.json contains invalid JSON");
113
+ throw new QuiverError('quiver_invalid', 'latest.json contains invalid JSON');
100
114
  }
101
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
102
- throw new QuiverError("quiver_invalid", "latest.json must be a JSON object");
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
- assertNoUnknownKeys(obj, ["manifest"], "latest.json");
106
- if (typeof obj["manifest"] !== "string" || obj["manifest"].length === 0) {
107
- throw new QuiverError("quiver_invalid", 'latest.json must have a non-empty string "manifest" field');
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["manifest"];
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("quiver_invalid", "Manifest file contains invalid JSON");
142
+ throw new QuiverError('quiver_invalid', 'Manifest file contains invalid JSON');
118
143
  }
119
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
120
- throw new QuiverError("quiver_invalid", "Manifest must be a JSON object");
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, ["version", "name", "quills"], "manifest");
124
- if (obj["version"] !== 1) {
125
- throw new QuiverError("quiver_invalid", `Manifest version must be 1, got ${String(obj["version"])}`);
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["name"] !== "string" || obj["name"].length === 0) {
128
- throw new QuiverError("quiver_invalid", 'Manifest must have a non-empty string "name" field');
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["quills"])) {
131
- throw new QuiverError("quiver_invalid", 'Manifest must have a "quills" array');
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["quills"].length; i++) {
135
- const entry = obj["quills"][i];
136
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
137
- throw new QuiverError("quiver_invalid", `manifest.quills[${i}] must be an object`);
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, ["name", "version", "bundle", "fonts"], `manifest.quills[${i}]`);
141
- if (typeof e["name"] !== "string" || e["name"].length === 0) {
142
- throw new QuiverError("quiver_invalid", `manifest.quills[${i}].name must be a non-empty string`);
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["version"] !== "string" ||
145
- !isCanonicalSemver(e["version"])) {
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["bundle"] !== "string" ||
149
- e["bundle"].length === 0) {
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
- validateBundleFileName(e["bundle"], `manifest.quills[${i}].bundle`);
153
- if (typeof e["fonts"] !== "object" ||
154
- e["fonts"] === null ||
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["fonts"];
179
+ const fonts = e['fonts'];
159
180
  for (const [k, v] of Object.entries(fonts)) {
160
- if (typeof v !== "string") {
161
- throw new QuiverError("quiver_invalid", `manifest.quills[${i}].fonts["${k}"] must be a string`);
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["name"],
167
- version: e["version"],
168
- bundle: e["bundle"],
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["name"],
175
- quills,
195
+ name: obj['name'],
196
+ quills
176
197
  };
177
198
  }
178
199
  // ─── Catalog assembly ─────────────────────────────────────────────────────────
179
200
  /**
180
- * Validate manifest bytes (`quiver_invalid` on format errors) and assemble a
181
- * Quiver backed by a BuiltLoader over `transport`. Shared by `loadBuiltQuiver`
182
- * (pointer-following) and `seedBuiltQuiver` (seed).
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 buildQuiverFromManifestBytes(transport, manifestBytes) {
185
- const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
186
- // catalog: name → versions (desc); index: "name@version" → entry (dedup).
187
- const catalogRaw = new Map();
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
- catalogRaw.set(entry.name, versions);
210
+ catalog.set(entry.name, versions);
198
211
  }
199
- for (const [, versions] of catalogRaw) {
212
+ for (const versions of catalog.values()) {
200
213
  versions.sort((a, b) => compareSemver(b, a));
201
214
  }
202
- const loader = new BuiltLoader(transport, index);
203
- return Quiver._fromLoader(manifest.name, catalogRaw, loader);
215
+ return catalog;
204
216
  }
205
- // ─── Main entry points ────────────────────────────────────────────────────────
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("latest.json");
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("transport_error", `Failed to fetch latest.json: ${err.message}`, { cause: err });
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
- validateManifestFileName(manifestFileName);
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.fetchBytes(manifestFileName);
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("transport_error", `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
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
- return buildQuiverFromManifestBytes(transport, manifestBytes);
239
- }
240
- /**
241
- * Seed a quiver from caller-provided manifest bytes, skipping the latest.json
242
- * pointer fetch. `transport` is used only for lazy bundle/font fetches.
243
- * Throws `quiver_invalid` on malformed bytes.
244
- */
245
- export function seedBuiltQuiver(transport, manifestBytes) {
246
- return buildQuiverFromManifestBytes(transport, manifestBytes);
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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Zip utilities — browser-safe (uses fflate only).
3
3
  */
4
- import { zipSync, unzipSync } from "fflate";
4
+ import { zipSync, unzipSync } from 'fflate';
5
5
  /**
6
6
  * Fixed epoch mtime for deterministic zip output.
7
7
  *
@@ -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 = "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error";
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 = "QuiverError";
11
+ this.name = 'QuiverError';
12
12
  this.code = code;
13
13
  this.ref = options?.ref;
14
14
  this.version = options?.version;
@@ -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 "./errors.js";
2
- export type { QuiverErrorCode } from "./errors.js";
3
- export { Quiver } from "./quiver.js";
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
- // (`Quiver.fromDir`, `fromPackage`, `build`) and the `BuildOptions` type
5
- // live at `@quillmark/quiver/node`.
6
- export { QuiverError } from "./errors.js";
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
- * Importing this module is the consumer's explicit declaration of intent:
5
- * "I am running in Node and want the Node-only Quiver factories." It exposes
6
- * the same `Quiver` class as the main entry, augmented with `fromDir`,
7
- * `fromPackage`, `fromBuiltDir`, `build`, and `buildPackage` static methods.
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
- * Side effect: at module evaluation time, the Node-only static methods are
10
- * installed on the shared `Quiver` constructor. Any other module that already
11
- * imports `Quiver` from the main entry will see the additional methods at
12
- * runtime — but TypeScript will only expose them on the binding imported from
13
- * here, so the import path remains the contract.
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
- * Bundler note: importing this entry pulls in `./source-loader.js`,
16
- * `./build.js`, and `./transports/fs-built-transport.js`, all of which
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
- import { Quiver as Base } from "./quiver.js";
22
- import { type BuildOptions } from "./build.js";
23
- type NodeQuiverStatics = {
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';