@holo-js/kernel 0.3.28 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
+ e3c435dda503d53760a65c69eb04a2a3f39e4e925d8d1a73214a0dd32e254fca
@@ -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
+ };
@@ -6,4 +6,5 @@ declare const fileContentSampleBytes = 65536;
6
6
  declare function mimeTypeFromFileName(fileName: string): string | null;
7
7
  declare function inspectFileContent(sample: Uint8Array, fileName?: string): Promise<FileContentInspection>;
8
8
 
9
- export { type FileContentInspection, fileContentSampleBytes, inspectFileContent, mimeTypeFromFileName };
9
+ export { fileContentSampleBytes, inspectFileContent, mimeTypeFromFileName };
10
+ export type { FileContentInspection };
@@ -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 };
@@ -1,3 +1,6 @@
1
- declare function verifyDecodedImage(_contents: Uint8Array, _mimeType: string): Promise<boolean>;
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>;
2
5
 
3
6
  export { verifyDecodedImage };
@@ -1,7 +1,12 @@
1
+ import {
2
+ imageDecodingLimits
3
+ } from "./chunk-K4RCM65I.mjs";
4
+
1
5
  // src/imageDecoding.browser.ts
2
- async function verifyDecodedImage(_contents, _mimeType) {
3
- return true;
6
+ async function verifyDecodedImage(_contents, _mimeType, aspectRatios = []) {
7
+ return { valid: true, aspectRatios: aspectRatios.map(() => true) };
4
8
  }
5
9
  export {
10
+ imageDecodingLimits,
6
11
  verifyDecodedImage
7
12
  };
@@ -1,3 +1,12 @@
1
- declare function verifyDecodedImage(_contents: Uint8Array, _mimeType: string): Promise<boolean>;
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>;
2
10
 
3
- export { verifyDecodedImage };
11
+ export { imageDecodingLimits, verifyDecodedImage };
12
+ export type { DecodedImageVerification };
@@ -1,7 +1,8 @@
1
- // src/imageDecoding.ts
2
- async function verifyDecodedImage(_contents, _mimeType) {
3
- throw new Error("[@holo-js/kernel] Server-side image validation must run on Node.js, where images can be fully decoded.");
4
- }
1
+ import {
2
+ imageDecodingLimits,
3
+ verifyDecodedImage
4
+ } from "./chunk-K4RCM65I.mjs";
5
5
  export {
6
+ imageDecodingLimits,
6
7
  verifyDecodedImage
7
8
  };
@@ -1,3 +1,6 @@
1
- declare function verifyDecodedImage(contents: Uint8Array, mimeType: string): Promise<boolean>;
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>;
2
5
 
3
6
  export { verifyDecodedImage };
@@ -1,6 +1,8 @@
1
+ import {
2
+ imageDecodingLimits
3
+ } from "./chunk-K4RCM65I.mjs";
4
+
1
5
  // src/imageDecoding.node.ts
2
- var maximumDecodedPixels = 4e7;
3
- var maximumFrames = 1e3;
4
6
  var decodedFormatMatchers = Object.freeze({
5
7
  "image/avif": (metadata) => metadata.format === "heif" && metadata.compression === "av1",
6
8
  "image/gif": (metadata) => metadata.format === "gif",
@@ -50,21 +52,30 @@ function hasCompleteGifStructure(bytes) {
50
52
  }
51
53
  return false;
52
54
  }
53
- async function verifyDecodedImage(contents, mimeType) {
55
+ async function verifyDecodedImage(contents, mimeType, aspectRatios = []) {
56
+ const invalid = { valid: false, aspectRatios: [] };
54
57
  const matchesDecodedFormat = decodedFormatMatchers[mimeType];
55
- if (!matchesDecodedFormat) return false;
56
- if (mimeType === "image/gif" && !hasCompleteGifStructure(contents)) return false;
58
+ if (!matchesDecodedFormat) return invalid;
59
+ if (mimeType === "image/gif" && !hasCompleteGifStructure(contents)) return invalid;
57
60
  const decode = await loadSharp();
58
61
  try {
59
- const image = decode(contents, { animated: true, failOn: "warning", limitInputPixels: maximumDecodedPixels });
62
+ const image = decode(contents, { animated: true, failOn: "warning", limitInputPixels: imageDecodingLimits.maximumDecodedPixels });
60
63
  const metadata = await image.metadata();
61
- if (!matchesDecodedFormat(metadata) || (metadata.pages ?? 1) > maximumFrames) return false;
64
+ if (!matchesDecodedFormat(metadata) || (metadata.pages ?? 1) > imageDecodingLimits.maximumFrames) return invalid;
62
65
  await image.stats();
63
- return true;
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
+ };
64
74
  } catch {
65
- return false;
75
+ return invalid;
66
76
  }
67
77
  }
68
78
  export {
79
+ imageDecodingLimits,
69
80
  verifyDecodedImage
70
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;
@@ -429,17 +430,78 @@ type HoloRequestInput = RequestUrlParts & {
429
430
  readonly request?: RequestUrlParts;
430
431
  };
431
432
  };
432
- type HoloTrustedProxyHeaders = 'x-forwarded' | 'forwarded';
433
- declare function resolveTrustedProxyHeaders(): HoloTrustedProxyHeaders | null;
434
- declare function readForwardedParameter(headers: HoloRequestHeaders | undefined, name: string): string | undefined;
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;
435
445
  declare class HoloRequestUrlError extends TypeError {
436
446
  readonly status = 400;
437
447
  constructor(message: string);
438
448
  }
439
449
  declare function resolveRequestUrl(input: HoloRequestInput): URL;
450
+ declare function resolveRoutedRequestUrl(input: HoloRequestInput): URL;
451
+ declare function resolveClientAddress(input: HoloRequestInput): string | undefined;
440
452
  declare const requestUrlInternals: Readonly<{
441
- readForwardedParameter: typeof readForwardedParameter;
442
- resolveTrustedProxyHeaders: typeof resolveTrustedProxyHeaders;
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;
443
466
  }>;
444
467
 
445
- 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 HoloRequestHeaders, type HoloRequestInput, HoloRequestUrlError, type HoloTrustedProxyHeaders, 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, requestUrlInternals, resolveHoloPluginModulePath, resolveNormalizedRedisConnection, resolveRequestUrl };
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
+ type ResponseCookieOptions = {
487
+ path: string;
488
+ domain?: string;
489
+ maxAge?: number;
490
+ expires?: Date;
491
+ secure?: boolean;
492
+ httpOnly?: boolean;
493
+ sameSite?: 'lax' | 'strict' | 'none';
494
+ partitioned?: boolean;
495
+ };
496
+ type ParsedResponseCookie = {
497
+ readonly name: string;
498
+ readonly value: string;
499
+ readonly options: ResponseCookieOptions;
500
+ };
501
+ declare function parseResponseCookie(cookie: string): ParsedResponseCookie | null;
502
+ declare const responseCookieInternals: {
503
+ parseResponseCookie: typeof parseResponseCookie;
504
+ };
505
+
506
+ 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, responseCookieInternals, sameOriginRedirectPath };
507
+ 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
  }
