@taprootio/docs-artifact 1.0.1 → 1.1.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.
Files changed (42) hide show
  1. package/README.md +196 -30
  2. package/bin/taproot-docs-conformance.js +34 -13
  3. package/bin/taproot-docs-validate.js +44 -9
  4. package/fixtures/README.md +19 -0
  5. package/fixtures/prebuilt/conformance.json +444 -0
  6. package/fixtures/prebuilt/golden/espalier.tar.gz +0 -0
  7. package/fixtures/prebuilt/invalid/duplicate-json-key.json +4 -0
  8. package/fixtures/prebuilt/valid/espalier/404.html +2 -0
  9. package/fixtures/prebuilt/valid/espalier/api/show-toast/index.html +5 -0
  10. package/fixtures/prebuilt/valid/espalier/assets/icons.svg +1 -0
  11. package/fixtures/prebuilt/valid/espalier/assets/pulse.svg +1 -0
  12. package/fixtures/prebuilt/valid/espalier/assets/search-worker.js +1 -0
  13. package/fixtures/prebuilt/valid/espalier/assets/search.wasm +0 -0
  14. package/fixtures/prebuilt/valid/espalier/assets/site.css +3 -0
  15. package/fixtures/prebuilt/valid/espalier/assets/site.js +2 -0
  16. package/fixtures/prebuilt/valid/espalier/dist/-6iE9DOe.css +1 -0
  17. package/fixtures/prebuilt/valid/espalier/dist/_AN3XUT_.css +1 -0
  18. package/fixtures/prebuilt/valid/espalier/guides/index.html +2 -0
  19. package/fixtures/prebuilt/valid/espalier/index.html +2 -0
  20. package/fixtures/prebuilt/valid/espalier/pagefind/index/abc.pf_index +0 -0
  21. package/fixtures/prebuilt/valid/espalier/pagefind/pagefind.js +4 -0
  22. package/fixtures/prebuilt/valid/espalier/taproot-docs-prebuilt-manifest.json +135 -0
  23. package/index.d.ts +9 -0
  24. package/node.d.ts +1 -0
  25. package/package.json +28 -5
  26. package/prebuilt-archive.d.ts +27 -0
  27. package/prebuilt-conformance.d.ts +27 -0
  28. package/prebuilt-node.d.ts +7 -0
  29. package/prebuilt.d.ts +128 -0
  30. package/schema/taproot-docs-prebuilt-manifest.schema.json +156 -0
  31. package/src/constants.js +4 -0
  32. package/src/index.js +13 -0
  33. package/src/json.js +50 -26
  34. package/src/node.js +2 -0
  35. package/src/prebuilt-archive.js +248 -0
  36. package/src/prebuilt-artifact-validator.js +236 -0
  37. package/src/prebuilt-conformance.js +151 -0
  38. package/src/prebuilt-constants.js +55 -0
  39. package/src/prebuilt-manifest-validator.js +654 -0
  40. package/src/prebuilt-node.js +518 -0
  41. package/src/prebuilt-path.js +129 -0
  42. package/src/prebuilt.js +20 -0
