@aiquants/markdown-explorer 0.0.0-stage → 0.1.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/CHANGELOG.md +92 -0
- package/LICENSE +21 -0
- package/README.md +588 -2
- package/dist/ExplorerSplit-BESGFcu6.cjs +1 -0
- package/dist/ExplorerSplit-DwEFmvQM.js +1362 -0
- package/dist/MultiLayout-DxPiARsv.js +896 -0
- package/dist/MultiLayout-r_qx1Mfz.cjs +1 -0
- package/dist/TreeLayout-9-20bbSM.cjs +1 -0
- package/dist/TreeLayout-CW7lgV_e.js +19 -0
- package/dist/client/MarkdownExplorer.d.cts +30 -0
- package/dist/client/MarkdownExplorer.d.ts +30 -0
- package/dist/client/storage.d.cts +11 -0
- package/dist/client/storage.d.ts +11 -0
- package/dist/core/config.d.cts +71 -0
- package/dist/core/config.d.ts +71 -0
- package/dist/core/contracts.d.cts +155 -0
- package/dist/core/contracts.d.ts +155 -0
- package/dist/core/geometry.d.cts +51 -0
- package/dist/core/geometry.d.ts +51 -0
- package/dist/core/labels.d.cts +175 -0
- package/dist/core/labels.d.ts +175 -0
- package/dist/core/location.d.cts +82 -0
- package/dist/core/location.d.ts +82 -0
- package/dist/core/paths.d.cts +17 -0
- package/dist/core/paths.d.ts +17 -0
- package/dist/core/revalidation.d.cts +2 -0
- package/dist/core/revalidation.d.ts +2 -0
- package/dist/core/theme.d.cts +6 -0
- package/dist/core/theme.d.ts +6 -0
- package/dist/core/treeSettings.d.cts +48 -0
- package/dist/core/treeSettings.d.ts +48 -0
- package/dist/core/viewSettings.d.cts +15 -0
- package/dist/core/viewSettings.d.ts +15 -0
- package/dist/dataRoute-Bkfwfp1R.js +194 -0
- package/dist/dataRoute-CKbdwkQ7.cjs +1 -0
- package/dist/index-BgkZ9ATV.js +2061 -0
- package/dist/index-y9tVvDW7.cjs +3 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +44 -0
- package/dist/location-Cb2Ak55B.js +382 -0
- package/dist/location-DWHQYntR.cjs +1 -0
- package/dist/sanitizeWorker.cjs +1 -0
- package/dist/sanitizeWorker.js +291 -0
- package/dist/server/anonymous.d.cts +10 -0
- package/dist/server/anonymous.d.ts +10 -0
- package/dist/server/createMarkdownExplorer.d.cts +12 -0
- package/dist/server/createMarkdownExplorer.d.ts +12 -0
- package/dist/server/errors.d.cts +26 -0
- package/dist/server/errors.d.ts +26 -0
- package/dist/server/fileSystemSource.d.cts +38 -0
- package/dist/server/fileSystemSource.d.ts +38 -0
- package/dist/server/ports.d.cts +107 -0
- package/dist/server/ports.d.ts +107 -0
- package/dist/server/sanitizeProtocol.d.cts +17 -0
- package/dist/server/sanitizeProtocol.d.ts +17 -0
- package/dist/server/sanitizer.d.cts +51 -0
- package/dist/server/sanitizer.d.ts +51 -0
- package/dist/server/workerPool.d.cts +42 -0
- package/dist/server/workerPool.d.ts +42 -0
- package/dist/server.cjs +1 -0
- package/dist/server.d.cts +7 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1253 -0
- package/dist/storage-Bkb2Axdi.cjs +1 -0
- package/dist/storage-DA4ItHzl.js +60 -0
- package/dist/storage.cjs +1 -0
- package/dist/storage.d.cts +1 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +4 -0
- package/dist/styles/markdown-explorer.css +3 -0
- package/docs/behavior/dom-hooks.md +95 -0
- package/docs/behavior/layout.md +203 -0
- package/docs/behavior/localization.md +47 -0
- package/docs/behavior/security.md +134 -0
- package/docs/behavior/url-contract.md +129 -0
- package/package.json +141 -3
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ActionFunctionArgs, data, HeadersFunction, LoaderFunctionArgs } from "react-router";
|
|
2
|
+
import { ExplorerConfig } from "../core/config.js";
|
|
3
|
+
import { ExplorerEntry, ExplorerFailureDocument, ExplorerHeading, ExplorerMediaType, ExplorerNotice, ExplorerPageData } from "../core/contracts.js";
|
|
4
|
+
import { ExplorerPathRules } from "../core/paths.js";
|
|
5
|
+
import { ExplorerLogger } from "./errors.js";
|
|
6
|
+
import { HtmlSanitizerOptions } from "./sanitizer.js";
|
|
7
|
+
export type ExplorerAuthentication<P> = {
|
|
8
|
+
readonly status: "signed-in";
|
|
9
|
+
readonly principal: P;
|
|
10
|
+
readonly headers?: HeadersInit;
|
|
11
|
+
} | {
|
|
12
|
+
readonly status: "signed-out";
|
|
13
|
+
readonly headers?: HeadersInit;
|
|
14
|
+
} | {
|
|
15
|
+
readonly status: "forbidden";
|
|
16
|
+
readonly headers?: HeadersInit;
|
|
17
|
+
};
|
|
18
|
+
export type ExplorerSourceContext<P> = {
|
|
19
|
+
readonly principal: P;
|
|
20
|
+
readonly request: Request;
|
|
21
|
+
readonly signal: AbortSignal;
|
|
22
|
+
readonly refresh: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type ExplorerBoundaryAnswer = {
|
|
25
|
+
readonly inside: false;
|
|
26
|
+
} | {
|
|
27
|
+
readonly inside: true;
|
|
28
|
+
readonly readPath: string;
|
|
29
|
+
};
|
|
30
|
+
export type ExplorerSourceBoundary<P> = {
|
|
31
|
+
readonly contains: (path: string) => boolean;
|
|
32
|
+
readonly confirm?: (context: ExplorerSourceContext<P>, path: string) => Promise<ExplorerBoundaryAnswer>;
|
|
33
|
+
};
|
|
34
|
+
export type ExplorerSource<P> = {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly label: string | ((principal: P) => string);
|
|
37
|
+
readonly boundary: ExplorerSourceBoundary<P>;
|
|
38
|
+
readonly tree: (context: ExplorerSourceContext<P>) => Promise<readonly ExplorerEntry[]>;
|
|
39
|
+
readonly children?: (context: ExplorerSourceContext<P>, path: string, readPath: string) => Promise<readonly ExplorerEntry[]>;
|
|
40
|
+
readonly cacheScope?: "principal" | "shared";
|
|
41
|
+
};
|
|
42
|
+
export type ExplorerByteRange = {
|
|
43
|
+
readonly offset: number;
|
|
44
|
+
readonly length: number;
|
|
45
|
+
};
|
|
46
|
+
export type ExplorerAssetFile = {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly contentType: string;
|
|
49
|
+
readonly size: number | null;
|
|
50
|
+
readonly version: string;
|
|
51
|
+
readonly open: (range: ExplorerByteRange | null) => Promise<ReadableStream<Uint8Array>>;
|
|
52
|
+
};
|
|
53
|
+
export type ExplorerHostDocument = {
|
|
54
|
+
readonly kind: "markdown";
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly htmlContent: string;
|
|
57
|
+
readonly headings: readonly ExplorerHeading[];
|
|
58
|
+
} | {
|
|
59
|
+
readonly kind: "text";
|
|
60
|
+
readonly name: string;
|
|
61
|
+
readonly text: string;
|
|
62
|
+
readonly truncated: boolean;
|
|
63
|
+
readonly version: string;
|
|
64
|
+
} | {
|
|
65
|
+
readonly kind: "image";
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly version: string;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: "media";
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly mediaType: ExplorerMediaType;
|
|
72
|
+
readonly version: string;
|
|
73
|
+
} | {
|
|
74
|
+
readonly kind: "file";
|
|
75
|
+
readonly name: string;
|
|
76
|
+
readonly version: string;
|
|
77
|
+
} | ExplorerFailureDocument;
|
|
78
|
+
export type MarkdownExplorerServerConfig<P> = {
|
|
79
|
+
readonly config: ExplorerConfig;
|
|
80
|
+
readonly authenticate: (request: Request) => Promise<ExplorerAuthentication<P>>;
|
|
81
|
+
readonly principalKey: (principal: P) => string;
|
|
82
|
+
readonly scopeSecret: string;
|
|
83
|
+
readonly sources: readonly ExplorerSource<P>[];
|
|
84
|
+
readonly document: (context: ExplorerSourceContext<P>, path: string, readPath: string) => Promise<ExplorerHostDocument>;
|
|
85
|
+
readonly asset: (context: ExplorerSourceContext<P>, path: string, readPath: string) => Promise<ExplorerAssetFile>;
|
|
86
|
+
readonly signedOutPage: (request: Request, headers: Headers) => Response;
|
|
87
|
+
readonly forbiddenPage?: (request: Request, headers: Headers) => Response;
|
|
88
|
+
readonly notices?: (context: ExplorerSourceContext<P>) => Promise<readonly ExplorerNotice[]>;
|
|
89
|
+
readonly deny?: ExplorerPathRules | false;
|
|
90
|
+
readonly treeCache?: {
|
|
91
|
+
readonly ttlMs?: number;
|
|
92
|
+
readonly maxEntries?: number;
|
|
93
|
+
readonly maxBytes?: number;
|
|
94
|
+
} | false;
|
|
95
|
+
readonly sanitizer?: HtmlSanitizerOptions;
|
|
96
|
+
readonly deferredTimeoutMs?: number;
|
|
97
|
+
readonly logger?: ExplorerLogger;
|
|
98
|
+
readonly onTreeRefreshed?: (key: string) => void;
|
|
99
|
+
};
|
|
100
|
+
export type ExplorerPageLoaderResult = ReturnType<typeof data<ExplorerPageData>>;
|
|
101
|
+
export type MarkdownExplorerServer = {
|
|
102
|
+
readonly loader: (args: LoaderFunctionArgs) => Promise<ExplorerPageLoaderResult>;
|
|
103
|
+
readonly headers: HeadersFunction;
|
|
104
|
+
readonly dataLoader: (args: LoaderFunctionArgs) => Promise<Response>;
|
|
105
|
+
readonly dataAction: (args: ActionFunctionArgs) => Promise<Response>;
|
|
106
|
+
readonly forgetTree: (key: string) => void;
|
|
107
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SanitizeJobInput = {
|
|
2
|
+
readonly html: string;
|
|
3
|
+
readonly iframeHosts: readonly string[];
|
|
4
|
+
readonly dataPath: string;
|
|
5
|
+
readonly maxNodes: number;
|
|
6
|
+
readonly maxAttributeChars: number;
|
|
7
|
+
readonly maxSanitizedChars: number;
|
|
8
|
+
};
|
|
9
|
+
export type SanitizeJobOutput = {
|
|
10
|
+
readonly status: "sanitized";
|
|
11
|
+
readonly html: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly status: "too-large";
|
|
14
|
+
} | {
|
|
15
|
+
readonly status: "error";
|
|
16
|
+
readonly message: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SanitizeJobInput = {
|
|
2
|
+
readonly html: string;
|
|
3
|
+
readonly iframeHosts: readonly string[];
|
|
4
|
+
readonly dataPath: string;
|
|
5
|
+
readonly maxNodes: number;
|
|
6
|
+
readonly maxAttributeChars: number;
|
|
7
|
+
readonly maxSanitizedChars: number;
|
|
8
|
+
};
|
|
9
|
+
export type SanitizeJobOutput = {
|
|
10
|
+
readonly status: "sanitized";
|
|
11
|
+
readonly html: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly status: "too-large";
|
|
14
|
+
} | {
|
|
15
|
+
readonly status: "error";
|
|
16
|
+
readonly message: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SanitizeJobInput, SanitizeJobOutput } from "./sanitizeProtocol.cjs";
|
|
2
|
+
import { WorkerPool } from "./workerPool.cjs";
|
|
3
|
+
export type HtmlSanitizerOptions = {
|
|
4
|
+
readonly workers?: number;
|
|
5
|
+
readonly deadlineMs?: number;
|
|
6
|
+
readonly maxHtmlBytes?: number;
|
|
7
|
+
readonly cacheBytes?: number;
|
|
8
|
+
readonly workerHeapMb?: number;
|
|
9
|
+
readonly jobsPerScope?: number;
|
|
10
|
+
readonly maxQueuedJobs?: number;
|
|
11
|
+
};
|
|
12
|
+
export type ResolvedHtmlSanitizerOptions = Required<HtmlSanitizerOptions>;
|
|
13
|
+
export declare const SANITIZER_SCOPE_HEADROOM = 3;
|
|
14
|
+
export declare const DEFAULT_SANITIZER_WORKERS = 2;
|
|
15
|
+
export declare const defaultSanitizerJobsPerScope: (maxPanels: number, workers: number) => number;
|
|
16
|
+
export declare const SANITIZER_QUEUED_READERS = 4;
|
|
17
|
+
export declare const SANITIZER_YOUNG_GENERATION_MB = 32;
|
|
18
|
+
export declare const DEFAULT_SANITIZER_WORKER_HEAP_MB = 512;
|
|
19
|
+
export declare const SANITIZER_WORKER_BASE_HEAP_MB = 64;
|
|
20
|
+
export declare const SANITIZER_HEAP_BYTES_PER_NODE = 4096;
|
|
21
|
+
export declare const SANITIZER_HEAP_BYTES_PER_HTML_BYTE = 32;
|
|
22
|
+
export declare const MIN_HTML_NODE_BUDGET = 4096;
|
|
23
|
+
export declare const SANITIZED_HTML_CHARS_PER_HTML_BYTE = 4;
|
|
24
|
+
export declare const htmlNodeBudget: ({ workerHeapMb, maxHtmlBytes }: Pick<ResolvedHtmlSanitizerOptions, "workerHeapMb" | "maxHtmlBytes">) => number;
|
|
25
|
+
export declare const htmlAttributeCharBudget: ({ maxHtmlBytes }: Pick<ResolvedHtmlSanitizerOptions, "maxHtmlBytes">) => number;
|
|
26
|
+
export declare const sanitizedHtmlLimit: ({ maxHtmlBytes }: Pick<ResolvedHtmlSanitizerOptions, "maxHtmlBytes">) => number;
|
|
27
|
+
export declare const defaultHtmlSanitizerOptions: (maxPanels: number) => ResolvedHtmlSanitizerOptions;
|
|
28
|
+
export declare const SANITIZED_HTML_CACHE_ENTRIES = 1024;
|
|
29
|
+
export type SanitizedHtml = {
|
|
30
|
+
readonly status: "sanitized";
|
|
31
|
+
readonly html: string;
|
|
32
|
+
} | {
|
|
33
|
+
readonly status: "rejected";
|
|
34
|
+
readonly reason: "too-large" | "failed";
|
|
35
|
+
readonly cause: Error | null;
|
|
36
|
+
};
|
|
37
|
+
export type SanitizeRequest = {
|
|
38
|
+
readonly scope: string;
|
|
39
|
+
readonly signal: AbortSignal;
|
|
40
|
+
};
|
|
41
|
+
export type HtmlSanitizerService = (html: string, request: SanitizeRequest) => Promise<SanitizedHtml>;
|
|
42
|
+
export type SanitizerPool = WorkerPool<SanitizeJobInput, SanitizeJobOutput>;
|
|
43
|
+
export declare const resolveHtmlSanitizerOptions: (value: unknown, maxPanels: number) => ResolvedHtmlSanitizerOptions;
|
|
44
|
+
export declare const sanitizerWorkerUrl: () => URL;
|
|
45
|
+
export declare const sharedSanitizerPool: (url: URL, options: Pick<ResolvedHtmlSanitizerOptions, "workers" | "deadlineMs" | "workerHeapMb" | "jobsPerScope" | "maxQueuedJobs">) => SanitizerPool;
|
|
46
|
+
export declare const createHtmlSanitizerService: ({ iframeHosts, dataPath, options, pool }: {
|
|
47
|
+
readonly iframeHosts: readonly string[];
|
|
48
|
+
readonly dataPath: string;
|
|
49
|
+
readonly options: ResolvedHtmlSanitizerOptions;
|
|
50
|
+
readonly pool: SanitizerPool;
|
|
51
|
+
}) => HtmlSanitizerService;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SanitizeJobInput, SanitizeJobOutput } from "./sanitizeProtocol.js";
|
|
2
|
+
import { WorkerPool } from "./workerPool.js";
|
|
3
|
+
export type HtmlSanitizerOptions = {
|
|
4
|
+
readonly workers?: number;
|
|
5
|
+
readonly deadlineMs?: number;
|
|
6
|
+
readonly maxHtmlBytes?: number;
|
|
7
|
+
readonly cacheBytes?: number;
|
|
8
|
+
readonly workerHeapMb?: number;
|
|
9
|
+
readonly jobsPerScope?: number;
|
|
10
|
+
readonly maxQueuedJobs?: number;
|
|
11
|
+
};
|
|
12
|
+
export type ResolvedHtmlSanitizerOptions = Required<HtmlSanitizerOptions>;
|
|
13
|
+
export declare const SANITIZER_SCOPE_HEADROOM = 3;
|
|
14
|
+
export declare const DEFAULT_SANITIZER_WORKERS = 2;
|
|
15
|
+
export declare const defaultSanitizerJobsPerScope: (maxPanels: number, workers: number) => number;
|
|
16
|
+
export declare const SANITIZER_QUEUED_READERS = 4;
|
|
17
|
+
export declare const SANITIZER_YOUNG_GENERATION_MB = 32;
|
|
18
|
+
export declare const DEFAULT_SANITIZER_WORKER_HEAP_MB = 512;
|
|
19
|
+
export declare const SANITIZER_WORKER_BASE_HEAP_MB = 64;
|
|
20
|
+
export declare const SANITIZER_HEAP_BYTES_PER_NODE = 4096;
|
|
21
|
+
export declare const SANITIZER_HEAP_BYTES_PER_HTML_BYTE = 32;
|
|
22
|
+
export declare const MIN_HTML_NODE_BUDGET = 4096;
|
|
23
|
+
export declare const SANITIZED_HTML_CHARS_PER_HTML_BYTE = 4;
|
|
24
|
+
export declare const htmlNodeBudget: ({ workerHeapMb, maxHtmlBytes }: Pick<ResolvedHtmlSanitizerOptions, "workerHeapMb" | "maxHtmlBytes">) => number;
|
|
25
|
+
export declare const htmlAttributeCharBudget: ({ maxHtmlBytes }: Pick<ResolvedHtmlSanitizerOptions, "maxHtmlBytes">) => number;
|
|
26
|
+
export declare const sanitizedHtmlLimit: ({ maxHtmlBytes }: Pick<ResolvedHtmlSanitizerOptions, "maxHtmlBytes">) => number;
|
|
27
|
+
export declare const defaultHtmlSanitizerOptions: (maxPanels: number) => ResolvedHtmlSanitizerOptions;
|
|
28
|
+
export declare const SANITIZED_HTML_CACHE_ENTRIES = 1024;
|
|
29
|
+
export type SanitizedHtml = {
|
|
30
|
+
readonly status: "sanitized";
|
|
31
|
+
readonly html: string;
|
|
32
|
+
} | {
|
|
33
|
+
readonly status: "rejected";
|
|
34
|
+
readonly reason: "too-large" | "failed";
|
|
35
|
+
readonly cause: Error | null;
|
|
36
|
+
};
|
|
37
|
+
export type SanitizeRequest = {
|
|
38
|
+
readonly scope: string;
|
|
39
|
+
readonly signal: AbortSignal;
|
|
40
|
+
};
|
|
41
|
+
export type HtmlSanitizerService = (html: string, request: SanitizeRequest) => Promise<SanitizedHtml>;
|
|
42
|
+
export type SanitizerPool = WorkerPool<SanitizeJobInput, SanitizeJobOutput>;
|
|
43
|
+
export declare const resolveHtmlSanitizerOptions: (value: unknown, maxPanels: number) => ResolvedHtmlSanitizerOptions;
|
|
44
|
+
export declare const sanitizerWorkerUrl: () => URL;
|
|
45
|
+
export declare const sharedSanitizerPool: (url: URL, options: Pick<ResolvedHtmlSanitizerOptions, "workers" | "deadlineMs" | "workerHeapMb" | "jobsPerScope" | "maxQueuedJobs">) => SanitizerPool;
|
|
46
|
+
export declare const createHtmlSanitizerService: ({ iframeHosts, dataPath, options, pool }: {
|
|
47
|
+
readonly iframeHosts: readonly string[];
|
|
48
|
+
readonly dataPath: string;
|
|
49
|
+
readonly options: ResolvedHtmlSanitizerOptions;
|
|
50
|
+
readonly pool: SanitizerPool;
|
|
51
|
+
}) => HtmlSanitizerService;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type WorkerHeapLimits = {
|
|
2
|
+
readonly maxOldGenerationSizeMb: number;
|
|
3
|
+
readonly maxYoungGenerationSizeMb: number;
|
|
4
|
+
};
|
|
5
|
+
export type WorkerPoolOptions = {
|
|
6
|
+
readonly url: URL;
|
|
7
|
+
readonly size: number;
|
|
8
|
+
readonly deadlineMs: number;
|
|
9
|
+
readonly jobsPerScope: number;
|
|
10
|
+
readonly maxQueuedJobs: number;
|
|
11
|
+
readonly heapLimits: WorkerHeapLimits;
|
|
12
|
+
};
|
|
13
|
+
export type WorkerJobTicket = {
|
|
14
|
+
readonly scope: string;
|
|
15
|
+
readonly signal: AbortSignal;
|
|
16
|
+
};
|
|
17
|
+
export type WorkerJobResult<O> = {
|
|
18
|
+
readonly status: "done";
|
|
19
|
+
readonly output: O;
|
|
20
|
+
} | {
|
|
21
|
+
readonly status: "timeout";
|
|
22
|
+
} | {
|
|
23
|
+
readonly status: "crashed";
|
|
24
|
+
readonly error: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: "unavailable";
|
|
27
|
+
readonly error: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
readonly status: "refused";
|
|
30
|
+
readonly limit: "scope" | "queue";
|
|
31
|
+
} | {
|
|
32
|
+
readonly status: "aborted";
|
|
33
|
+
};
|
|
34
|
+
export type WorkerPool<I, O> = {
|
|
35
|
+
readonly run: (input: I, ticket: WorkerJobTicket) => Promise<WorkerJobResult<O>>;
|
|
36
|
+
readonly workers: () => number;
|
|
37
|
+
};
|
|
38
|
+
export declare const WORKER_READY_MESSAGE: Readonly<{
|
|
39
|
+
ready: true;
|
|
40
|
+
}>;
|
|
41
|
+
export declare const jobsRunningPerScope: (size: number) => number;
|
|
42
|
+
export declare const createWorkerPool: <I, O>({ url, size, deadlineMs, jobsPerScope, maxQueuedJobs, heapLimits }: WorkerPoolOptions) => WorkerPool<I, O>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type WorkerHeapLimits = {
|
|
2
|
+
readonly maxOldGenerationSizeMb: number;
|
|
3
|
+
readonly maxYoungGenerationSizeMb: number;
|
|
4
|
+
};
|
|
5
|
+
export type WorkerPoolOptions = {
|
|
6
|
+
readonly url: URL;
|
|
7
|
+
readonly size: number;
|
|
8
|
+
readonly deadlineMs: number;
|
|
9
|
+
readonly jobsPerScope: number;
|
|
10
|
+
readonly maxQueuedJobs: number;
|
|
11
|
+
readonly heapLimits: WorkerHeapLimits;
|
|
12
|
+
};
|
|
13
|
+
export type WorkerJobTicket = {
|
|
14
|
+
readonly scope: string;
|
|
15
|
+
readonly signal: AbortSignal;
|
|
16
|
+
};
|
|
17
|
+
export type WorkerJobResult<O> = {
|
|
18
|
+
readonly status: "done";
|
|
19
|
+
readonly output: O;
|
|
20
|
+
} | {
|
|
21
|
+
readonly status: "timeout";
|
|
22
|
+
} | {
|
|
23
|
+
readonly status: "crashed";
|
|
24
|
+
readonly error: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: "unavailable";
|
|
27
|
+
readonly error: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
readonly status: "refused";
|
|
30
|
+
readonly limit: "scope" | "queue";
|
|
31
|
+
} | {
|
|
32
|
+
readonly status: "aborted";
|
|
33
|
+
};
|
|
34
|
+
export type WorkerPool<I, O> = {
|
|
35
|
+
readonly run: (input: I, ticket: WorkerJobTicket) => Promise<WorkerJobResult<O>>;
|
|
36
|
+
readonly workers: () => number;
|
|
37
|
+
};
|
|
38
|
+
export declare const WORKER_READY_MESSAGE: Readonly<{
|
|
39
|
+
ready: true;
|
|
40
|
+
}>;
|
|
41
|
+
export declare const jobsRunningPerScope: (size: number) => number;
|
|
42
|
+
export declare const createWorkerPool: <I, O>({ url, size, deadlineMs, jobsPerScope, maxQueuedJobs, heapLimits }: WorkerPoolOptions) => WorkerPool<I, O>;
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("./dataRoute-CKbdwkQ7.cjs"),Ze=require("node:crypto"),Se=require("react-router"),ee=require("./location-DWHQYntR.cjs"),te=require("node:fs"),q=require("node:path"),Te=require("node:url"),mt=require("node:worker_threads"),fe=require("node:fs/promises"),pt=require("node:stream");var xe=typeof document<"u"?document.currentScript:null;const Me=Object.freeze({kind:"anonymous"}),ht=Object.freeze({authenticate:async()=>({status:"signed-in",principal:Me}),principalKey:()=>Me.kind,signedOutPage:(e,t)=>new Response(null,{status:401,headers:t})}),wt="application/octet-stream",qe="application/pdf",gt=new Set(["image/png","image/apng","image/jpeg","image/gif","image/webp","image/avif","image/bmp","image/x-icon","image/vnd.microsoft.icon","audio/mpeg","audio/mp4","audio/aac","audio/ogg","audio/opus","audio/wav","audio/wave","audio/x-wav","audio/webm","audio/flac","audio/x-flac","audio/x-m4a","video/mp4","video/webm","video/ogg","video/quicktime",qe]),et="default-src 'none'; media-src 'self'; style-src 'unsafe-inline'",Et=`${et}; sandbox`,yt=`${et}; sandbox allow-same-origin`,ve=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u,bt=/^[\t\n\r ]+|[\t\n\r ]+$/gu,Ne=/^[\t ]+|[\t ]+$/gu,St=e=>{const t=[];let r="",n=0;for(;;){for(;n<e.length&&e[n]!=='"'&&e[n]!==",";)r+=e[n],n+=1;if(n<e.length&&e[n]==='"'){const s=n;for(n+=1;n<e.length&&e[n]!=='"';)n+=e[n]==="\\"?2:1;if(n=Math.min(n+1,e.length),r+=e.slice(s,n),n<e.length)continue}if(t.push(r.replace(Ne,"")),r="",n>=e.length)return t;n+=1}},Tt=e=>{const t=e.replace(bt,""),r=t.indexOf("/");if(r<0)return null;const n=t.slice(0,r),s=t.indexOf(";",r+1),o=t.slice(r+1,s<0?t.length:s).replace(/[\t\n\r ]+$/u,"");return ve.test(n)&&ve.test(o)?`${n}/${o}`.toLowerCase():null},xt=e=>{let t=null;for(const r of St(e)){const n=Tt(r);n!==null&&n!=="*/*"&&(t=n)}return t},Ot=/['()*]/gu,Rt=e=>encodeURIComponent(e).replace(Ot,t=>`%${t.charCodeAt(0).toString(16).toUpperCase()}`),_t=Object.freeze({"Cache-Control":"private, max-age=60, stale-while-revalidate=600, no-transform",Vary:"Cookie"}),kt=(e,t)=>{const r=xt(e),n=r!==null&>.has(r),s=n&&(r.startsWith("audio/")||r.startsWith("video/"));return{contentType:r===null?wt:r,inline:n,headers:{"X-Content-Type-Options":"nosniff","Content-Disposition":`${n?"inline":"attachment"}; filename*=UTF-8''${Rt(t)}`,...r===qe?{}:{"Content-Security-Policy":s?yt:Et},"Cross-Origin-Resource-Policy":"same-origin"}}},At=/^bytes=(?:(\d+)-(\d*)|-(\d+))$/iu,pe=Object.freeze({kind:"whole"}),Le=Object.freeze({kind:"unsatisfiable"}),Nt=(e,t,r)=>{const n=e.get("Range");if(n===null)return pe;const s=e.get("If-Range");if(s!==null&&s!==r)return pe;const o=At.exec(n);if(o===null)return pe;const[,c,m,g]=o;if(g!==void 0){const h=Number(g);if(h===0)return Le;const A=Math.max(0,t-h);return{kind:"partial",range:{offset:A,length:t-A}}}const x=Number(c),l=m===""?Number.POSITIVE_INFINITY:Number(m);return l<x?pe:x>=t?Le:{kind:"partial",range:{offset:x,length:Math.min(l,t-1)-x+1}}},$e="W/",Pt=e=>{const t=[];let r=0,n=!1;for(let s=0;s<e.length;s+=1)e[s]==='"'?n=!n:e[s]===","&&!n&&(t.push(e.slice(r,s).replace(Ne,"")),r=s+1);return t.push(e.slice(r).replace(Ne,"")),t},je=e=>e.startsWith($e)?e.slice($e.length):e,It=(e,t)=>{if(e===null)return!1;const r=Pt(e);if(r.length===1&&r[0]==="*")return!0;const n=je(t);return r.some(s=>je(s)===n)},Mt=(e,t)=>{let r=t;return e.pipeThrough(new TransformStream({transform:(n,s)=>{const o=n.byteLength>r?n.subarray(0,r):n;r-=o.byteLength,s.enqueue(o),r===0&&s.terminate()},flush:n=>{r>0&&n.error(new RangeError("[markdown-explorer] the asset ended before its declared length"))}}))},Oe=async(e,t,r,n,s)=>{if(n!==null&&r.set("Content-Length",String(n)),e.method.toUpperCase()==="HEAD")return new Response(null,{status:t,headers:r});const o=await s();return new Response(n===null?o:Mt(o,n),{status:t,headers:r})},vt=(e,t)=>{const r=new Headers(e);for(const[n,s]of Object.entries({"Accept-Ranges":"bytes","Content-Range":`bytes */${t}`,"Cache-Control":"private, no-store","X-Content-Type-Options":"nosniff"}))r.set(n,s);return new Response(null,{status:416,headers:r})},Lt=async(e,t,r)=>{const n=`"${t.version}"`,s=kt(t.contentType,t.name),o=new Headers(r);for(const[g,x]of Object.entries({...s.headers,..._t,ETag:n}))o.set(g,x);if(It(e.headers.get("If-None-Match"),n))return new Response(null,{status:304,headers:o});if(o.set("Content-Type",s.contentType),t.size===null)return Oe(e,200,o,null,()=>t.open(null));if(t.size===0)return o.set("Content-Length","0"),new Response(null,{status:200,headers:o});o.set("Accept-Ranges","bytes");const c=Nt(e.headers,t.size,n);if(c.kind==="unsatisfiable")return vt(r,t.size);if(c.kind==="whole")return Oe(e,200,o,t.size,()=>t.open(null));const{range:m}=c;return o.set("Content-Range",`bytes ${m.offset}-${m.offset+m.length-1}/${t.size}`),Oe(e,206,o,m.length,()=>t.open(m))},Re=(e,t)=>{const r=new AbortController,n=t(AbortSignal.any([e.signal,r.signal]));return new Promise(s=>{let o;const c=setTimeout(()=>{s(e.late),o=setTimeout(()=>r.abort(),e.handOverMs)},e.timeoutMs),m=()=>{clearTimeout(c),clearTimeout(o)};n.then(g=>{m(),s(g)},()=>{m(),s(e.late)})})},$t=Object.freeze({error:(e,t)=>{console.error(e,t)}}),tt=Symbol.for("@aiquants/markdown-explorer:ExplorerError");class me extends Error{reason;code;constructor(t,r={}){if(!y.isExplorerFailureReason(t))throw new RangeError(`[markdown-explorer] ExplorerError needs a failure reason; got ${JSON.stringify(t)}`);if(r.code!==void 0&&!y.isExplorerFailureCode(r.code))throw new RangeError(`[markdown-explorer] an ExplorerError code must be 1 to 64 lowercase letters, digits or hyphens; got ${JSON.stringify(r.code)}`);super(`[markdown-explorer] ${t}`,r.cause===void 0?void 0:{cause:r.cause}),this.name="ExplorerError",this.reason=t,this.code=r.code,Object.defineProperty(this,tt,{value:!0})}}const jt=e=>e instanceof Error&&Object.hasOwn(e,tt)&&y.isExplorerFailureReason(e.reason),Dt=e=>typeof e=="object"&&e!==null&&e.name==="AbortError",Pe=(e,t,r)=>{try{e.error(t,r)}catch(n){console.error("[markdown-explorer] the logger threw while reporting a failure",{operation:r.operation,error:n})}},ne=(e,t)=>Pe(e,`[markdown-explorer] ${t.operation} failed`,t),zt=(e,t)=>Pe(e,`[markdown-explorer] ${t.operation} left out an entry`,t),Ct=(e,t)=>Pe(e,`[markdown-explorer] ${t.operation} refused a path the deny rules match`,{...t,error:new RangeError("[markdown-explorer] the path matches a deny rule")}),De=(e,t)=>{e===null||typeof e!="object"&&typeof e!="function"||Promise.resolve(e).then(void 0,t)},re=(e,t,r,n)=>jt(e)?y.failureOf(e.reason,e.code):(Dt(e)||r.aborted||ne(n,{...t,error:e}),{reason:"failed"}),ze=(e,t)=>{if(!Number.isSafeInteger(t)||t<=0)throw new RangeError(`[markdown-explorer] a cache ${e} must be a positive integer; got ${String(t)}`)},rt=(e,t)=>{ze("capacity",e),t!==void 0&&ze("weight budget",t.max);const r=new Map;let n=0;const s=o=>{const c=r.get(o);c!==void 0&&(r.delete(o),n-=c.weight)};return{get:o=>{const c=r.get(o);if(c!==void 0)return r.delete(o),r.set(o,c),c.value},set:(o,c)=>{s(o);const m=t===void 0?0:t.of(o,c);if(!(t!==void 0&&m>t.max)){r.set(o,{value:c,weight:m}),n+=m;for(const g of r.keys()){if(r.size<=e&&(t===void 0||n<=t.max))break;s(g)}}},delete:s,size:()=>r.size,weight:()=>n}},nt=e=>Math.max(1,e-1),Ht=2147483647,Ce=Object.freeze({status:"aborted"}),se=(e,t,r)=>{const n=(e.get(t)??0)+r;n===0?e.delete(t):e.set(t,n)},ce=(e,t,r)=>{if(!Number.isSafeInteger(t)||t<=0||t>r)throw new RangeError(`[markdown-explorer] a worker pool's ${e} must be an integer in 1..${String(r)}; got ${String(t)}`)},Bt=e=>e!==null&&typeof e=="object"&&e.ready===!0&&!("id"in e),Ft=(e,t)=>e!==null&&typeof e=="object"&&e.id===t&&"output"in e,Ut=({url:e,size:t,deadlineMs:r,jobsPerScope:n,maxQueuedJobs:s,heapLimits:o})=>{ce("size",t,Number.MAX_SAFE_INTEGER),ce("deadline",r,Ht),ce("jobsPerScope",n,Number.MAX_SAFE_INTEGER),ce("maxQueuedJobs",s,Number.MAX_SAFE_INTEGER),ce("maxOldGenerationSizeMb",o.maxOldGenerationSizeMb,Number.MAX_SAFE_INTEGER),ce("maxYoungGenerationSizeMb",o.maxYoungGenerationSizeMb,Number.MAX_SAFE_INTEGER);const c=Object.freeze({maxOldGenerationSizeMb:o.maxOldGenerationSizeMb,maxYoungGenerationSizeMb:o.maxYoungGenerationSizeMb}),m=new Set,g=[],x=new Map,l=new Map,h=nt(t);let A=0;const k=(u,a)=>{const{job:E}=u;u.timer!==null&&clearTimeout(u.timer),u.timer=null,u.job=null,u.sent=!1,E!==null&&(se(x,E.scope,-1),se(l,E.scope,-1),E.settle(a))},I=u=>{const[a]=g.splice(u,1);a?.detach()},R=()=>{let u=-1,a=h,E=Number.POSITIVE_INFINITY;for(const[S,_]of g.entries()){const P=x.get(_.scope)??0,C=l.get(_.scope)??0;(P<a||P===a&&u>=0&&C<E)&&(u=S,a=P,E=C)}return u},O=(u,a)=>{m.has(u)&&(m.delete(u),k(u,a),u.worker.terminate().catch(()=>{}),D())},L=u=>{u.timer=setTimeout(()=>{O(u,{status:"timeout"})},r)},b=(u,a)=>u.sent?{status:"crashed",error:a}:{status:"unavailable",error:a},N=(u,a)=>{u.sent=!0,L(u);try{u.worker.postMessage({id:a.id,input:a.input})}catch(E){k(u,{status:"crashed",error:E}),u.worker.unref()}},z=()=>{const u={worker:new mt.Worker(e,{resourceLimits:c}),job:null,sent:!1,timer:null,ready:!1};return u.worker.unref(),u.timer=setTimeout(()=>{O(u,{status:"unavailable",error:new Error(`[markdown-explorer] a worker did not start within ${String(r)} ms`)})},r),u.worker.on("message",a=>{if(!u.ready&&Bt(a)){u.ready=!0,u.timer!==null&&clearTimeout(u.timer),u.timer=null,u.job===null?u.worker.unref():N(u,u.job),u.job===null&&D();return}const{job:E}=u;if(E===null||!Ft(a,E.id)){O(u,b(u,new Error("[markdown-explorer] a worker sent a message that answers no job")));return}k(u,{status:"done",output:a.output}),u.worker.unref(),D()}),u.worker.on("error",a=>{O(u,b(u,a))}),u.worker.on("messageerror",a=>{O(u,b(u,a))}),u.worker.on("exit",a=>{O(u,b(u,new Error(`[markdown-explorer] a worker exited with code ${String(a)}`)))}),m.add(u),u},D=()=>{for(;;){const u=R(),a=g[u];if(a===void 0)return;let E;for(const _ of m)if(_.job===null){E=_;break}if(E===void 0&&m.size<t)try{E=z()}catch(_){I(u),se(l,a.scope,-1),a.settle({status:"unavailable",error:_});continue}if(E===void 0)return;I(u),se(x,a.scope,1);const S=E;S.job=a,S.worker.ref(),S.ready&&N(S,a)}};return{run:(u,{scope:a,signal:E})=>new Promise(S=>{if(E.aborted){S(Ce);return}if((l.get(a)??0)>=n){S({status:"refused",limit:"scope"});return}A+=1;const _={id:A,input:u,scope:a,settle:S,detach:()=>{}};g.push(_),se(l,a,1),D();const P=g.indexOf(_);if(P<0)return;if(g.length>s){I(P),se(l,a,-1),S({status:"refused",limit:"queue"});return}const C=()=>{const M=g.indexOf(_);M<0||(I(M),se(l,a,-1),S(Ce))};E.addEventListener("abort",C,{once:!0}),_.detach=()=>{E.removeEventListener("abort",C)}}),workers:()=>m.size}},Ie=1024*1024,Xt=3e4,Gt=4*Ie,Jt=64*Ie,Yt=3,Wt=2,Kt=(e,t)=>e+nt(t)+Yt,Vt=4,Qt=32,Zt=512,qt=64,er=4096,tr=32,He=4096,rr=4,ot=({workerHeapMb:e,maxHtmlBytes:t})=>Math.floor(((e-qt)*Ie-t*tr)/er),nr=({maxHtmlBytes:e})=>e,or=({maxHtmlBytes:e})=>rr*e,sr=1024,ir=2147483647,ar=64,cr=2**29,ur=1024*1024,lr=64,Be=Object.freeze({status:"rejected",reason:"too-large",cause:null}),_e=Object.freeze({status:"rejected",reason:"failed",cause:null}),ie=(e,t,r,n)=>{if(t===void 0)return r;if(typeof t!="number")throw new TypeError(`[markdown-explorer] sanitizer.${e} must be a number; got ${typeof t}`);if(!Number.isSafeInteger(t)||t<=0||t>n)throw new RangeError(`[markdown-explorer] sanitizer.${e} must be an integer in 1..${String(n)}; got ${String(t)}`);return t},dr=(e,t)=>{if(e!==void 0&&(e===null||typeof e!="object"))throw new TypeError("[markdown-explorer] sanitizer must be an object of { workers, deadlineMs, maxHtmlBytes, cacheBytes, workerHeapMb, jobsPerScope, maxQueuedJobs }");const{workers:r,deadlineMs:n,maxHtmlBytes:s,cacheBytes:o,workerHeapMb:c,jobsPerScope:m,maxQueuedJobs:g}=e??{},x=ie("workers",r,Wt,ar),l=ie("jobsPerScope",m,Kt(t,x),Number.MAX_SAFE_INTEGER),h=Object.freeze({workers:x,deadlineMs:ie("deadlineMs",n,Xt,ir),maxHtmlBytes:ie("maxHtmlBytes",s,Gt,cr),cacheBytes:ie("cacheBytes",o,Jt,Number.MAX_SAFE_INTEGER),workerHeapMb:ie("workerHeapMb",c,Zt,ur),jobsPerScope:l,maxQueuedJobs:ie("maxQueuedJobs",g,Vt*l,Number.MAX_SAFE_INTEGER)});if(h.jobsPerScope<t)throw new RangeError(`[markdown-explorer] sanitizer.jobsPerScope must be at least multiView.maxPanels (${String(t)}), because a page sends the document of every panel to the sanitizer at once for one reader; got ${String(h.jobsPerScope)}`);if(h.maxQueuedJobs<t)throw new RangeError(`[markdown-explorer] sanitizer.maxQueuedJobs must be at least multiView.maxPanels (${String(t)}), because a reader whose page finds every worker busy waits with the document of every panel; got ${String(h.maxQueuedJobs)}`);if(ot(h)<He)throw new RangeError(`[markdown-explorer] sanitizer.workerHeapMb (${String(h.workerHeapMb)} MiB) must leave room for ${String(He)} parsed nodes beside HTML of sanitizer.maxHtmlBytes (${String(h.maxHtmlBytes)} bytes); raise workerHeapMb or lower maxHtmlBytes`);return h},fr=()=>{const e=typeof document>"u"?require("url").pathToFileURL(__filename).href:xe&&xe.tagName.toUpperCase()==="SCRIPT"&&xe.src||new URL("server.cjs",document.baseURI).href,t=new URL(`./sanitizeWorker${q.extname(Te.fileURLToPath(e))}`,e);if(!te.existsSync(Te.fileURLToPath(t)))throw new Error(`[markdown-explorer] the HTML sanitizer worker is missing: ${Te.fileURLToPath(t)}`);return t},Fe=new Map,mr=(e,t)=>{const r=[e.href,t.workers,t.deadlineMs,t.workerHeapMb,t.jobsPerScope,t.maxQueuedJobs].map(String).join("\0");let n=Fe.get(r);return n===void 0&&(n=Ut({url:e,size:t.workers,deadlineMs:t.deadlineMs,jobsPerScope:t.jobsPerScope,maxQueuedJobs:t.maxQueuedJobs,heapLimits:{maxOldGenerationSizeMb:t.workerHeapMb,maxYoungGenerationSizeMb:Qt}}),Fe.set(r,n)),n},pr=e=>{if(e===null||typeof e!="object")return!1;const{status:t,html:r,message:n}=e;return t==="sanitized"&&typeof r=="string"||t==="too-large"||t==="error"&&typeof n=="string"},hr=e=>e instanceof Error&&e.code==="ERR_WORKER_OUT_OF_MEMORY",wr=({iframeHosts:e,dataPath:t,options:r,pool:n})=>{const s=Object.freeze([...e]),o=ot(r),c=nr(r),m=or(r),g=rt(sr,{max:r.cacheBytes,of:(R,O)=>2*R.length+(O.status==="sanitized"?2*O.html.length:lr)}),x=new Map,l=new Map,h=R=>{x.get(R.key)===R&&x.delete(R.key);const O=l.get(R.hash);O?.delete(R),O?.size===0&&l.delete(R.hash)},A=async(R,O,L)=>{const b=await n.run({html:R,iframeHosts:s,dataPath:t,maxNodes:o,maxAttributeChars:c,maxSanitizedChars:m},{scope:O,signal:L});if(b.status==="refused"){const z=b.limit==="scope"?`its reader already has ${String(r.jobsPerScope)} documents being sanitized or waiting`:`${String(r.maxQueuedJobs)} documents are already waiting`;return{outcome:{status:"rejected",reason:"failed",cause:new Error(`[markdown-explorer] the HTML sanitizer refused a document: ${z}`)},lasting:!1}}if(b.status==="aborted")return{outcome:_e,lasting:!1};if(b.status==="timeout")return{outcome:{status:"rejected",reason:"too-large",cause:new Error(`[markdown-explorer] the HTML sanitizer did not finish within ${String(r.deadlineMs)} ms`)},lasting:!0};if(b.status==="unavailable")return{outcome:{status:"rejected",reason:"failed",cause:new Error("[markdown-explorer] the HTML sanitizer worker could not start",{cause:b.error})},lasting:!1};if(b.status==="crashed"&&hr(b.error))return{outcome:{status:"rejected",reason:"too-large",cause:new Error(`[markdown-explorer] the HTML sanitizer ran out of its ${String(r.workerHeapMb)} MiB worker heap`,{cause:b.error})},lasting:!0};if(b.status==="crashed")return{outcome:{status:"rejected",reason:"failed",cause:new Error("[markdown-explorer] the HTML sanitizer worker failed",{cause:b.error})},lasting:!0};const{output:N}=b;return pr(N)?N.status==="too-large"?{outcome:Be,lasting:!0}:N.status==="error"?{outcome:{status:"rejected",reason:"failed",cause:new Error(`[markdown-explorer] the HTML sanitizer failed: ${N.message}`)},lasting:!0}:N.html.length>m?{outcome:{status:"rejected",reason:"too-large",cause:new Error(`[markdown-explorer] the HTML sanitizer's output of ${String(N.html.length)} characters exceeds its limit of ${String(m)}`)},lasting:!0}:{outcome:{status:"sanitized",html:N.html},lasting:!0}:{outcome:{status:"rejected",reason:"failed",cause:new Error("[markdown-explorer] the HTML sanitizer worker answered something that is not an outcome")},lasting:!0}},k=(R,O,L,b)=>{let N=()=>{};const z=new Promise(a=>{N=a}),D={hash:O,key:L,outcome:z,answer:N,controller:new AbortController,waiters:0};x.set(L,D);const u=l.get(O);return u===void 0?l.set(O,new Set([D])):u.add(D),A(R,b,D.controller.signal).catch(a=>({outcome:{status:"rejected",reason:"failed",cause:a instanceof Error?a:new Error(String(a))},lasting:!0})).then(({outcome:a,lasting:E})=>{if(h(D),D.answer(a),!E)return;const S=g.get(O),_=S??(a.status==="rejected"?{...a,cause:null}:a);S===void 0&&g.set(O,_);for(const P of l.get(O)??[])h(P),P.answer(_),P.controller.abort()}),D},I=(R,O)=>new Promise(L=>{R.waiters+=1;const b=()=>{R.waiters-=1,L(_e),!(R.waiters>0)&&(h(R),R.controller.abort())};O.addEventListener("abort",b,{once:!0}),R.outcome.then(N=>{O.removeEventListener("abort",b),L(N)})});return(R,{scope:O,signal:L})=>{if(R.length>r.maxHtmlBytes||Buffer.byteLength(R,"utf8")>r.maxHtmlBytes)return Promise.resolve(Be);const b=Ze.createHash("sha256").update(R,"utf8").digest("base64"),N=g.get(b);if(N!==void 0)return Promise.resolve(N);if(L.aborted)return Promise.resolve(_e);const z=`${b}\0${O}`,D=x.get(z);return I(D===void 0?k(R,b,z,O):D,L)}},be=e=>({result:e,json:JSON.stringify(e)}),ke=Object.freeze(be(Object.freeze({status:"error",failure:Object.freeze({reason:"failed"})}))),gr=e=>e.status==="ok"&&e.entries.length>0,Er=({ttlMs:e,maxEntries:t,maxBytes:r,now:n})=>{const s=rt(t,{max:r,of:(l,h)=>2*h.tree.json.length}),o=new Map,c=new Map,m=(l,h)=>{const A=new AbortController,k={waiters:0,superseded:!1,started:!1,loadTree:h},I=c.get(l),R=(z,D)=>{const u=be(z);return!(k.superseded||A.signal.aborted)&&gr(z)&&s.set(l,{tree:u,loadedAt:D}),u},L=(async()=>{if(I!==void 0&&await I,A.signal.aborted)return ke;const z=n();return k.started=!0,R(await k.loadTree(A.signal,z),z)})().finally(()=>{o.get(l)===b&&o.delete(l)}),b={promise:L,controller:A,state:k};o.set(l,b);const N=L.then(()=>{},()=>{});return c.set(l,N),N.then(()=>{c.get(l)===N&&c.delete(l)}),b},g=(l,h,A)=>{h.state.waiters+=1;const k=()=>{h.state.waiters-=1,!(h.state.waiters>0)&&(o.get(l)===h&&o.delete(l),h.controller.abort())};return A.addEventListener("abort",k,{once:!0}),h.promise.finally(()=>{A.removeEventListener("abort",k)})},x=l=>{s.delete(l);const h=o.get(l);h?.state.started&&(h.state.superseded=!0,o.delete(l))};return{load:(l,h,A)=>{if(h.aborted)return Promise.resolve(ke);const k=s.get(l);if(k!==void 0){if(n()-k.loadedAt<e)return Promise.resolve(k.tree);s.delete(l)}const I=o.get(l);return g(l,I===void 0?m(l,A):I,h)},refresh:(l,h,A)=>{if(h.aborted)return Promise.resolve(ke);x(l);const k=o.get(l);return k===void 0?g(l,m(l,A),h):(k.state.loadTree=A,g(l,k,h))},forget:x}},yr=e=>({load:(t,r,n)=>n(r,e()).then(be),refresh:(t,r,n)=>n(r,e()).then(be),forget:()=>{}}),v=e=>{throw new TypeError(`[markdown-explorer] ${e}`)},br=e=>{if(e===null||typeof e!="object")return null;const{name:t,path:r}=e;return typeof t!="string"||typeof r!="string"?null:t===""?{path:r,reason:"an empty name"}:y.isAcceptableDocumentPath(r)?null:{path:r,reason:"a path the browser cannot address (empty, too long, a lone surrogate, a control character or a backslash)"}},Sr=(e,t,r,n,s)=>{const o=new Set,c=g=>{if(!Array.isArray(g))return v("a source returned entries that are not an array");const x=[];for(const l of g){const h=m(l);h!==null&&x.push(h)}return x},m=g=>{const x=br(g);if(x!==null)return n(x),null;const l=y.entryViolation(g);if(l!==null)return v(l);const h=g;if(o.has(h))return v("a source returned entries that contain themselves");const{name:A,path:k,type:I,mimeType:R,children:O,lazy:L}=h;if(t.deniesName(A)||t.deniesPath(k))return null;if(!s(k))return n({path:k,reason:"a path outside the source's boundary"}),null;const b=R===void 0?{name:A,path:k}:{name:A,path:k,mimeType:R};if(I==="file")return{...b,type:I};if(L===!0)return r?{...b,type:I,lazy:!0,children:[]}:v("a lazy directory came from a source without a children function");if(O===void 0)return{...b,type:I};o.add(h);const N=c(O);return o.delete(h),{...b,type:I,children:N}};return c(e)},Tr=/^[\x21\x23-\x7E]{1,256}$/u,st=e=>typeof e=="string"&&Tr.test(e),xr=new Set(["text","image","media","file"]),Or=e=>typeof e=="string"&&xr.has(e),Rr=(e,t)=>{if(e!==null&&typeof e=="object"){const s=e,{kind:o}=s;if(Or(o)){if(Object.hasOwn(s,"url"))return v(`a ${o} document carries its version, not a url`);if(typeof s.name!="string")return v("a document needs a name string");if(!st(s.version))return v(`a ${o} document needs a version of 1 to 256 visible ASCII characters other than '"'`);const c=t(s.version);switch(o){case"image":case"file":return{kind:o,name:s.name,url:c};case"media":{const{mediaType:m}=s;return y.isExplorerMediaType(m)?{kind:o,name:s.name,url:c,mediaType:m}:v(`unknown media type ${typeof m=="string"?JSON.stringify(m.slice(0,64)):typeof m}`)}case"text":return typeof s.text!="string"||typeof s.truncated!="boolean"?v("a text document needs text and truncated"):{kind:o,name:s.name,text:s.text,truncated:s.truncated,url:c}}}}const r=y.documentViolation(e);if(r!==null)return v(r);const n=e;switch(n.kind){case"failure":{const{reason:s,code:o}=n.failure;return{kind:"failure",failure:y.failureOf(s,o)}}case"markdown":return n.headings.length>y.EXPLORER_MAX_HEADINGS?{kind:"failure",failure:y.failureOf("too-large")}:{kind:"unsanitized-markdown",name:n.name,html:n.htmlContent,headings:n.headings.map(({text:s,depth:o,id:c})=>({text:s,depth:o,id:c}))}}},_r=e=>{if(e===null||typeof e!="object")return v("an asset file must be an object");const{name:t,contentType:r,size:n,version:s,open:o}=e;return typeof t!="string"||t===""?v("an asset file needs a non-empty name"):typeof r!="string"?v("an asset file needs a contentType string ('' when unknown)"):n!==null&&!(typeof n=="number"&&Number.isSafeInteger(n)&&n>=0)?v("an asset file's size must be null or a non-negative safe integer"):st(s)?typeof o!="function"?v("an asset file needs an open function"):Object.freeze({name:t,contentType:r,size:n,version:s,open:o}):v(`an asset file needs a version of 1 to 256 visible ASCII characters other than '"'`)},kr=e=>Array.isArray(e)?e.map(t=>{if(t===null||typeof t!="object")return v("a notice must be an object");const{id:r,tone:n,title:s,body:o,action:c}=t;if(typeof r!="string"||r===""||typeof s!="string")return v("a notice needs an id and a title");if(!y.isExplorerNoticeTone(n))return v(`unknown notice tone ${typeof n=="string"?JSON.stringify(n.slice(0,64)):typeof n}`);if(o!==void 0&&typeof o!="string")return v("a notice body must be a string");const g={id:r,tone:n,title:s,...o===void 0?{}:{body:o}};if(c===void 0)return g;if(c===null||typeof c!="object")return v("a notice action must be an object");const{label:x,href:l}=c;return typeof x!="string"||!y.isAcceptableUrl(l,y.EXPLORER_LINK_PROTOCOLS)?v("a notice action needs a label and an http(s), mailto or relative href"):{...g,action:{label:x,href:l}}}):v("notices must be an array"),Ar=4e3,he=Object.freeze({ttlMs:3e5,maxEntries:64,maxBytes:64*1024*1024}),Ue=32,it="private, no-store",Nr=Object.freeze({"Content-Type":"application/json; charset=utf-8","Cache-Control":it,"X-Content-Type-Options":"nosniff","Content-Security-Policy":"default-src 'none'; frame-ancestors 'none'; sandbox","Cross-Origin-Resource-Policy":"same-origin"}),Pr=22,Ir=2147483647,Ae=Object.freeze([]),Xe=Object.freeze(["contains","confirm"]),J=(e,t,r)=>{if(!(r&&t===void 0)&&typeof t!="function")throw new TypeError(`[markdown-explorer] ${e} must be a function; got ${typeof t}`)},ye=(e,t,r)=>{if(typeof t!="number")throw new TypeError(`[markdown-explorer] ${e} must be a number; got ${typeof t}`);if(!Number.isSafeInteger(t)||t<=0||t>r)throw new RangeError(`[markdown-explorer] ${e} must be an integer in 1..${String(r)}; got ${String(t)}`);return t},Mr=e=>{if(e===null||typeof e!="object"||!Object.isFrozen(e))throw new TypeError("[markdown-explorer] config must be the value returned by defineExplorerConfig");return ee.defineExplorerConfig(e)},vr=e=>{if(typeof e!="string")throw new TypeError(`[markdown-explorer] scopeSecret must be a string; got ${typeof e}`);if(e.length<Ue)throw new RangeError(`[markdown-explorer] scopeSecret must have at least ${String(Ue)} characters; got ${String(e.length)}`);return e},Lr=(e,t)=>{if(!ee.isPlainObject(t))throw new TypeError(`[markdown-explorer] ${e}.boundary must be a plain object { contains, confirm? }`);for(const r of Object.keys(t))if(!Xe.includes(r))throw new RangeError(`[markdown-explorer] ${e}.boundary.${r} is not an option; expected ${Xe.join(", ")}`);J(`${e}.boundary.contains`,t.contains,!1),J(`${e}.boundary.confirm`,t.confirm,!0)},$r=e=>{if(!Array.isArray(e))throw new TypeError("[markdown-explorer] sources must be an array");const t=e;if(t.length===0)throw new RangeError("[markdown-explorer] sources must hold at least one source");const r=new Set;for(const[n,s]of t.entries()){if(s===null||typeof s!="object")throw new TypeError(`[markdown-explorer] sources[${String(n)}] must be an object`);const{id:o,label:c,boundary:m,tree:g,children:x,cacheScope:l}=s;if(!ee.isExplorerSourceId(o))throw new RangeError(`[markdown-explorer] sources[${String(n)}].id must use lowercase letters, digits and "-" and start with a letter or digit; got ${typeof o=="string"?JSON.stringify(o):typeof o}`);const h=`source ${JSON.stringify(o)}`;if(r.has(o))throw new RangeError(`[markdown-explorer] source ids must be unique; ${JSON.stringify(o)} repeats at sources[${String(n)}]`);if(r.add(o),typeof c!="string"&&typeof c!="function")throw new TypeError(`[markdown-explorer] ${h}.label must be a string or a function`);if(typeof c=="string"&&c.trim()==="")throw new RangeError(`[markdown-explorer] ${h}.label must not be blank`);if(Lr(h,m),J(`${h}.tree`,g,!1),J(`${h}.children`,x,!0),l!==void 0&&l!=="principal"&&l!=="shared")throw new RangeError(`[markdown-explorer] ${h}.cacheScope must be "principal" or "shared"; got ${JSON.stringify(l)}`)}return t},jr=e=>{if(e===!1)return!1;if(e===void 0)return he;if(e===null||typeof e!="object")throw new TypeError("[markdown-explorer] treeCache must be { ttlMs, maxEntries, maxBytes } or false");const{ttlMs:t,maxEntries:r,maxBytes:n}=e;return{ttlMs:t===void 0?he.ttlMs:ye("treeCache.ttlMs",t,Number.MAX_SAFE_INTEGER),maxEntries:r===void 0?he.maxEntries:ye("treeCache.maxEntries",r,Number.MAX_SAFE_INTEGER),maxBytes:n===void 0?he.maxBytes:ye("treeCache.maxBytes",n,Number.MAX_SAFE_INTEGER)}},Ge=e=>{if(e===null||typeof e!="object")throw new TypeError("[markdown-explorer] authenticate must resolve to { status, ... }");const{status:t}=e;if(t!=="signed-in"&&t!=="signed-out"&&t!=="forbidden")throw new TypeError(`[markdown-explorer] authenticate resolved to an unknown status ${JSON.stringify(t)}`);return e},Je=(e,t)=>{if(!(t instanceof Response))throw new TypeError(`[markdown-explorer] ${e} must return a Response`);return t},Ye=(e,t)=>{if(typeof t!="string"||t==="")throw new TypeError("[markdown-explorer] principalKey must return a non-empty string");return Ze.createHmac("sha256",e).update(t,"utf8").digest("base64url").slice(0,Pr)},Dr=e=>e.view==="multi"?e.docs:e.doc===null?[]:[e.doc],we=(e,t)=>{const r=e.getAll(t),[n]=r;return r.length===1&&n!==void 0&&n!==""?n:null},ue=(e,t,r,n={})=>{const s=new Headers(r);for(const[o,c]of Object.entries({...Nr,...n}))s.set(o,c);return new Response(t,{status:e,headers:s})},U=(e,t,r)=>ue(y.EXPLORER_FAILURE_STATUS[e.reason],JSON.stringify({failure:e}),t,r),We=e=>{if(e.headers.get(y.EXPLORER_REFRESH_HEADER)!==y.EXPLORER_REFRESH_HEADER_VALUE)return!1;const t=e.headers.get("Sec-Fetch-Site");return t===null||t==="same-origin"},zr=({parentHeaders:e,loaderHeaders:t,errorHeaders:r})=>{const n=new Headers,s=r===void 0?[e,t]:[e,t,r];for(const o of s)for(const[c,m]of o)c!=="set-cookie"&&n.set(c,m);return n},Cr=e=>{if(e===null||typeof e!="object")throw new TypeError("[markdown-explorer] the server configuration must be an object");const t=Mr(e.config),r=vr(e.scopeSecret);J("authenticate",e.authenticate,!1),J("principalKey",e.principalKey,!1),J("document",e.document,!1),J("asset",e.asset,!1),J("signedOutPage",e.signedOutPage,!1),J("forbiddenPage",e.forbiddenPage,!0),J("notices",e.notices,!0),J("onTreeRefreshed",e.onTreeRefreshed,!0);const n=$r(e.sources),s=ee.withExplorerSources(t,n.map(i=>i.id)),o=e.logger;if(o!==void 0&&(o===null||typeof o!="object"||typeof o.error!="function"))throw new TypeError("[markdown-explorer] logger must be an object with an error function");const c=e.logger===void 0?$t:e.logger,m=y.resolvePathRules(e.deny),g=e.deferredTimeoutMs===void 0?Ar:ye("deferredTimeoutMs",e.deferredTimeoutMs,Ir),x=jr(e.treeCache),l=()=>Date.now(),h=x===!1?yr(l):Er({...x,now:l}),A=dr(e.sanitizer,t.multiView.maxPanels),k=wr({iframeHosts:t.iframeHosts,dataPath:t.dataPath,options:A,pool:mr(fr(),A)}),I=new Map(n.map(i=>[i.id,i])),R=ee.documentStyleNames(t),{authenticate:O,principalKey:L,signedOutPage:b,forbiddenPage:N,onTreeRefreshed:z}=e,D=(i,w)=>{if(typeof i.label=="string")return i.label;const d=i.label(w);if(typeof d!="string"||d.trim()==="")throw new TypeError(`[markdown-explorer] the label of source ${JSON.stringify(i.id)} must be a non-blank string`);return d},u=(i,w)=>i.cacheScope==="shared"?i.id:`${i.id}\0${w}`,a=(i,w)=>{const d=f=>ne(c,{operation:"boundary",sourceId:i.id,path:w,error:f});let p;try{p=i.boundary.contains(w)}catch(f){return d(f),!1}return typeof p=="boolean"?p:(De(p,d),d(new TypeError(`[markdown-explorer] boundary.contains of source ${JSON.stringify(i.id)} must return a boolean; got ${typeof p}`)),!1)},E=async(i,w,d)=>{const{confirm:p}=i.boundary;if(p===void 0)return{status:"inside",readPath:d};let f;try{f=await p(w,d)}catch(V){return{status:"error",failure:re(V,{operation:"boundary",sourceId:i.id,path:d},w.signal,c)}}const T=V=>(ne(c,{operation:"boundary",sourceId:i.id,path:d,error:new TypeError(`[markdown-explorer] boundary.confirm of source ${JSON.stringify(i.id)} ${V}`)}),{status:"outside"});if(f===null||typeof f!="object")return T(`must resolve to { inside, readPath? }; got ${f===null?"null":typeof f}`);const{inside:j,readPath:H}=f;return j===!1?{status:"outside"}:j!==!0||typeof H!="string"?T("must resolve to { inside: false } or { inside: true, readPath: string }"):!y.isAcceptableDocumentPath(H)||m.deniesPath(H)?(ne(c,{operation:"boundary",sourceId:i.id,path:d,error:new RangeError(`[markdown-explorer] boundary.confirm of source ${JSON.stringify(i.id)} answered a readPath the explorer does not serve (unacceptable or denied)`)}),{status:"error",failure:{reason:"failed"}}):{status:"inside",readPath:H}},S=async(i,w)=>{const d=n.filter(f=>a(f,w));if(d.length===0)return{status:"error",failure:{reason:"forbidden"}};if(d.some(f=>f.boundary.confirm===void 0))return{status:"inside",readPath:w};let p=null;for(const f of d){const T=await E(f,i,w);if(T.status==="inside")return T;T.status==="error"&&p===null&&(p=T.failure)}return{status:"error",failure:p===null?{reason:"forbidden"}:p}},_=(i,w,d)=>{const p=({path:f,reason:T})=>zt(c,{operation:d.operation,sourceId:d.sourceId,path:f,error:new TypeError(`[markdown-explorer] an entry with ${T} was left out`)});try{return{status:"ok",entries:Sr(i,m,w.children!==void 0,p,f=>a(w,f))}}catch(f){return ne(c,{...d,error:f}),{status:"error",failure:{reason:"failed"}}}},P=async(i,w,d)=>{let p;try{p=await i.tree(w)}catch(T){return{status:"error",failure:re(T,{operation:"tree",sourceId:i.id},w.signal,c)}}const f=_(p,i,{operation:"tree",sourceId:i.id});return f.status==="ok"?{status:"ok",entries:f.entries,loadedAt:d}:f},C=(i,w,d,p)=>(f,T)=>P(i,{principal:w,request:d,signal:f,refresh:p},T),M=(i,w,d,p,f)=>h.load(u(i,d),f,C(i,w,p,!1)),X=(i,w,d,p)=>h.refresh(u(i,d),p.signal,C(i,w,p,!0)),G=(i,w)=>{if(z===void 0)return;const d=p=>ne(c,{operation:"refresh",sourceId:i.id,error:p});try{De(z(w),d)}catch(p){d(p)}},F=async(i,w,d,p)=>{if(m.deniesPath(p)||!a(i,p))return{status:"error",failure:{reason:"forbidden"}};const f=await E(i,d,p);if(f.status==="outside")return{status:"error",failure:{reason:"forbidden"}};if(f.status==="error")return{status:"error",failure:f.failure};let T;try{T=await w(d,p,f.readPath)}catch(j){return{status:"error",failure:re(j,{operation:"children",sourceId:i.id,path:p},d.signal,c)}}return _(T,i,{operation:"children",sourceId:i.id,path:p})},Z=async(i,w,d)=>{if(m.deniesPath(d))return y.failureDocument("forbidden");const p=await S(i,d);if(p.status==="error")return{kind:"failure",failure:p.failure};let f;try{f=await e.document(i,d,p.readPath)}catch(H){return{kind:"failure",failure:re(H,{operation:"document",path:d},i.signal,c)}}let T;try{T=Rr(f,H=>y.explorerAssetHref(t.dataPath,d,H))}catch(H){return ne(c,{operation:"document",path:d,error:H}),y.failureDocument("failed")}if(T.kind!=="unsanitized-markdown")return T;const j=await k(T.html,{scope:w,signal:i.signal});return j.status==="sanitized"?{kind:"markdown",name:T.name,htmlContent:j.html,headings:T.headings}:(j.cause!==null&&ne(c,{operation:"document",path:d,error:j.cause}),y.failureDocument(j.reason))},$=async i=>{if(e.notices===void 0)return Ae;try{return kr(await e.notices(i))}catch(w){return re(w,{operation:"notices"},i.signal,c),Ae}},Y=async(i,w,d)=>{const p=we(w,y.EXPLORER_DATA_QUERY.path);if(p===null||!y.isAcceptableDocumentPath(p))return U({reason:"invalid"},d);if(m.deniesPath(p))return Ct(c,{operation:"asset",path:p}),U({reason:"forbidden"},d);const f=await S(i,p);if(f.status==="error")return U(f.failure,d);try{const T=_r(await e.asset(i,p,f.readPath));return await Lt(i.request,T,d)}catch(T){return U(re(T,{operation:"asset",path:p},i.signal,c),d)}},W=async({request:i,url:w,params:d})=>{const p=Ge(await O(i)),f=new Headers(p.headers);if(f.set("Cache-Control",it),p.status==="signed-out")throw Je("signedOutPage",b(i,f));if(p.status==="forbidden")throw N===void 0?Se.data(null,{status:403,headers:f}):Je("forbiddenPage",N(i,f));const T=ee.resolveExplorerLocation(s,d["*"],w.searchParams);if(T.kind==="redirect")throw Se.redirect(T.href,{headers:f});const{location:j}=T,{principal:H}=p,V=Ye(r,L(H)),de=I.get(j.source);if(de===void 0)throw new RangeError(`[markdown-explorer] no source for ${JSON.stringify(j.source)}`);const oe={signal:i.signal,timeoutMs:g,late:null,handOverMs:g},Q={scope:V,sources:n.map(B=>({id:B.id,label:D(B,H)})),settings:ee.parseViewSettings(ee.readCookieValue(i.headers.get("Cookie"),t.settingsCookie),R),tree:j.view==="single"?null:{sourceId:j.source,result:Re(oe,B=>M(de,H,V,i,B).then(ae=>ae.result))},documents:Dr(j).map(B=>({path:B,document:Re(oe,ae=>Z({principal:H,request:i,signal:ae,refresh:!1},V,B))})),notices:Re({signal:i.signal,timeoutMs:g,late:Ae,handOverMs:0},B=>$({principal:H,request:i,signal:B,refresh:!1}))};return Se.data(Q,{headers:f})},K=async(i,w,d)=>{let p;try{p=Ge(await O(w))}catch(B){return U(re(B,{operation:"authenticate"},w.signal,c))}const f=new Headers(p.headers);if(p.status==="signed-out")return U({reason:"unauthorized"},f);if(p.status==="forbidden")return U({reason:"forbidden"},f);const{principal:T}=p;if(i==="asset")return Y({principal:T,request:w,signal:w.signal,refresh:!1},d,f);const j=Ye(r,L(T));f.set(y.EXPLORER_SCOPE_HEADER,j);const H={principal:T,request:w,signal:w.signal,refresh:!1},V=()=>U({reason:"invalid"},f);if(i==="document"){const B=we(d,y.EXPLORER_DATA_QUERY.path);if(B===null||!y.isAcceptableDocumentPath(B))return V();const ae=w.headers.has(y.EXPLORER_REFRESH_HEADER);return ae&&!We(w)?U({reason:"forbidden"},f):ue(200,JSON.stringify(await Z({...H,refresh:ae},j,B)),f)}const de=we(d,y.EXPLORER_DATA_QUERY.source),oe=we(d,y.EXPLORER_DATA_QUERY.path);if(de===null||i==="children"&&(oe===null||!y.isAcceptableDocumentPath(oe)))return V();const Q=I.get(de);if(Q===void 0)return U({reason:"not-found"},f);if(i==="tree")return ue(200,(await M(Q,T,j,w,w.signal)).json,f);if(i==="refresh"){const B=await X(Q,T,j,w);return w.signal.aborted||G(Q,u(Q,j)),ue(200,B.json,f)}return Q.children===void 0||oe===null?U({reason:"not-found"},f):ue(200,JSON.stringify(await F(Q,Q.children,H,oe)),f)},le=async(i,w,d)=>{if(new URL(i.url).pathname.endsWith(".data"))return U({reason:"not-found"});if(!y.isExplorerDataOperation(d))return U({reason:"not-found"});const p=i.method.toUpperCase(),f=d==="refresh";if(f?p!=="POST":p!=="GET"&&p!=="HEAD")return ue(405,JSON.stringify({failure:{reason:"invalid"}}),void 0,{Allow:f?"POST":"GET, HEAD"});if(f&&!We(i))return U({reason:"forbidden"});try{return await K(d,i,w.searchParams)}catch(T){return U(re(T,{operation:d},i.signal,c))}};return Object.freeze({loader:W,headers:zr,dataLoader:({request:i,url:w,params:d})=>le(i,w,d["*"]),dataAction:({request:i,url:w,params:d})=>le(i,w,d["*"]),forgetTree:i=>{if(typeof i!="string")throw new TypeError(`[markdown-explorer] forgetTree needs the string key onTreeRefreshed received; got ${typeof i}`);h.forget(i)}})},ge=Object.freeze({maxEntries:2e4,maxDocumentBytes:2*1024*1024,textPreviewBytes:8*1024,walkConcurrency:8}),Hr=new Set(["md","markdown","mdx"]),at=new Map([...[["png","image/png"],["jpg","image/jpeg"],["jpeg","image/jpeg"],["gif","image/gif"],["svg","image/svg+xml"],["webp","image/webp"],["ico","image/x-icon"],["bmp","image/bmp"],["tif","image/tiff"],["tiff","image/tiff"],["avif","image/avif"]].map(([e,t])=>[e,{kind:"image",contentType:t}]),...[["mp4","video/mp4"],["m4v","video/mp4"],["webm","video/webm"],["mov","video/quicktime"],["avi","video/x-msvideo"],["mkv","video/x-matroska"],["ogv","video/ogg"]].map(([e,t])=>[e,{kind:"media",mediaType:"video",contentType:t}]),...[["mp3","audio/mpeg"],["wav","audio/wav"],["ogg","audio/ogg"],["oga","audio/ogg"],["opus","audio/ogg"],["m4a","audio/mp4"],["flac","audio/flac"],["aac","audio/aac"],["wma","audio/x-ms-wma"]].map(([e,t])=>[e,{kind:"media",mediaType:"audio",contentType:t}]),["pdf",{kind:"media",mediaType:"pdf",contentType:"application/pdf"}]]),Br=new Set(["txt","text","log","csv","tsv","json","jsonc","json5","ndjson","yaml","yml","toml","ini","cfg","conf","properties","xml","xsl","xsd","dtd","html","htm","xhtml","css","scss","sass","less","styl","js","mjs","cjs","jsx","ts","mts","cts","tsx","vue","svelte","astro","py","pyi","rb","go","rs","java","kt","kts","scala","groovy","gradle","c","h","cc","cpp","cxx","hpp","hh","hxx","cs","fs","vb","swift","m","mm","php","pl","pm","r","jl","lua","dart","ex","exs","erl","hrl","hs","ml","mli","clj","cljs","elm","nim","zig","v","sol","tf","hcl","nix","sh","bash","zsh","fish","ps1","psm1","bat","cmd","sql","graphql","gql","proto","thrift","avsc","rst","adoc","asciidoc","tex","bib","org","textile","diff","patch","srt","vtt","ics","vcf","gitignore","gitattributes","editorconfig","dockerignore","npmignore","prettierrc","eslintrc","babelrc","mailmap"]),Fr=new Set(["readme","license","licence","copying","notice","authors","contributors","changelog","changes","todo","makefile","dockerfile","containerfile","procfile","gemfile","rakefile","vagrantfile","jenkinsfile","brewfile","codeowners"]),ct=typeof te.constants.O_NOFOLLOW=="number"?te.constants.O_NOFOLLOW:0,ut=typeof te.constants.O_NONBLOCK=="number"?te.constants.O_NONBLOCK:0,Ke=e=>`${e.size}-${e.mtimeNs}`,Ur=e=>e!==""&&e!=="."&&e!==".."&&!e.includes(":")&&!e.endsWith(".")&&!e.endsWith(" "),Ve=e=>y.isAcceptableDocumentPath(e)&&e===e.normalize("NFC")&&e.split("/").every(Ur),lt=e=>{const t=e.toLowerCase(),r=t.lastIndexOf(".");return r<0?"":t.slice(r+1)},Xr=e=>{const t=lt(e);if(Hr.has(t))return{kind:"markdown"};const r=at.get(t);if(r!==void 0)return r.kind==="image"?{kind:"image"}:{kind:"media",mediaType:r.mediaType};const n=e.toLowerCase();return Br.has(t)||!n.includes(".")&&Fr.has(n)?{kind:"text"}:{kind:"file"}},dt=e=>{switch(typeof e=="object"&&e!==null?e.code:void 0){case"ENOENT":case"ENOTDIR":return"not-found";case"ELOOP":case"EACCES":case"EPERM":return"forbidden";case"ENAMETOOLONG":return"invalid";case"EISDIR":return"unsupported";default:return null}},ft=e=>{const t=dt(e);return t===null?e:new me(t)},Gr=e=>{const t=typeof e=="object"&&e!==null?e.code:void 0;return t==="EACCES"||t==="EPERM"||t==="ENOENT"||t==="ENOTDIR"},Qe=async(e,t)=>{const r=[];for await(const n of e.createReadStream({start:0,end:t-1,autoClose:!1}))r.push(n);return Buffer.concat(r)},Jr=(e,t)=>e!==null?{start:e.offset,end:e.offset+e.length-1}:t===0n?{}:{start:0,end:Number(t)-1},Yr=async(e,t,r,n)=>{let s;try{s=await fe.open(e,te.constants.O_RDONLY|ct|ut);const g=await s.stat({bigint:!0});if(!g.isFile()||g.ino!==t.ino||g.dev!==t.dev||g.size!==t.size||g.mtimeNs!==t.mtimeNs)throw new me("not-found")}catch(g){throw await s?.close(),ft(g)}const o=s.createReadStream(Jr(r,t.size)),c=pt.Readable.toWeb(o),m=()=>{o.destroy(n.reason)};return n.aborted?m():n.addEventListener("abort",m,{once:!0}),c},Wr=(e,t)=>{const r=ee.NAME_COLLATOR.compare(e,t);return r!==0?r:e===t?0:e<t?-1:1},Kr=e=>{let t=0;const r=[];return async n=>{t<e?t+=1:await new Promise(s=>r.push(s));try{return await n()}finally{const s=r.shift();s===void 0?t-=1:s()}}},Vr=(e,t)=>{if(e.isSymbolicLink())return"symbolic-link";if(!(e.isDirectory()||e.isFile()))return"special-file";const{name:r}=e;return r!==r.normalize("NFC")?"not-nfc":r.includes(":")?"colon":r.endsWith(".")||r.endsWith(" ")?"trailing-dot-or-space":y.isAcceptableDocumentPath(r)?y.isAcceptableDocumentPath(t)?null:"path-too-long":"control-character-or-backslash"},Qr=new Set(["id","label","root","pathPrefix","parse","deny","maxEntries","maxDocumentBytes","textPreviewBytes","walkConcurrency","cacheScope","reportSkipped"]),Ee=(e,t,r)=>{if(t===void 0)return r;if(typeof t!="number"||!Number.isSafeInteger(t)||t<=0)throw new RangeError(`[markdown-explorer] ${e} must be a positive integer; got ${String(t)}`);return t},Zr=e=>{if(e===null||typeof e!="object")throw new TypeError("[markdown-explorer] createFileSystemSource needs an options object");for(const a of Object.keys(e))if(!Qr.has(a))throw new TypeError(`[markdown-explorer] createFileSystemSource does not know the option ${JSON.stringify(a)}`);const{id:t,label:r,root:n,parse:s}=e;if(typeof t!="string"||t==="")throw new TypeError("[markdown-explorer] a file-system source needs a non-empty id");if(typeof r!="string"||r.trim()==="")throw new TypeError("[markdown-explorer] a file-system source needs a non-blank label");if(typeof s!="function")throw new TypeError("[markdown-explorer] a file-system source needs a parse function");if(typeof n!="string"||!q.isAbsolute(n))throw new RangeError(`[markdown-explorer] root must be an absolute path; got ${JSON.stringify(n)}`);let o;try{o=te.statSync(n).isDirectory()}catch(a){throw new RangeError(`[markdown-explorer] root does not exist: ${n}`,{cause:a})}if(!o)throw new RangeError(`[markdown-explorer] root is not a directory: ${n}`);const c=te.realpathSync(n),m=e.pathPrefix;if(typeof m!="string")throw new TypeError('[markdown-explorer] pathPrefix is required: "" for a source over the whole root, or relative segments ending with "/"');if(m!==""&&!(m.endsWith("/")&&Ve(m.slice(0,-1))))throw new RangeError(`[markdown-explorer] pathPrefix must be empty or relative segments ending with "/"; got ${JSON.stringify(m)}`);const g=y.resolvePathRules(e.deny),x=Ee("maxEntries",e.maxEntries,ge.maxEntries),l=Ee("maxDocumentBytes",e.maxDocumentBytes,ge.maxDocumentBytes),h=Ee("textPreviewBytes",e.textPreviewBytes,ge.textPreviewBytes),A=Ee("walkConcurrency",e.walkConcurrency,ge.walkConcurrency),k=e.cacheScope===void 0?"principal":e.cacheScope;if(k!=="principal"&&k!=="shared")throw new RangeError(`[markdown-explorer] cacheScope must be "principal" or "shared"; got ${JSON.stringify(k)}`);const I=e.reportSkipped;if(I!==void 0&&typeof I!="function")throw new TypeError("[markdown-explorer] reportSkipped must be a function");const R=I===void 0?a=>{console.warn(`[markdown-explorer] the file-system source ${JSON.stringify(a.sourceId)} does not list ${JSON.stringify(a.path)}: ${a.reason}`)}:I,O=new Set,L=a=>{const E=q.resolve(c,...a.split("/")),S=q.relative(c,E);return S===""||S===".."||S.startsWith(`..${q.sep}`)||q.isAbsolute(S)?null:E},b=(a,E)=>{const S=`${E}\0${a}`;O.has(S)||O.size>=x||(O.add(S),R({sourceId:t,path:a,reason:E}))},N=async({signal:a})=>{a.throwIfAborted();const E=new AbortController,S=()=>{E.abort(a.reason)};a.addEventListener("abort",S,{once:!0});const _=Kr(A);let P=0;const C=async(M,X)=>{const G=await _(async()=>{E.signal.throwIfAborted();try{return await fe.readdir(M,{withFileTypes:!0})}catch($){if(X===""||!Gr($))throw $;return null}});if(E.signal.throwIfAborted(),G===null)return b(`${m}${X}`,"unreadable-directory"),null;const F=[];for(const $ of G){if(g.deniesName($.name))continue;const Y=X===""?$.name:`${X}/${$.name}`,W=`${m}${Y}`,K=Vr($,W);if(K!==null){b(W,K);continue}F.push({dirent:$,relativePath:Y,path:W})}if(P+=F.length,P>x)throw new me("too-large");return F.sort(($,Y)=>Wr($.dirent.name,Y.dirent.name)),(await Promise.all(F.map(async({dirent:$,relativePath:Y,path:W})=>{if(!$.isDirectory())return{name:$.name,path:W,type:"file"};const K=await C(q.join(M,$.name),Y);return K===null?null:{name:$.name,path:W,type:"directory",children:K}}))).filter($=>$!==null)};try{const M=await C(c,"");if(M===null)throw new Error("[markdown-explorer] the source's root could not be listed");return M}catch(M){throw E.abort(),M}finally{a.removeEventListener("abort",S)}},z=async a=>{if(!a.startsWith(m))return{status:"refused",reason:"not-found"};const E=a.slice(m.length);if(!Ve(E))return{status:"refused",reason:"invalid"};if(g.deniesPath(E))return{status:"refused",reason:"forbidden"};const S=L(E);if(S===null)return{status:"refused",reason:"forbidden"};const _=await fe.lstat(S,{bigint:!0});if(_.isSymbolicLink())return{status:"refused",reason:"forbidden"};if(!_.isFile())return{status:"refused",reason:"unsupported"};const P=await fe.realpath(S);return P!==S||g.deniesPath(q.relative(c,P).split(q.sep).join("/"))?{status:"refused",reason:"forbidden"}:{status:"file",target:S,name:E.slice(E.lastIndexOf("/")+1),stats:_}},D=async(a,E,S)=>{let _;try{const P=await z(S);if(P.status==="refused")return y.failureDocument(P.reason);const{target:C,name:M,stats:X}=P,G=Xr(M),F=Ke(X);if(G.kind==="image")return{kind:"image",name:M,version:F};if(G.kind==="media")return{kind:"media",name:M,mediaType:G.mediaType,version:F};if(G.kind==="file")return{kind:"file",name:M,version:F};_=await fe.open(C,te.constants.O_RDONLY|ct|ut);const Z=await _.stat({bigint:!0});if(!Z.isFile()||Z.ino!==X.ino||Z.dev!==X.dev)return y.failureDocument("forbidden");if(G.kind==="markdown"){if(Z.size>BigInt(l))return y.failureDocument("too-large");const K=await Qe(_,l+1);if(K.length>l)return y.failureDocument("too-large");const le=await s(new TextDecoder("utf-8").decode(K),E);return{kind:"markdown",name:M,htmlContent:le.htmlContent,headings:le.headings}}const $=await Qe(_,h);if($.includes(0))return{kind:"file",name:M,version:F};const Y=Z.size>BigInt(h),W=new TextDecoder("utf-8").decode($,{stream:Y});return{kind:"text",name:M,text:W,truncated:Y,version:F}}catch(P){const C=dt(P);if(C===null)throw P;return y.failureDocument(C)}finally{await _?.close()}},u=async(a,E,S)=>{let _;try{_=await z(S)}catch(F){throw ft(F)}if(_.status==="refused")throw new me(_.reason);const{target:P,name:C,stats:M}=_,X=at.get(lt(C)),G=X===void 0?"":X.contentType;return{name:C,contentType:G,size:Number(M.size),version:Ke(M),open:F=>Yr(P,M,F,a.signal)}};return Object.freeze({id:t,label:r,cacheScope:k,boundary:Object.freeze({contains:a=>a.startsWith(m)}),tree:N,document:D,asset:u})};exports.DEFAULT_DENIED_PATH_SEGMENTS=y.DEFAULT_DENIED_PATH_SEGMENTS;exports.MAX_DOCUMENT_PATH_LENGTH=y.MAX_DOCUMENT_PATH_LENGTH;exports.isAcceptableDocumentPath=y.isAcceptableDocumentPath;exports.isDeniedPath=y.isDeniedPath;exports.ExplorerError=me;exports.anonymousAccess=ht;exports.createFileSystemSource=Zr;exports.createMarkdownExplorer=Cr;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_DENIED_PATH_SEGMENTS, type ExplorerPathRules, isAcceptableDocumentPath, isDeniedPath, MAX_DOCUMENT_PATH_LENGTH } from "./core/paths.cjs";
|
|
2
|
+
export { type AnonymousAccess, type AnonymousPrincipal, anonymousAccess } from "./server/anonymous.cjs";
|
|
3
|
+
export { createMarkdownExplorer } from "./server/createMarkdownExplorer.cjs";
|
|
4
|
+
export { ExplorerError, type ExplorerLogDetail, type ExplorerLogger, type ExplorerOperation } from "./server/errors.cjs";
|
|
5
|
+
export { createFileSystemSource, type FileSystemSkippedEntry, type FileSystemSkipReason, type FileSystemSource, type FileSystemSourceOptions } from "./server/fileSystemSource.cjs";
|
|
6
|
+
export type { ExplorerAssetFile, ExplorerAuthentication, ExplorerBoundaryAnswer, ExplorerByteRange, ExplorerHostDocument, ExplorerPageLoaderResult, ExplorerSource, ExplorerSourceBoundary, ExplorerSourceContext, MarkdownExplorerServer, MarkdownExplorerServerConfig, } from "./server/ports.cjs";
|
|
7
|
+
export type { HtmlSanitizerOptions } from "./server/sanitizer.cjs";
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_DENIED_PATH_SEGMENTS, type ExplorerPathRules, isAcceptableDocumentPath, isDeniedPath, MAX_DOCUMENT_PATH_LENGTH } from "./core/paths.js";
|
|
2
|
+
export { type AnonymousAccess, type AnonymousPrincipal, anonymousAccess } from "./server/anonymous.js";
|
|
3
|
+
export { createMarkdownExplorer } from "./server/createMarkdownExplorer.js";
|
|
4
|
+
export { ExplorerError, type ExplorerLogDetail, type ExplorerLogger, type ExplorerOperation } from "./server/errors.js";
|
|
5
|
+
export { createFileSystemSource, type FileSystemSkippedEntry, type FileSystemSkipReason, type FileSystemSource, type FileSystemSourceOptions } from "./server/fileSystemSource.js";
|
|
6
|
+
export type { ExplorerAssetFile, ExplorerAuthentication, ExplorerBoundaryAnswer, ExplorerByteRange, ExplorerHostDocument, ExplorerPageLoaderResult, ExplorerSource, ExplorerSourceBoundary, ExplorerSourceContext, MarkdownExplorerServer, MarkdownExplorerServerConfig, } from "./server/ports.js";
|
|
7
|
+
export type { HtmlSanitizerOptions } from "./server/sanitizer.js";
|