@@ -428,11 +442,23 @@ function defineRedisConfig(config) {
428
442
  }
429
443
 
430
444
  // src/requestUrl.ts
431
- function resolveTrustedProxyHeaders() {
432
- const value = typeof process === "undefined" ? void 0 : process.env.HOLO_SECURITY_TRUST_PROXY?.trim().toLowerCase();
433
- if (value === "forwarded") return "forwarded";
434
- if (value === "x-forwarded" || value === "1" || value === "true" || value === "yes" || value === "on") return "x-forwarded";
435
- return null;
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
+ }
436
462
  }
437
463
  function isHeaderTuples(headers) {
438
464
  return Array.isArray(headers);
@@ -463,57 +489,6 @@ function readHeader(headers, name) {
463
489
  }
464
490
  return void 0;
465
491
  }
466
- function firstForwardedValue(value) {
467
- return value?.split(",", 1)[0]?.trim().replace(/^"|"$/g, "");
468
- }
469
- function firstForwardedElement(value) {
470
- const pairs = /* @__PURE__ */ new Map();
471
- let key = "";
472
- let current = "";
473
- let readingValue = false;
474
- let quoted = false;
475
- let escaped = false;
476
- const commit = () => {
477
- const name = key.trim().toLowerCase();
478
- if (readingValue && name && !pairs.has(name)) pairs.set(name, current.trim());
479
- key = "";
480
- current = "";
481
- readingValue = false;
482
- };
483
- for (const character of value) {
484
- if (escaped) {
485
- current += character;
486
- escaped = false;
487
- } else if (quoted) {
488
- if (character === "\\") escaped = true;
489
- else if (character === '"') quoted = false;
490
- else current += character;
491
- } else if (character === ",") {
492
- break;
493
- } else if (character === ";") {
494
- commit();
495
- } else if (character === "=" && !readingValue) {
496
- readingValue = true;
497
- } else if (character === '"' && readingValue) {
498
- quoted = true;
499
- } else if (readingValue) {
500
- current += character;
501
- } else {
502
- key += character;
503
- }
504
- }
505
- commit();
506
- return pairs;
507
- }
508
- function readForwardedParameter(headers, name) {
509
- const forwarded = readHeader(headers, "forwarded");
510
- return forwarded === void 0 ? void 0 : firstForwardedElement(forwarded).get(name);
511
- }
512
- function readTrustedProxyValue(headers, trustedProxyHeaders, name) {
513
- if (trustedProxyHeaders === "forwarded") return readForwardedParameter(headers, name);
514
- if (trustedProxyHeaders === "x-forwarded") return firstForwardedValue(readHeader(headers, `x-forwarded-${name}`));
515
- return void 0;
516
- }
517
492
  var HoloRequestUrlError = class extends TypeError {
518
493
  status = 400;
519
494
  constructor(message) {
@@ -522,25 +497,47 @@ var HoloRequestUrlError = class extends TypeError {
522
497
  }
523
498
  };
524
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
+ }
525
513
  function parseRequestUrl(value, invalidMessage) {
526
514
  if (!URL.canParse(String(value), "http://localhost")) throw new HoloRequestUrlError(invalidMessage);
527
515
  return new URL(value, "http://localhost");
528
516
  }