package/index.d.ts CHANGED
@@ -175,6 +175,10 @@ export interface DocsArtifactLimits {
175
175
 
176
176
  export const MANIFEST_FILE_NAME: "taproot-docs-manifest.json";
177
177
  export const SCHEMA_VERSION: 1;
178
+ export const MANAGED_MANIFEST_FILE_NAME: "taproot-docs-manifest.json";
179
+ export const MANAGED_SCHEMA_VERSION: 1;
180
+ export const MANAGED_MODE: "managed";
181
+ export const MANAGED_ARCHIVE_FORMAT: "taproot-docs-tar-gzip-v1";
178
182
  export const HTML_FRAGMENT_CAPABILITY: "taproot.docs.fragments.html.v1";
179
183
  export const SUPPORTED_CAPABILITIES: readonly string[];
180
184
  export const ASSET_MEDIA_TYPES: readonly DocsAssetMediaType[];
@@ -192,6 +196,9 @@ export function normalizeRoute(value: unknown): NormalizedPath | InvalidPath;
192
196
  export function validateManifest(input: unknown, options?: ValidationOptions): ValidationResult<DocsArtifactManifest>;
193
197
  export function assertValidManifest(input: unknown, options?: ValidationOptions): DocsArtifactManifest;
194
198
  export function serializeManifest(input: unknown, options?: ValidationOptions): string;
199
+ export const validateManagedManifest: typeof validateManifest;
200
+ export const assertValidManagedManifest: typeof assertValidManifest;
201
+ export const serializeManagedManifest: typeof serializeManifest;
195
202
  export function validateArtifact(
196
203
  manifest: unknown,
197
204
  files: Iterable<ArtifactFileEntry>,
@@ -202,3 +209,5 @@ export function assertValidArtifact(
202
209
  files: Iterable<ArtifactFileEntry>,
203
210
  options?: ValidationOptions,
204
211
  ): Promise<ValidatedArtifact>;
212
+ export const validateManagedArtifact: typeof validateArtifact;
213
+ export const assertValidManagedArtifact: typeof assertValidArtifact;
package/node.d.ts CHANGED
@@ -4,3 +4,4 @@ export function validateArtifactDirectory(
4
4
  rootDirectory: string,
5
5
  options?: ValidationOptions,
6
6
  ): Promise<ValidationResult<ValidatedArtifact>>;
7
+ export const validateManagedArtifactDirectory: typeof validateArtifactDirectory;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@taprootio/docs-artifact",
3
- "version": "1.0.1",
4
- "description": "Canonical Taproot Docs artifact schema, validator, and conformance fixtures",
3
+ "version": "1.1.1",
4
+ "description": "Canonical managed and prebuilt Taproot Docs artifact contracts",
5
5
  "type": "module",
6
6
  "license": "ISC",
7
7
  "repository": {
@@ -25,8 +25,26 @@
25
25
  "types": "./conformance.d.ts",
26
26
  "import": "./src/conformance.js"
27
27
  },
28
+ "./prebuilt": {
29
+ "types": "./prebuilt.d.ts",
30
+ "import": "./src/prebuilt.js"
31
+ },
32
+ "./prebuilt/archive": {
33
+ "types": "./prebuilt-archive.d.ts",
34
+ "import": "./src/prebuilt-archive.js"
35
+ },
36
+ "./prebuilt/node": {
37
+ "types": "./prebuilt-node.d.ts",
38
+ "import": "./src/prebuilt-node.js"
39
+ },
40
+ "./prebuilt/conformance": {
41
+ "types": "./prebuilt-conformance.d.ts",
42
+ "import": "./src/prebuilt-conformance.js"
43
+ },
28
44
  "./schema": "./schema/taproot-docs-manifest.schema.json",
29
- "./fixtures/conformance.json": "./fixtures/conformance.json"
45
+ "./prebuilt/schema": "./schema/taproot-docs-prebuilt-manifest.schema.json",
46
+ "./fixtures/conformance.json": "./fixtures/conformance.json",
47
+ "./fixtures/prebuilt/conformance.json": "./fixtures/prebuilt/conformance.json"
30
48
  },
31
49
  "bin": {
32
50
  "taproot-docs-validate": "./bin/taproot-docs-validate.js"
@@ -40,12 +58,17 @@
40
58
  "README.md",
41
59
  "index.d.ts",
42
60
  "node.d.ts",
43
- "conformance.d.ts"
61
+ "conformance.d.ts",
62
+ "prebuilt.d.ts",
63
+ "prebuilt-archive.d.ts",
64
+ "prebuilt-node.d.ts",
65
+ "prebuilt-conformance.d.ts"
44
66
  ],
45
67
  "scripts": {
46
68
  "test": "node --test",
47
69
  "conformance": "node ./bin/taproot-docs-conformance.js",
48
- "prepack": "npm test && npm run conformance"
70
+ "conformance:prebuilt": "node ./bin/taproot-docs-conformance.js --mode prebuilt",
71
+ "prepack": "npm test && npm run conformance && npm run conformance:prebuilt"
49
72
  },
50
73
  "publishConfig": {
51
74
  "access": "public",
@@ -0,0 +1,27 @@
1
+ import type { PrebuiltValidationOptions, ValidatedPrebuiltArtifact } from "./prebuilt.js";
2
+
3
+ export { DocsArtifactValidationError } from "./index.js";
4
+
5
+ export interface DeterministicPrebuiltArchive {
6
+ readonly format: "taproot-docs-prebuilt-tar-gzip-v1";
7
+ readonly bytes: Uint8Array;
8
+ readonly byteLength: number;
9
+ readonly contentHash: `sha256:${string}`;
10
+ }
11
+
12
+ export class PrebuiltArchiveError extends Error {
13
+ readonly code: string;
14
+ readonly field?: string;
15
+ constructor(code: string, message: string, field?: string);
16
+ }
17
+
18
+ /**
19
+ * Encodes an isolated snapshot returned by `validatePrebuiltArtifact` or
20
+ * `validatePrebuiltArtifactDirectory` into the frozen prebuilt archive format.
21
+ * Throws `PrebuiltArchiveError` for inventory, size, or hash faults and
22
+ * `DocsArtifactValidationError` when the snapshot manifest fails revalidation.
23
+ */
24
+ export function createDeterministicPrebuiltArchive(
25
+ snapshot: ValidatedPrebuiltArtifact,
26
+ options?: PrebuiltValidationOptions,
27
+ ): DeterministicPrebuiltArchive;
@@ -0,0 +1,27 @@
1
+ import type { ArtifactFileEntry } from "./index.js";
2
+ import type { ValidationResult } from "./index.js";
3
+ import type { ValidatedPrebuiltArtifact } from "./prebuilt.js";
4
+
5
+ export interface PrebuiltNodeConformanceScenario {
6
+ type: "replace-file-with-directory-before-open";
7
+ path: string;
8
+ }
9
+
10
+ export interface PrebuiltArtifactConformanceCase {
11
+ name: string;
12
+ valid: boolean;
13
+ expectedCodes: string[];
14
+ manifest: Uint8Array;
15
+ files: ArtifactFileEntry[];
16
+ nodeScenario?: PrebuiltNodeConformanceScenario;
17
+ expectedArchive?: {
18
+ bytes: Uint8Array;
19
+ byteLength: number;
20
+ sha256: `sha256:${string}`;
21
+ };
22
+ }
23
+
24
+ export function loadPrebuiltConformanceCases(): Promise<PrebuiltArtifactConformanceCase[]>;
25
+ export function validatePrebuiltConformanceCase(
26
+ fixture: PrebuiltArtifactConformanceCase,
27
+ ): Promise<ValidationResult<ValidatedPrebuiltArtifact>>;
@@ -0,0 +1,7 @@
1
+ import type { ValidationResult } from "./index.js";
2
+ import type { PrebuiltValidationOptions, ValidatedPrebuiltArtifact } from "./prebuilt.js";
3
+
4
+ export function validatePrebuiltArtifactDirectory(
5
+ rootDirectory: string,
6
+ options?: PrebuiltValidationOptions,
7
+ ): Promise<ValidationResult<ValidatedPrebuiltArtifact>>;
package/prebuilt.d.ts ADDED
@@ -0,0 +1,128 @@
1
+ import type {
2
+ ArtifactFileContent,
3
+ ArtifactFileEntry,
4
+ DocsArtifactBuild,
5
+ DocsArtifactSource,
6
+ NormalizedPath,
7
+ InvalidPath,
8
+ ValidationResult,
9
+ } from "./index.js";
10
+
11
+ export type PrebuiltMediaType =
12
+ | "application/json; charset=utf-8"
13
+ | "application/manifest+json; charset=utf-8"
14
+ | "application/octet-stream"
15
+ | "application/wasm"
16
+ | "application/xml; charset=utf-8"
17
+ | "font/otf"
18
+ | "font/ttf"
19
+ | "font/woff"
20
+ | "font/woff2"
21
+ | "image/avif"
22
+ | "image/gif"
23
+ | "image/jpeg"
24
+ | "image/png"
25
+ | "image/svg+xml"
26
+ | "image/vnd.microsoft.icon"
27
+ | "image/webp"
28
+ | "text/css; charset=utf-8"
29
+ | "text/html; charset=utf-8"
30
+ | "text/javascript; charset=utf-8"
31
+ | "text/plain; charset=utf-8";
32
+
33
+ export interface PrebuiltArtifactCapabilities {
34
+ required: string[];
35
+ optional: string[];
36
+ }
37
+
38
+ export interface PrebuiltFile {
39
+ path: string;
40
+ mediaType: PrebuiltMediaType;
41
+ bytes: number;
42
+ sha256: string;
43
+ }
44
+
45
+ export interface PrebuiltResource {
46
+ key: string;
47
+ file: string;
48
+ title: string;
49
+ }
50
+
51
+ export interface PrebuiltRedirect {
52
+ from: string;
53
+ toResourceKey: string;
54
+ status: 301 | 308;
55
+ }
56
+
57
+ export interface PrebuiltArtifactManifest {
58
+ schemaVersion: 1;
59
+ mode: "prebuilt";
60
+ source: DocsArtifactSource;
61
+ build: DocsArtifactBuild;
62
+ capabilities: PrebuiltArtifactCapabilities;
63
+ notFoundFile: "404.html";
64
+ files: PrebuiltFile[];
65
+ resources: PrebuiltResource[];
66
+ redirects: PrebuiltRedirect[];
67
+ }
68
+
69
+ export interface PrebuiltValidationOptions {
70
+ supportedCapabilities?: Iterable<string> & object;
71
+ }
72
+
73
+ export type PrebuiltManifestInput = PrebuiltArtifactManifest | string | Uint8Array | ArrayBuffer;
74
+
75
+ export interface ValidatedPrebuiltArtifact {
76
+ manifest: PrebuiltArtifactManifest;
77
+ /** Caller-isolated snapshots of the exact validated bytes, in manifest path order. */
78
+ files: Array<{ path: string; content: Uint8Array }>;
79
+ fileCount: number;
80
+ totalBytes: number;
81
+ }
82
+
83
+ export interface PrebuiltArtifactLimits {
84
+ readonly manifestBytes: number;
85
+ readonly manifestObjectWork: number;
86
+ readonly manifestObjectDepth: number;
87
+ readonly artifactBytes: number;
88
+ readonly fileBytes: number;
89
+ readonly files: number;
90
+ readonly resources: number;
91
+ readonly redirects: number;
92
+ readonly artifactPathBytes: number;
93
+ readonly directoryDepth: number;
94
+ readonly directoryEntries: number;
95
+ readonly supportedCapabilities: number;
96
+ readonly resourceKey: number;
97
+ readonly title: number;
98
+ readonly routeBytes: number;
99
+ }
100
+
101
+ export const PREBUILT_MANIFEST_FILE_NAME: "taproot-docs-prebuilt-manifest.json";
102
+ export const PREBUILT_SCHEMA_VERSION: 1;
103
+ export const PREBUILT_MODE: "prebuilt";
104
+ export const PREBUILT_ARCHIVE_FORMAT: "taproot-docs-prebuilt-tar-gzip-v1";
105
+ export const PREBUILT_FILES_CAPABILITY: "taproot.docs.prebuilt.files.v1";
106
+ export const PREBUILT_NOT_FOUND_FILE: "404.html";
107
+ export const PREBUILT_SUPPORTED_CAPABILITIES: readonly string[];
108
+ export const PREBUILT_LIMITS: Readonly<PrebuiltArtifactLimits>;
109
+ export const PREBUILT_MEDIA_TYPES: readonly PrebuiltMediaType[];
110
+ export const PREBUILT_TEXT_MEDIA_TYPES: readonly PrebuiltMediaType[];
111
+
112
+ export { DocsArtifactValidationError } from "./index.js";
113
+ export function normalizePrebuiltArtifactPath(value: unknown): NormalizedPath | InvalidPath;
114
+ export function normalizePrebuiltRedirectRoute(value: unknown): NormalizedPath | InvalidPath;
115
+ export function prebuiltFileRoute(value: unknown): NormalizedPath | InvalidPath;
116
+ export function validatePrebuiltManifest(input: unknown, options?: PrebuiltValidationOptions): ValidationResult<PrebuiltArtifactManifest>;
117
+ export function assertValidPrebuiltManifest(input: unknown, options?: PrebuiltValidationOptions): PrebuiltArtifactManifest;
118
+ export function serializePrebuiltManifest(input: unknown, options?: PrebuiltValidationOptions): string;
119
+ export function validatePrebuiltArtifact(
120
+ manifest: unknown,
121
+ files: Iterable<ArtifactFileEntry>,
122
+ options?: PrebuiltValidationOptions,
123
+ ): Promise<ValidationResult<ValidatedPrebuiltArtifact>>;
124
+ export function assertValidPrebuiltArtifact(
125
+ manifest: unknown,
126
+ files: Iterable<{ path: string; content: ArtifactFileContent }>,
127
+ options?: PrebuiltValidationOptions,
128
+ ): Promise<ValidatedPrebuiltArtifact>;
@@ -0,0 +1,156 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:taproot:docs:prebuilt-artifact:manifest:v1",
4
+ "title": "Taproot Docs prebuilt artifact manifest",
5
+ "description": "Closed byte-preserving static Docs contract. Cross-reference, ASCII case-fold, redirect-source grammar, route-collision, required-array ordering, extension-bound media types, USTAR, aggregate-byte, UTF-8, and exact-inventory rules are additionally enforced by @taprootio/docs-artifact/prebuilt.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "mode", "source", "build", "capabilities", "notFoundFile", "files", "resources", "redirects"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "mode": { "const": "prebuilt" },
12
+ "source": { "$ref": "#/$defs/source" },
13
+ "build": { "$ref": "#/$defs/build" },
14
+ "capabilities": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["required", "optional"],
18
+ "properties": {
19
+ "required": {
20
+ "type": "array",
21
+ "maxItems": 100,
22
+ "uniqueItems": true,
23
+ "contains": { "const": "taproot.docs.prebuilt.files.v1" },
24
+ "items": { "$ref": "#/$defs/resourceKey" }
25
+ },
26
+ "optional": {
27
+ "type": "array",
28
+ "maxItems": 100,
29
+ "uniqueItems": true,
30
+ "items": { "$ref": "#/$defs/resourceKey" }
31
+ }
32
+ }
33
+ },
34
+ "notFoundFile": { "const": "404.html" },
35
+ "files": {
36
+ "type": "array",
37
+ "minItems": 1,
38
+ "maxItems": 25000,
39
+ "items": { "$ref": "#/$defs/file" }
40
+ },
41
+ "resources": {
42
+ "type": "array",
43
+ "maxItems": 25000,
44
+ "items": { "$ref": "#/$defs/resource" }
45
+ },
46
+ "redirects": {
47
+ "type": "array",
48
+ "maxItems": 10000,
49
+ "items": { "$ref": "#/$defs/redirect" }
50
+ }
51
+ },
52
+ "$defs": {
53
+ "resourceKey": {
54
+ "type": "string",
55
+ "minLength": 1,
56
+ "maxLength": 200,
57
+ "pattern": "^[a-z0-9]+(?:[._:/-][a-z0-9]+)*$"
58
+ },
59
+ "hash": {
60
+ "type": "string",
61
+ "pattern": "^sha256:[0-9a-f]{64}$"
62
+ },
63
+ "source": {
64
+ "type": "object",
65
+ "additionalProperties": false,
66
+ "required": ["provider", "repositoryId", "repository", "repositoryUrl", "revision", "ref"],
67
+ "properties": {
68
+ "provider": { "const": "github" },
69
+ "repositoryId": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^[A-Za-z0-9_.:-]+$" },
70
+ "repository": { "type": "string", "minLength": 3, "maxLength": 300, "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" },
71
+ "repositoryUrl": { "type": "string", "format": "uri", "maxLength": 2000, "pattern": "^https://" },
72
+ "revision": { "type": "string", "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$" },
73
+ "ref": { "type": "string", "maxLength": 500, "pattern": "^refs/(?:heads|tags)/[A-Za-z0-9][A-Za-z0-9._/-]*$" }
74
+ }
75
+ },
76
+ "build": {
77
+ "type": "object",
78
+ "additionalProperties": false,
79
+ "required": ["producer", "producerVersion", "configurationSha256", "sourceDateEpoch"],
80
+ "properties": {
81
+ "producer": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^(?:@[a-z0-9._-]+/)?[a-z0-9._-]+$" },
82
+ "producerVersion": { "type": "string", "maxLength": 100, "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$" },
83
+ "configurationSha256": { "$ref": "#/$defs/hash" },
84
+ "sourceDateEpoch": { "type": "integer", "minimum": 0, "maximum": 253402300799 }
85
+ }
86
+ },
87
+ "path": {
88
+ "type": "string",
89
+ "$comment": "Runtime validation additionally enforces the 255-byte total, 64-directory, Windows-device, manifest-name, case-fold, and USTAR name/prefix constraints.",
90
+ "minLength": 1,
91
+ "maxLength": 255,
92
+ "pattern": "^(?:[A-Za-z0-9]|[A-Za-z0-9_-][A-Za-z0-9._-]*[A-Za-z0-9])(?:/(?:[A-Za-z0-9]|[A-Za-z0-9_-][A-Za-z0-9._-]*[A-Za-z0-9]))*$"
93
+ },
94
+ "mediaType": {
95
+ "enum": [
96
+ "application/json; charset=utf-8",
97
+ "application/manifest+json; charset=utf-8",
98
+ "application/octet-stream",
99
+ "application/wasm",
100
+ "application/xml; charset=utf-8",
101
+ "font/otf",
102
+ "font/ttf",
103
+ "font/woff",
104
+ "font/woff2",
105
+ "image/avif",
106
+ "image/gif",
107
+ "image/jpeg",
108
+ "image/png",
109
+ "image/svg+xml",
110
+ "image/vnd.microsoft.icon",
111
+ "image/webp",
112
+ "text/css; charset=utf-8",
113
+ "text/html; charset=utf-8",
114
+ "text/javascript; charset=utf-8",
115
+ "text/plain; charset=utf-8"
116
+ ]
117
+ },
118
+ "file": {
119
+ "type": "object",
120
+ "additionalProperties": false,
121
+ "required": ["path", "mediaType", "bytes", "sha256"],
122
+ "properties": {
123
+ "path": { "$ref": "#/$defs/path" },
124
+ "mediaType": { "$ref": "#/$defs/mediaType" },
125
+ "bytes": { "type": "integer", "minimum": 0, "maximum": 67108864 },
126
+ "sha256": { "$ref": "#/$defs/hash" }
127
+ }
128
+ },
129
+ "resource": {
130
+ "type": "object",
131
+ "additionalProperties": false,
132
+ "required": ["key", "file", "title"],
133
+ "properties": {
134
+ "key": { "$ref": "#/$defs/resourceKey" },
135
+ "file": { "$ref": "#/$defs/path" },
136
+ "title": { "type": "string", "minLength": 1, "maxLength": 300 }
137
+ }
138
+ },
139
+ "redirect": {
140
+ "type": "object",
141
+ "additionalProperties": false,
142
+ "required": ["from", "toResourceKey", "status"],
143
+ "properties": {
144
+ "from": {
145
+ "type": "string",
146
+ "$comment": "Runtime validation additionally enforces portable ASCII segments; forbids percent signs, backslashes, queries, fragments, dot segments, empty segments, Windows device aliases, the reserved manifest route, and the routing-control route; and requires ASCII case-fold uniqueness.",
147
+ "minLength": 1,
148
+ "maxLength": 2000,
149
+ "pattern": "^/"
150
+ },
151
+ "toResourceKey": { "$ref": "#/$defs/resourceKey" },
152
+ "status": { "enum": [301, 308] }
153
+ }
154
+ }
155
+ }
156
+ }
package/src/constants.js CHANGED
@@ -1,5 +1,9 @@
1
1
  export const MANIFEST_FILE_NAME = "taproot-docs-manifest.json";
