alepha 0.13.0 → 0.13.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/dist/api-jobs/index.d.ts +26 -26
- package/dist/api-users/index.d.ts +1 -1
- package/dist/cli/{dist-Sz2EXvQX.cjs → dist-Dl9Vl7Ur.js} +17 -13
- package/dist/cli/{dist-BBPjuQ56.js.map → dist-Dl9Vl7Ur.js.map} +1 -1
- package/dist/cli/index.d.ts +3 -11
- package/dist/cli/index.js +106 -74
- package/dist/cli/index.js.map +1 -1
- package/dist/email/index.js +71 -73
- package/dist/email/index.js.map +1 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/index.d.ts +4 -4
- package/dist/retry/index.d.ts +1 -1
- package/dist/retry/index.js +2 -2
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/security/index.d.ts +28 -28
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server-health/index.d.ts +17 -17
- package/dist/server-metrics/index.js +170 -174
- package/dist/server-metrics/index.js.map +1 -1
- package/dist/server-security/index.d.ts +9 -9
- package/dist/vite/index.js +4 -5
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +7 -7
- package/package.json +52 -103
- package/src/cli/apps/AlephaPackageBuilderCli.ts +7 -2
- package/src/cli/assets/appRouterTs.ts +9 -0
- package/src/cli/assets/indexHtml.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +10 -0
- package/src/cli/commands/CoreCommands.ts +6 -5
- package/src/cli/commands/DrizzleCommands.ts +65 -57
- package/src/cli/commands/VerifyCommands.ts +1 -1
- package/src/cli/services/ProjectUtils.ts +44 -38
- package/src/orm/providers/DrizzleKitProvider.ts +1 -1
- package/src/retry/descriptors/$retry.ts +5 -3
- package/src/server/providers/NodeHttpServerProvider.ts +1 -1
- package/src/vite/helpers/boot.ts +3 -3
- package/dist/api-files/index.cjs +0 -1293
- package/dist/api-files/index.cjs.map +0 -1
- package/dist/api-files/index.d.cts +0 -829
- package/dist/api-jobs/index.cjs +0 -274
- package/dist/api-jobs/index.cjs.map +0 -1
- package/dist/api-jobs/index.d.cts +0 -654
- package/dist/api-notifications/index.cjs +0 -380
- package/dist/api-notifications/index.cjs.map +0 -1
- package/dist/api-notifications/index.d.cts +0 -289
- package/dist/api-parameters/index.cjs +0 -66
- package/dist/api-parameters/index.cjs.map +0 -1
- package/dist/api-parameters/index.d.cts +0 -84
- package/dist/api-users/index.cjs +0 -6009
- package/dist/api-users/index.cjs.map +0 -1
- package/dist/api-users/index.d.cts +0 -4740
- package/dist/api-verifications/index.cjs +0 -407
- package/dist/api-verifications/index.cjs.map +0 -1
- package/dist/api-verifications/index.d.cts +0 -207
- package/dist/batch/index.cjs +0 -408
- package/dist/batch/index.cjs.map +0 -1
- package/dist/batch/index.d.cts +0 -330
- package/dist/bin/index.cjs +0 -17
- package/dist/bin/index.cjs.map +0 -1
- package/dist/bin/index.d.cts +0 -1
- package/dist/bucket/index.cjs +0 -303
- package/dist/bucket/index.cjs.map +0 -1
- package/dist/bucket/index.d.cts +0 -355
- package/dist/cache/index.cjs +0 -241
- package/dist/cache/index.cjs.map +0 -1
- package/dist/cache/index.d.cts +0 -202
- package/dist/cache-redis/index.cjs +0 -84
- package/dist/cache-redis/index.cjs.map +0 -1
- package/dist/cache-redis/index.d.cts +0 -40
- package/dist/cli/chunk-DSlc6foC.cjs +0 -43
- package/dist/cli/dist-BBPjuQ56.js +0 -2778
- package/dist/cli/dist-Sz2EXvQX.cjs.map +0 -1
- package/dist/cli/index.cjs +0 -1241
- package/dist/cli/index.cjs.map +0 -1
- package/dist/cli/index.d.cts +0 -422
- package/dist/command/index.cjs +0 -693
- package/dist/command/index.cjs.map +0 -1
- package/dist/command/index.d.cts +0 -340
- package/dist/core/index.cjs +0 -2264
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -1927
- package/dist/datetime/index.cjs +0 -318
- package/dist/datetime/index.cjs.map +0 -1
- package/dist/datetime/index.d.cts +0 -145
- package/dist/email/index.cjs +0 -10874
- package/dist/email/index.cjs.map +0 -1
- package/dist/email/index.d.cts +0 -186
- package/dist/fake/index.cjs +0 -34641
- package/dist/fake/index.cjs.map +0 -1
- package/dist/fake/index.d.cts +0 -74
- package/dist/file/index.cjs +0 -1212
- package/dist/file/index.cjs.map +0 -1
- package/dist/file/index.d.cts +0 -698
- package/dist/lock/index.cjs +0 -226
- package/dist/lock/index.cjs.map +0 -1
- package/dist/lock/index.d.cts +0 -361
- package/dist/lock-redis/index.cjs +0 -113
- package/dist/lock-redis/index.cjs.map +0 -1
- package/dist/lock-redis/index.d.cts +0 -24
- package/dist/logger/index.cjs +0 -521
- package/dist/logger/index.cjs.map +0 -1
- package/dist/logger/index.d.cts +0 -281
- package/dist/orm/index.cjs +0 -2986
- package/dist/orm/index.cjs.map +0 -1
- package/dist/orm/index.d.cts +0 -2213
- package/dist/queue/index.cjs +0 -1044
- package/dist/queue/index.cjs.map +0 -1
- package/dist/queue/index.d.cts +0 -1265
- package/dist/queue-redis/index.cjs +0 -873
- package/dist/queue-redis/index.cjs.map +0 -1
- package/dist/queue-redis/index.d.cts +0 -82
- package/dist/redis/index.cjs +0 -153
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -82
- package/dist/retry/index.cjs +0 -146
- package/dist/retry/index.cjs.map +0 -1
- package/dist/retry/index.d.cts +0 -172
- package/dist/router/index.cjs +0 -111
- package/dist/router/index.cjs.map +0 -1
- package/dist/router/index.d.cts +0 -46
- package/dist/scheduler/index.cjs +0 -576
- package/dist/scheduler/index.cjs.map +0 -1
- package/dist/scheduler/index.d.cts +0 -145
- package/dist/security/index.cjs +0 -2402
- package/dist/security/index.cjs.map +0 -1
- package/dist/security/index.d.cts +0 -598
- package/dist/server/index.cjs +0 -1680
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.d.cts +0 -810
- package/dist/server-auth/index.cjs +0 -3146
- package/dist/server-auth/index.cjs.map +0 -1
- package/dist/server-auth/index.d.cts +0 -1164
- package/dist/server-cache/index.cjs +0 -252
- package/dist/server-cache/index.cjs.map +0 -1
- package/dist/server-cache/index.d.cts +0 -164
- package/dist/server-compress/index.cjs +0 -141
- package/dist/server-compress/index.cjs.map +0 -1
- package/dist/server-compress/index.d.cts +0 -38
- package/dist/server-cookies/index.cjs +0 -234
- package/dist/server-cookies/index.cjs.map +0 -1
- package/dist/server-cookies/index.d.cts +0 -144
- package/dist/server-cors/index.cjs +0 -201
- package/dist/server-cors/index.cjs.map +0 -1
- package/dist/server-cors/index.d.cts +0 -140
- package/dist/server-health/index.cjs +0 -62
- package/dist/server-health/index.cjs.map +0 -1
- package/dist/server-health/index.d.cts +0 -58
- package/dist/server-helmet/index.cjs +0 -131
- package/dist/server-helmet/index.cjs.map +0 -1
- package/dist/server-helmet/index.d.cts +0 -97
- package/dist/server-links/index.cjs +0 -992
- package/dist/server-links/index.cjs.map +0 -1
- package/dist/server-links/index.d.cts +0 -513
- package/dist/server-metrics/index.cjs +0 -4535
- package/dist/server-metrics/index.cjs.map +0 -1
- package/dist/server-metrics/index.d.cts +0 -35
- package/dist/server-multipart/index.cjs +0 -237
- package/dist/server-multipart/index.cjs.map +0 -1
- package/dist/server-multipart/index.d.cts +0 -50
- package/dist/server-proxy/index.cjs +0 -186
- package/dist/server-proxy/index.cjs.map +0 -1
- package/dist/server-proxy/index.d.cts +0 -234
- package/dist/server-rate-limit/index.cjs +0 -241
- package/dist/server-rate-limit/index.cjs.map +0 -1
- package/dist/server-rate-limit/index.d.cts +0 -183
- package/dist/server-security/index.cjs +0 -316
- package/dist/server-security/index.cjs.map +0 -1
- package/dist/server-security/index.d.cts +0 -173
- package/dist/server-static/index.cjs +0 -170
- package/dist/server-static/index.cjs.map +0 -1
- package/dist/server-static/index.d.cts +0 -121
- package/dist/server-swagger/index.cjs +0 -1021
- package/dist/server-swagger/index.cjs.map +0 -1
- package/dist/server-swagger/index.d.cts +0 -382
- package/dist/sms/index.cjs +0 -221
- package/dist/sms/index.cjs.map +0 -1
- package/dist/sms/index.d.cts +0 -130
- package/dist/thread/index.cjs +0 -350
- package/dist/thread/index.cjs.map +0 -1
- package/dist/thread/index.d.cts +0 -260
- package/dist/topic/index.cjs +0 -282
- package/dist/topic/index.cjs.map +0 -1
- package/dist/topic/index.d.cts +0 -523
- package/dist/topic-redis/index.cjs +0 -71
- package/dist/topic-redis/index.cjs.map +0 -1
- package/dist/topic-redis/index.d.cts +0 -42
- package/dist/vite/index.cjs +0 -1077
- package/dist/vite/index.cjs.map +0 -1
- package/dist/vite/index.d.cts +0 -542
- package/dist/websocket/index.cjs +0 -1117
- package/dist/websocket/index.cjs.map +0 -1
- package/dist/websocket/index.d.cts +0 -861
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import * as alepha1 from "alepha";
|
|
2
|
-
import { Alepha } from "alepha";
|
|
3
|
-
import * as alepha_server0 from "alepha/server";
|
|
4
|
-
import { Histogram, Registry } from "prom-client";
|
|
5
|
-
|
|
6
|
-
//#region src/server-metrics/providers/ServerMetricsProvider.d.ts
|
|
7
|
-
declare class ServerMetricsProvider {
|
|
8
|
-
protected readonly register: Registry;
|
|
9
|
-
protected readonly alepha: Alepha;
|
|
10
|
-
protected httpRequestDuration?: Histogram<string>;
|
|
11
|
-
readonly options: ServerMetricsProviderOptions;
|
|
12
|
-
readonly metrics: alepha_server0.RouteDescriptor<alepha_server0.RequestConfigSchema>;
|
|
13
|
-
protected readonly onStart: alepha1.HookDescriptor<"start">;
|
|
14
|
-
protected readonly onRequest: alepha1.HookDescriptor<"server:onRequest">;
|
|
15
|
-
protected readonly onResponse: alepha1.HookDescriptor<"server:onResponse">;
|
|
16
|
-
}
|
|
17
|
-
interface ServerMetricsProviderOptions {
|
|
18
|
-
prefix?: string;
|
|
19
|
-
gcDurationBuckets?: number[];
|
|
20
|
-
eventLoopMonitoringPrecision?: number;
|
|
21
|
-
labels?: object;
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/server-metrics/index.d.ts
|
|
25
|
-
/**
|
|
26
|
-
* This module provides prometheus metrics for the Alepha server.
|
|
27
|
-
* Metrics are exposed at the `/metrics` endpoint.
|
|
28
|
-
*
|
|
29
|
-
* @see {@link ServerMetricsProvider}
|
|
30
|
-
* @module alepha.server.metrics
|
|
31
|
-
*/
|
|
32
|
-
declare const AlephaServerMetrics: alepha1.Service<alepha1.Module>;
|
|
33
|
-
//#endregion
|
|
34
|
-
export { AlephaServerMetrics, ServerMetricsProvider, ServerMetricsProviderOptions };
|
|
35
|
-
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
//#region rolldown:runtime
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
-
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: ((k) => from[k]).bind(null, key),
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return to;
|
|
21
|
-
};
|
|
22
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
-
value: mod,
|
|
24
|
-
enumerable: true
|
|
25
|
-
}) : target, mod));
|
|
26
|
-
|
|
27
|
-
//#endregion
|
|
28
|
-
let alepha = require("alepha");
|
|
29
|
-
let alepha_server = require("alepha/server");
|
|
30
|
-
let node_crypto = require("node:crypto");
|
|
31
|
-
let node_fs = require("node:fs");
|
|
32
|
-
let node_fs_promises = require("node:fs/promises");
|
|
33
|
-
let node_os = require("node:os");
|
|
34
|
-
node_os = __toESM(node_os);
|
|
35
|
-
let node_stream_web = require("node:stream/web");
|
|
36
|
-
let alepha_logger = require("alepha/logger");
|
|
37
|
-
|
|
38
|
-
//#region src/server-multipart/providers/ServerMultipartProvider.ts
|
|
39
|
-
const envSchema = alepha.t.object({
|
|
40
|
-
SERVER_MULTIPART_LIMIT: alepha.t.integer({
|
|
41
|
-
default: 1e7,
|
|
42
|
-
min: 0,
|
|
43
|
-
description: "Maximum total size of multipart request body in bytes."
|
|
44
|
-
}),
|
|
45
|
-
SERVER_MULTIPART_FILE_LIMIT: alepha.t.integer({
|
|
46
|
-
default: 5e6,
|
|
47
|
-
min: 0,
|
|
48
|
-
description: "Maximum size of a single file in bytes."
|
|
49
|
-
}),
|
|
50
|
-
SERVER_MULTIPART_FILE_COUNT: alepha.t.integer({
|
|
51
|
-
default: 10,
|
|
52
|
-
min: 1,
|
|
53
|
-
description: "Maximum number of files allowed in a single request."
|
|
54
|
-
})
|
|
55
|
-
});
|
|
56
|
-
var ServerMultipartProvider = class {
|
|
57
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
58
|
-
env = (0, alepha.$env)(envSchema);
|
|
59
|
-
log = (0, alepha_logger.$logger)();
|
|
60
|
-
onRequest = (0, alepha.$hook)({
|
|
61
|
-
on: "server:onRequest",
|
|
62
|
-
handler: async ({ route, request }) => {
|
|
63
|
-
if (request.body) return;
|
|
64
|
-
if (!route.schema?.body) return;
|
|
65
|
-
let webRequest;
|
|
66
|
-
if (request.raw.web?.req) webRequest = request.raw.web.req;
|
|
67
|
-
else if (request.raw.node?.req) webRequest = new Request(request.url, {
|
|
68
|
-
method: request.method,
|
|
69
|
-
headers: request.headers,
|
|
70
|
-
body: node_stream_web.ReadableStream.from(request.raw.node.req),
|
|
71
|
-
duplex: "half"
|
|
72
|
-
});
|
|
73
|
-
if (!webRequest) return;
|
|
74
|
-
const contentType = request.headers["content-type"];
|
|
75
|
-
const contentLength = request.headers["content-length"];
|
|
76
|
-
if (contentLength) {
|
|
77
|
-
const size = Number.parseInt(contentLength, 10);
|
|
78
|
-
if (!Number.isNaN(size) && size > this.env.SERVER_MULTIPART_LIMIT) {
|
|
79
|
-
this.log.error(`Multipart request size limit exceeded: ${size} > ${this.env.SERVER_MULTIPART_LIMIT}`);
|
|
80
|
-
throw new alepha_server.HttpError({
|
|
81
|
-
status: 413,
|
|
82
|
-
message: `Request body size limit exceeded. Maximum allowed: ${this.env.SERVER_MULTIPART_LIMIT} bytes`
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (!contentType?.startsWith("multipart/form-data")) {
|
|
87
|
-
if (!(0, alepha_server.isMultipart)(route)) return;
|
|
88
|
-
throw new alepha_server.HttpError({
|
|
89
|
-
status: 415,
|
|
90
|
-
message: `Invalid content-type: ${contentType} - only "multipart/form-data" is accepted`
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
const { body, cleanup } = await this.handleMultipartBodyFromWeb(route, webRequest);
|
|
94
|
-
request.body = body;
|
|
95
|
-
request.metadata.multipart = { cleanup };
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
onResponse = (0, alepha.$hook)({
|
|
99
|
-
on: "server:onResponse",
|
|
100
|
-
handler: async ({ request }) => {
|
|
101
|
-
const cleanup = request.metadata.multipart?.cleanup;
|
|
102
|
-
if (typeof cleanup === "function") await cleanup();
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
async handleMultipartBodyFromWeb(route, request) {
|
|
106
|
-
let formData;
|
|
107
|
-
try {
|
|
108
|
-
formData = await request.formData();
|
|
109
|
-
} catch (error) {
|
|
110
|
-
throw new alepha_server.HttpError({
|
|
111
|
-
status: 400,
|
|
112
|
-
message: "Malformed multipart/form-data"
|
|
113
|
-
}, error);
|
|
114
|
-
}
|
|
115
|
-
const body = {};
|
|
116
|
-
const tempFiles = [];
|
|
117
|
-
const cleanupOnError = async () => {
|
|
118
|
-
for (const file of tempFiles) try {
|
|
119
|
-
await file.cleanup();
|
|
120
|
-
} catch {}
|
|
121
|
-
};
|
|
122
|
-
try {
|
|
123
|
-
let fileCount = 0;
|
|
124
|
-
let totalSize = 0;
|
|
125
|
-
if (route.schema?.body && alepha.t.schema.isObject(route.schema.body)) {
|
|
126
|
-
for (const [key, value] of Object.entries(route.schema.body.properties)) if (alepha.t.schema.isSchema(value)) if ((0, alepha.isTypeFile)(value)) {
|
|
127
|
-
const file = formData.get(key);
|
|
128
|
-
if (file && typeof file === "object" && "arrayBuffer" in file) {
|
|
129
|
-
const blob = file;
|
|
130
|
-
fileCount++;
|
|
131
|
-
if (fileCount > this.env.SERVER_MULTIPART_FILE_COUNT) {
|
|
132
|
-
this.log.error(`Too many files in multipart request: ${fileCount} > ${this.env.SERVER_MULTIPART_FILE_COUNT}`);
|
|
133
|
-
throw new alepha_server.HttpError({
|
|
134
|
-
status: 413,
|
|
135
|
-
message: `Too many files. Maximum allowed: ${this.env.SERVER_MULTIPART_FILE_COUNT}`
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
if (blob.size > this.env.SERVER_MULTIPART_FILE_LIMIT) {
|
|
139
|
-
this.log.error(`File "${key}" exceeds size limit: ${blob.size} > ${this.env.SERVER_MULTIPART_FILE_LIMIT}`);
|
|
140
|
-
throw new alepha_server.HttpError({
|
|
141
|
-
status: 413,
|
|
142
|
-
message: `File "${key}" exceeds size limit. Maximum allowed: ${this.env.SERVER_MULTIPART_FILE_LIMIT} bytes`
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
totalSize += blob.size;
|
|
146
|
-
if (totalSize > this.env.SERVER_MULTIPART_LIMIT) {
|
|
147
|
-
this.log.error(`Total multipart size exceeds limit: ${totalSize} > ${this.env.SERVER_MULTIPART_LIMIT}`);
|
|
148
|
-
throw new alepha_server.HttpError({
|
|
149
|
-
status: 413,
|
|
150
|
-
message: `Total request size exceeds limit. Maximum allowed: ${this.env.SERVER_MULTIPART_LIMIT} bytes`
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
const hybridFile = await this.createHybridFile(blob, key);
|
|
154
|
-
body[key] = hybridFile;
|
|
155
|
-
tempFiles.push(hybridFile);
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
const fieldValue = formData.get(key);
|
|
159
|
-
if (fieldValue !== null) {
|
|
160
|
-
const stringValue = typeof fieldValue === "string" ? fieldValue : "";
|
|
161
|
-
body[key] = this.alepha.codec.decode(value, stringValue);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
body,
|
|
167
|
-
cleanup: async () => {
|
|
168
|
-
for (const file of tempFiles) await file.cleanup();
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
} catch (error) {
|
|
172
|
-
await cleanupOnError();
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* This is a legacy code, previously we used "busboy" to parse multipart in Node.js environment.
|
|
178
|
-
* Now we rely on Web Request's formData() method, which is supported in modern Node.js versions.
|
|
179
|
-
* However, we still need to create temporary files for uploaded files to provide a consistent File-like interface.
|
|
180
|
-
*
|
|
181
|
-
* TODO: In future, we might want to refactor this to avoid using temporary files if not necessary?
|
|
182
|
-
*/
|
|
183
|
-
async createHybridFile(file, fieldName) {
|
|
184
|
-
const tmpPath = `${node_os.tmpdir()}/${(0, node_crypto.randomUUID)()}`;
|
|
185
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
186
|
-
await (0, node_fs_promises.writeFile)(tmpPath, Buffer.from(arrayBuffer));
|
|
187
|
-
const fileName = file.name || `${fieldName}_${Date.now()}`;
|
|
188
|
-
return {
|
|
189
|
-
_state: {
|
|
190
|
-
cleanup: false,
|
|
191
|
-
size: file.size,
|
|
192
|
-
tmpPath
|
|
193
|
-
},
|
|
194
|
-
name: fileName,
|
|
195
|
-
type: file.type || "application/octet-stream",
|
|
196
|
-
lastModified: file.lastModified || Date.now(),
|
|
197
|
-
filepath: tmpPath,
|
|
198
|
-
get size() {
|
|
199
|
-
return this._state.size;
|
|
200
|
-
},
|
|
201
|
-
stream() {
|
|
202
|
-
return (0, node_fs.createReadStream)(tmpPath);
|
|
203
|
-
},
|
|
204
|
-
async arrayBuffer() {
|
|
205
|
-
const content = await (0, node_fs_promises.readFile)(tmpPath);
|
|
206
|
-
return content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
207
|
-
},
|
|
208
|
-
text: async () => {
|
|
209
|
-
return await (0, node_fs_promises.readFile)(tmpPath, "utf-8");
|
|
210
|
-
},
|
|
211
|
-
async cleanup() {
|
|
212
|
-
if (this._state.cleanup) return;
|
|
213
|
-
await (0, node_fs_promises.unlink)(tmpPath);
|
|
214
|
-
this._state.cleanup = true;
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
//#endregion
|
|
221
|
-
//#region src/server-multipart/index.ts
|
|
222
|
-
/**
|
|
223
|
-
* This module provides support for handling multipart/form-data requests.
|
|
224
|
-
* It allows to parse body data containing t.file().
|
|
225
|
-
*
|
|
226
|
-
* @see {@link ServerMultipartProvider}
|
|
227
|
-
* @module alepha.server.multipart
|
|
228
|
-
*/
|
|
229
|
-
const AlephaServerMultipart = (0, alepha.$module)({
|
|
230
|
-
name: "alepha.server.multipart",
|
|
231
|
-
services: [alepha_server.AlephaServer, ServerMultipartProvider]
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
//#endregion
|
|
235
|
-
exports.AlephaServerMultipart = AlephaServerMultipart;
|
|
236
|
-
exports.ServerMultipartProvider = ServerMultipartProvider;
|
|
237
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["t","Alepha","webRequest: Request | undefined","WebStream","HttpError","formData: FormData","body: Record<string, any>","tempFiles: HybridFile[]","os","AlephaServer"],"sources":["../../src/server-multipart/providers/ServerMultipartProvider.ts","../../src/server-multipart/index.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { createReadStream } from \"node:fs\";\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport { ReadableStream as WebStream } from \"node:stream/web\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n type FileLike,\n isTypeFile,\n t,\n} from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { HttpError, isMultipart, type ServerRoute } from \"alepha/server\";\n\nconst envSchema = t.object({\n SERVER_MULTIPART_LIMIT: t.integer({\n default: 10_000_000, // 10MB total\n min: 0,\n description: \"Maximum total size of multipart request body in bytes.\",\n }),\n SERVER_MULTIPART_FILE_LIMIT: t.integer({\n default: 5_000_000, // 5MB per file\n min: 0,\n description: \"Maximum size of a single file in bytes.\",\n }),\n SERVER_MULTIPART_FILE_COUNT: t.integer({\n default: 10,\n min: 1,\n description: \"Maximum number of files allowed in a single request.\",\n }),\n});\n\nexport class ServerMultipartProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly env = $env(envSchema);\n protected readonly log = $logger();\n\n public readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n // already parsed (e.g. by body parser)\n if (request.body) {\n return;\n }\n\n // we do not parse body if no schema\n if (!route.schema?.body) {\n return;\n }\n\n let webRequest: Request | undefined;\n\n if (request.raw.web?.req) {\n webRequest = request.raw.web.req;\n } else if (request.raw.node?.req) {\n webRequest = new Request(request.url, {\n method: request.method,\n headers: request.headers,\n body: WebStream.from(request.raw.node.req) as ReadableStream,\n duplex: \"half\",\n } as RequestInit & { duplex: \"half\" });\n }\n\n if (!webRequest) {\n return;\n }\n\n const contentType = request.headers[\"content-type\"];\n\n // Check content-length before processing to fail fast on oversized requests\n const contentLength = request.headers[\"content-length\"];\n if (contentLength) {\n const size = Number.parseInt(contentLength, 10);\n if (!Number.isNaN(size) && size > this.env.SERVER_MULTIPART_LIMIT) {\n this.log.error(\n `Multipart request size limit exceeded: ${size} > ${this.env.SERVER_MULTIPART_LIMIT}`,\n );\n throw new HttpError({\n status: 413,\n message: `Request body size limit exceeded. Maximum allowed: ${this.env.SERVER_MULTIPART_LIMIT} bytes`,\n });\n }\n }\n\n if (!contentType?.startsWith(\"multipart/form-data\")) {\n if (!isMultipart(route)) {\n return;\n }\n\n // route expects multipart but content-type is not correct! reject with 415\n throw new HttpError({\n status: 415,\n message: `Invalid content-type: ${contentType} - only \"multipart/form-data\" is accepted`,\n });\n }\n\n const { body, cleanup } = await this.handleMultipartBodyFromWeb(\n route,\n webRequest,\n );\n\n request.body = body;\n request.metadata.multipart = { cleanup };\n },\n });\n\n public readonly onResponse = $hook({\n on: \"server:onResponse\",\n handler: async ({ request }) => {\n const cleanup = request.metadata.multipart?.cleanup;\n if (typeof cleanup === \"function\") {\n await cleanup();\n }\n },\n });\n\n public async handleMultipartBodyFromWeb(\n route: ServerRoute,\n request: Request,\n ): Promise<{\n body: Record<string, unknown>;\n cleanup: () => Promise<void>;\n }> {\n let formData: FormData;\n\n try {\n // Parse the FormData from the request\n formData = await request.formData();\n } catch (error) {\n throw new HttpError(\n {\n status: 400,\n message: \"Malformed multipart/form-data\",\n },\n error,\n );\n }\n\n const body: Record<string, any> = {};\n const tempFiles: HybridFile[] = [];\n\n // Helper to clean up temp files on error\n const cleanupOnError = async () => {\n for (const file of tempFiles) {\n try {\n await file.cleanup();\n } catch {\n // Ignore cleanup errors during error handling\n }\n }\n };\n\n try {\n let fileCount = 0;\n let totalSize = 0;\n\n if (route.schema?.body && t.schema.isObject(route.schema.body)) {\n for (const [key, value] of Object.entries(\n route.schema.body.properties,\n )) {\n if (t.schema.isSchema(value)) {\n if (isTypeFile(value)) {\n const file = formData.get(key);\n // Check if file is a Blob (File extends Blob in Web APIs)\n if (file && typeof file === \"object\" && \"arrayBuffer\" in file) {\n const blob = file as Blob;\n\n // Validate file count\n fileCount++;\n if (fileCount > this.env.SERVER_MULTIPART_FILE_COUNT) {\n this.log.error(\n `Too many files in multipart request: ${fileCount} > ${this.env.SERVER_MULTIPART_FILE_COUNT}`,\n );\n throw new HttpError({\n status: 413,\n message: `Too many files. Maximum allowed: ${this.env.SERVER_MULTIPART_FILE_COUNT}`,\n });\n }\n\n // Validate individual file size\n if (blob.size > this.env.SERVER_MULTIPART_FILE_LIMIT) {\n this.log.error(\n `File \"${key}\" exceeds size limit: ${blob.size} > ${this.env.SERVER_MULTIPART_FILE_LIMIT}`,\n );\n throw new HttpError({\n status: 413,\n message: `File \"${key}\" exceeds size limit. Maximum allowed: ${this.env.SERVER_MULTIPART_FILE_LIMIT} bytes`,\n });\n }\n\n // Validate total size\n totalSize += blob.size;\n if (totalSize > this.env.SERVER_MULTIPART_LIMIT) {\n this.log.error(\n `Total multipart size exceeds limit: ${totalSize} > ${this.env.SERVER_MULTIPART_LIMIT}`,\n );\n throw new HttpError({\n status: 413,\n message: `Total request size exceeds limit. Maximum allowed: ${this.env.SERVER_MULTIPART_LIMIT} bytes`,\n });\n }\n\n const hybridFile = await this.createHybridFile(blob, key);\n body[key] = hybridFile;\n tempFiles.push(hybridFile);\n }\n } else {\n const fieldValue = formData.get(key);\n if (fieldValue !== null) {\n // FormData values are either string or File/Blob\n const stringValue =\n typeof fieldValue === \"string\" ? fieldValue : \"\";\n body[key] = this.alepha.codec.decode(value, stringValue);\n }\n }\n }\n }\n }\n\n return {\n body,\n cleanup: async () => {\n for (const file of tempFiles) {\n await file.cleanup();\n }\n },\n };\n } catch (error) {\n // Clean up any temp files that were created before the error\n await cleanupOnError();\n throw error;\n }\n }\n\n /**\n * This is a legacy code, previously we used \"busboy\" to parse multipart in Node.js environment.\n * Now we rely on Web Request's formData() method, which is supported in modern Node.js versions.\n * However, we still need to create temporary files for uploaded files to provide a consistent File-like interface.\n *\n * TODO: In future, we might want to refactor this to avoid using temporary files if not necessary?\n */\n protected async createHybridFile(\n file: Blob,\n fieldName: string,\n ): Promise<HybridFile> {\n const tmpPath = `${os.tmpdir()}/${randomUUID()}`;\n\n // Get file data\n const arrayBuffer = await file.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n // Write to temp file\n await writeFile(tmpPath, buffer);\n\n // Get file name - check if it has name property (File type)\n const fileName = (file as any).name || `${fieldName}_${Date.now()}`;\n\n const hybridFile: HybridFile = {\n _state: {\n cleanup: false,\n size: file.size,\n tmpPath,\n },\n name: fileName,\n type: file.type || \"application/octet-stream\",\n lastModified: (file as any).lastModified || Date.now(),\n filepath: tmpPath,\n get size() {\n return this._state.size;\n },\n stream() {\n return createReadStream(tmpPath);\n },\n async arrayBuffer() {\n const content = await readFile(tmpPath);\n return content.buffer.slice(\n content.byteOffset,\n content.byteOffset + content.byteLength,\n ) as ArrayBuffer;\n },\n text: async () => {\n return await readFile(tmpPath, \"utf-8\");\n },\n async cleanup() {\n if (this._state.cleanup) {\n return;\n }\n\n await unlink(tmpPath); // clean up the temp file\n this._state.cleanup = true;\n },\n };\n\n return hybridFile;\n }\n}\n\ninterface HybridFile extends FileLike {\n cleanup(): Promise<void>;\n _state: {\n cleanup: boolean;\n size: number;\n tmpPath: string;\n };\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerMultipartProvider } from \"./providers/ServerMultipartProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerMultipartProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * This module provides support for handling multipart/form-data requests.\n * It allows to parse body data containing t.file().\n *\n * @see {@link ServerMultipartProvider}\n * @module alepha.server.multipart\n */\nexport const AlephaServerMultipart = $module({\n name: \"alepha.server.multipart\",\n services: [AlephaServer, ServerMultipartProvider],\n});\n"],"mappings":"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"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import * as alepha1 from "alepha";
|
|
2
|
-
import { Alepha, FileLike } from "alepha";
|
|
3
|
-
import * as alepha_logger0 from "alepha/logger";
|
|
4
|
-
import { ServerRoute } from "alepha/server";
|
|
5
|
-
|
|
6
|
-
//#region src/server-multipart/providers/ServerMultipartProvider.d.ts
|
|
7
|
-
declare class ServerMultipartProvider {
|
|
8
|
-
protected readonly alepha: Alepha;
|
|
9
|
-
protected readonly env: {
|
|
10
|
-
SERVER_MULTIPART_LIMIT: number;
|
|
11
|
-
SERVER_MULTIPART_FILE_LIMIT: number;
|
|
12
|
-
SERVER_MULTIPART_FILE_COUNT: number;
|
|
13
|
-
};
|
|
14
|
-
protected readonly log: alepha_logger0.Logger;
|
|
15
|
-
readonly onRequest: alepha1.HookDescriptor<"server:onRequest">;
|
|
16
|
-
readonly onResponse: alepha1.HookDescriptor<"server:onResponse">;
|
|
17
|
-
handleMultipartBodyFromWeb(route: ServerRoute, request: Request): Promise<{
|
|
18
|
-
body: Record<string, unknown>;
|
|
19
|
-
cleanup: () => Promise<void>;
|
|
20
|
-
}>;
|
|
21
|
-
/**
|
|
22
|
-
* This is a legacy code, previously we used "busboy" to parse multipart in Node.js environment.
|
|
23
|
-
* Now we rely on Web Request's formData() method, which is supported in modern Node.js versions.
|
|
24
|
-
* However, we still need to create temporary files for uploaded files to provide a consistent File-like interface.
|
|
25
|
-
*
|
|
26
|
-
* TODO: In future, we might want to refactor this to avoid using temporary files if not necessary?
|
|
27
|
-
*/
|
|
28
|
-
protected createHybridFile(file: Blob, fieldName: string): Promise<HybridFile>;
|
|
29
|
-
}
|
|
30
|
-
interface HybridFile extends FileLike {
|
|
31
|
-
cleanup(): Promise<void>;
|
|
32
|
-
_state: {
|
|
33
|
-
cleanup: boolean;
|
|
34
|
-
size: number;
|
|
35
|
-
tmpPath: string;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region src/server-multipart/index.d.ts
|
|
40
|
-
/**
|
|
41
|
-
* This module provides support for handling multipart/form-data requests.
|
|
42
|
-
* It allows to parse body data containing t.file().
|
|
43
|
-
*
|
|
44
|
-
* @see {@link ServerMultipartProvider}
|
|
45
|
-
* @module alepha.server.multipart
|
|
46
|
-
*/
|
|
47
|
-
declare const AlephaServerMultipart: alepha1.Service<alepha1.Module>;
|
|
48
|
-
//#endregion
|
|
49
|
-
export { AlephaServerMultipart, ServerMultipartProvider };
|
|
50
|
-
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
let alepha = require("alepha");
|
|
2
|
-
let alepha_server = require("alepha/server");
|
|
3
|
-
let node_stream_web = require("node:stream/web");
|
|
4
|
-
let alepha_logger = require("alepha/logger");
|
|
5
|
-
|
|
6
|
-
//#region src/server-proxy/descriptors/$proxy.ts
|
|
7
|
-
/**
|
|
8
|
-
* Creates a proxy descriptor to forward requests to another server.
|
|
9
|
-
*
|
|
10
|
-
* This descriptor enables you to create reverse proxy functionality, allowing your Alepha server
|
|
11
|
-
* to forward requests to other services while maintaining a unified API surface. It's particularly
|
|
12
|
-
* useful for microservice architectures, API gateways, or when you need to aggregate multiple
|
|
13
|
-
* services behind a single endpoint.
|
|
14
|
-
*
|
|
15
|
-
* **Key Features**
|
|
16
|
-
*
|
|
17
|
-
* - **Path-based routing**: Match specific paths or patterns to proxy
|
|
18
|
-
* - **Dynamic targets**: Support both static and dynamic target resolution
|
|
19
|
-
* - **Request/Response hooks**: Modify requests before forwarding and responses after receiving
|
|
20
|
-
* - **URL rewriting**: Transform URLs before forwarding to the target
|
|
21
|
-
* - **Conditional proxying**: Enable/disable proxies based on environment or conditions
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* **Basic proxy setup:**
|
|
25
|
-
* ```ts
|
|
26
|
-
* import { $proxy } from "alepha/server/proxy";
|
|
27
|
-
*
|
|
28
|
-
* class ApiGateway {
|
|
29
|
-
* // Forward all /api/* requests to external service
|
|
30
|
-
* api = $proxy({
|
|
31
|
-
* path: "/api/*",
|
|
32
|
-
* target: "https://api.example.com"
|
|
33
|
-
* });
|
|
34
|
-
* }
|
|
35
|
-
* ```
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* **Dynamic target with environment-based routing:**
|
|
39
|
-
* ```ts
|
|
40
|
-
* class ApiGateway {
|
|
41
|
-
* // Route to different environments based on configuration
|
|
42
|
-
* api = $proxy({
|
|
43
|
-
* path: "/api/*",
|
|
44
|
-
* target: () => process.env.NODE_ENV === "production"
|
|
45
|
-
* ? "https://api.prod.example.com"
|
|
46
|
-
* : "https://api.dev.example.com"
|
|
47
|
-
* });
|
|
48
|
-
* }
|
|
49
|
-
* ```
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* **Advanced proxy with request/response modification:**
|
|
53
|
-
* ```ts
|
|
54
|
-
* class SecureProxy {
|
|
55
|
-
* secure = $proxy({
|
|
56
|
-
* path: "/secure/*",
|
|
57
|
-
* target: "https://secure-api.example.com",
|
|
58
|
-
* beforeRequest: async (request, proxyRequest) => {
|
|
59
|
-
* // Add authentication headers
|
|
60
|
-
* proxyRequest.headers = {
|
|
61
|
-
* ...proxyRequest.headers,
|
|
62
|
-
* 'Authorization': `Bearer ${await getServiceToken()}`,
|
|
63
|
-
* 'X-Forwarded-For': request.headers['x-forwarded-for'] || request.ip
|
|
64
|
-
* };
|
|
65
|
-
* },
|
|
66
|
-
* afterResponse: async (request, proxyResponse) => {
|
|
67
|
-
* // Log response for monitoring
|
|
68
|
-
* console.log(`Proxied ${request.url} -> ${proxyResponse.status}`);
|
|
69
|
-
* },
|
|
70
|
-
* rewrite: (url) => {
|
|
71
|
-
* // Remove /secure prefix when forwarding
|
|
72
|
-
* url.pathname = url.pathname.replace('/secure', '');
|
|
73
|
-
* }
|
|
74
|
-
* });
|
|
75
|
-
* }
|
|
76
|
-
* ```
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* **Conditional proxy based on feature flags:**
|
|
80
|
-
* ```ts
|
|
81
|
-
* class FeatureProxy {
|
|
82
|
-
* newApi = $proxy({
|
|
83
|
-
* path: "/v2/*",
|
|
84
|
-
* target: "https://new-api.example.com",
|
|
85
|
-
* disabled: !process.env.ENABLE_V2_API // Disable if feature flag is off
|
|
86
|
-
* });
|
|
87
|
-
* }
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
|
-
const $proxy = (options) => {
|
|
91
|
-
return (0, alepha.createDescriptor)(ProxyDescriptor, options);
|
|
92
|
-
};
|
|
93
|
-
var ProxyDescriptor = class extends alepha.Descriptor {};
|
|
94
|
-
$proxy[alepha.KIND] = ProxyDescriptor;
|
|
95
|
-
|
|
96
|
-
//#endregion
|
|
97
|
-
//#region src/server-proxy/providers/ServerProxyProvider.ts
|
|
98
|
-
var ServerProxyProvider = class {
|
|
99
|
-
log = (0, alepha_logger.$logger)();
|
|
100
|
-
routerProvider = (0, alepha.$inject)(alepha_server.ServerRouterProvider);
|
|
101
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
102
|
-
configure = (0, alepha.$hook)({
|
|
103
|
-
on: "configure",
|
|
104
|
-
handler: () => {
|
|
105
|
-
for (const proxy of this.alepha.descriptors($proxy)) this.createProxy(proxy.options);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
createProxy(options) {
|
|
109
|
-
if (options.disabled) return;
|
|
110
|
-
const path = options.path;
|
|
111
|
-
const target = typeof options.target === "function" ? options.target() : options.target;
|
|
112
|
-
if (!path.endsWith("/*")) throw new alepha.AlephaError("Proxy path should end with '/*'");
|
|
113
|
-
const handler = this.createProxyHandler(target, options);
|
|
114
|
-
for (const method of alepha_server.routeMethods) this.routerProvider.createRoute({
|
|
115
|
-
method,
|
|
116
|
-
path,
|
|
117
|
-
handler
|
|
118
|
-
});
|
|
119
|
-
this.log.info("Proxying", {
|
|
120
|
-
path,
|
|
121
|
-
target
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
createProxyHandler(target, options) {
|
|
125
|
-
return async (request) => {
|
|
126
|
-
const url = new URL(target + request.url.pathname);
|
|
127
|
-
if (request.url.search) url.search = request.url.search;
|
|
128
|
-
options.rewrite?.(url);
|
|
129
|
-
const requestInit = {
|
|
130
|
-
url: url.toString(),
|
|
131
|
-
method: request.method,
|
|
132
|
-
headers: {
|
|
133
|
-
...request.headers,
|
|
134
|
-
"accept-encoding": "identity"
|
|
135
|
-
},
|
|
136
|
-
body: this.getRawRequestBody(request)
|
|
137
|
-
};
|
|
138
|
-
if (requestInit.body) requestInit.duplex = "half";
|
|
139
|
-
if (options.beforeRequest) await options.beforeRequest(request, requestInit);
|
|
140
|
-
this.log.debug("Proxying request", {
|
|
141
|
-
url: url.toString(),
|
|
142
|
-
method: request.method,
|
|
143
|
-
headers: request.headers
|
|
144
|
-
});
|
|
145
|
-
const response = await fetch(requestInit.url, requestInit);
|
|
146
|
-
request.reply.status = response.status;
|
|
147
|
-
request.reply.headers = Object.fromEntries(response.headers.entries());
|
|
148
|
-
request.reply.body = response.body;
|
|
149
|
-
this.log.debug("Received response", {
|
|
150
|
-
status: request.reply.status,
|
|
151
|
-
headers: request.reply.headers
|
|
152
|
-
});
|
|
153
|
-
if (options.afterResponse) await options.afterResponse(request, response);
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
getRawRequestBody(req) {
|
|
157
|
-
const { method } = req;
|
|
158
|
-
if (method === "GET" || method === "HEAD" || method === "OPTIONS") return;
|
|
159
|
-
if (req.raw?.web?.req) return req.raw.web.req.body;
|
|
160
|
-
if (req.raw?.node?.req) {
|
|
161
|
-
const nodeReq = req.raw.node.req;
|
|
162
|
-
return node_stream_web.ReadableStream.from(nodeReq);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
//#endregion
|
|
168
|
-
//#region src/server-proxy/index.ts
|
|
169
|
-
/**
|
|
170
|
-
* Plugin for Alepha that provides a proxy server functionality.
|
|
171
|
-
*
|
|
172
|
-
* @see {@link $proxy}
|
|
173
|
-
* @module alepha.server.proxy
|
|
174
|
-
*/
|
|
175
|
-
const AlephaServerProxy = (0, alepha.$module)({
|
|
176
|
-
name: "alepha.server.proxy",
|
|
177
|
-
descriptors: [$proxy],
|
|
178
|
-
services: [alepha_server.AlephaServer, ServerProxyProvider]
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
//#endregion
|
|
182
|
-
exports.$proxy = $proxy;
|
|
183
|
-
exports.AlephaServerProxy = AlephaServerProxy;
|
|
184
|
-
exports.ProxyDescriptor = ProxyDescriptor;
|
|
185
|
-
exports.ServerProxyProvider = ServerProxyProvider;
|
|
186
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["Descriptor","KIND","ServerRouterProvider","Alepha","AlephaError","routeMethods","WebStream","AlephaServer"],"sources":["../../src/server-proxy/descriptors/$proxy.ts","../../src/server-proxy/providers/ServerProxyProvider.ts","../../src/server-proxy/index.ts"],"sourcesContent":["import { type Async, createDescriptor, Descriptor, KIND } from \"alepha\";\nimport type { ServerRequest } from \"alepha/server\";\n\n/**\n * Creates a proxy descriptor to forward requests to another server.\n *\n * This descriptor enables you to create reverse proxy functionality, allowing your Alepha server\n * to forward requests to other services while maintaining a unified API surface. It's particularly\n * useful for microservice architectures, API gateways, or when you need to aggregate multiple\n * services behind a single endpoint.\n *\n * **Key Features**\n *\n * - **Path-based routing**: Match specific paths or patterns to proxy\n * - **Dynamic targets**: Support both static and dynamic target resolution\n * - **Request/Response hooks**: Modify requests before forwarding and responses after receiving\n * - **URL rewriting**: Transform URLs before forwarding to the target\n * - **Conditional proxying**: Enable/disable proxies based on environment or conditions\n *\n * @example\n * **Basic proxy setup:**\n * ```ts\n * import { $proxy } from \"alepha/server/proxy\";\n *\n * class ApiGateway {\n * // Forward all /api/* requests to external service\n * api = $proxy({\n * path: \"/api/*\",\n * target: \"https://api.example.com\"\n * });\n * }\n * ```\n *\n * @example\n * **Dynamic target with environment-based routing:**\n * ```ts\n * class ApiGateway {\n * // Route to different environments based on configuration\n * api = $proxy({\n * path: \"/api/*\",\n * target: () => process.env.NODE_ENV === \"production\"\n * ? \"https://api.prod.example.com\"\n * : \"https://api.dev.example.com\"\n * });\n * }\n * ```\n *\n * @example\n * **Advanced proxy with request/response modification:**\n * ```ts\n * class SecureProxy {\n * secure = $proxy({\n * path: \"/secure/*\",\n * target: \"https://secure-api.example.com\",\n * beforeRequest: async (request, proxyRequest) => {\n * // Add authentication headers\n * proxyRequest.headers = {\n * ...proxyRequest.headers,\n * 'Authorization': `Bearer ${await getServiceToken()}`,\n * 'X-Forwarded-For': request.headers['x-forwarded-for'] || request.ip\n * };\n * },\n * afterResponse: async (request, proxyResponse) => {\n * // Log response for monitoring\n * console.log(`Proxied ${request.url} -> ${proxyResponse.status}`);\n * },\n * rewrite: (url) => {\n * // Remove /secure prefix when forwarding\n * url.pathname = url.pathname.replace('/secure', '');\n * }\n * });\n * }\n * ```\n *\n * @example\n * **Conditional proxy based on feature flags:**\n * ```ts\n * class FeatureProxy {\n * newApi = $proxy({\n * path: \"/v2/*\",\n * target: \"https://new-api.example.com\",\n * disabled: !process.env.ENABLE_V2_API // Disable if feature flag is off\n * });\n * }\n * ```\n */\nexport const $proxy = (options: ProxyDescriptorOptions): ProxyDescriptor => {\n return createDescriptor(ProxyDescriptor, options);\n};\n\nexport type ProxyDescriptorOptions = {\n /**\n * Path pattern to match for proxying requests.\n *\n * Supports wildcards and path parameters:\n * - `/api/*` - Matches all paths starting with `/api/`\n * - `/api/v1/*` - Matches all paths starting with `/api/v1/`\n * - `/users/:id` - Matches `/users/123`, `/users/abc`, etc.\n *\n * @example \"/api/*\"\n * @example \"/secure/admin/*\"\n * @example \"/users/:id/posts\"\n */\n path: string;\n\n /**\n * Target URL to which matching requests should be forwarded.\n *\n * Can be either:\n * - **Static string**: A fixed URL like `\"https://api.example.com\"`\n * - **Dynamic function**: A function that returns the URL, enabling runtime target resolution\n *\n * The target URL will be combined with the remaining path from the original request.\n *\n * @example \"https://api.example.com\"\n * @example () => process.env.API_URL || \"http://localhost:3001\"\n */\n target: string | (() => string);\n\n /**\n * Whether this proxy is disabled.\n *\n * When `true`, requests matching the path will not be proxied and will be handled\n * by other routes or return 404. Useful for feature toggles or conditional proxying.\n *\n * @default false\n * @example !process.env.ENABLE_PROXY\n */\n disabled?: boolean;\n\n /**\n * Hook called before forwarding the request to the target server.\n *\n * Use this to:\n * - Add authentication headers\n * - Modify request headers or body\n * - Add request tracking/logging\n * - Transform the request before forwarding\n *\n * @param request - The original incoming server request\n * @param proxyRequest - The request that will be sent to the target (modifiable)\n *\n * @example\n * ```ts\n * beforeRequest: async (request, proxyRequest) => {\n * proxyRequest.headers = {\n * ...proxyRequest.headers,\n * 'Authorization': `Bearer ${await getToken()}`,\n * 'X-Request-ID': generateRequestId()\n * };\n * }\n * ```\n */\n beforeRequest?: (\n request: ServerRequest,\n proxyRequest: RequestInit,\n ) => Async<void>;\n\n /**\n * Hook called after receiving the response from the target server.\n *\n * Use this to:\n * - Log response details for monitoring\n * - Add custom headers to the response\n * - Transform response data\n * - Handle error responses\n *\n * @param request - The original incoming server request\n * @param proxyResponse - The response received from the target server\n *\n * @example\n * ```ts\n * afterResponse: async (request, proxyResponse) => {\n * console.log(`Proxy ${request.method} ${request.url} -> ${proxyResponse.status}`);\n *\n * if (!proxyResponse.ok) {\n * await logError(`Proxy error: ${proxyResponse.status}`, { request, response: proxyResponse });\n * }\n * }\n * ```\n */\n afterResponse?: (\n request: ServerRequest,\n proxyResponse: Response,\n ) => Async<void>;\n\n /**\n * Function to rewrite the URL before sending to the target server.\n *\n * Use this to:\n * - Remove or add path prefixes\n * - Transform path parameters\n * - Modify query parameters\n * - Change the URL structure entirely\n *\n * The function receives a mutable URL object and should modify it in-place.\n *\n * @param url - The URL object to modify (mutable)\n *\n * @example\n * ```ts\n * // Remove /api prefix when forwarding\n * rewrite: (url) => {\n * url.pathname = url.pathname.replace('/api', '');\n * }\n * ```\n *\n * @example\n * ```ts\n * // Add version prefix\n * rewrite: (url) => {\n * url.pathname = `/v2${url.pathname}`;\n * }\n * ```\n */\n rewrite?: (url: URL) => void;\n\n // TODO: Add retry functionality\n // retry?: RetryOptions;\n};\n\nexport class ProxyDescriptor extends Descriptor<ProxyDescriptorOptions> {}\n\n$proxy[KIND] = ProxyDescriptor;\n","import { ReadableStream as WebStream } from \"node:stream/web\";\nimport { $hook, $inject, Alepha, AlephaError } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport {\n routeMethods,\n type ServerHandler,\n type ServerRequest,\n ServerRouterProvider,\n} from \"alepha/server\";\nimport { $proxy, type ProxyDescriptorOptions } from \"../descriptors/$proxy.ts\";\n\nexport class ServerProxyProvider {\n protected readonly log = $logger();\n protected readonly routerProvider = $inject(ServerRouterProvider);\n protected readonly alepha = $inject(Alepha);\n\n protected readonly configure = $hook({\n on: \"configure\",\n handler: () => {\n for (const proxy of this.alepha.descriptors($proxy)) {\n this.createProxy(proxy.options);\n }\n },\n });\n\n public createProxy(options: ProxyDescriptorOptions): void {\n if (options.disabled) {\n return;\n }\n\n const path = options.path;\n const target =\n typeof options.target === \"function\" ? options.target() : options.target;\n\n if (!path.endsWith(\"/*\")) {\n throw new AlephaError(\"Proxy path should end with '/*'\");\n }\n\n // Extract base path without /*\n const handler = this.createProxyHandler(target, options);\n\n for (const method of routeMethods) {\n this.routerProvider.createRoute({\n method,\n path,\n handler,\n });\n }\n\n this.log.info(\"Proxying\", { path, target });\n }\n\n public createProxyHandler(\n target: string,\n options: Omit<ProxyDescriptorOptions, \"path\">,\n ): ServerHandler {\n return async (request) => {\n const url = new URL(target + request.url.pathname);\n if (request.url.search) {\n url.search = request.url.search;\n }\n\n options.rewrite?.(url);\n\n const requestInit = {\n url: url.toString(),\n method: request.method,\n headers: {\n ...request.headers,\n \"accept-encoding\": \"identity\", // ignore compression\n },\n body: this.getRawRequestBody(request),\n };\n\n if (requestInit.body) {\n (requestInit as any).duplex = \"half\";\n }\n\n if (options.beforeRequest) {\n await options.beforeRequest(request, requestInit);\n }\n\n this.log.debug(\"Proxying request\", {\n url: url.toString(),\n method: request.method,\n headers: request.headers,\n });\n\n const response = await fetch(requestInit.url, requestInit);\n\n request.reply.status = response.status;\n request.reply.headers = Object.fromEntries(response.headers.entries());\n request.reply.body = response.body;\n\n this.log.debug(\"Received response\", {\n status: request.reply.status,\n headers: request.reply.headers,\n });\n\n if (options.afterResponse) {\n await options.afterResponse(request, response);\n }\n };\n }\n\n private getRawRequestBody(req: ServerRequest): ReadableStream | undefined {\n const { method } = req;\n\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n return;\n }\n\n if (req.raw?.web?.req) {\n return req.raw.web.req.body as ReadableStream;\n }\n\n if (req.raw?.node?.req) {\n const nodeReq = req.raw.node.req;\n return WebStream.from(nodeReq) as ReadableStream;\n }\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $proxy } from \"./descriptors/$proxy.ts\";\nimport { ServerProxyProvider } from \"./providers/ServerProxyProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./descriptors/$proxy.ts\";\nexport * from \"./providers/ServerProxyProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha that provides a proxy server functionality.\n *\n * @see {@link $proxy}\n * @module alepha.server.proxy\n */\nexport const AlephaServerProxy = $module({\n name: \"alepha.server.proxy\",\n descriptors: [$proxy],\n services: [AlephaServer, ServerProxyProvider],\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,MAAa,UAAU,YAAqD;AAC1E,qCAAwB,iBAAiB,QAAQ;;AAsInD,IAAa,kBAAb,cAAqCA,kBAAmC;AAExE,OAAOC,eAAQ;;;;ACpNf,IAAa,sBAAb,MAAiC;CAC/B,AAAmB,kCAAe;CAClC,AAAmB,qCAAyBC,mCAAqB;CACjE,AAAmB,6BAAiBC,cAAO;CAE3C,AAAmB,8BAAkB;EACnC,IAAI;EACJ,eAAe;AACb,QAAK,MAAM,SAAS,KAAK,OAAO,YAAY,OAAO,CACjD,MAAK,YAAY,MAAM,QAAQ;;EAGpC,CAAC;CAEF,AAAO,YAAY,SAAuC;AACxD,MAAI,QAAQ,SACV;EAGF,MAAM,OAAO,QAAQ;EACrB,MAAM,SACJ,OAAO,QAAQ,WAAW,aAAa,QAAQ,QAAQ,GAAG,QAAQ;AAEpE,MAAI,CAAC,KAAK,SAAS,KAAK,CACtB,OAAM,IAAIC,mBAAY,kCAAkC;EAI1D,MAAM,UAAU,KAAK,mBAAmB,QAAQ,QAAQ;AAExD,OAAK,MAAM,UAAUC,2BACnB,MAAK,eAAe,YAAY;GAC9B;GACA;GACA;GACD,CAAC;AAGJ,OAAK,IAAI,KAAK,YAAY;GAAE;GAAM;GAAQ,CAAC;;CAG7C,AAAO,mBACL,QACA,SACe;AACf,SAAO,OAAO,YAAY;GACxB,MAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,IAAI,SAAS;AAClD,OAAI,QAAQ,IAAI,OACd,KAAI,SAAS,QAAQ,IAAI;AAG3B,WAAQ,UAAU,IAAI;GAEtB,MAAM,cAAc;IAClB,KAAK,IAAI,UAAU;IACnB,QAAQ,QAAQ;IAChB,SAAS;KACP,GAAG,QAAQ;KACX,mBAAmB;KACpB;IACD,MAAM,KAAK,kBAAkB,QAAQ;IACtC;AAED,OAAI,YAAY,KACd,CAAC,YAAoB,SAAS;AAGhC,OAAI,QAAQ,cACV,OAAM,QAAQ,cAAc,SAAS,YAAY;AAGnD,QAAK,IAAI,MAAM,oBAAoB;IACjC,KAAK,IAAI,UAAU;IACnB,QAAQ,QAAQ;IAChB,SAAS,QAAQ;IAClB,CAAC;GAEF,MAAM,WAAW,MAAM,MAAM,YAAY,KAAK,YAAY;AAE1D,WAAQ,MAAM,SAAS,SAAS;AAChC,WAAQ,MAAM,UAAU,OAAO,YAAY,SAAS,QAAQ,SAAS,CAAC;AACtE,WAAQ,MAAM,OAAO,SAAS;AAE9B,QAAK,IAAI,MAAM,qBAAqB;IAClC,QAAQ,QAAQ,MAAM;IACtB,SAAS,QAAQ,MAAM;IACxB,CAAC;AAEF,OAAI,QAAQ,cACV,OAAM,QAAQ,cAAc,SAAS,SAAS;;;CAKpD,AAAQ,kBAAkB,KAAgD;EACxE,MAAM,EAAE,WAAW;AAEnB,MAAI,WAAW,SAAS,WAAW,UAAU,WAAW,UACtD;AAGF,MAAI,IAAI,KAAK,KAAK,IAChB,QAAO,IAAI,IAAI,IAAI,IAAI;AAGzB,MAAI,IAAI,KAAK,MAAM,KAAK;GACtB,MAAM,UAAU,IAAI,IAAI,KAAK;AAC7B,UAAOC,+BAAU,KAAK,QAAQ;;;;;;;;;;;;;ACpGpC,MAAa,wCAA4B;CACvC,MAAM;CACN,aAAa,CAAC,OAAO;CACrB,UAAU,CAACC,4BAAc,oBAAoB;CAC9C,CAAC"}
|