529
- function resolveRequestUrl(input) {
530
- const nested = input instanceof Request ? void 0 : input.request ?? input.web?.request ?? input.req ?? input.node?.req;
531
- const headers = input.headers ?? nested?.headers;
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);
532
529
  const value = input.url ?? nested?.url ?? input.path ?? (nested && "path" in nested ? nested.path : void 0) ?? "/";
533
530
  const invalidUrlMessage = "Request URLs must use http or https without credentials.";
534
531
  const url = parseRequestUrl(value, invalidUrlMessage);
535
532
  if (url.username || url.password || url.protocol !== "http:" && url.protocol !== "https:") {
536
533
  throw new HoloRequestUrlError(invalidUrlMessage);
537
534
  }
538
- const trustedProxyHeaders = resolveTrustedProxyHeaders();
539
- const protocol = (readTrustedProxyValue(headers, trustedProxyHeaders, "proto") ?? url.protocol.slice(0, -1)).toLowerCase();
535
+ const forwarded = resolveForwardedRequest(headers);
536
+ const protocol = (forwarded?.protocol ?? url.protocol.slice(0, -1)).toLowerCase();
540
537
  if (protocol !== "http" && protocol !== "https") {
541
538
  throw new HoloRequestUrlError("Request protocols must be http or https.");
542
539
  }