2
2
  export const SCHEMA_VERSION = 1;
3
+ export const MANAGED_MANIFEST_FILE_NAME = MANIFEST_FILE_NAME;
4
+ export const MANAGED_SCHEMA_VERSION = SCHEMA_VERSION;
5
+ export const MANAGED_MODE = "managed";
6
+ export const MANAGED_ARCHIVE_FORMAT = "taproot-docs-tar-gzip-v1";
3
7
  export const HTML_FRAGMENT_CAPABILITY = "taproot.docs.fragments.html.v1";
4
8
 
5
9
  export const SUPPORTED_CAPABILITIES = Object.freeze([
package/src/index.js CHANGED
@@ -5,6 +5,10 @@ export {
5
5
  FRAGMENT_ROLES,
6
6
  HTML_FRAGMENT_CAPABILITY,
7
7
  LIMITS,
8
+ MANAGED_ARCHIVE_FORMAT,
9
+ MANAGED_MANIFEST_FILE_NAME,
10
+ MANAGED_MODE,
11
+ MANAGED_SCHEMA_VERSION,
8
12
  MANIFEST_FILE_NAME,
9
13
  RESERVED_ROUTE_PREFIXES,
10
14
  RESERVED_ROUTES,
@@ -13,6 +17,15 @@ export {
13
17
  SUPPORTED_CAPABILITIES,
14
18
  } from "./constants.js";
15
19
  export { assertValidArtifact, validateArtifact } from "./artifact-validator.js";
20
+ export {
21
+ assertValidArtifact as assertValidManagedArtifact,
22
+ validateArtifact as validateManagedArtifact,
23
+ } from "./artifact-validator.js";
16
24
  export { DocsArtifactValidationError } from "./errors.js";
17
25
  export { assertValidManifest, serializeManifest, validateManifest } from "./manifest-validator.js";
26
+ export {
27
+ assertValidManifest as assertValidManagedManifest,
28
+ serializeManifest as serializeManagedManifest,
29
+ validateManifest as validateManagedManifest,
30
+ } from "./manifest-validator.js";
18
31
  export { normalizeArtifactPath, normalizeRoute, normalizeSourcePath } from "./path.js";
package/src/json.js CHANGED
@@ -11,9 +11,10 @@ class StrictJsonError extends Error {
11
11
  }
12
12
 
13
13
  class StrictJsonParser {
14
- constructor(text) {
14
+ constructor(text, maximumDepth = LIMITS.manifestObjectDepth) {
15
15
  this.text = text;
16
16
  this.offset = 0;
17
+ this.maximumDepth = maximumDepth;
17
18
  }
18
19
 
19
20
  parse() {
@@ -27,8 +28,8 @@ class StrictJsonParser {
27
28
  }
28
29
 
29
30
  #parseValue(path, depth) {
30
- if (depth > 64) {
31
- throw new StrictJsonError("json.too_deep", "JSON nesting may not exceed 64 levels.", path);
31
+ if (depth > this.maximumDepth) {
32
+ throw new StrictJsonError("json.too_deep", `JSON nesting may not exceed ${this.maximumDepth} levels.`, path);
32
33
  }
33
34
  const char = this.text[this.offset];
34
35
  if (char === "{") return this.#parseObject(path, depth + 1);
@@ -210,10 +211,10 @@ export function classifyManifestInput(input) {
210
211
  return { kind: "object" };
211
212
  }
212
213
 
213
- function asBytes(input, classification = classifyManifestInput(input)) {
214
+ function asBytes(input, classification = classifyManifestInput(input), maximumBytes = LIMITS.manifestBytes) {
214
215
  if (typeof input === "string") {
215
- if (input.length > LIMITS.manifestBytes) {
216
- throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${LIMITS.manifestBytes}.`);
216
+ if (input.length > maximumBytes) {
217
+ throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${maximumBytes}.`);
217
218
  }
218
219
  let byteLength = 0;
219
220
  for (let offset = 0; offset < input.length; offset += 1) {
@@ -230,39 +231,44 @@ function asBytes(input, classification = classifyManifestInput(input)) {
230
231
  } else {
231
232
  byteLength += first <= 0x7f ? 1 : first <= 0x7ff ? 2 : 3;
232
233
  }
233
- if (byteLength > LIMITS.manifestBytes) {
234
- throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${LIMITS.manifestBytes}.`);
234
+ if (byteLength > maximumBytes) {
235
+ throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${maximumBytes}.`);
235
236
  }
236
237
  }
237
238
  return new TextEncoder().encode(input);
238
239
  }
239
240
  if (classification.kind === "uint8array" || classification.kind === "arraybuffer") {
240
- const snapshot = snapshotBinaryInput(input, LIMITS.manifestBytes, classification);
241
+ const snapshot = snapshotBinaryInput(input, maximumBytes, classification);
241
242
  if (snapshot.kind === "too_large") {
242
- throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${LIMITS.manifestBytes}.`);
243
+ throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${maximumBytes}.`);
243
244
  }
244
245
  if (snapshot.kind === "bytes") return snapshot.bytes;
245
246
  }
246
247
  throw new TypeError("Manifest input must be a string, Uint8Array, or ArrayBuffer.");
247
248
  }
248
249
 
249
- export function parseManifestJson(input, classification = classifyManifestInput(input)) {
250
+ export function parseManifestJson(
251
+ input,
252
+ classification = classifyManifestInput(input),
253
+ maximumBytes = LIMITS.manifestBytes,
254
+ maximumDepth = LIMITS.manifestObjectDepth,
255
+ ) {
250
256
  let bytes;
251
257
  try {
252
- bytes = asBytes(input, classification);
258
+ bytes = asBytes(input, classification, maximumBytes);
253
259
  } catch (error) {
254
260
  if (error instanceof StrictJsonError) {
255
261
  return { ok: false, errors: [sanitizeValidationError(error)] };
256
262
  }
257
263
  return { ok: false, errors: [sanitizeValidationError({ code: "json.invalid_input", path: "$", message: error.message })] };
258
264
  }
259
- if (bytes.byteLength > LIMITS.manifestBytes) {
265
+ if (bytes.byteLength > maximumBytes) {
260
266
  return {
261
267
  ok: false,
262
268
  errors: [sanitizeValidationError({
263
269
  code: "manifest.too_large",
264
270
  path: "$",
265
- message: `Manifest bytes may not exceed ${LIMITS.manifestBytes}.`,
271
+ message: `Manifest bytes may not exceed ${maximumBytes}.`,
266
272
  })],
267
273
  };
268
274
  }
@@ -278,7 +284,7 @@ export function parseManifestJson(input, classification = classifyManifestInput(
278
284
  }
279
285
 
280
286
  try {
281
- return { ok: true, value: new StrictJsonParser(text).parse() };
287
+ return { ok: true, value: new StrictJsonParser(text, maximumDepth).parse() };
282
288
  } catch (error) {
283
289
  if (error instanceof StrictJsonError) {
284
290
  return { ok: false, errors: [sanitizeValidationError(error)] };
@@ -380,17 +386,29 @@ function jsonStringByteLength(value, maximumBytes) {
380
386
  return bytes;
381
387
  }
382
388
 
383
- export function preflightManifestObject(value) {
389
+ export function preflightManifestObject(value, limits = {}) {
390
+ const maximumBytes = limits.manifestBytes ?? LIMITS.manifestBytes;
391
+ const maximumWork = limits.manifestObjectWork ?? LIMITS.manifestObjectWork;
392
+ const maximumDepth = limits.manifestObjectDepth ?? LIMITS.manifestObjectDepth;
393
+ const stopAtByteLimit = limits.stopAtByteLimit === true;
384
394
  const visiting = new WeakSet();
385
395
  const cachedSubtrees = new WeakMap();
386
396
  const state = { work: 0 };
387
- const add = (left, right) => Math.min(LIMITS.manifestBytes + 1, left + right);
397
+ const byteLimitError = () => new ManifestObjectPreflightError(
398
+ "manifest.too_large",
399
+ `Canonical manifest bytes may not exceed ${maximumBytes}.`,
400
+ );
401
+ const checkByteLimit = (bytes) => {
402
+ if (stopAtByteLimit && bytes > maximumBytes) throw byteLimitError();
403
+ return Math.min(maximumBytes + 1, bytes);
404
+ };
405
+ const add = (left, right) => checkByteLimit(left + right);
388
406
  const consumeWork = (logicalWork = 1) => {
389
407
  state.work += logicalWork;
390
- if (state.work > LIMITS.manifestObjectWork) {
408
+ if (state.work > maximumWork) {
391
409
  throw new ManifestObjectPreflightError(
392
410
  "manifest.too_large",
393
- `Manifest object validation work may not exceed ${LIMITS.manifestObjectWork} values.`,
411
+ `Manifest object validation work may not exceed ${maximumWork} values.`,
394
412
  );
395
413
  }
396
414
  };
@@ -398,7 +416,7 @@ export function preflightManifestObject(value) {
398
416
  if (typeof candidate === "string") {
399
417
  consumeWork();
400
418
  return {
401
- bytes: jsonStringByteLength(candidate, LIMITS.manifestBytes),
419
+ bytes: jsonStringByteLength(candidate, maximumBytes),
402
420
  logicalWork: 1,
403
421
  relativeDepth: -1,
404
422
  snapshot: candidate,
@@ -425,19 +443,19 @@ export function preflightManifestObject(value) {
425
443
  }
426
444
  const cached = cachedSubtrees.get(candidate);
427
445
  if (cached !== undefined) {
428
- if (depth + cached.relativeDepth > LIMITS.manifestObjectDepth) {
446
+ if (depth + cached.relativeDepth > maximumDepth) {
429
447
  throw new ManifestObjectPreflightError(
430
448
  "manifest.object_too_deep",
431
- `Manifest object nesting may not exceed ${LIMITS.manifestObjectDepth} levels.`,
449
+ `Manifest object nesting may not exceed ${maximumDepth} levels.`,
432
450
  );
433
451
  }
434
452
  consumeWork(cached.logicalWork);
435
453
  return cached;
436
454
  }
437
- if (depth > LIMITS.manifestObjectDepth) {
455
+ if (depth > maximumDepth) {
438
456
  throw new ManifestObjectPreflightError(
439
457
  "manifest.object_too_deep",
440
- `Manifest object nesting may not exceed ${LIMITS.manifestObjectDepth} levels.`,
458
+ `Manifest object nesting may not exceed ${maximumDepth} levels.`,
441
459
  );
442
460
  }
443
461
  const workAtStart = state.work;
@@ -471,11 +489,17 @@ export function preflightManifestObject(value) {
471
489
  snapshot = Object.create(null);
472
490
  let first = true;
473
491
  for (const key of Object.keys(candidate)) {
492
+ if (stopAtByteLimit) {
493
+ let minimumBytes = bytes;
494
+ if (!first) minimumBytes = add(minimumBytes, 1);
495
+ minimumBytes = add(minimumBytes, jsonStringByteLength(key, maximumBytes));
496
+ add(minimumBytes, 1);
497
+ }
474
498
  const child = visit(Reflect.get(candidate, key), depth + 1);
475
499
  if (child.snapshot === OMITTED_JSON_VALUE) continue;
476
500
  if (!first) bytes = add(bytes, 1);
477
501
  first = false;
478
- bytes = add(bytes, jsonStringByteLength(key, LIMITS.manifestBytes));
502
+ bytes = add(bytes, jsonStringByteLength(key, maximumBytes));
479
503
  bytes = add(bytes, 1);
480
504
  bytes = add(bytes, child.bytes);
481
505
  relativeDepth = Math.max(relativeDepth, child.relativeDepth + 1);
@@ -497,7 +521,7 @@ export function preflightManifestObject(value) {
497
521
  const result = visit(value, 0);
498
522
  return {
499
523
  ok: true,
500
- exceedsByteLimit: result.bytes > LIMITS.manifestBytes,
524
+ exceedsByteLimit: result.bytes > maximumBytes,
501
525
  value: result.snapshot === OMITTED_JSON_VALUE ? undefined : result.snapshot,
502
526
  };
503
527
  } catch (error) {