@quillmark/quiver 0.28.0 → 0.30.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/MIGRATION.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Upgrade notes per range. The [CHANGELOG](CHANGELOG.md) is the full record; this page is the subset that breaks a working consumer, ordered by how late you find out.
|
|
4
4
|
|
|
5
|
+
## 0.29 → 0.30
|
|
6
|
+
|
|
7
|
+
### Rebuild every artifact, and move quiver and quillkit together
|
|
8
|
+
|
|
9
|
+
A built quiver is `quiver.json` beside one bundle per quill and a `fonts/` directory. The hashed manifest and `store/` are gone, and `latest.json` carries only `format`, now 2. An old artifact under 0.30 is a `transport_error` on `quiver.json`; a 0.30 artifact under an older reader is refused with the upgrade named. Re-run `build`; nothing in the source layout changes.
|
|
10
|
+
|
|
11
|
+
`quillkit`'s client carries its own copy of this package's reader and packs through the collection's, so a collection upgrades `@quillmark/quiver` in the same change as `quillkit`. `quillkit site` and `quillkit studio` refuse a pack with no `quiver.json`, naming the upgrade.
|
|
12
|
+
|
|
13
|
+
A host rule naming `quiver/latest.json` now names `quiver/quiver.json`.
|
|
14
|
+
|
|
15
|
+
### Runtime, not build time
|
|
16
|
+
|
|
17
|
+
**`Quiver.fromBuiltUrl(url, { seed })` takes no `seed`.** TypeScript refuses the second argument; plain JavaScript ignores it, so a seeded call fetches what it used to hold. Hold the whole artifact and call `Quiver.fromBuiltFiles(files)`, or fetch it.
|
|
18
|
+
|
|
19
|
+
**Fetched bytes are not checked against their names**, and no fetch carries a byte ceiling; a bundle's unpack budget is unchanged. A bundle that does not open as a zip and a font that does not open as a font are refused, and `build` refuses a font file of the second kind where it packs it. Serve the artifact over `https`, which is what answers a corrupted byte.
|
|
20
|
+
|
|
5
21
|
## 0.16 → 0.19
|
|
6
22
|
|
|
7
23
|
### Runtime, not build time
|
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ const quiver = await fromBuiltDir('./static/quills/my-quiver');
|
|
|
99
99
|
|
|
100
100
|
## Server-side runtime (no filesystem)
|
|
101
101
|
|
|
102
|
-
A serverless function's packed artifact is not on a path the invocation can read, so `fromBuiltDir` cannot see it. Hand over the bytes instead, keyed by artifact-relative path as `build` writes them (`
|
|
102
|
+
A serverless function's packed artifact is not on a path the invocation can read, so `fromBuiltDir` cannot see it. Hand over the bytes instead, keyed by artifact-relative path as `build` writes them (`quiver.json`, `<name>@<x.y.z>.<digest>.zip`, `fonts/<hash>`):
|
|
103
103
|
|
|
104
104
|
```ts
|
|
105
105
|
import { Quiver } from '@quillmark/quiver';
|
|
@@ -109,30 +109,17 @@ const quiver = await Quiver.fromBuiltFiles(artifactFiles); // Map<string, Uint8A
|
|
|
109
109
|
|
|
110
110
|
Nothing is fetched, so nothing self-fetches over your own load balancer. The map must carry the whole artifact; a path it lacks is a `transport_error` naming that path.
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
## The `quiver.json` index
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
const quiver = await Quiver.fromBuiltUrl('/quills/my-quiver/', {
|
|
116
|
-
seed: new Map([
|
|
117
|
-
['latest.json', pointerBytes],
|
|
118
|
-
[manifestName, manifestBytes]
|
|
119
|
-
])
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
The seed answers first and the URL serves what it does not carry. Seeding `latest.json` also settles which catalog the process reads at deploy time rather than at cache-revalidation time. Seeded bytes are checked against the digest in their name exactly as fetched bytes are.
|
|
124
|
-
|
|
125
|
-
## The `latest.json` pointer
|
|
126
|
-
|
|
127
|
-
`Quiver.fromBuiltUrl(url)` first fetches `<url>/latest.json`, a stable-named pointer to the current manifest. Everything behind that pointer is content-addressed and checked against the digest in its name; the pointer itself is not, so a cache layer can serve a stale one and silently pin the client to the old catalog. It is therefore the one request fetched `no-cache` (revalidate with the origin; a 304 still serves from disk), and every other request is `force-cache`, a digest-carrying name being entitled to whatever the cache already holds. Both are the browser layer only: a stale CDN edge is answered by that host's cache headers, and `quillkit`'s README states the whole contract for a deploy serving a client over one.
|
|
114
|
+
`Quiver.fromBuiltUrl(url)` first fetches `<url>/quiver.json`: the format and the catalog, naming each quill's bundle and fonts. Every other name carries the digest of its own bytes, so a changed file is a changed name and is safe to cache forever; `quiver.json` is not, and a cache layer serving a stale one silently pins the client to the old catalog. It is therefore the one request fetched `no-cache` (revalidate with the origin; a 304 still serves from disk), and every other request is `force-cache`, read once more with `reload` if what the cache answered fails. Both are the browser layer only: a stale CDN edge is answered by that host's cache headers, and `quillkit`'s README states the whole contract for a deploy serving a client over one.
|
|
128
115
|
|
|
129
|
-
|
|
116
|
+
A `getQuill` reads one bundle and the fonts it names, nothing of another quill. Fonts are stored once by hash, so a release that edits a template re-downloads that quill's bundle and not its fonts. A tab still holding the previous release's `quiver.json` rereads it once when a read fails.
|
|
130
117
|
|
|
131
118
|
## What a quiver is trusted to be
|
|
132
119
|
|
|
133
120
|
**A quill is a template the backend executes, so loading a quiver runs its author's code.** Point one at a source you would take a dependency from — an npm package or a git tag, pinned like any other — because nothing here sandboxes a quill, and a collection assembled from anywhere else is a decision to make on purpose.
|
|
134
121
|
|
|
135
|
-
|
|
122
|
+
Nothing checks fetched bytes against their names: the digests are there for caching, and what answers a corrupted or substituted byte is the channel. `quiver.json`, the bundles and the client itself arrive over the same one, which is the whole of why a built quiver is served over `https`.
|
|
136
123
|
|
|
137
124
|
## Error handling
|
|
138
125
|
|
package/dist/build.d.ts
CHANGED
|
@@ -9,22 +9,22 @@
|
|
|
9
9
|
export interface BuildOptions {
|
|
10
10
|
/**
|
|
11
11
|
* Pack versions below `MIN_PUBLISHED_VERSION` too. Off by default: the
|
|
12
|
-
* artifact is a deployment, and the draft space is not part of one.
|
|
13
|
-
*
|
|
12
|
+
* artifact is a deployment, and the draft space is not part of one. A
|
|
13
|
+
* viewer of the collection as it stands turns it on.
|
|
14
14
|
*/
|
|
15
15
|
drafts?: boolean;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Reads a Source Quiver, validates it, and writes the build output to outDir.
|
|
19
19
|
*
|
|
20
|
-
* Output layout. Every name but the
|
|
21
|
-
* names, which is what
|
|
20
|
+
* Output layout. Every name but the two JSON documents carries the SHA-256 of what it
|
|
21
|
+
* names, which is what makes it safe to cache forever:
|
|
22
22
|
* outDir/
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* quiver.json # the format and the catalog
|
|
24
|
+
* latest.json # the format alone, for a reader of format 1
|
|
25
25
|
* <name>@<version>.<sha256:32>.zip # one bundle per quill
|
|
26
|
-
*
|
|
27
|
-
* <sha256>
|
|
26
|
+
* fonts/
|
|
27
|
+
* <sha256> # dehydrated font bytes (full hash, no ext)
|
|
28
28
|
*
|
|
29
29
|
* A generation is assembled beside outDir and moved in whole, so a reader
|
|
30
30
|
* fetching mid-build reads the previous one rather than a torn tree, and a
|
|
@@ -32,7 +32,7 @@ export interface BuildOptions {
|
|
|
32
32
|
*
|
|
33
33
|
* Versions below `MIN_PUBLISHED_VERSION` are drafts and are left out unless
|
|
34
34
|
* `options.drafts` asks for them; a quill with nothing above the floor is
|
|
35
|
-
* absent from
|
|
35
|
+
* absent from `quiver.json` entirely.
|
|
36
36
|
*
|
|
37
37
|
* Throws:
|
|
38
38
|
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
package/dist/build.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { QuiverError } from './errors.js';
|
|
9
9
|
import { packFiles } from './bundle.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { FORMAT, INDEX, LEGACY_POINTER } from './format.js';
|
|
11
|
+
import { isFont } from './signature.js';
|
|
12
12
|
import { isDraft } from './semver.js';
|
|
13
|
+
/** Hex chars of SHA-256 in a bundle's name: enough that a changed bundle never reuses one. */
|
|
14
|
+
const NAME_DIGEST_LENGTH = 32;
|
|
13
15
|
/** Font file extensions recognised by the builder (case-insensitive). */
|
|
14
16
|
const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
15
17
|
/** The two trees a build owns beside its output: where a generation is assembled, and
|
|
@@ -52,14 +54,14 @@ function assertSafeOutDir(path, sourceDir, outDir) {
|
|
|
52
54
|
/**
|
|
53
55
|
* Reads a Source Quiver, validates it, and writes the build output to outDir.
|
|
54
56
|
*
|
|
55
|
-
* Output layout. Every name but the
|
|
56
|
-
* names, which is what
|
|
57
|
+
* Output layout. Every name but the two JSON documents carries the SHA-256 of what it
|
|
58
|
+
* names, which is what makes it safe to cache forever:
|
|
57
59
|
* outDir/
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
+
* quiver.json # the format and the catalog
|
|
61
|
+
* latest.json # the format alone, for a reader of format 1
|
|
60
62
|
* <name>@<version>.<sha256:32>.zip # one bundle per quill
|
|
61
|
-
*
|
|
62
|
-
* <sha256>
|
|
63
|
+
* fonts/
|
|
64
|
+
* <sha256> # dehydrated font bytes (full hash, no ext)
|
|
63
65
|
*
|
|
64
66
|
* A generation is assembled beside outDir and moved in whole, so a reader
|
|
65
67
|
* fetching mid-build reads the previous one rather than a torn tree, and a
|
|
@@ -67,7 +69,7 @@ function assertSafeOutDir(path, sourceDir, outDir) {
|
|
|
67
69
|
*
|
|
68
70
|
* Versions below `MIN_PUBLISHED_VERSION` are drafts and are left out unless
|
|
69
71
|
* `options.drafts` asks for them; a quill with nothing above the floor is
|
|
70
|
-
* absent from
|
|
72
|
+
* absent from `quiver.json` entirely.
|
|
71
73
|
*
|
|
72
74
|
* Throws:
|
|
73
75
|
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
|
@@ -111,14 +113,12 @@ export async function buildQuiver(sourceDir, outDir, options = {}) {
|
|
|
111
113
|
async function packGeneration() {
|
|
112
114
|
try {
|
|
113
115
|
await rm(stage, { recursive: true, force: true });
|
|
114
|
-
await mkdir(join(stage, '
|
|
116
|
+
await mkdir(join(stage, 'fonts'), { recursive: true });
|
|
115
117
|
}
|
|
116
118
|
catch (err) {
|
|
117
119
|
throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
|
|
118
120
|
}
|
|
119
|
-
const
|
|
120
|
-
// Font hashes already in the store. The store is content-addressed, so a font
|
|
121
|
-
// shared across quills or versions is written once.
|
|
121
|
+
const quills = [];
|
|
122
122
|
const stored = new Set();
|
|
123
123
|
for (const [quillName, versions] of catalog) {
|
|
124
124
|
for (const version of versions) {
|
|
@@ -133,17 +133,22 @@ export async function buildQuiver(sourceDir, outDir, options = {}) {
|
|
|
133
133
|
contentRecord[rel] = bytes;
|
|
134
134
|
continue;
|
|
135
135
|
}
|
|
136
|
-
//
|
|
137
|
-
//
|
|
136
|
+
// The loader refuses what does not open as a font, so the build refuses it
|
|
137
|
+
// first, where the author it names can do something about it.
|
|
138
|
+
if (!isFont(bytes, rel)) {
|
|
139
|
+
throw new QuiverError('quiver_invalid', `Quill "${quillName}@${version}": "${rel}" does not open as the font its extension names`, { quiverName: meta.name, version });
|
|
140
|
+
}
|
|
141
|
+
// Full width: fonts are keyed by hash, so two distinct fonts sharing a
|
|
142
|
+
// prefix would merge into one file.
|
|
138
143
|
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
139
144
|
fonts[rel] = hash;
|
|
140
145
|
if (stored.has(hash))
|
|
141
146
|
continue;
|
|
142
147
|
try {
|
|
143
|
-
await writeFile(join(stage, '
|
|
148
|
+
await writeFile(join(stage, 'fonts', hash), bytes);
|
|
144
149
|
}
|
|
145
150
|
catch (err) {
|
|
146
|
-
throw new QuiverError('transport_error', `Failed to write font
|
|
151
|
+
throw new QuiverError('transport_error', `Failed to write font "${join(outDir, 'fonts', hash)}": ${err.message}`, { cause: err });
|
|
147
152
|
}
|
|
148
153
|
stored.add(hash);
|
|
149
154
|
}
|
|
@@ -168,35 +173,23 @@ export async function buildQuiver(sourceDir, outDir, options = {}) {
|
|
|
168
173
|
catch (err) {
|
|
169
174
|
throw new QuiverError('transport_error', `Failed to write bundle "${join(outDir, bundleName)}": ${err.message}`, { cause: err });
|
|
170
175
|
}
|
|
171
|
-
|
|
176
|
+
quills.push({ name: quillName, version, bundle: bundleName, fonts });
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
// The format is stamped here and read first, so a client older than the tree says so
|
|
180
|
+
// rather than misreading it.
|
|
181
|
+
const index = {
|
|
182
|
+
format: FORMAT,
|
|
176
183
|
name: meta.name,
|
|
177
184
|
...(meta.description === undefined ? {} : { description: meta.description }),
|
|
178
|
-
quills
|
|
185
|
+
quills
|
|
179
186
|
};
|
|
180
|
-
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
181
|
-
const manifestHash = createHash('sha256')
|
|
182
|
-
.update(manifestJson)
|
|
183
|
-
.digest('hex')
|
|
184
|
-
.slice(0, NAME_DIGEST_LENGTH);
|
|
185
|
-
const manifestFileName = `manifest.${manifestHash}.json`;
|
|
186
|
-
try {
|
|
187
|
-
await writeFile(join(stage, manifestFileName), manifestJson, 'utf-8');
|
|
188
|
-
}
|
|
189
|
-
catch (err) {
|
|
190
|
-
throw new QuiverError('transport_error', `Failed to write manifest "${join(outDir, manifestFileName)}": ${err.message}`, { cause: err });
|
|
191
|
-
}
|
|
192
|
-
// The format is stamped here and read first, so a client older than the tree
|
|
193
|
-
// says so rather than misreading it.
|
|
194
|
-
const pointer = { format: POINTER_FORMAT, manifest: manifestFileName };
|
|
195
187
|
try {
|
|
196
|
-
await writeFile(join(stage,
|
|
188
|
+
await writeFile(join(stage, INDEX), JSON.stringify(index, null, 2), 'utf-8');
|
|
189
|
+
await writeFile(join(stage, LEGACY_POINTER), JSON.stringify({ format: FORMAT }), 'utf-8');
|
|
197
190
|
}
|
|
198
191
|
catch (err) {
|
|
199
|
-
throw new QuiverError('transport_error', `Failed to write
|
|
192
|
+
throw new QuiverError('transport_error', `Failed to write "${join(outDir, INDEX)}" or its legacy pointer: ${err.message}`, { cause: err });
|
|
200
193
|
}
|
|
201
194
|
// `prev` is cleared first: a rename onto a non-empty directory fails, and the
|
|
202
195
|
// tree left by an interrupted run is one.
|
package/dist/built-loader.d.ts
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
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 type { Quiver } from './quiver.js';
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Bytes at an artifact-relative path, under a `fetch` cache mode. `no-cache` is
|
|
10
|
+
* `quiver.json`'s, the one name read that carries no digest; every other name is read
|
|
11
|
+
* `force-cache`, and `reload` once a cached answer failed, since a digest in a name
|
|
12
|
+
* does not stop a host's page from being cached under it.
|
|
13
13
|
*/
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* Everything else is safe to cache forever by construction, and a transport over a
|
|
21
|
-
* cache says so itself rather than leaving the fact to a host's headers.
|
|
22
|
-
*
|
|
23
|
-
* `maxBytes` is what the response may weigh, required because only the caller knows what
|
|
24
|
-
* it asked for: a pointer, a bundle and a font carry their own ceilings. A transport
|
|
25
|
-
* holding a stream refuses past it before the bytes are resident; one over a file or a
|
|
26
|
-
* held map ignores it.
|
|
27
|
-
*/
|
|
28
|
-
export interface FetchOptions {
|
|
29
|
-
maxBytes: number;
|
|
30
|
-
revalidate?: boolean;
|
|
31
|
-
}
|
|
32
|
-
/** Load a build-output quiver via the given transport. */
|
|
33
|
-
export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
|
|
14
|
+
export type ReadMode = 'no-cache' | 'force-cache' | 'reload';
|
|
15
|
+
export type ArtifactReader = (path: string, mode: ReadMode) => Promise<Uint8Array>;
|
|
16
|
+
export declare function httpReader(base: string): ArtifactReader;
|
|
17
|
+
/** Over bytes the caller holds; a leading `./` or `/` on a key is dropped. */
|
|
18
|
+
export declare function filesReader(files: ReadonlyMap<string, Uint8Array>): ArtifactReader;
|
|
19
|
+
export declare function loadBuiltQuiver(read: ArtifactReader): Promise<Quiver>;
|