543
- const host = readTrustedProxyValue(headers, trustedProxyHeaders, "host") ?? readHeader(headers, "host") ?? url.host;
540
+ const host = forwarded?.host ?? readHeader(headers, "host") ?? url.host;
544
541
  if (!host || /[\s,/@\\?#]/u.test(host)) {
545
542
  throw new HoloRequestUrlError(invalidRequestHostMessage);
546
543
  }
@@ -548,22 +545,227 @@ function resolveRequestUrl(input) {
548
545
  if (origin.pathname !== "/" || origin.username || origin.password || origin.search || origin.hash) {
549
546
  throw new HoloRequestUrlError(invalidRequestHostMessage);
550
547
  }
548
+ if (forwarded?.port !== void 0) origin.port = validForwardedPort(forwarded.port);
551
549
  url.protocol = origin.protocol;
552
550
  url.host = origin.host;
553
551
  url.port = origin.port;
552
+ const prefix = forwarded?.prefix === void 0 ? "" : validForwardedPrefix(forwarded.prefix);
553
+ if (applyPrefix) url.pathname = `${prefix}${url.pathname}`;
554
554
  return url;
555
555
  }
556
- var requestUrlInternals = Object.freeze({ readForwardedParameter, resolveTrustedProxyHeaders });
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
+ }
685
+
686
+ // src/responseCookie.ts
687
+ function safeDecodeCookieSegment(value) {
688
+ try {
689
+ return decodeURIComponent(value);
690
+ } catch {
691
+ return value;
692
+ }
693
+ }
694
+ function parseResponseCookie(cookie) {
695
+ const [nameValue = "", ...attributes] = cookie.split(";");
696
+ const separator = nameValue.indexOf("=");
697
+ if (!nameValue || separator <= 0) {
698
+ return null;
699
+ }
700
+ const options = { path: "/", secure: false, httpOnly: false };
701
+ for (const rawAttribute of attributes) {
702
+ const attribute = rawAttribute.trim();
703
+ if (!attribute) {
704
+ continue;
705
+ }
706
+ const attributeSeparator = attribute.indexOf("=");
707
+ const key = (attributeSeparator === -1 ? attribute : attribute.slice(0, attributeSeparator)).trim().toLowerCase();
708
+ const value = attributeSeparator === -1 ? "" : attribute.slice(attributeSeparator + 1).trim();
709
+ switch (key) {
710
+ case "path":
711
+ options.path = value || "/";
712
+ break;
713
+ case "domain":
714
+ options.domain = value;
715
+ break;
716
+ case "max-age": {
717
+ const maxAge = Number(value);
718
+ if (Number.isFinite(maxAge)) {
719
+ options.maxAge = maxAge;
720
+ }
721
+ break;
722
+ }
723
+ case "expires": {
724
+ const expires = new Date(value);
725
+ if (!Number.isNaN(expires.getTime())) {
726
+ options.expires = expires;
727
+ }
728
+ break;
729
+ }
730
+ case "secure":
731
+ options.secure = true;
732
+ break;
733
+ case "httponly":
734
+ options.httpOnly = true;
735
+ break;
736
+ case "samesite":
737
+ if (value.toLowerCase() === "lax" || value.toLowerCase() === "strict" || value.toLowerCase() === "none") {
738
+ options.sameSite = value.toLowerCase();
739
+ }
740
+ break;
741
+ case "partitioned":
742
+ options.partitioned = value ? value.toLowerCase() === "true" : true;
743
+ break;
744
+ }
745
+ }
746
+ return {
747
+ name: safeDecodeCookieSegment(nameValue.slice(0, separator)),
748
+ value: safeDecodeCookieSegment(nameValue.slice(separator + 1)),
749
+ options
750
+ };
751
+ }
752
+ var responseCookieInternals = { parseResponseCookie };
557
753
  export {
754
+ COOKIE_WRITE_UNAVAILABLE,
558
755
  DEFAULT_HOLO_PROJECT_PATHS,
756
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
757
+ DEFAULT_MAX_UPLOAD_BODY_BYTES,
559
758
  DEFAULT_REDIS_CONNECTION,
560
759
  DEFAULT_REDIS_DB,
561
760
  DEFAULT_REDIS_HOST,
562
761
  DEFAULT_REDIS_PORT,
563
762
  HOLO_PROJECT_PREPARE_API_VERSION,
564
763
  HoloProjectPrepareError,
764
+ HoloRequestBodyTooLargeError,
565
765
  HoloRequestUrlError,
566
766
  RuntimeLifecycle,
767
+ cookieEncryptionInternals,
768
+ createCookieWriteUnavailableError,
567
769
  createRuntimeLifecycle,
568
770
  defineHoloPlugin,
569
771
  defineHoloProject,
@@ -574,12 +776,25 @@ export {
574
776
  loadHoloPluginBootModules,
575
777
  loadHoloPluginContributionModules,
576
778
  loadHoloPluginDefinitions,
779
+ localStoragePathInternals,
577
780
  normalizeHoloHttpError,
578
781
  normalizeHoloPluginDefinition,
579
782
  normalizeHoloProjectConfig,
580
783
  normalizeRedisConfig,
784
+ readBoundedBufferedBody,
785
+ readBoundedCachedBody,
786
+ readBoundedNodeRequestBody,
787
+ readBoundedRequestBody,
788
+ readBoundedRequestFormData,
789
+ readBoundedRequestJson,
790
+ readBoundedRequestText,
791
+ requestBodyInternals,
792
+ requestBodyLimitFor,
581
793
  requestUrlInternals,
582
794
  resolveHoloPluginModulePath,
583
795
  resolveNormalizedRedisConnection,
584
- resolveRequestUrl
796
+ resolveRequestUrl,
797
+ resolveRetryBackoffSeconds,
798
+ responseCookieInternals,
799
+ sameOriginRedirectPath
585
800
  };
@@ -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,45 @@
1
1
  {
2
2
  "name": "@holo-js/kernel",
3
- "version": "0.3.28",
4
- "description": "Holo-JS Framework - dependency-free contracts, plugins, and runtime lifecycle",
3
+ "version": "0.4.1",
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"
17
27
  },
28
+ "./managed-resources": {
29
+ "types": "./dist/managed-resources.d.ts",
30
+ "import": "./dist/managed-resources.mjs",
31
+ "default": "./dist/managed-resources.mjs"
32
+ },
18
33
  "./file-content": {
19
34
  "types": "./dist/file-content.d.ts",
20
35
  "import": "./dist/file-content.mjs",
21
36
  "default": "./dist/file-content.mjs"
22
37
  },
38
+ "./request-body": {
39
+ "types": "./dist/request-body.d.ts",
40
+ "import": "./dist/request-body.mjs",
41
+ "default": "./dist/request-body.mjs"
42
+ },
23
43
  "./image-decoding": {
24
44
  "types": "./dist/image-decoding.d.ts",
25
45
  "browser": "./dist/image-decoding.browser.mjs",
@@ -30,7 +50,8 @@
30
50
  "main": "./dist/index.mjs",
31
51
  "types": "./dist/index.d.ts",
32
52
  "files": [
33
- "dist"
53
+ "dist",
54
+ "LICENSE"
34
55
  ],
35
56
  "scripts": {
36
57
  "build": "tsup",
@@ -38,25 +59,23 @@
38
59
  "typecheck": "tsc -p tsconfig.json --noEmit",
39
60
  "test": "vitest --run"
40
61
  },
62
+ "dependencies": {
63
+ "file-type": "^22.2.0"
64
+ },
41
65
  "devDependencies": {
42
- "@types/node": "^22.10.2",
43
- "file-type": "^22.2.0",
44
- "sharp": "^0.35.4",
45
- "tsup": "^8.3.5",
46
- "typescript": "^5.7.2",
47
- "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"
48
71
  },
49
72
  "engines": {
50
- "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
73
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
51
74
  },
52
75
  "peerDependencies": {
53
- "file-type": "^22.2.0",
54
- "sharp": "^0.35.4"
76
+ "sharp": "^0.35.5"
55
77
  },
56
78
  "peerDependenciesMeta": {
57
- "file-type": {
58
- "optional": true
59
- },
60
79
  "sharp": {
61
80
  "optional": true
62
81
  }