@holo-js/kernel 0.3.27 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holo-JS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ 9eb8051fbe40b02e51b66e4640091d67d598fe4ce40e80d8d7454e882cb62bc6
@@ -0,0 +1,185 @@
1
+ // src/requestBody.ts
2
+ var DEFAULT_MAX_REQUEST_BODY_BYTES = 1024 * 1024;
3
+ var DEFAULT_MAX_UPLOAD_BODY_BYTES = 8 * 1024 * 1024;
4
+ var maxRequestBodyBytes = DEFAULT_MAX_REQUEST_BODY_BYTES;
5
+ var maxUploadBodyBytes = DEFAULT_MAX_UPLOAD_BODY_BYTES;
6
+ function isMultipartContentType(contentType) {
7
+ return contentType?.split(";", 1)[0]?.trim().toLowerCase() === "multipart/form-data";
8
+ }
9
+ function requestBodyLimitFor(contentType) {
10
+ return isMultipartContentType(contentType) ? maxUploadBodyBytes : maxRequestBodyBytes;
11
+ }
12
+ var HoloRequestBodyTooLargeError = class extends Error {
13
+ status = 413;
14
+ constructor(limit) {
15
+ super(`[@holo-js/kernel] Request body exceeds the ${limit}-byte limit.`);
16
+ this.name = "HoloRequestBodyTooLargeError";
17
+ }
18
+ };
19
+ function resolveLimit(maxBytes) {
20
+ return maxBytes ?? maxRequestBodyBytes;
21
+ }
22
+ function concatChunks(chunks, total) {
23
+ const bytes = new Uint8Array(total);
24
+ let offset = 0;
25
+ for (const chunk of chunks) {
26
+ bytes.set(chunk, offset);
27
+ offset += chunk.byteLength;
28
+ }
29
+ return bytes;
30
+ }
31
+ async function readBoundedRequestBody(request, maxBytes) {
32
+ const limit = maxBytes ?? requestBodyLimitFor(request.headers.get("content-type"));
33
+ const declared = request.headers.get("content-length");
34
+ if (declared !== null && Number(declared) > limit) {
35
+ throw new HoloRequestBodyTooLargeError(limit);
36
+ }
37
+ if (!request.body) {
38
+ return new Uint8Array(0);
39
+ }
40
+ const reader = request.body.getReader();
41
+ const chunks = [];
42
+ let total = 0;
43
+ try {
44
+ for (; ; ) {
45
+ const { done, value } = await reader.read();
46
+ if (done) break;
47
+ total += value.byteLength;
48
+ if (total > limit) {
49
+ throw new HoloRequestBodyTooLargeError(limit);
50
+ }
51
+ chunks.push(value);
52
+ }
53
+ } catch (error) {
54
+ void reader.cancel(error).catch(() => void 0);
55
+ throw error;
56
+ }
57
+ return concatChunks(chunks, total);
58
+ }
59
+ function isWebReadableStream(value) {
60
+ return typeof value === "object" && value !== null && typeof value.getReader === "function";
61
+ }
62
+ function isPlainBufferedObject(value) {
63
+ return typeof value === "object" && value !== null && value.constructor === Object;
64
+ }
65
+ async function readWebStreamBounded(stream, limit) {
66
+ const reader = stream.getReader();
67
+ const chunks = [];
68
+ let total = 0;
69
+ try {
70
+ for (; ; ) {
71
+ const { done, value } = await reader.read();
72
+ if (done) break;
73
+ total += value.byteLength;
74
+ if (total > limit) {
75
+ throw new HoloRequestBodyTooLargeError(limit);
76
+ }
77
+ chunks.push(value);
78
+ }
79
+ } catch (error) {
80
+ void reader.cancel(error).catch(() => void 0);
81
+ throw error;
82
+ }
83
+ return concatChunks(chunks, total);
84
+ }
85
+ async function readBoundedCachedBody(cached, maxBytes) {
86
+ return (await readBoundedBufferedBody(cached, maxBytes))?.bytes;
87
+ }
88
+ async function readBoundedBufferedBody(cached, maxBytes) {
89
+ const value = await cached;
90
+ const limit = resolveLimit(maxBytes);
91
+ if (value instanceof Uint8Array) {
92
+ if (value.byteLength > limit) {
93
+ throw new HoloRequestBodyTooLargeError(limit);
94
+ }
95
+ return { bytes: new Uint8Array(value) };
96
+ }
97
+ if (value instanceof ArrayBuffer) {
98
+ if (value.byteLength > limit) {
99
+ throw new HoloRequestBodyTooLargeError(limit);
100
+ }
101
+ return { bytes: new Uint8Array(value.slice(0)) };
102
+ }
103
+ if (typeof value === "string" || value instanceof URLSearchParams) {
104
+ const bytes = new TextEncoder().encode(value.toString());
105
+ if (bytes.byteLength > limit) {
106
+ throw new HoloRequestBodyTooLargeError(limit);
107
+ }
108
+ return { bytes };
109
+ }
110
+ if (typeof FormData !== "undefined" && value instanceof FormData) {
111
+ const encoded = new Response(value);
112
+ const bytes = new Uint8Array(await encoded.arrayBuffer());
113
+ if (bytes.byteLength > limit) {
114
+ throw new HoloRequestBodyTooLargeError(limit);
115
+ }
116
+ return { bytes, contentType: encoded.headers.get("content-type") ?? void 0 };
117
+ }
118
+ if (isWebReadableStream(value)) {
119
+ return { bytes: await readWebStreamBounded(value, limit) };
120
+ }
121
+ if (typeof value === "object" && value !== null && Symbol.asyncIterator in value) {
122
+ return { bytes: await readBoundedNodeRequestBody(value, limit) };
123
+ }
124
+ if (isPlainBufferedObject(value)) {
125
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
126
+ if (bytes.byteLength > limit) {
127
+ throw new HoloRequestBodyTooLargeError(limit);
128
+ }
129
+ return { bytes };
130
+ }
131
+ return void 0;
132
+ }
133
+ async function readBoundedRequestText(request, maxBytes) {
134
+ return new TextDecoder().decode(await readBoundedRequestBody(request, maxBytes));
135
+ }
136
+ async function readBoundedRequestJson(request, maxBytes) {
137
+ return JSON.parse(await readBoundedRequestText(request, maxBytes));
138
+ }
139
+ async function readBoundedRequestFormData(request, maxBytes) {
140
+ const bytes = await readBoundedRequestBody(request, maxBytes);
141
+ return new Response(bytes, { headers: { "content-type": request.headers.get("content-type") ?? "" } }).formData();
142
+ }
143
+ async function readBoundedNodeRequestBody(stream, maxBytes, declaredLength) {
144
+ const limit = resolveLimit(maxBytes);
145
+ if (declaredLength != null && Number(declaredLength) > limit) {
146
+ throw new HoloRequestBodyTooLargeError(limit);
147
+ }
148
+ const chunks = [];
149
+ let total = 0;
150
+ for await (const chunk of stream) {
151
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
152
+ total += bytes.byteLength;
153
+ if (total > limit) {
154
+ throw new HoloRequestBodyTooLargeError(limit);
155
+ }
156
+ chunks.push(bytes);
157
+ }
158
+ return concatChunks(chunks, total);
159
+ }
160
+ var requestBodyInternals = Object.freeze({
161
+ configureMaxRequestBodyBytes(bytes) {
162
+ maxRequestBodyBytes = bytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES;
163
+ },
164
+ configureMaxUploadBodyBytes(bytes) {
165
+ maxUploadBodyBytes = bytes ?? DEFAULT_MAX_UPLOAD_BODY_BYTES;
166
+ },
167
+ getMaxRequestBodyBytes() {
168
+ return maxRequestBodyBytes;
169
+ }
170
+ });
171
+
172
+ export {
173
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
174
+ DEFAULT_MAX_UPLOAD_BODY_BYTES,
175
+ requestBodyLimitFor,
176
+ HoloRequestBodyTooLargeError,
177
+ readBoundedRequestBody,
178
+ readBoundedCachedBody,
179
+ readBoundedBufferedBody,
180
+ readBoundedRequestText,
181
+ readBoundedRequestJson,
182
+ readBoundedRequestFormData,
183
+ readBoundedNodeRequestBody,
184
+ requestBodyInternals
185
+ };
@@ -0,0 +1,13 @@
1
+ // src/imageDecoding.ts
2
+ var imageDecodingLimits = Object.freeze({
3
+ maximumDecodedPixels: 4e7,
4
+ maximumFrames: 1e3
5
+ });
6
+ async function verifyDecodedImage(_contents, _mimeType, _aspectRatios = []) {
7
+ throw new Error("[@holo-js/kernel] Server-side image validation must run on Node.js, where images can be fully decoded.");
8
+ }
9
+
10
+ export {
11
+ imageDecodingLimits,
12
+ verifyDecodedImage
13
+ };
@@ -0,0 +1,52 @@
1
+ // src/errorReporting.ts
2
+ function errorReportingState() {
3
+ const runtime = globalThis;
4
+ runtime.__holoErrorReporting__ ??= {};
5
+ return runtime.__holoErrorReporting__;
6
+ }
7
+ function describeContext(context) {
8
+ return `[holo:${context.capability}] ${context.operation}`;
9
+ }
10
+ function logReporterFailure(error, context, reporterError) {
11
+ console.error(describeContext(context), error, reporterError);
12
+ }
13
+ function isUnreported(error, exceptions) {
14
+ return exceptions.dontReport?.some((errorClass) => error instanceof errorClass) ?? false;
15
+ }
16
+ function deliver(exceptions, error, context) {
17
+ if (exceptions && isUnreported(error, exceptions)) return;
18
+ if (!exceptions?.report) {
19
+ console.error(describeContext(context), error);
20
+ return;
21
+ }
22
+ void Promise.resolve(exceptions.report(error, context)).catch((reporterError) => logReporterFailure(error, context, reporterError));
23
+ }
24
+ function reportWith(exceptions, error, context) {
25
+ try {
26
+ deliver(exceptions, error, context);
27
+ } catch (reporterError) {
28
+ logReporterFailure(error, context, reporterError);
29
+ }
30
+ }
31
+ function configure(exceptions) {
32
+ const state = errorReportingState();
33
+ const previous = state.exceptions;
34
+ state.exceptions = exceptions;
35
+ return previous;
36
+ }
37
+ function reportError(error, context) {
38
+ reportWith(errorReportingState().exceptions, error, context);
39
+ }
40
+ function captureErrorReporter() {
41
+ const exceptions = errorReportingState().exceptions;
42
+ return (error, context) => reportWith(exceptions, error, context);
43
+ }
44
+ var errorReportingInternals = {
45
+ configure
46
+ };
47
+
48
+ export {
49
+ reportError,
50
+ captureErrorReporter,
51
+ errorReportingInternals
52
+ };
@@ -0,0 +1,19 @@
1
+ interface HoloErrorReportContext {
2
+ readonly capability: string;
3
+ readonly operation: string;
4
+ }
5
+ type HoloUnreportedErrorClass = new (...args: never[]) => Error;
6
+ interface HoloExceptionsConfig {
7
+ report?(error: unknown, context: HoloErrorReportContext): void | Promise<void>;
8
+ dontReport?: readonly HoloUnreportedErrorClass[];
9
+ }
10
+ type HoloErrorReporter = (error: unknown, context: HoloErrorReportContext) => void;
11
+ declare function configure(exceptions: HoloExceptionsConfig | undefined): HoloExceptionsConfig | undefined;
12
+ declare function reportError(error: unknown, context: HoloErrorReportContext): void;
13
+ declare function captureErrorReporter(): HoloErrorReporter;
14
+ declare const errorReportingInternals: {
15
+ configure: typeof configure;
16
+ };
17
+
18
+ export { captureErrorReporter, errorReportingInternals, reportError };
19
+ export type { HoloErrorReportContext, HoloErrorReporter, HoloExceptionsConfig, HoloUnreportedErrorClass };
@@ -0,0 +1,10 @@
1
+ import {
2
+ captureErrorReporter,
3
+ errorReportingInternals,
4
+ reportError
5
+ } from "./chunk-QQ5YD7HZ.mjs";
6
+ export {
7
+ captureErrorReporter,
8
+ errorReportingInternals,
9
+ reportError
10
+ };
@@ -0,0 +1,10 @@
1
+ type FileContentInspection = {
2
+ readonly mimeType: string | null;
3
+ readonly passive: boolean;
4
+ };
5
+ declare const fileContentSampleBytes = 65536;
6
+ declare function mimeTypeFromFileName(fileName: string): string | null;
7
+ declare function inspectFileContent(sample: Uint8Array, fileName?: string): Promise<FileContentInspection>;
8
+
9
+ export { fileContentSampleBytes, inspectFileContent, mimeTypeFromFileName };
10
+ export type { FileContentInspection };
@@ -0,0 +1,54 @@
1
+ // src/fileContent.ts
2
+ var fileContentSampleBytes = 65536;
3
+ var mimeTypesByExtension = Object.freeze({
4
+ avif: "image/avif",
5
+ css: "text/css",
6
+ csv: "text/csv",
7
+ gif: "image/gif",
8
+ htm: "text/html",
9
+ html: "text/html",
10
+ jpeg: "image/jpeg",
11
+ jpg: "image/jpeg",
12
+ json: "application/json",
13
+ md: "text/markdown",
14
+ mp3: "audio/mpeg",
15
+ pdf: "application/pdf",
16
+ png: "image/png",
17
+ svg: "image/svg+xml",
18
+ txt: "text/plain",
19
+ webp: "image/webp",
20
+ woff: "font/woff",
21
+ woff2: "font/woff2",
22
+ xml: "application/xml"
23
+ });
24
+ var passiveDocumentMimeTypes = /* @__PURE__ */ new Set([
25
+ "application/json",
26
+ "application/pdf",
27
+ "text/css",
28
+ "text/csv",
29
+ "text/markdown",
30
+ "text/plain"
31
+ ]);
32
+ function isPassiveMimeType(mimeType) {
33
+ if (!mimeType) return false;
34
+ return passiveDocumentMimeTypes.has(mimeType) || /^(?:image|audio|video|font)\//u.test(mimeType) && !mimeType.endsWith("+xml");
35
+ }
36
+ function mimeTypeFromFileName(fileName) {
37
+ const baseName = fileName.slice(Math.max(fileName.lastIndexOf("/"), fileName.lastIndexOf("\\")) + 1);
38
+ const separator = baseName.lastIndexOf(".");
39
+ if (separator < 1) return null;
40
+ return mimeTypesByExtension[baseName.slice(separator + 1).toLowerCase()] ?? null;
41
+ }
42
+ async function inspectFileContent(sample, fileName) {
43
+ const { fileTypeFromBuffer, supportedMimeTypes } = await import("file-type");
44
+ const detectableMimeTypes = supportedMimeTypes;
45
+ const detected = await fileTypeFromBuffer(sample.subarray(0, fileContentSampleBytes)).catch(() => void 0);
46
+ const named = fileName ? mimeTypeFromFileName(fileName) : null;
47
+ const mimeType = detected?.mime ?? (named && !detectableMimeTypes.has(named) ? named : null);
48
+ return { mimeType, passive: isPassiveMimeType(mimeType) };
49
+ }
50
+ export {
51
+ fileContentSampleBytes,
52
+ inspectFileContent,
53
+ mimeTypeFromFileName
54
+ };
@@ -8,4 +8,5 @@ type NormalizedHoloHttpError = {
8
8
  declare function isHoloHttpErrorStatus(status: number): status is HoloHttpErrorStatus;
9
9
  declare function normalizeHoloHttpError(error: unknown): NormalizedHoloHttpError | undefined;
10
10
 
11
- export { type HoloHttpErrorStatus, type NormalizedHoloHttpError, isHoloHttpErrorStatus, normalizeHoloHttpError };
11
+ export { isHoloHttpErrorStatus, normalizeHoloHttpError };
12
+ export type { HoloHttpErrorStatus, NormalizedHoloHttpError };
@@ -0,0 +1,6 @@
1
+ import { DecodedImageVerification } from './image-decoding.js';
2
+ export { imageDecodingLimits } from './image-decoding.js';
3
+
4
+ declare function verifyDecodedImage(_contents: Uint8Array, _mimeType: string, aspectRatios?: readonly (readonly number[])[]): Promise<DecodedImageVerification>;
5
+
6
+ export { verifyDecodedImage };
@@ -0,0 +1,12 @@
1
+ import {
2
+ imageDecodingLimits
3
+ } from "./chunk-K4RCM65I.mjs";
4
+
5
+ // src/imageDecoding.browser.ts
6
+ async function verifyDecodedImage(_contents, _mimeType, aspectRatios = []) {
7
+ return { valid: true, aspectRatios: aspectRatios.map(() => true) };
8
+ }
9
+ export {
10
+ imageDecodingLimits,
11
+ verifyDecodedImage
12
+ };
@@ -0,0 +1,12 @@
1
+ declare const imageDecodingLimits: Readonly<{
2
+ maximumDecodedPixels: 40000000;
3
+ maximumFrames: 1000;
4
+ }>;
5
+ interface DecodedImageVerification {
6
+ readonly valid: boolean;
7
+ readonly aspectRatios: readonly boolean[];
8
+ }
9
+ declare function verifyDecodedImage(_contents: Uint8Array, _mimeType: string, _aspectRatios?: readonly (readonly number[])[]): Promise<DecodedImageVerification>;
10
+
11
+ export { imageDecodingLimits, verifyDecodedImage };
12
+ export type { DecodedImageVerification };
@@ -0,0 +1,8 @@
1
+ import {
2
+ imageDecodingLimits,
3
+ verifyDecodedImage
4
+ } from "./chunk-K4RCM65I.mjs";
5
+ export {
6
+ imageDecodingLimits,
7
+ verifyDecodedImage
8
+ };
@@ -0,0 +1,6 @@
1
+ import { DecodedImageVerification } from './image-decoding.js';
2
+ export { imageDecodingLimits } from './image-decoding.js';
3
+
4
+ declare function verifyDecodedImage(contents: Uint8Array, mimeType: string, aspectRatios?: readonly (readonly number[])[]): Promise<DecodedImageVerification>;
5
+
6
+ export { verifyDecodedImage };
@@ -0,0 +1,81 @@
1
+ import {
2
+ imageDecodingLimits
3
+ } from "./chunk-K4RCM65I.mjs";
4
+
5
+ // src/imageDecoding.node.ts
6
+ var decodedFormatMatchers = Object.freeze({
7
+ "image/avif": (metadata) => metadata.format === "heif" && metadata.compression === "av1",
8
+ "image/gif": (metadata) => metadata.format === "gif",
9
+ "image/jpeg": (metadata) => metadata.format === "jpeg",
10
+ "image/png": (metadata) => metadata.format === "png",
11
+ "image/tiff": (metadata) => metadata.format === "tiff",
12
+ "image/webp": (metadata) => metadata.format === "webp"
13
+ });
14
+ function isMissingModule(error) {
15
+ if (!(error instanceof Error)) return false;
16
+ const code = error.code;
17
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || isMissingModule(error.cause);
18
+ }
19
+ async function loadSharp() {
20
+ try {
21
+ return (await import("sharp")).default;
22
+ } catch (error) {
23
+ if (!isMissingModule(error)) throw error;
24
+ throw new Error('[@holo-js/kernel] Server-side image validation requires the optional "sharp" package. Install sharp in your application.', { cause: error });
25
+ }
26
+ }
27
+ function skipSubBlocks(bytes, start) {
28
+ let offset = start;
29
+ while (offset < bytes.length) {
30
+ const size = bytes[offset] ?? 0;
31
+ offset += 1;
32
+ if (size === 0) return offset;
33
+ offset += size;
34
+ }
35
+ return null;
36
+ }
37
+ function colorTableBytes(flags) {
38
+ return (flags & 128) === 0 ? 0 : 3 * (1 << (flags & 7) + 1);
39
+ }
40
+ function hasCompleteGifStructure(bytes) {
41
+ let offset = 13 + colorTableBytes(bytes[10] ?? 0);
42
+ while (offset !== null && offset < bytes.length) {
43
+ const introducer = bytes[offset];
44
+ if (introducer === 59) return true;
45
+ if (introducer === 33) {
46
+ offset = skipSubBlocks(bytes, offset + 2);
47
+ } else if (introducer === 44) {
48
+ offset = skipSubBlocks(bytes, offset + 10 + colorTableBytes(bytes[offset + 9] ?? 0) + 1);
49
+ } else {
50
+ return false;
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+ async function verifyDecodedImage(contents, mimeType, aspectRatios = []) {
56
+ const invalid = { valid: false, aspectRatios: [] };
57
+ const matchesDecodedFormat = decodedFormatMatchers[mimeType];
58
+ if (!matchesDecodedFormat) return invalid;
59
+ if (mimeType === "image/gif" && !hasCompleteGifStructure(contents)) return invalid;
60
+ const decode = await loadSharp();
61
+ try {
62
+ const image = decode(contents, { animated: true, failOn: "warning", limitInputPixels: imageDecodingLimits.maximumDecodedPixels });
63
+ const metadata = await image.metadata();
64
+ if (!matchesDecodedFormat(metadata) || (metadata.pages ?? 1) > imageDecodingLimits.maximumFrames) return invalid;
65
+ await image.stats();
66
+ const width = metadata.width;
67
+ const height = metadata.pageHeight ?? metadata.height;
68
+ if (!width || !height) return invalid;
69
+ const precision = 1 / (Math.max((width + height) / 2, height) + 1);
70
+ return {
71
+ valid: true,
72
+ aspectRatios: aspectRatios.map((ratios) => ratios.some((ratio) => Math.abs(ratio - width / height) <= precision))
73
+ };
74
+ } catch {
75
+ return invalid;
76
+ }
77
+ }
78
+ export {
79
+ imageDecodingLimits,
80
+ verifyDecodedImage
81
+ };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { HoloHttpErrorStatus, NormalizedHoloHttpError, isHoloHttpErrorStatus, normalizeHoloHttpError } from './http-errors.js';
2
+ export { BufferedRequestBody, DEFAULT_MAX_REQUEST_BODY_BYTES, DEFAULT_MAX_UPLOAD_BODY_BYTES, HoloRequestBodyTooLargeError, readBoundedBufferedBody, readBoundedCachedBody, readBoundedNodeRequestBody, readBoundedRequestBody, readBoundedRequestFormData, readBoundedRequestJson, readBoundedRequestText, requestBodyInternals, requestBodyLimitFor } from './request-body.js';
2
3
 
3
4
  interface RuntimeLifecycleContext {
4
5
  readonly projectRoot: string;
@@ -409,4 +410,78 @@ declare function resolveNormalizedRedisConnection(config: NormalizedHoloRedisCon
409
410
  declare function normalizeRedisConfig(config?: HoloRedisConfig): NormalizedHoloRedisConfig;
410
411
  declare function defineRedisConfig<TConfig extends HoloRedisConfig>(config: TConfig): Readonly<TConfig>;
411
412
 
412
- export { type ConcurrencyOptions, DEFAULT_HOLO_PROJECT_PATHS, DEFAULT_REDIS_CONNECTION, DEFAULT_REDIS_DB, DEFAULT_REDIS_HOST, DEFAULT_REDIS_PORT, type DatabaseLogger, HOLO_PROJECT_PREPARE_API_VERSION, type HoloDatabaseDriver, type HoloGeneratedProjectArtifact, type HoloManagedProjectArtifact, type HoloPluginBroadcastContributions, type HoloPluginCacheContributions, type HoloPluginCliContributions, type HoloPluginConfigContributions, type HoloPluginContributions, type HoloPluginDefinition, type HoloPluginDependencyContributions, type HoloPluginFrameworkContribution, type HoloPluginFrameworkTsconfigKind, type HoloPluginMailContributions, type HoloPluginMigrationContributions, type HoloPluginNotificationContributions, type HoloPluginPackageManager, type HoloPluginProjectContributions, type HoloPluginQueueContributions, type HoloPluginRuntimeContributions, type HoloPluginRuntimeMapContributions, type HoloPluginRuntimeModule, type HoloProjectArtifactContents, type HoloProjectConfig, type HoloProjectConnectionConfig, type HoloProjectDatabaseConfig, type HoloProjectPaths, type HoloProjectPrepareChange, type HoloProjectPrepareChangeKind, type HoloProjectPrepareCommand, type HoloProjectPrepareContext, type HoloProjectPrepareDiagnostic, HoloProjectPrepareError, type HoloProjectPrepareFailure, type HoloProjectPrepareFramework, type HoloProjectPrepareFullRetry, type HoloProjectPrepareLogger, type HoloProjectPrepareModule, type HoloProjectPreparePlugin, type HoloProjectPrepareResult, type HoloProjectPrepareRun, type HoloProjectPrepareSource, type HoloProjectPrepareWatch, type HoloProjectPreparedResult, type HoloProjectPreparer, type HoloRedisClusterNodeConfig, type HoloRedisConfig, type HoloRedisConnectionConfig, type LoadedHoloPluginDefinition, type MigrationErrorLog, type MigrationStartLog, type MigrationSuccessLog, type NormalizedHoloProjectConfig, type NormalizedHoloRedisClusterNodeConfig, type NormalizedHoloRedisConfig, type NormalizedHoloRedisConnectionConfig, type PluginLoadOptions, type QueryErrorLog, type QueryStartLog, type QuerySuccessLog, type RuntimeContribution, RuntimeLifecycle, type RuntimeLifecycleContext, type SeederErrorLog, type SeederStartLog, type SeederSuccessLog, type TransactionLog, type TransactionScopeKind, createRuntimeLifecycle, defineHoloPlugin, defineHoloProject, defineHoloProjectPreparer, defineRedisConfig, holoRedisDefaults, loadHoloPluginBootModules, loadHoloPluginContributionModules, loadHoloPluginDefinitions, normalizeHoloPluginDefinition, normalizeHoloProjectConfig, normalizeRedisConfig, resolveHoloPluginModulePath, resolveNormalizedRedisConnection };
413
+ type HoloRequestHeaders = Headers | ReadonlyArray<readonly [string, string]> | Record<string, string | readonly string[] | undefined> | {
414
+ readonly get?: (name: string) => string | null | undefined;
415
+ readonly forEach?: (callback: (value: string, key: string) => void) => void;
416
+ readonly entries?: () => Iterable<readonly [string, string]>;
417
+ };
418
+ type RequestUrlParts = {
419
+ readonly url?: string | URL;
420
+ readonly path?: string;
421
+ readonly headers?: HoloRequestHeaders;
422
+ };
423
+ type HoloRequestInput = RequestUrlParts & {
424
+ readonly request?: Request;
425
+ readonly req?: RequestUrlParts;
426
+ readonly node?: {
427
+ readonly req?: RequestUrlParts;
428
+ };
429
+ readonly web?: {
430
+ readonly request?: RequestUrlParts;
431
+ };
432
+ };
433
+ type HoloForwardedRequest = {
434
+ readonly address?: string;
435
+ readonly protocol?: string;
436
+ readonly host?: string;
437
+ readonly port?: string;
438
+ readonly prefix?: string;
439
+ };
440
+ type HoloForwardedRequestResolver = (readHeader: (name: string) => string | undefined, connectionAddress: string | undefined) => HoloForwardedRequest;
441
+ type HoloConnectionAddressProvider = () => string | undefined;
442
+ declare function configureForwardedRequestResolver(resolver: HoloForwardedRequestResolver | undefined): void;
443
+ declare function provideConnectionAddress(provider: HoloConnectionAddressProvider | undefined): void;
444
+ declare function currentConnectionAddress(): string | undefined;
445
+ declare class HoloRequestUrlError extends TypeError {
446
+ readonly status = 400;
447
+ constructor(message: string);
448
+ }
449
+ declare function resolveRequestUrl(input: HoloRequestInput): URL;
450
+ declare function resolveRoutedRequestUrl(input: HoloRequestInput): URL;
451
+ declare function resolveClientAddress(input: HoloRequestInput): string | undefined;
452
+ declare const requestUrlInternals: Readonly<{
453
+ configureForwardedRequestResolver: typeof configureForwardedRequestResolver;
454
+ currentConnectionAddress: typeof currentConnectionAddress;
455
+ provideConnectionAddress: typeof provideConnectionAddress;
456
+ resolveClientAddress: typeof resolveClientAddress;
457
+ resolveRoutedRequestUrl: typeof resolveRoutedRequestUrl;
458
+ }>;
459
+ declare function sameOriginRedirectPath(path: string | undefined): string;
460
+
461
+ declare function encryptCookieValue(name: string, value: string, appKey: string | undefined): string;
462
+ declare function decryptCookieValue(name: string, value: string, appKey: string | undefined): string | undefined;
463
+ declare const cookieEncryptionInternals: Readonly<{
464
+ decryptCookieValue: typeof decryptCookieValue;
465
+ encryptCookieValue: typeof encryptCookieValue;
466
+ }>;
467
+
468
+ declare function windowsStorageNameProblem(name: string): string | null;
469
+ declare function segmentProblem(segment: string): string | null;
470
+ declare function assertValidSegment(segment: string): void;
471
+ declare function splitLocalStoragePath(path: string): string[];
472
+ declare function isReservedLocalStorageName(name: string | undefined): boolean;
473
+ declare const localStoragePathInternals: {
474
+ assertValidSegment: typeof assertValidSegment;
475
+ isReservedLocalStorageName: typeof isReservedLocalStorageName;
476
+ segmentProblem: typeof segmentProblem;
477
+ splitLocalStoragePath: typeof splitLocalStoragePath;
478
+ windowsStorageNameProblem: typeof windowsStorageNameProblem;
479
+ };
480
+
481
+ declare const COOKIE_WRITE_UNAVAILABLE = "cookie_write_unavailable";
482
+ declare function createCookieWriteUnavailableError(message: string, cause?: unknown): Error;
483
+
484
+ declare function resolveRetryBackoffSeconds(backoff: number | readonly number[], attempt: number): number;
485
+
486
+ export { COOKIE_WRITE_UNAVAILABLE, DEFAULT_HOLO_PROJECT_PATHS, DEFAULT_REDIS_CONNECTION, DEFAULT_REDIS_DB, DEFAULT_REDIS_HOST, DEFAULT_REDIS_PORT, HOLO_PROJECT_PREPARE_API_VERSION, HoloProjectPrepareError, HoloRequestUrlError, RuntimeLifecycle, cookieEncryptionInternals, createCookieWriteUnavailableError, createRuntimeLifecycle, defineHoloPlugin, defineHoloProject, defineHoloProjectPreparer, defineRedisConfig, holoRedisDefaults, loadHoloPluginBootModules, loadHoloPluginContributionModules, loadHoloPluginDefinitions, localStoragePathInternals, normalizeHoloPluginDefinition, normalizeHoloProjectConfig, normalizeRedisConfig, requestUrlInternals, resolveHoloPluginModulePath, resolveNormalizedRedisConnection, resolveRequestUrl, resolveRetryBackoffSeconds, sameOriginRedirectPath };
487
+ export type { ConcurrencyOptions, DatabaseLogger, HoloConnectionAddressProvider, HoloDatabaseDriver, HoloForwardedRequest, HoloForwardedRequestResolver, HoloGeneratedProjectArtifact, HoloManagedProjectArtifact, HoloPluginBroadcastContributions, HoloPluginCacheContributions, HoloPluginCliContributions, HoloPluginConfigContributions, HoloPluginContributions, HoloPluginDefinition, HoloPluginDependencyContributions, HoloPluginFrameworkContribution, HoloPluginFrameworkTsconfigKind, HoloPluginMailContributions, HoloPluginMigrationContributions, HoloPluginNotificationContributions, HoloPluginPackageManager, HoloPluginProjectContributions, HoloPluginQueueContributions, HoloPluginRuntimeContributions, HoloPluginRuntimeMapContributions, HoloPluginRuntimeModule, HoloProjectArtifactContents, HoloProjectConfig, HoloProjectConnectionConfig, HoloProjectDatabaseConfig, HoloProjectPaths, HoloProjectPrepareChange, HoloProjectPrepareChangeKind, HoloProjectPrepareCommand, HoloProjectPrepareContext, HoloProjectPrepareDiagnostic, HoloProjectPrepareFailure, HoloProjectPrepareFramework, HoloProjectPrepareFullRetry, HoloProjectPrepareLogger, HoloProjectPrepareModule, HoloProjectPreparePlugin, HoloProjectPrepareResult, HoloProjectPrepareRun, HoloProjectPrepareSource, HoloProjectPrepareWatch, HoloProjectPreparedResult, HoloProjectPreparer, HoloRedisClusterNodeConfig, HoloRedisConfig, HoloRedisConnectionConfig, HoloRequestHeaders, HoloRequestInput, LoadedHoloPluginDefinition, MigrationErrorLog, MigrationStartLog, MigrationSuccessLog, NormalizedHoloProjectConfig, NormalizedHoloRedisClusterNodeConfig, NormalizedHoloRedisConfig, NormalizedHoloRedisConnectionConfig, PluginLoadOptions, QueryErrorLog, QueryStartLog, QuerySuccessLog, RuntimeContribution, RuntimeLifecycleContext, SeederErrorLog, SeederStartLog, SeederSuccessLog, TransactionLog, TransactionScopeKind };
package/dist/index.mjs CHANGED
@@ -2,6 +2,20 @@ import {
2
2
  isHoloHttpErrorStatus,
3
3
  normalizeHoloHttpError
4
4
  } from "./chunk-UIGMROG3.mjs";
5
+ import {
6
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
7
+ DEFAULT_MAX_UPLOAD_BODY_BYTES,
8
+ HoloRequestBodyTooLargeError,
9
+ readBoundedBufferedBody,
10
+ readBoundedCachedBody,
11
+ readBoundedNodeRequestBody,
12
+ readBoundedRequestBody,
13
+ readBoundedRequestFormData,
14
+ readBoundedRequestJson,
15
+ readBoundedRequestText,
16
+ requestBodyInternals,
17
+ requestBodyLimitFor
18
+ } from "./chunk-CGVSGOEO.mjs";
5
19
 
6
20
  // src/lifecycle.ts
7
21
  function orderContributions(contributions) {
@@ -51,7 +65,7 @@ var RuntimeLifecycle = class {
51
65
  try {
52
66
  await this.dispose(context);
53
67
  } catch (cleanupError) {
54
- throw new AggregateError([error, cleanupError], "[Holo Runtime] Initialization and rollback both failed.");
68
+ throw new AggregateError([error, cleanupError], "[Holo Runtime] Initialization and rollback both failed.", { cause: cleanupError });
55
69
  }
56
70
  throw error;
57
71
  }
@@ -168,7 +182,7 @@ async function loadHoloPluginDefinitions(projectRoot, packageNames, options = {}
168
182
  try {
169
183
  manifest = JSON.parse(await readFile(packageJsonPath, "utf8"));
170
184
  } catch (error) {
171
- if (isRecord(error) && error.code === "ENOENT") throw new Error(`Cannot find module '${packageName}/package.json'`);
185
+ if (isRecord(error) && error.code === "ENOENT") throw new Error(`Cannot find module '${packageName}/package.json'`, { cause: error });
172
186
  throw error;
173
187
  }
174
188
  if (!isRecord(manifest.holo) || !normalizeString(manifest.holo.plugin)) {
@@ -325,7 +339,7 @@ function normalizeUrl(value, label) {
325
339
  if (error instanceof Error && error.message.startsWith("[Holo Redis]")) {
326
340
  throw error;
327
341
  }
328
- throw new Error(`[Holo Redis] ${label} must be a valid redis:// or rediss:// URL.`);
342
+ throw new Error(`[Holo Redis] ${label} must be a valid redis:// or rediss:// URL.`, { cause: error });
329
343
  }
330
344
  return normalized;
331
345
  }
@@ -426,15 +440,264 @@ function normalizeRedisConfig(config = {}) {
426
440
  function defineRedisConfig(config) {
427
441
  return Object.freeze({ ...config });
428
442
  }
443
+
444
+ // src/requestUrl.ts
445
+ function requestForwardingState() {
446
+ const runtime = globalThis;
447
+ runtime.__holoRequestForwarding__ ??= {};
448
+ return runtime.__holoRequestForwarding__;
449
+ }
450
+ function configureForwardedRequestResolver(resolver) {
451
+ requestForwardingState().resolveForwardedRequest = resolver;
452
+ }
453
+ function provideConnectionAddress(provider) {
454
+ requestForwardingState().connectionAddress = provider;
455
+ }
456
+ function currentConnectionAddress() {
457
+ try {
458
+ return requestForwardingState().connectionAddress?.();
459
+ } catch {
460
+ return void 0;
461
+ }
462
+ }
463
+ function isHeaderTuples(headers) {
464
+ return Array.isArray(headers);
465
+ }
466
+ function readHeader(headers, name) {
467
+ if (!headers) return void 0;
468
+ if (isHeaderTuples(headers)) {
469
+ return headers.find(([key]) => key.toLowerCase() === name)?.[1];
470
+ }
471
+ if ("get" in headers && typeof headers.get === "function") return headers.get(name) ?? void 0;
472
+ if ("entries" in headers && typeof headers.entries === "function") {
473
+ for (const [key, value] of headers.entries()) {
474
+ if (key.toLowerCase() === name) return value;
475
+ }
476
+ return void 0;
477
+ }
478
+ if ("forEach" in headers && typeof headers.forEach === "function") {
479
+ let result;
480
+ headers.forEach((value, key) => {
481
+ if (key.toLowerCase() === name) result = value;
482
+ });
483
+ return result;
484
+ }
485
+ for (const [key, value] of Object.entries(headers)) {
486
+ if (key.toLowerCase() === name) {
487
+ return typeof value === "string" ? value : Array.isArray(value) ? value.join(",") : void 0;
488
+ }
489
+ }
490
+ return void 0;
491
+ }
492
+ var HoloRequestUrlError = class extends TypeError {
493
+ status = 400;
494
+ constructor(message) {
495
+ super(`[@holo-js/kernel] ${message}`);
496
+ this.name = "HoloRequestUrlError";
497
+ }
498
+ };
499
+ var invalidRequestHostMessage = "Request hosts must be a valid hostname and optional port.";
500
+ var invalidRequestPortMessage = "Request ports must be a number from 0 to 65535.";
501
+ var invalidRequestPrefixMessage = 'Request prefixes must be a path starting with a single "/".';
502
+ function validForwardedPort(port) {
503
+ if (!/^\d{1,5}$/u.test(port) || Number(port) > 65535) throw new HoloRequestUrlError(invalidRequestPortMessage);
504
+ return port;
505
+ }
506
+ function validForwardedPrefix(prefix) {
507
+ const trimmed = prefix.replace(/\/+$/u, "");
508
+ if (trimmed && (!trimmed.startsWith("/") || trimmed.startsWith("//") || /[\s?#\\]/u.test(trimmed))) {
509
+ throw new HoloRequestUrlError(invalidRequestPrefixMessage);
510
+ }
511
+ return trimmed;
512
+ }
513
+ function parseRequestUrl(value, invalidMessage) {
514
+ if (!URL.canParse(String(value), "http://localhost")) throw new HoloRequestUrlError(invalidMessage);
515
+ return new URL(value, "http://localhost");
516
+ }
517
+ function nestedRequest(input) {
518
+ return input instanceof Request ? void 0 : input.request ?? input.web?.request ?? input.req ?? input.node?.req;
519
+ }
520
+ function requestHeaders(input) {
521
+ return input.headers ?? nestedRequest(input)?.headers;
522
+ }
523
+ function resolveForwardedRequest(headers) {
524
+ return requestForwardingState().resolveForwardedRequest?.((name) => readHeader(headers, name), currentConnectionAddress());
525
+ }
526
+ function resolveUrl(input, applyPrefix) {
527
+ const nested = nestedRequest(input);
528
+ const headers = requestHeaders(input);
529
+ const value = input.url ?? nested?.url ?? input.path ?? (nested && "path" in nested ? nested.path : void 0) ?? "/";
530
+ const invalidUrlMessage = "Request URLs must use http or https without credentials.";
531
+ const url = parseRequestUrl(value, invalidUrlMessage);
532
+ if (url.username || url.password || url.protocol !== "http:" && url.protocol !== "https:") {
533
+ throw new HoloRequestUrlError(invalidUrlMessage);
534
+ }
535
+ const forwarded = resolveForwardedRequest(headers);
536
+ const protocol = (forwarded?.protocol ?? url.protocol.slice(0, -1)).toLowerCase();
537
+ if (protocol !== "http" && protocol !== "https") {
538
+ throw new HoloRequestUrlError("Request protocols must be http or https.");
539
+ }
540
+ const host = forwarded?.host ?? readHeader(headers, "host") ?? url.host;
541
+ if (!host || /[\s,/@\\?#]/u.test(host)) {
542
+ throw new HoloRequestUrlError(invalidRequestHostMessage);
543
+ }
544
+ const origin = parseRequestUrl(`${protocol}://${host}`, invalidRequestHostMessage);
545
+ if (origin.pathname !== "/" || origin.username || origin.password || origin.search || origin.hash) {
546
+ throw new HoloRequestUrlError(invalidRequestHostMessage);
547
+ }
548
+ if (forwarded?.port !== void 0) origin.port = validForwardedPort(forwarded.port);
549
+ url.protocol = origin.protocol;
550
+ url.host = origin.host;
551
+ url.port = origin.port;
552
+ const prefix = forwarded?.prefix === void 0 ? "" : validForwardedPrefix(forwarded.prefix);
553
+ if (applyPrefix) url.pathname = `${prefix}${url.pathname}`;
554
+ return url;
555
+ }
556
+ function resolveRequestUrl(input) {
557
+ return resolveUrl(input, true);
558
+ }
559
+ function resolveRoutedRequestUrl(input) {
560
+ return resolveUrl(input, false);
561
+ }
562
+ function resolveClientAddress(input) {
563
+ if (!requestForwardingState().resolveForwardedRequest) return currentConnectionAddress();
564
+ return resolveForwardedRequest(requestHeaders(input))?.address;
565
+ }
566
+ var requestUrlInternals = Object.freeze({
567
+ configureForwardedRequestResolver,
568
+ currentConnectionAddress,
569
+ provideConnectionAddress,
570
+ resolveClientAddress,
571
+ resolveRoutedRequestUrl
572
+ });
573
+ function hasControlCharacter(value) {
574
+ for (const character of value) {
575
+ const code = character.codePointAt(0) ?? 0;
576
+ if (code < 32 || code === 127) return true;
577
+ }
578
+ return false;
579
+ }
580
+ function sameOriginRedirectPath(path) {
581
+ if (!path || !path.startsWith("/") || path.startsWith("//") || path.includes("\\") || hasControlCharacter(path)) {
582
+ return "/";
583
+ }
584
+ return path;
585
+ }
586
+
587
+ // src/cookieEncryption.ts
588
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
589
+ var IV_BYTES = 12;
590
+ var TAG_BYTES = 16;
591
+ function cookieEncryptionKey(appKey) {
592
+ const key = appKey?.trim();
593
+ if (!key) {
594
+ throw new Error('[@holo-js/kernel] Holo cookies are encrypted with the application key. Set APP_KEY or run "holo key:generate".');
595
+ }
596
+ return createHmac("sha256", key).update("holo-js:cookie-encryption").digest();
597
+ }
598
+ function encryptCookieValue(name, value, appKey) {
599
+ const iv = randomBytes(IV_BYTES);
600
+ const cipher = createCipheriv("aes-256-gcm", cookieEncryptionKey(appKey), iv);
601
+ cipher.setAAD(Buffer.from(name, "utf8"));
602
+ const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
603
+ return Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString("base64url");
604
+ }
605
+ function decryptCookieValue(name, value, appKey) {
606
+ const key = cookieEncryptionKey(appKey);
607
+ const payload = Buffer.from(value, "base64url");
608
+ if (payload.toString("base64url") !== value) return void 0;
609
+ if (payload.length < IV_BYTES + TAG_BYTES) return void 0;
610
+ try {
611
+ const decipher = createDecipheriv("aes-256-gcm", key, payload.subarray(0, IV_BYTES));
612
+ decipher.setAAD(Buffer.from(name, "utf8"));
613
+ decipher.setAuthTag(payload.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));
614
+ return Buffer.concat([decipher.update(payload.subarray(IV_BYTES + TAG_BYTES)), decipher.final()]).toString("utf8");
615
+ } catch {
616
+ return void 0;
617
+ }
618
+ }
619
+ var cookieEncryptionInternals = Object.freeze({ decryptCookieValue, encryptCookieValue });
620
+
621
+ // src/localStoragePath.ts
622
+ var WINDOWS_DEVICE_NAMES = /* @__PURE__ */ new Set([
623
+ "CON",
624
+ "PRN",
625
+ "AUX",
626
+ "NUL",
627
+ ...["COM", "LPT"].flatMap((device) => ["1", "2", "3", "4", "5", "6", "7", "8", "9", "\xB9", "\xB2", "\xB3"].map((digit) => `${device}${digit}`))
628
+ ]);
629
+ function windowsStorageNameProblem(name) {
630
+ const hasControlCharacter2 = Array.from(name).some((character) => character.charCodeAt(0) < 32);
631
+ if (hasControlCharacter2 || /[<>:"|?*]/u.test(name)) {
632
+ return 'Windows file names cannot contain < > : " | ? * or control characters';
633
+ }
634
+ if (/[. ]$/u.test(name)) {
635
+ return "Windows file names cannot end with a dot or a space";
636
+ }
637
+ const baseName = (name.split(".", 1)[0] ?? "").replace(/ +$/u, "").toUpperCase();
638
+ if (WINDOWS_DEVICE_NAMES.has(baseName)) {
639
+ return "Windows reserves this device name";
640
+ }
641
+ return null;
642
+ }
643
+ function segmentProblem(segment) {
644
+ if (segment === "..") {
645
+ return 'Storage paths must not contain ".." segments';
646
+ }
647
+ if (segment === "" || segment === "." || /[\\/]/u.test(segment) || segment.includes("\0")) {
648
+ return "Storage path segments must be file or folder names";
649
+ }
650
+ return process.platform === "win32" ? windowsStorageNameProblem(segment) : null;
651
+ }
652
+ function assertValidSegment(segment) {
653
+ const problem = segmentProblem(segment);
654
+ if (problem) throw new Error(`[Holo Storage] ${problem}.`);
655
+ }
656
+ function splitLocalStoragePath(path) {
657
+ return path.trimStart().replace(/\\/g, "/").split("/").filter(Boolean);
658
+ }
659
+ function isReservedLocalStorageName(name) {
660
+ return name?.endsWith("$") === true;
661
+ }
662
+ var localStoragePathInternals = {
663
+ assertValidSegment,
664
+ isReservedLocalStorageName,
665
+ segmentProblem,
666
+ splitLocalStoragePath,
667
+ windowsStorageNameProblem
668
+ };
669
+
670
+ // src/cookieWriteUnavailable.ts
671
+ var COOKIE_WRITE_UNAVAILABLE = "cookie_write_unavailable";
672
+ function createCookieWriteUnavailableError(message, cause) {
673
+ return Object.assign(new Error(message, { cause }), {
674
+ code: COOKIE_WRITE_UNAVAILABLE
675
+ });
676
+ }
677
+
678
+ // src/retryBackoff.ts
679
+ function resolveRetryBackoffSeconds(backoff, attempt) {
680
+ if (typeof backoff === "number") {
681
+ return backoff;
682
+ }
683
+ return backoff[Math.min(Math.max(attempt - 1, 0), backoff.length - 1)] ?? 0;
684
+ }
429
685
  export {
686
+ COOKIE_WRITE_UNAVAILABLE,
430
687
  DEFAULT_HOLO_PROJECT_PATHS,
688
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
689
+ DEFAULT_MAX_UPLOAD_BODY_BYTES,
431
690
  DEFAULT_REDIS_CONNECTION,
432
691
  DEFAULT_REDIS_DB,
433
692
  DEFAULT_REDIS_HOST,
434
693
  DEFAULT_REDIS_PORT,
435
694
  HOLO_PROJECT_PREPARE_API_VERSION,
436
695
  HoloProjectPrepareError,
696
+ HoloRequestBodyTooLargeError,
697
+ HoloRequestUrlError,
437
698
  RuntimeLifecycle,
699
+ cookieEncryptionInternals,
700
+ createCookieWriteUnavailableError,
438
701
  createRuntimeLifecycle,
439
702
  defineHoloPlugin,
440
703
  defineHoloProject,
@@ -445,10 +708,24 @@ export {
445
708
  loadHoloPluginBootModules,
446
709
  loadHoloPluginContributionModules,
447
710
  loadHoloPluginDefinitions,
711
+ localStoragePathInternals,
448
712
  normalizeHoloHttpError,
449
713
  normalizeHoloPluginDefinition,
450
714
  normalizeHoloProjectConfig,
451
715
  normalizeRedisConfig,
716
+ readBoundedBufferedBody,
717
+ readBoundedCachedBody,
718
+ readBoundedNodeRequestBody,
719
+ readBoundedRequestBody,
720
+ readBoundedRequestFormData,
721
+ readBoundedRequestJson,
722
+ readBoundedRequestText,
723
+ requestBodyInternals,
724
+ requestBodyLimitFor,
725
+ requestUrlInternals,
452
726
  resolveHoloPluginModulePath,
453
- resolveNormalizedRedisConnection
727
+ resolveNormalizedRedisConnection,
728
+ resolveRequestUrl,
729
+ resolveRetryBackoffSeconds,
730
+ sameOriginRedirectPath
454
731
  };
@@ -0,0 +1,24 @@
1
+ declare function throwCapabilityFailures(failures: readonly unknown[]): void;
2
+ interface ReleasableResource {
3
+ close?(): void | Promise<void>;
4
+ disconnect?(): void | Promise<void>;
5
+ }
6
+ interface HeldResource {
7
+ hold<TResult>(operation: () => TResult): TResult;
8
+ }
9
+ interface ResourceLease<TValue> extends HeldResource {
10
+ readonly value: TValue;
11
+ }
12
+ interface ManagedResources<TConfig, TValue extends ReleasableResource> {
13
+ acquire(config: TConfig, create: (config: TConfig) => TValue): Promise<ResourceLease<TValue>>;
14
+ commit(): Promise<void>;
15
+ discard(): Promise<void>;
16
+ dispose(): Promise<void>;
17
+ }
18
+ declare function createManagedResources<TConfig, TValue extends ReleasableResource>(options?: {
19
+ readonly connect?: (value: TValue) => void | Promise<void>;
20
+ }): ManagedResources<TConfig, TValue>;
21
+ declare function holdOperations<TTarget extends object>(target: TTarget, lease: HeldResource): TTarget;
22
+
23
+ export { createManagedResources, holdOperations, throwCapabilityFailures };
24
+ export type { HeldResource, ManagedResources, ReleasableResource, ResourceLease };
@@ -0,0 +1,155 @@
1
+ import {
2
+ reportError
3
+ } from "./chunk-QQ5YD7HZ.mjs";
4
+
5
+ // src/managedResources.ts
6
+ import { isDeepStrictEqual } from "util";
7
+ function throwCapabilityFailures(failures) {
8
+ if (failures.length === 1) throw failures[0];
9
+ if (failures.length > 1) throw new AggregateError(failures, "Optional capability cleanup failed.");
10
+ }
11
+ async function releaseManagedResources(adapters) {
12
+ const failures = [];
13
+ for (const adapter of [...new Set(adapters)].reverse()) {
14
+ try {
15
+ if (adapter.close) await adapter.close();
16
+ else await adapter.disconnect?.();
17
+ } catch (error) {
18
+ failures.push(error);
19
+ }
20
+ }
21
+ throwCapabilityFailures(failures);
22
+ }
23
+ function createManagedResources(options = {}) {
24
+ let active = [];
25
+ let retained = [];
26
+ let claimed = [];
27
+ let opened = [];
28
+ let disposing = false;
29
+ let disposalFailures = [];
30
+ const releasing = /* @__PURE__ */ new Set();
31
+ const findSameConfig = (entries, config) => entries.find((entry) => isDeepStrictEqual(entry.config, config));
32
+ const track = (promise) => {
33
+ releasing.add(promise);
34
+ promise.then(() => releasing.delete(promise));
35
+ };
36
+ const reportReplacedResourceFailure = (error) => reportError(error, {
37
+ capability: "runtime",
38
+ operation: "releaseReplacedResource"
39
+ });
40
+ const startClose = (entry, onFailure) => {
41
+ entry.state = "closing";
42
+ retained = retained.filter((candidate) => candidate !== entry);
43
+ return releaseManagedResources([entry.value]).catch(onFailure).finally(() => entry.onDrained?.());
44
+ };
45
+ const retireEntry = (entry, onFailure) => {
46
+ if (entry.pending === 0) return startClose(entry, onFailure);
47
+ entry.state = "draining";
48
+ entry.onFailure = onFailure;
49
+ return new Promise((resolve) => {
50
+ entry.onDrained = resolve;
51
+ });
52
+ };
53
+ const finishOperation = (entry) => {
54
+ entry.pending -= 1;
55
+ if (entry.pending !== 0 || entry.state === "closing") return;
56
+ const awaitingClose = entry.state === "draining" || retained.includes(entry) && !claimed.includes(entry);
57
+ if (awaitingClose) track(startClose(entry, entry.onFailure ?? reportReplacedResourceFailure));
58
+ };
59
+ const createEntry = (config, value) => {
60
+ const entry = {
61
+ config,
62
+ value,
63
+ pending: 0,
64
+ state: "open",
65
+ lease: {
66
+ value,
67
+ hold(operation) {
68
+ if (entry.state === "closing") throw new Error("[@holo-js/kernel] The managed resource is closed.");
69
+ entry.pending += 1;
70
+ let result;
71
+ try {
72
+ result = operation();
73
+ } catch (error) {
74
+ finishOperation(entry);
75
+ throw error;
76
+ }
77
+ if (result instanceof Promise) {
78
+ result.finally(() => finishOperation(entry)).catch(() => void 0);
79
+ } else {
80
+ finishOperation(entry);
81
+ }
82
+ return result;
83
+ }
84
+ }
85
+ };
86
+ return entry;
87
+ };
88
+ return {
89
+ async acquire(config, create) {
90
+ if (disposing) throw new Error("[@holo-js/kernel] Managed resources are disposed; acquire a new registry instead.");
91
+ const reusable = findSameConfig(claimed, config) ?? findSameConfig(active, config) ?? findSameConfig(retained, config);
92
+ if (reusable) {
93
+ if (!claimed.includes(reusable)) claimed.push(reusable);
94
+ return reusable.lease;
95
+ }
96
+ const entry = createEntry(config, create(config));
97
+ opened.push(entry);
98
+ await options.connect?.(entry.value);
99
+ claimed.push(entry);
100
+ return entry.lease;
101
+ },
102
+ async commit() {
103
+ const previouslyOpen = [...active, ...retained];
104
+ active = claimed;
105
+ claimed = [];
106
+ opened = [];
107
+ retained = previouslyOpen.filter((entry) => !active.includes(entry));
108
+ const idle = retained.filter((entry) => entry.pending === 0);
109
+ retained = retained.filter((entry) => entry.pending > 0);
110
+ await Promise.all(idle.map((entry) => retireEntry(entry, reportReplacedResourceFailure)));
111
+ },
112
+ async discard() {
113
+ const revivedIdle = claimed.filter((entry) => retained.includes(entry) && entry.pending === 0);
114
+ retained = retained.filter((entry) => !revivedIdle.includes(entry));
115
+ const entries = [...opened, ...revivedIdle];
116
+ claimed = [];
117
+ opened = [];
118
+ const failures = [];
119
+ await Promise.all(entries.map((entry) => retireEntry(entry, (error) => {
120
+ failures.push(error);
121
+ })));
122
+ throwCapabilityFailures(failures);
123
+ },
124
+ async dispose() {
125
+ disposing = true;
126
+ disposalFailures = [];
127
+ const entries = /* @__PURE__ */ new Set([...active, ...retained, ...claimed, ...opened]);
128
+ active = [];
129
+ retained = [];
130
+ claimed = [];
131
+ opened = [];
132
+ for (const entry of entries) {
133
+ track(retireEntry(entry, (error) => {
134
+ disposalFailures.push(error);
135
+ }));
136
+ }
137
+ while (releasing.size > 0) await Promise.all([...releasing]);
138
+ throwCapabilityFailures(disposalFailures);
139
+ }
140
+ };
141
+ }
142
+ function holdOperations(target, lease) {
143
+ return new Proxy(target, {
144
+ get(source, property) {
145
+ const value = Reflect.get(source, property, source);
146
+ if (typeof value !== "function") return value;
147
+ return (...args) => lease.hold(() => Reflect.apply(value, source, args));
148
+ }
149
+ });
150
+ }
151
+ export {
152
+ createManagedResources,
153
+ holdOperations,
154
+ throwCapabilityFailures
155
+ };
@@ -0,0 +1,26 @@
1
+ declare const DEFAULT_MAX_REQUEST_BODY_BYTES: number;
2
+ declare const DEFAULT_MAX_UPLOAD_BODY_BYTES: number;
3
+ declare function requestBodyLimitFor(contentType: string | null | undefined): number;
4
+ declare class HoloRequestBodyTooLargeError extends Error {
5
+ readonly status = 413;
6
+ constructor(limit: number);
7
+ }
8
+ declare function readBoundedRequestBody(request: Request, maxBytes?: number): Promise<Uint8Array<ArrayBuffer>>;
9
+ type BufferedRequestBody = {
10
+ readonly bytes: Uint8Array<ArrayBuffer>;
11
+ readonly contentType?: string;
12
+ };
13
+ declare function readBoundedCachedBody(cached: unknown, maxBytes?: number): Promise<Uint8Array<ArrayBuffer> | undefined>;
14
+ declare function readBoundedBufferedBody(cached: unknown, maxBytes?: number): Promise<BufferedRequestBody | undefined>;
15
+ declare function readBoundedRequestText(request: Request, maxBytes?: number): Promise<string>;
16
+ declare function readBoundedRequestJson(request: Request, maxBytes?: number): Promise<unknown>;
17
+ declare function readBoundedRequestFormData(request: Request, maxBytes?: number): Promise<FormData>;
18
+ declare function readBoundedNodeRequestBody(stream: AsyncIterable<Uint8Array | string>, maxBytes?: number, declaredLength?: string | null): Promise<Uint8Array<ArrayBuffer>>;
19
+ declare const requestBodyInternals: Readonly<{
20
+ configureMaxRequestBodyBytes(bytes: number | undefined): void;
21
+ configureMaxUploadBodyBytes(bytes: number | undefined): void;
22
+ getMaxRequestBodyBytes(): number;
23
+ }>;
24
+
25
+ export { DEFAULT_MAX_REQUEST_BODY_BYTES, DEFAULT_MAX_UPLOAD_BODY_BYTES, HoloRequestBodyTooLargeError, readBoundedBufferedBody, readBoundedCachedBody, readBoundedNodeRequestBody, readBoundedRequestBody, readBoundedRequestFormData, readBoundedRequestJson, readBoundedRequestText, requestBodyInternals, requestBodyLimitFor };
26
+ export type { BufferedRequestBody };
@@ -0,0 +1,28 @@
1
+ import {
2
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
3
+ DEFAULT_MAX_UPLOAD_BODY_BYTES,
4
+ HoloRequestBodyTooLargeError,
5
+ readBoundedBufferedBody,
6
+ readBoundedCachedBody,
7
+ readBoundedNodeRequestBody,
8
+ readBoundedRequestBody,
9
+ readBoundedRequestFormData,
10
+ readBoundedRequestJson,
11
+ readBoundedRequestText,
12
+ requestBodyInternals,
13
+ requestBodyLimitFor
14
+ } from "./chunk-CGVSGOEO.mjs";
15
+ export {
16
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
17
+ DEFAULT_MAX_UPLOAD_BODY_BYTES,
18
+ HoloRequestBodyTooLargeError,
19
+ readBoundedBufferedBody,
20
+ readBoundedCachedBody,
21
+ readBoundedNodeRequestBody,
22
+ readBoundedRequestBody,
23
+ readBoundedRequestFormData,
24
+ readBoundedRequestJson,
25
+ readBoundedRequestText,
26
+ requestBodyInternals,
27
+ requestBodyLimitFor
28
+ };
package/package.json CHANGED
@@ -1,25 +1,57 @@
1
1
  {
2
2
  "name": "@holo-js/kernel",
3
- "version": "0.3.27",
4
- "description": "Holo-JS Framework - dependency-free contracts, plugins, and runtime lifecycle",
3
+ "version": "0.4.0",
4
+ "description": "Holo-JS Framework - contracts, plugins, and runtime lifecycle",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cobraprojects/holo-js.git",
10
+ "directory": "packages/kernel"
11
+ },
7
12
  "exports": {
8
13
  ".": {
9
14
  "types": "./dist/index.d.ts",
10
15
  "import": "./dist/index.mjs",
11
16
  "default": "./dist/index.mjs"
12
17
  },
18
+ "./error-reporting": {
19
+ "types": "./dist/error-reporting.d.ts",
20
+ "import": "./dist/error-reporting.mjs",
21
+ "default": "./dist/error-reporting.mjs"
22
+ },
13
23
  "./http-errors": {
14
24
  "types": "./dist/http-errors.d.ts",
15
25
  "import": "./dist/http-errors.mjs",
16
26
  "default": "./dist/http-errors.mjs"
27
+ },
28
+ "./managed-resources": {
29
+ "types": "./dist/managed-resources.d.ts",
30
+ "import": "./dist/managed-resources.mjs",
31
+ "default": "./dist/managed-resources.mjs"
32
+ },
33
+ "./file-content": {
34
+ "types": "./dist/file-content.d.ts",
35
+ "import": "./dist/file-content.mjs",
36
+ "default": "./dist/file-content.mjs"
37
+ },
38
+ "./request-body": {
39
+ "types": "./dist/request-body.d.ts",
40
+ "import": "./dist/request-body.mjs",
41
+ "default": "./dist/request-body.mjs"
42
+ },
43
+ "./image-decoding": {
44
+ "types": "./dist/image-decoding.d.ts",
45
+ "browser": "./dist/image-decoding.browser.mjs",
46
+ "node": "./dist/image-decoding.node.mjs",
47
+ "default": "./dist/image-decoding.mjs"
17
48
  }
18
49
  },
19
50
  "main": "./dist/index.mjs",
20
51
  "types": "./dist/index.d.ts",
21
52
  "files": [
22
- "dist"
53
+ "dist",
54
+ "LICENSE"
23
55
  ],
24
56
  "scripts": {
25
57
  "build": "tsup",
@@ -27,13 +59,25 @@
27
59
  "typecheck": "tsc -p tsconfig.json --noEmit",
28
60
  "test": "vitest --run"
29
61
  },
62
+ "dependencies": {
63
+ "file-type": "^22.2.0"
64
+ },
30
65
  "devDependencies": {
31
- "@types/node": "^22.10.2",
32
- "tsup": "^8.3.5",
33
- "typescript": "^5.7.2",
34
- "vitest": "^4.1.5"
66
+ "@types/node": "^22.20.5",
67
+ "sharp": "^0.35.5",
68
+ "tsup": "^9.0.0",
69
+ "typescript": "^6.0.3",
70
+ "vitest": "^5.0.5"
35
71
  },
36
72
  "engines": {
37
- "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
73
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
74
+ },
75
+ "peerDependencies": {
76
+ "sharp": "^0.35.5"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "sharp": {
80
+ "optional": true
81
+ }
38
82
  }
39
83
  }