@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.
- package/README.md +196 -30
- package/bin/taproot-docs-conformance.js +34 -13
- package/bin/taproot-docs-validate.js +44 -9
- package/fixtures/README.md +19 -0
- package/fixtures/prebuilt/conformance.json +444 -0
- package/fixtures/prebuilt/golden/espalier.tar.gz +0 -0
- package/fixtures/prebuilt/invalid/duplicate-json-key.json +4 -0
- package/fixtures/prebuilt/valid/espalier/404.html +2 -0
- package/fixtures/prebuilt/valid/espalier/api/show-toast/index.html +5 -0
- package/fixtures/prebuilt/valid/espalier/assets/icons.svg +1 -0
- package/fixtures/prebuilt/valid/espalier/assets/pulse.svg +1 -0
- package/fixtures/prebuilt/valid/espalier/assets/search-worker.js +1 -0
- package/fixtures/prebuilt/valid/espalier/assets/search.wasm +0 -0
- package/fixtures/prebuilt/valid/espalier/assets/site.css +3 -0
- package/fixtures/prebuilt/valid/espalier/assets/site.js +2 -0
- package/fixtures/prebuilt/valid/espalier/dist/-6iE9DOe.css +1 -0
- package/fixtures/prebuilt/valid/espalier/dist/_AN3XUT_.css +1 -0
- package/fixtures/prebuilt/valid/espalier/guides/index.html +2 -0
- package/fixtures/prebuilt/valid/espalier/index.html +2 -0
- package/fixtures/prebuilt/valid/espalier/pagefind/index/abc.pf_index +0 -0
- package/fixtures/prebuilt/valid/espalier/pagefind/pagefind.js +4 -0
- package/fixtures/prebuilt/valid/espalier/taproot-docs-prebuilt-manifest.json +135 -0
- package/index.d.ts +9 -0
- package/node.d.ts +1 -0
- package/package.json +28 -5
- package/prebuilt-archive.d.ts +27 -0
- package/prebuilt-conformance.d.ts +27 -0
- package/prebuilt-node.d.ts +7 -0
- package/prebuilt.d.ts +128 -0
- package/schema/taproot-docs-prebuilt-manifest.schema.json +156 -0
- package/src/constants.js +4 -0
- package/src/index.js +13 -0
- package/src/json.js +50 -26
- package/src/node.js +2 -0
- package/src/prebuilt-archive.js +248 -0
- package/src/prebuilt-artifact-validator.js +236 -0
- package/src/prebuilt-conformance.js +151 -0
- package/src/prebuilt-constants.js +55 -0
- package/src/prebuilt-manifest-validator.js +654 -0
- package/src/prebuilt-node.js +518 -0
- package/src/prebuilt-path.js +129 -0
- 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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/docs-artifact",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Canonical Taproot Docs artifact
|
|
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
|
-
"./
|
|
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
|
-
"
|
|
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 >
|
|
31
|
-
throw new StrictJsonError("json.too_deep",
|
|
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 >
|
|
216
|
-
throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${
|
|
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 >
|
|
234
|
-
throw new StrictJsonError("manifest.too_large", `Manifest bytes may not exceed ${
|
|
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,
|
|
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 ${
|
|
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(
|
|
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 >
|
|
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 ${
|
|
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
|
|
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 >
|
|
408
|
+
if (state.work > maximumWork) {
|
|
391
409
|
throw new ManifestObjectPreflightError(
|
|
392
410
|
"manifest.too_large",
|
|
393
|
-
`Manifest object validation work may not exceed ${
|
|
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,
|
|
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 >
|
|
446
|
+
if (depth + cached.relativeDepth > maximumDepth) {
|
|
429
447
|
throw new ManifestObjectPreflightError(
|
|
430
448
|
"manifest.object_too_deep",
|
|
431
|
-
`Manifest object nesting may not exceed ${
|
|
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 >
|
|
455
|
+
if (depth > maximumDepth) {
|
|
438
456
|
throw new ManifestObjectPreflightError(
|
|
439
457
|
"manifest.object_too_deep",
|
|
440
|
-
`Manifest object nesting may not exceed ${
|
|
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,
|
|
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 >
|
|
524
|
+
exceedsByteLimit: result.bytes > maximumBytes,
|
|
501
525
|
value: result.snapshot === OMITTED_JSON_VALUE ? undefined : result.snapshot,
|
|
502
526
|
};
|
|
503
527
|
} catch (error) {
|