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.
Files changed (195) hide show
  1. package/dist/api-jobs/index.d.ts +26 -26
  2. package/dist/api-users/index.d.ts +1 -1
  3. package/dist/cli/{dist-Sz2EXvQX.cjs → dist-Dl9Vl7Ur.js} +17 -13
  4. package/dist/cli/{dist-BBPjuQ56.js.map → dist-Dl9Vl7Ur.js.map} +1 -1
  5. package/dist/cli/index.d.ts +3 -11
  6. package/dist/cli/index.js +106 -74
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/email/index.js +71 -73
  9. package/dist/email/index.js.map +1 -1
  10. package/dist/orm/index.d.ts +1 -1
  11. package/dist/orm/index.js.map +1 -1
  12. package/dist/queue/index.d.ts +4 -4
  13. package/dist/retry/index.d.ts +1 -1
  14. package/dist/retry/index.js +2 -2
  15. package/dist/retry/index.js.map +1 -1
  16. package/dist/scheduler/index.d.ts +6 -6
  17. package/dist/security/index.d.ts +28 -28
  18. package/dist/server/index.js +1 -1
  19. package/dist/server/index.js.map +1 -1
  20. package/dist/server-health/index.d.ts +17 -17
  21. package/dist/server-metrics/index.js +170 -174
  22. package/dist/server-metrics/index.js.map +1 -1
  23. package/dist/server-security/index.d.ts +9 -9
  24. package/dist/vite/index.js +4 -5
  25. package/dist/vite/index.js.map +1 -1
  26. package/dist/websocket/index.d.ts +7 -7
  27. package/package.json +52 -103
  28. package/src/cli/apps/AlephaPackageBuilderCli.ts +7 -2
  29. package/src/cli/assets/appRouterTs.ts +9 -0
  30. package/src/cli/assets/indexHtml.ts +2 -1
  31. package/src/cli/assets/mainBrowserTs.ts +10 -0
  32. package/src/cli/commands/CoreCommands.ts +6 -5
  33. package/src/cli/commands/DrizzleCommands.ts +65 -57
  34. package/src/cli/commands/VerifyCommands.ts +1 -1
  35. package/src/cli/services/ProjectUtils.ts +44 -38
  36. package/src/orm/providers/DrizzleKitProvider.ts +1 -1
  37. package/src/retry/descriptors/$retry.ts +5 -3
  38. package/src/server/providers/NodeHttpServerProvider.ts +1 -1
  39. package/src/vite/helpers/boot.ts +3 -3
  40. package/dist/api-files/index.cjs +0 -1293
  41. package/dist/api-files/index.cjs.map +0 -1
  42. package/dist/api-files/index.d.cts +0 -829
  43. package/dist/api-jobs/index.cjs +0 -274
  44. package/dist/api-jobs/index.cjs.map +0 -1
  45. package/dist/api-jobs/index.d.cts +0 -654
  46. package/dist/api-notifications/index.cjs +0 -380
  47. package/dist/api-notifications/index.cjs.map +0 -1
  48. package/dist/api-notifications/index.d.cts +0 -289
  49. package/dist/api-parameters/index.cjs +0 -66
  50. package/dist/api-parameters/index.cjs.map +0 -1
  51. package/dist/api-parameters/index.d.cts +0 -84
  52. package/dist/api-users/index.cjs +0 -6009
  53. package/dist/api-users/index.cjs.map +0 -1
  54. package/dist/api-users/index.d.cts +0 -4740
  55. package/dist/api-verifications/index.cjs +0 -407
  56. package/dist/api-verifications/index.cjs.map +0 -1
  57. package/dist/api-verifications/index.d.cts +0 -207
  58. package/dist/batch/index.cjs +0 -408
  59. package/dist/batch/index.cjs.map +0 -1
  60. package/dist/batch/index.d.cts +0 -330
  61. package/dist/bin/index.cjs +0 -17
  62. package/dist/bin/index.cjs.map +0 -1
  63. package/dist/bin/index.d.cts +0 -1
  64. package/dist/bucket/index.cjs +0 -303
  65. package/dist/bucket/index.cjs.map +0 -1
  66. package/dist/bucket/index.d.cts +0 -355
  67. package/dist/cache/index.cjs +0 -241
  68. package/dist/cache/index.cjs.map +0 -1
  69. package/dist/cache/index.d.cts +0 -202
  70. package/dist/cache-redis/index.cjs +0 -84
  71. package/dist/cache-redis/index.cjs.map +0 -1
  72. package/dist/cache-redis/index.d.cts +0 -40
  73. package/dist/cli/chunk-DSlc6foC.cjs +0 -43
  74. package/dist/cli/dist-BBPjuQ56.js +0 -2778
  75. package/dist/cli/dist-Sz2EXvQX.cjs.map +0 -1
  76. package/dist/cli/index.cjs +0 -1241
  77. package/dist/cli/index.cjs.map +0 -1
  78. package/dist/cli/index.d.cts +0 -422
  79. package/dist/command/index.cjs +0 -693
  80. package/dist/command/index.cjs.map +0 -1
  81. package/dist/command/index.d.cts +0 -340
  82. package/dist/core/index.cjs +0 -2264
  83. package/dist/core/index.cjs.map +0 -1
  84. package/dist/core/index.d.cts +0 -1927
  85. package/dist/datetime/index.cjs +0 -318
  86. package/dist/datetime/index.cjs.map +0 -1
  87. package/dist/datetime/index.d.cts +0 -145
  88. package/dist/email/index.cjs +0 -10874
  89. package/dist/email/index.cjs.map +0 -1
  90. package/dist/email/index.d.cts +0 -186
  91. package/dist/fake/index.cjs +0 -34641
  92. package/dist/fake/index.cjs.map +0 -1
  93. package/dist/fake/index.d.cts +0 -74
  94. package/dist/file/index.cjs +0 -1212
  95. package/dist/file/index.cjs.map +0 -1
  96. package/dist/file/index.d.cts +0 -698
  97. package/dist/lock/index.cjs +0 -226
  98. package/dist/lock/index.cjs.map +0 -1
  99. package/dist/lock/index.d.cts +0 -361
  100. package/dist/lock-redis/index.cjs +0 -113
  101. package/dist/lock-redis/index.cjs.map +0 -1
  102. package/dist/lock-redis/index.d.cts +0 -24
  103. package/dist/logger/index.cjs +0 -521
  104. package/dist/logger/index.cjs.map +0 -1
  105. package/dist/logger/index.d.cts +0 -281
  106. package/dist/orm/index.cjs +0 -2986
  107. package/dist/orm/index.cjs.map +0 -1
  108. package/dist/orm/index.d.cts +0 -2213
  109. package/dist/queue/index.cjs +0 -1044
  110. package/dist/queue/index.cjs.map +0 -1
  111. package/dist/queue/index.d.cts +0 -1265
  112. package/dist/queue-redis/index.cjs +0 -873
  113. package/dist/queue-redis/index.cjs.map +0 -1
  114. package/dist/queue-redis/index.d.cts +0 -82
  115. package/dist/redis/index.cjs +0 -153
  116. package/dist/redis/index.cjs.map +0 -1
  117. package/dist/redis/index.d.cts +0 -82
  118. package/dist/retry/index.cjs +0 -146
  119. package/dist/retry/index.cjs.map +0 -1
  120. package/dist/retry/index.d.cts +0 -172
  121. package/dist/router/index.cjs +0 -111
  122. package/dist/router/index.cjs.map +0 -1
  123. package/dist/router/index.d.cts +0 -46
  124. package/dist/scheduler/index.cjs +0 -576
  125. package/dist/scheduler/index.cjs.map +0 -1
  126. package/dist/scheduler/index.d.cts +0 -145
  127. package/dist/security/index.cjs +0 -2402
  128. package/dist/security/index.cjs.map +0 -1
  129. package/dist/security/index.d.cts +0 -598
  130. package/dist/server/index.cjs +0 -1680
  131. package/dist/server/index.cjs.map +0 -1
  132. package/dist/server/index.d.cts +0 -810
  133. package/dist/server-auth/index.cjs +0 -3146
  134. package/dist/server-auth/index.cjs.map +0 -1
  135. package/dist/server-auth/index.d.cts +0 -1164
  136. package/dist/server-cache/index.cjs +0 -252
  137. package/dist/server-cache/index.cjs.map +0 -1
  138. package/dist/server-cache/index.d.cts +0 -164
  139. package/dist/server-compress/index.cjs +0 -141
  140. package/dist/server-compress/index.cjs.map +0 -1
  141. package/dist/server-compress/index.d.cts +0 -38
  142. package/dist/server-cookies/index.cjs +0 -234
  143. package/dist/server-cookies/index.cjs.map +0 -1
  144. package/dist/server-cookies/index.d.cts +0 -144
  145. package/dist/server-cors/index.cjs +0 -201
  146. package/dist/server-cors/index.cjs.map +0 -1
  147. package/dist/server-cors/index.d.cts +0 -140
  148. package/dist/server-health/index.cjs +0 -62
  149. package/dist/server-health/index.cjs.map +0 -1
  150. package/dist/server-health/index.d.cts +0 -58
  151. package/dist/server-helmet/index.cjs +0 -131
  152. package/dist/server-helmet/index.cjs.map +0 -1
  153. package/dist/server-helmet/index.d.cts +0 -97
  154. package/dist/server-links/index.cjs +0 -992
  155. package/dist/server-links/index.cjs.map +0 -1
  156. package/dist/server-links/index.d.cts +0 -513
  157. package/dist/server-metrics/index.cjs +0 -4535
  158. package/dist/server-metrics/index.cjs.map +0 -1
  159. package/dist/server-metrics/index.d.cts +0 -35
  160. package/dist/server-multipart/index.cjs +0 -237
  161. package/dist/server-multipart/index.cjs.map +0 -1
  162. package/dist/server-multipart/index.d.cts +0 -50
  163. package/dist/server-proxy/index.cjs +0 -186
  164. package/dist/server-proxy/index.cjs.map +0 -1
  165. package/dist/server-proxy/index.d.cts +0 -234
  166. package/dist/server-rate-limit/index.cjs +0 -241
  167. package/dist/server-rate-limit/index.cjs.map +0 -1
  168. package/dist/server-rate-limit/index.d.cts +0 -183
  169. package/dist/server-security/index.cjs +0 -316
  170. package/dist/server-security/index.cjs.map +0 -1
  171. package/dist/server-security/index.d.cts +0 -173
  172. package/dist/server-static/index.cjs +0 -170
  173. package/dist/server-static/index.cjs.map +0 -1
  174. package/dist/server-static/index.d.cts +0 -121
  175. package/dist/server-swagger/index.cjs +0 -1021
  176. package/dist/server-swagger/index.cjs.map +0 -1
  177. package/dist/server-swagger/index.d.cts +0 -382
  178. package/dist/sms/index.cjs +0 -221
  179. package/dist/sms/index.cjs.map +0 -1
  180. package/dist/sms/index.d.cts +0 -130
  181. package/dist/thread/index.cjs +0 -350
  182. package/dist/thread/index.cjs.map +0 -1
  183. package/dist/thread/index.d.cts +0 -260
  184. package/dist/topic/index.cjs +0 -282
  185. package/dist/topic/index.cjs.map +0 -1
  186. package/dist/topic/index.d.cts +0 -523
  187. package/dist/topic-redis/index.cjs +0 -71
  188. package/dist/topic-redis/index.cjs.map +0 -1
  189. package/dist/topic-redis/index.d.cts +0 -42
  190. package/dist/vite/index.cjs +0 -1077
  191. package/dist/vite/index.cjs.map +0 -1
  192. package/dist/vite/index.d.cts +0 -542
  193. package/dist/websocket/index.cjs +0 -1117
  194. package/dist/websocket/index.cjs.map +0 -1
  195. 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,MAAM,YAAYA,SAAE,OAAO;CACzB,wBAAwBA,SAAE,QAAQ;EAChC,SAAS;EACT,KAAK;EACL,aAAa;EACd,CAAC;CACF,6BAA6BA,SAAE,QAAQ;EACrC,SAAS;EACT,KAAK;EACL,aAAa;EACd,CAAC;CACF,6BAA6BA,SAAE,QAAQ;EACrC,SAAS;EACT,KAAK;EACL,aAAa;EACd,CAAC;CACH,CAAC;AAEF,IAAa,0BAAb,MAAqC;CACnC,AAAmB,6BAAiBC,cAAO;CAC3C,AAAmB,uBAAW,UAAU;CACxC,AAAmB,kCAAe;CAElC,AAAgB,8BAAkB;EAChC,IAAI;EACJ,SAAS,OAAO,EAAE,OAAO,cAAc;AAErC,OAAI,QAAQ,KACV;AAIF,OAAI,CAAC,MAAM,QAAQ,KACjB;GAGF,IAAIC;AAEJ,OAAI,QAAQ,IAAI,KAAK,IACnB,cAAa,QAAQ,IAAI,IAAI;YACpB,QAAQ,IAAI,MAAM,IAC3B,cAAa,IAAI,QAAQ,QAAQ,KAAK;IACpC,QAAQ,QAAQ;IAChB,SAAS,QAAQ;IACjB,MAAMC,+BAAU,KAAK,QAAQ,IAAI,KAAK,IAAI;IAC1C,QAAQ;IACT,CAAqC;AAGxC,OAAI,CAAC,WACH;GAGF,MAAM,cAAc,QAAQ,QAAQ;GAGpC,MAAM,gBAAgB,QAAQ,QAAQ;AACtC,OAAI,eAAe;IACjB,MAAM,OAAO,OAAO,SAAS,eAAe,GAAG;AAC/C,QAAI,CAAC,OAAO,MAAM,KAAK,IAAI,OAAO,KAAK,IAAI,wBAAwB;AACjE,UAAK,IAAI,MACP,0CAA0C,KAAK,KAAK,KAAK,IAAI,yBAC9D;AACD,WAAM,IAAIC,wBAAU;MAClB,QAAQ;MACR,SAAS,sDAAsD,KAAK,IAAI,uBAAuB;MAChG,CAAC;;;AAIN,OAAI,CAAC,aAAa,WAAW,sBAAsB,EAAE;AACnD,QAAI,gCAAa,MAAM,CACrB;AAIF,UAAM,IAAIA,wBAAU;KAClB,QAAQ;KACR,SAAS,yBAAyB,YAAY;KAC/C,CAAC;;GAGJ,MAAM,EAAE,MAAM,YAAY,MAAM,KAAK,2BACnC,OACA,WACD;AAED,WAAQ,OAAO;AACf,WAAQ,SAAS,YAAY,EAAE,SAAS;;EAE3C,CAAC;CAEF,AAAgB,+BAAmB;EACjC,IAAI;EACJ,SAAS,OAAO,EAAE,cAAc;GAC9B,MAAM,UAAU,QAAQ,SAAS,WAAW;AAC5C,OAAI,OAAO,YAAY,WACrB,OAAM,SAAS;;EAGpB,CAAC;CAEF,MAAa,2BACX,OACA,SAIC;EACD,IAAIC;AAEJ,MAAI;AAEF,cAAW,MAAM,QAAQ,UAAU;WAC5B,OAAO;AACd,SAAM,IAAID,wBACR;IACE,QAAQ;IACR,SAAS;IACV,EACD,MACD;;EAGH,MAAME,OAA4B,EAAE;EACpC,MAAMC,YAA0B,EAAE;EAGlC,MAAM,iBAAiB,YAAY;AACjC,QAAK,MAAM,QAAQ,UACjB,KAAI;AACF,UAAM,KAAK,SAAS;WACd;;AAMZ,MAAI;GACF,IAAI,YAAY;GAChB,IAAI,YAAY;AAEhB,OAAI,MAAM,QAAQ,QAAQP,SAAE,OAAO,SAAS,MAAM,OAAO,KAAK,EAC5D;SAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAChC,MAAM,OAAO,KAAK,WACnB,CACC,KAAIA,SAAE,OAAO,SAAS,MAAM,CAC1B,4BAAe,MAAM,EAAE;KACrB,MAAM,OAAO,SAAS,IAAI,IAAI;AAE9B,SAAI,QAAQ,OAAO,SAAS,YAAY,iBAAiB,MAAM;MAC7D,MAAM,OAAO;AAGb;AACA,UAAI,YAAY,KAAK,IAAI,6BAA6B;AACpD,YAAK,IAAI,MACP,wCAAwC,UAAU,KAAK,KAAK,IAAI,8BACjE;AACD,aAAM,IAAII,wBAAU;QAClB,QAAQ;QACR,SAAS,oCAAoC,KAAK,IAAI;QACvD,CAAC;;AAIJ,UAAI,KAAK,OAAO,KAAK,IAAI,6BAA6B;AACpD,YAAK,IAAI,MACP,SAAS,IAAI,wBAAwB,KAAK,KAAK,KAAK,KAAK,IAAI,8BAC9D;AACD,aAAM,IAAIA,wBAAU;QAClB,QAAQ;QACR,SAAS,SAAS,IAAI,yCAAyC,KAAK,IAAI,4BAA4B;QACrG,CAAC;;AAIJ,mBAAa,KAAK;AAClB,UAAI,YAAY,KAAK,IAAI,wBAAwB;AAC/C,YAAK,IAAI,MACP,uCAAuC,UAAU,KAAK,KAAK,IAAI,yBAChE;AACD,aAAM,IAAIA,wBAAU;QAClB,QAAQ;QACR,SAAS,sDAAsD,KAAK,IAAI,uBAAuB;QAChG,CAAC;;MAGJ,MAAM,aAAa,MAAM,KAAK,iBAAiB,MAAM,IAAI;AACzD,WAAK,OAAO;AACZ,gBAAU,KAAK,WAAW;;WAEvB;KACL,MAAM,aAAa,SAAS,IAAI,IAAI;AACpC,SAAI,eAAe,MAAM;MAEvB,MAAM,cACJ,OAAO,eAAe,WAAW,aAAa;AAChD,WAAK,OAAO,KAAK,OAAO,MAAM,OAAO,OAAO,YAAY;;;;AAOlE,UAAO;IACL;IACA,SAAS,YAAY;AACnB,UAAK,MAAM,QAAQ,UACjB,OAAM,KAAK,SAAS;;IAGzB;WACM,OAAO;AAEd,SAAM,gBAAgB;AACtB,SAAM;;;;;;;;;;CAWV,MAAgB,iBACd,MACA,WACqB;EACrB,MAAM,UAAU,GAAGI,QAAG,QAAQ,CAAC,gCAAe;EAG9C,MAAM,cAAc,MAAM,KAAK,aAAa;AAI5C,wCAAgB,SAHD,OAAO,KAAK,YAAY,CAGP;EAGhC,MAAM,WAAY,KAAa,QAAQ,GAAG,UAAU,GAAG,KAAK,KAAK;AAsCjE,SApC+B;GAC7B,QAAQ;IACN,SAAS;IACT,MAAM,KAAK;IACX;IACD;GACD,MAAM;GACN,MAAM,KAAK,QAAQ;GACnB,cAAe,KAAa,gBAAgB,KAAK,KAAK;GACtD,UAAU;GACV,IAAI,OAAO;AACT,WAAO,KAAK,OAAO;;GAErB,SAAS;AACP,yCAAwB,QAAQ;;GAElC,MAAM,cAAc;IAClB,MAAM,UAAU,qCAAe,QAAQ;AACvC,WAAO,QAAQ,OAAO,MACpB,QAAQ,YACR,QAAQ,aAAa,QAAQ,WAC9B;;GAEH,MAAM,YAAY;AAChB,WAAO,qCAAe,SAAS,QAAQ;;GAEzC,MAAM,UAAU;AACd,QAAI,KAAK,OAAO,QACd;AAGF,uCAAa,QAAQ;AACrB,SAAK,OAAO,UAAU;;GAEzB;;;;;;;;;;;;;ACrRL,MAAa,4CAAgC;CAC3C,MAAM;CACN,UAAU,CAACC,4BAAc,wBAAwB;CAClD,CAAC"}
@@ -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"}