@quillmark/quiver 0.15.0 → 0.17.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.
@@ -1,34 +1,54 @@
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 { createQuiver } from './quiver.js';
13
+ /**
14
+ * Fetch, then check the bytes against the digest their name carries. That
15
+ * check is what makes "content-addressed, safe to cache forever" a property
16
+ * rather than a hope: it catches a corrupted CDN object, a partial sync, and a
17
+ * name reused across releases. A mismatch is a transport failure (the bytes
18
+ * that arrived are not the bytes asked for), so the caches that evict on error
19
+ * let a retry succeed.
8
20
  *
9
- * NO static node: imports — this module is safe to load in browser contexts.
21
+ * Where no digest primitive exists (a page served over plain http, which is
22
+ * not a secure context) the fetch passes through unchecked.
10
23
  */
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}"`);
24
+ async function fetchVerified(transport, path, expected) {
25
+ const bytes = await transport.fetchBytes(path);
26
+ const actual = await sha256Hex(bytes);
27
+ if (actual !== undefined && !actual.startsWith(expected)) {
28
+ throw new QuiverError('transport_error', `Digest mismatch for "${path}": the bytes hash to ${actual.slice(0, expected.length)}, not ${expected}`);
22
29
  }
30
+ return bytes;
23
31
  }
24
- function validateBundleFileName(bundle, context) {
25
- if (!BUNDLE_FILENAME_RE.test(bundle)) {
26
- throw new QuiverError("quiver_invalid", `${context}: bundle filename is invalid: "${bundle}"`);
27
- }
32
+ // ─── Path validation ──────────────────────────────────────────────────────────
33
+ // Each name carries the digest of what it names; the capture group is the
34
+ // digest the fetch is checked against, so path validation and verification
35
+ // read the same character span. `build` writes exactly NAME_DIGEST_LENGTH
36
+ // chars for bundles and manifests and the full 64 for store entries.
37
+ const DIGEST = `[0-9a-f]{${NAME_DIGEST_LENGTH},}`;
38
+ const MANIFEST_FILENAME_RE = new RegExp(`^manifest\\.(${DIGEST})\\.json$`);
39
+ const BUNDLE_FILENAME_RE = new RegExp(`^[A-Za-z0-9_.-]+@[0-9]+\\.[0-9]+\\.[0-9]+\\.(${DIGEST})\\.zip$`);
40
+ const FONT_HASH_RE = /^[0-9a-f]{64}$/;
41
+ /** The digest a content-addressed filename carries. Throws `quiver_invalid`. */
42
+ function digestOfName(re, name, what) {
43
+ const match = re.exec(name);
44
+ if (match === null) {
45
+ throw new QuiverError('quiver_invalid', `${what} is invalid: "${name}"`);
46
+ }
47
+ return match[1];
28
48
  }
29
49
  function validateFontHash(hash, context) {
30
50
  if (!FONT_HASH_RE.test(hash)) {
31
- throw new QuiverError("quiver_invalid", `${context}: font hash is invalid: "${hash}"`);
51
+ throw new QuiverError('quiver_invalid', `${context}: font hash is invalid: "${hash}"`);
32
52
  }
33
53
  }
34
54
  // ─── BuiltLoader implementation ─────────────────────────────────────────────
@@ -44,16 +64,11 @@ class BuiltLoader {
44
64
  this.index = index;
45
65
  }
46
66
  async loadTree(name, version) {
67
+ // The catalog the outer Quiver resolves against derives from this index,
68
+ // so every ref reaching here has an entry.
47
69
  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
70
  // 1. Fetch + unpack bundle zip.
56
- const zipBytes = await this.transport.fetchBytes(entry.bundle);
71
+ const zipBytes = await fetchVerified(this.transport, entry.bundle, digestOfName(BUNDLE_FILENAME_RE, entry.bundle, 'bundle filename'));
57
72
  const files = unpackFiles(zipBytes);
58
73
  // 2. Rehydrate fonts from store (coalesced).
59
74
  const fontEntries = Object.entries(entry.fonts);
@@ -71,9 +86,7 @@ class BuiltLoader {
71
86
  fetchFont(hash) {
72
87
  let promise = this.fontCache.get(hash);
73
88
  if (!promise) {
74
- promise = this.transport
75
- .fetchBytes(`store/${hash}`)
76
- .catch((err) => {
89
+ promise = fetchVerified(this.transport, `store/${hash}`, hash).catch((err) => {
77
90
  this.fontCache.delete(hash);
78
91
  throw err;
79
92
  });
@@ -86,7 +99,7 @@ class BuiltLoader {
86
99
  function assertNoUnknownKeys(obj, allowed, context) {
87
100
  for (const key of Object.keys(obj)) {
88
101
  if (!allowed.includes(key)) {
89
- throw new QuiverError("quiver_invalid", `${context}: unknown field "${key}"`);
102
+ throw new QuiverError('quiver_invalid', `${context}: unknown field "${key}"`);
90
103
  }
91
104
  }
92
105
  }
@@ -96,17 +109,17 @@ function parsePointer(raw) {
96
109
  parsed = JSON.parse(raw);
97
110
  }
98
111
  catch {
99
- throw new QuiverError("quiver_invalid", "Quiver.json contains invalid JSON");
112
+ throw new QuiverError('quiver_invalid', 'latest.json contains invalid JSON');
100
113
  }
101
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
102
- throw new QuiverError("quiver_invalid", "Quiver.json must be a JSON object");
114
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
115
+ throw new QuiverError('quiver_invalid', 'latest.json must be a JSON object');
103
116
  }
104
117
  const obj = parsed;
105
- assertNoUnknownKeys(obj, ["manifest"], "Quiver.json");
106
- if (typeof obj["manifest"] !== "string" || obj["manifest"].length === 0) {
107
- throw new QuiverError("quiver_invalid", 'Quiver.json must have a non-empty string "manifest" field');
118
+ assertNoUnknownKeys(obj, ['manifest'], 'latest.json');
119
+ if (typeof obj['manifest'] !== 'string' || obj['manifest'].length === 0) {
120
+ throw new QuiverError('quiver_invalid', 'latest.json must have a non-empty string "manifest" field');
108
121
  }
109
- return obj["manifest"];
122
+ return obj['manifest'];
110
123
  }
111
124
  function parseManifest(raw) {
112
125
  let parsed;
@@ -114,119 +127,123 @@ function parseManifest(raw) {
114
127
  parsed = JSON.parse(raw);
115
128
  }
116
129
  catch {
117
- throw new QuiverError("quiver_invalid", "Manifest file contains invalid JSON");
130
+ throw new QuiverError('quiver_invalid', 'Manifest file contains invalid JSON');
118
131
  }
119
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
120
- throw new QuiverError("quiver_invalid", "Manifest must be a JSON object");
132
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
133
+ throw new QuiverError('quiver_invalid', 'Manifest must be a JSON object');
121
134
  }
122
135
  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"])}`);
136
+ assertNoUnknownKeys(obj, ['version', 'name', 'quills'], 'manifest');
137
+ if (obj['version'] !== 1) {
138
+ throw new QuiverError('quiver_invalid', `Manifest version must be 1, got ${String(obj['version'])}`);
126
139
  }
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');
140
+ if (typeof obj['name'] !== 'string' || obj['name'].length === 0) {
141
+ throw new QuiverError('quiver_invalid', 'Manifest must have a non-empty string "name" field');
129
142
  }
130
- if (!Array.isArray(obj["quills"])) {
131
- throw new QuiverError("quiver_invalid", 'Manifest must have a "quills" array');
143
+ if (!Array.isArray(obj['quills'])) {
144
+ throw new QuiverError('quiver_invalid', 'Manifest must have a "quills" array');
132
145
  }
133
146
  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`);
147
+ for (let i = 0; i < obj['quills'].length; i++) {
148
+ const entry = obj['quills'][i];
149
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
150
+ throw new QuiverError('quiver_invalid', `manifest.quills[${i}] must be an object`);
138
151
  }
139
152
  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`);
153
+ assertNoUnknownKeys(e, ['name', 'version', 'bundle', 'fonts'], `manifest.quills[${i}]`);
154
+ if (typeof e['name'] !== 'string' || e['name'].length === 0) {
155
+ throw new QuiverError('quiver_invalid', `manifest.quills[${i}].name must be a non-empty string`);
143
156
  }
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"])}"`);
157
+ if (typeof e['version'] !== 'string' || !isCanonicalSemver(e['version'])) {
158
+ throw new QuiverError('quiver_invalid', `manifest.quills[${i}].version must be canonical semver (x.y.z), got "${String(e['version'])}"`);
147
159
  }
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`);
160
+ if (typeof e['bundle'] !== 'string' || e['bundle'].length === 0) {
161
+ throw new QuiverError('quiver_invalid', `manifest.quills[${i}].bundle must be a non-empty string`);
151
162
  }
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`);
163
+ digestOfName(BUNDLE_FILENAME_RE, e['bundle'], `manifest.quills[${i}].bundle filename`);
164
+ if (typeof e['fonts'] !== 'object' || e['fonts'] === null || Array.isArray(e['fonts'])) {
165
+ throw new QuiverError('quiver_invalid', `manifest.quills[${i}].fonts must be an object`);
157
166
  }
158
- const fonts = e["fonts"];
167
+ const fonts = e['fonts'];
159
168
  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`);
169
+ if (typeof v !== 'string') {
170
+ throw new QuiverError('quiver_invalid', `manifest.quills[${i}].fonts["${k}"] must be a string`);
162
171
  }
163
172
  validateFontHash(v, `manifest.quills[${i}].fonts["${k}"]`);
164
173
  }
165
174
  quills.push({
166
- name: e["name"],
167
- version: e["version"],
168
- bundle: e["bundle"],
169
- fonts: fonts,
175
+ name: e['name'],
176
+ version: e['version'],
177
+ bundle: e['bundle'],
178
+ fonts: fonts
170
179
  });
171
180
  }
172
181
  return {
173
182
  version: 1,
174
- name: obj["name"],
175
- quills,
183
+ name: obj['name'],
184
+ quills
176
185
  };
177
186
  }
187
+ // ─── Catalog assembly ─────────────────────────────────────────────────────────
188
+ /**
189
+ * Name → versions (descending), derived from the index. One structure backs
190
+ * both the loader's lookups and the Quiver's catalog, so the two cannot
191
+ * disagree about what the manifest holds.
192
+ */
193
+ function catalogOf(index) {
194
+ const catalog = new Map();
195
+ for (const entry of index.values()) {
196
+ const versions = catalog.get(entry.name) ?? [];
197
+ versions.push(entry.version);
198
+ catalog.set(entry.name, versions);
199
+ }
200
+ for (const versions of catalog.values()) {
201
+ versions.sort((a, b) => compareSemver(b, a));
202
+ }
203
+ return catalog;
204
+ }
178
205
  // ─── Main entry point ─────────────────────────────────────────────────────────
179
206
  /**
180
207
  * Load a build-output quiver via the given transport.
181
208
  *
182
- * 1. Fetches Quiver.json (pointer) and parses it.
209
+ * 1. Fetches latest.json (pointer) and parses it.
183
210
  * 2. Fetches the manifest file it points to and validates it.
184
211
  * 3. Builds a catalog from manifest entries (versions sorted descending).
185
212
  * 4. Returns a Quiver instance backed by a BuiltLoader.
186
213
  */
187
214
  export async function loadBuiltQuiver(transport) {
188
- // 1. Fetch and parse pointer.
215
+ // 1. Fetch and parse pointer. It is the one name that is not
216
+ // content-addressed, so it is also the one fetch that must revalidate.
189
217
  let pointerBytes;
190
218
  try {
191
- pointerBytes = await transport.fetchBytes("Quiver.json");
219
+ pointerBytes = await transport.fetchBytes('latest.json', { revalidate: true });
192
220
  }
193
221
  catch (err) {
194
222
  if (err instanceof QuiverError)
195
223
  throw err;
196
- throw new QuiverError("transport_error", `Failed to fetch Quiver.json: ${err.message}`, { cause: err });
224
+ throw new QuiverError('transport_error', `Failed to fetch latest.json: ${err.message}`, { cause: err });
197
225
  }
198
226
  const manifestFileName = parsePointer(new TextDecoder().decode(pointerBytes));
199
- validateManifestFileName(manifestFileName);
227
+ const manifestDigest = digestOfName(MANIFEST_FILENAME_RE, manifestFileName, 'Pointer manifest filename');
200
228
  // 2. Fetch and parse manifest.
201
229
  let manifestBytes;
202
230
  try {
203
- manifestBytes = await transport.fetchBytes(manifestFileName);
231
+ manifestBytes = await fetchVerified(transport, manifestFileName, manifestDigest);
204
232
  }
205
233
  catch (err) {
206
234
  if (err instanceof QuiverError)
207
235
  throw err;
208
- throw new QuiverError("transport_error", `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
236
+ throw new QuiverError('transport_error', `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
209
237
  }
238
+ // 3–4. Validate manifest + assemble catalog/loader.
210
239
  const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
211
- // 3. Build catalog: name → versions sorted descending.
212
- // Also build index map: "name@version" → entry (with duplicate detection).
213
- const catalogRaw = new Map();
214
240
  const index = new Map();
215
241
  for (const entry of manifest.quills) {
216
242
  const key = `${entry.name}@${entry.version}`;
217
243
  if (index.has(key)) {
218
- throw new QuiverError("quiver_invalid", `Duplicate quill entry in manifest: "${key}"`);
244
+ throw new QuiverError('quiver_invalid', `Duplicate quill entry in manifest: "${key}"`);
219
245
  }
220
246
  index.set(key, entry);
221
- const versions = catalogRaw.get(entry.name) ?? [];
222
- versions.push(entry.version);
223
- catalogRaw.set(entry.name, versions);
224
- }
225
- for (const [, versions] of catalogRaw) {
226
- versions.sort((a, b) => compareSemver(b, a));
227
247
  }
228
- // 4. Build loader.
229
- const loader = new BuiltLoader(transport, index);
230
- // 5. Return Quiver via internal factory.
231
- return Quiver._fromLoader(manifest.name, catalogRaw, loader);
248
+ return createQuiver(manifest.name, catalogOf(index), new BuiltLoader(transport, index));
232
249
  }
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;
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
- * `Quiver.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';