@supacloud/elysia 0.10.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,45 @@
1
1
  // src/index.ts
2
- import { Elysia } from "elysia";
2
+ import { Elysia as Elysia2 } from "elysia";
3
+ import { decodeCommandPreview } from "@supacloud/contracts";
4
+ import {
5
+ provideToken,
6
+ runInRequestContext
7
+ } from "@supacloud/app";
8
+ import { REQUEST_CONTEXT } from "@supacloud/app";
9
+
10
+ // src/command-errors.ts
11
+ function commandErrorCode(error) {
12
+ if (!(error instanceof Error) || error.name !== "CommandError" || !("code" in error))
13
+ return;
14
+ switch (error.code) {
15
+ case "COMMAND_INPUT_INVALID":
16
+ case "COMMAND_REJECTED":
17
+ case "COMMAND_IDEMPOTENCY_CONFLICT":
18
+ case "COMMAND_INPUT_EXPIRED":
19
+ case "COMMAND_UNAVAILABLE":
20
+ case "COMMAND_RECEIPT_INVALID":
21
+ case "COMMAND_OUTCOME_UNKNOWN":
22
+ return error.code;
23
+ default:
24
+ return;
25
+ }
26
+ }
27
+ function commandErrorStatus(code) {
28
+ switch (code) {
29
+ case "COMMAND_INPUT_INVALID":
30
+ return 400;
31
+ case "COMMAND_REJECTED":
32
+ return 403;
33
+ case "COMMAND_IDEMPOTENCY_CONFLICT":
34
+ return 409;
35
+ case "COMMAND_INPUT_EXPIRED":
36
+ return 410;
37
+ case "COMMAND_UNAVAILABLE":
38
+ case "COMMAND_RECEIPT_INVALID":
39
+ case "COMMAND_OUTCOME_UNKNOWN":
40
+ return 503;
41
+ }
42
+ }
3
43
 
4
44
  // src/execution.ts
5
45
  async function observeExecution(observer, event, next) {
@@ -7,7 +47,11 @@ async function observeExecution(observer, event, next) {
7
47
  if (!observer)
8
48
  return;
9
49
  try {
10
- Promise.resolve(observer(Object.freeze({ ...event, phase, durationMs }))).catch(() => {});
50
+ Promise.resolve(observer(Object.freeze({
51
+ ...event,
52
+ phase,
53
+ ...durationMs === undefined ? {} : { durationMs }
54
+ }))).catch(() => {});
11
55
  } catch {}
12
56
  };
13
57
  const started = performance.now();
@@ -27,7 +71,374 @@ function executionRequestId(context) {
27
71
  const value = context.requestId;
28
72
  return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value) ? value : undefined;
29
73
  }
74
+ function executionTrace(context) {
75
+ const requestId = executionRequestId(context);
76
+ return requestId === undefined ? {} : { requestId };
77
+ }
30
78
 
79
+ // src/schema_contract.ts
80
+ import {
81
+ getSchemaValidator,
82
+ StatusMap,
83
+ ElysiaCustomStatusResponse
84
+ } from "elysia";
85
+ function defineRouteContract(schemas) {
86
+ return Object.freeze({ ...schemas });
87
+ }
88
+ var TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
89
+ var RESPONSE_STATUS_MIN = 100;
90
+ var RESPONSE_STATUS_MAX = 599;
91
+ var RESPONSE_FAMILY_PATTERN = /^([1-5])(?:xx|XX)$/;
92
+ var RESPONSE_STATUS_PATTERN = /^[1-5]\d{2}$/;
93
+ function isSchema(value) {
94
+ return value !== null && typeof value === "object" && ((TYPEBOX_KIND in value) || ("~standard" in value));
95
+ }
96
+ function isJsonSchemaObject(value) {
97
+ if (value === null || typeof value !== "object" || Array.isArray(value))
98
+ return false;
99
+ return [
100
+ "$schema",
101
+ "$id",
102
+ "$ref",
103
+ "$defs",
104
+ "definitions",
105
+ "type",
106
+ "properties",
107
+ "patternProperties",
108
+ "required",
109
+ "items",
110
+ "prefixItems",
111
+ "contains",
112
+ "allOf",
113
+ "anyOf",
114
+ "oneOf",
115
+ "not",
116
+ "enum",
117
+ "const",
118
+ "additionalProperties",
119
+ "minLength",
120
+ "maxLength",
121
+ "minimum",
122
+ "maximum",
123
+ "format",
124
+ "pattern"
125
+ ].some((key) => (key in value));
126
+ }
127
+ function invalidResponseSelector(selector) {
128
+ throw new TypeError(`Unsupported response selector "${selector}". Use an HTTP status (100-599), ` + "a status family such as 4XX/5XX, or default.");
129
+ }
130
+ function conflictingResponseSchemas() {
131
+ throw new TypeError("Route contracts cannot declare both response and responses. " + "Remove response and use responses: { 200: Schema }.");
132
+ }
133
+ function duplicateResponseSelector(selector, existing) {
134
+ throw new TypeError(`Duplicate response selectors "${existing}" and "${selector}" differ only by case. ` + `Keep one ${selector.toUpperCase()} selector.`);
135
+ }
136
+ function responseSchemaForStatus(responses, status) {
137
+ const exact = Object.hasOwn(responses, status) ? responses[status] : Object.hasOwn(responses, String(status)) ? responses[String(status)] : undefined;
138
+ if (exact !== undefined || Object.hasOwn(responses, status) || Object.hasOwn(responses, String(status))) {
139
+ return exact;
140
+ }
141
+ const family = `${Math.floor(status / 100)}XX`;
142
+ if (Object.hasOwn(responses, family))
143
+ return responses[family];
144
+ const lowerFamily = family.toLowerCase();
145
+ if (Object.hasOwn(responses, lowerFamily))
146
+ return responses[lowerFamily];
147
+ return Object.hasOwn(responses, "default") ? responses.default : undefined;
148
+ }
149
+ function responseStatusDeclared(responses, status) {
150
+ return responseSchemaForStatus(responses, status) !== undefined || Object.hasOwn(responses, status) || Object.hasOwn(responses, String(status));
151
+ }
152
+ function responseStatusOf(value, configuredStatus) {
153
+ if (value instanceof Response)
154
+ return value.status;
155
+ if (value instanceof ElysiaCustomStatusResponse) {
156
+ const code = value.code;
157
+ if (typeof code === "number" && Number.isInteger(code) && code >= RESPONSE_STATUS_MIN && code <= RESPONSE_STATUS_MAX) {
158
+ return code;
159
+ }
160
+ }
161
+ if (typeof configuredStatus === "number")
162
+ return configuredStatus;
163
+ if (typeof configuredStatus === "string") {
164
+ const mapped = StatusMap[configuredStatus];
165
+ if (typeof mapped === "number")
166
+ return mapped;
167
+ }
168
+ return 200;
169
+ }
170
+ function assertResponseStatusDeclared(responses, status) {
171
+ if (responseStatusDeclared(responses, status))
172
+ return;
173
+ throw new TypeError("Response validation failed");
174
+ }
175
+ function toElysiaResponseMap(responses) {
176
+ const exact = new Map;
177
+ const families = new Map;
178
+ const familySelectors = new Map;
179
+ let defaultSchema;
180
+ let hasDefault = false;
181
+ let requiresExpansion = false;
182
+ for (const [selector, schema] of Object.entries(responses)) {
183
+ if (RESPONSE_STATUS_PATTERN.test(selector)) {
184
+ exact.set(Number(selector), schema);
185
+ continue;
186
+ }
187
+ if (selector === "default") {
188
+ defaultSchema = schema;
189
+ hasDefault = true;
190
+ requiresExpansion = true;
191
+ continue;
192
+ }
193
+ const family = RESPONSE_FAMILY_PATTERN.exec(selector);
194
+ if (family) {
195
+ const canonicalSelector = selector.toLowerCase();
196
+ const existingSelector = familySelectors.get(canonicalSelector);
197
+ if (existingSelector !== undefined)
198
+ duplicateResponseSelector(selector, existingSelector);
199
+ familySelectors.set(canonicalSelector, selector);
200
+ families.set(Number(family[1]), schema);
201
+ requiresExpansion = true;
202
+ continue;
203
+ }
204
+ invalidResponseSelector(selector);
205
+ }
206
+ if (!requiresExpansion)
207
+ return responses;
208
+ const expanded = {};
209
+ if (hasDefault) {
210
+ for (let status = RESPONSE_STATUS_MIN;status <= RESPONSE_STATUS_MAX; status++) {
211
+ expanded[status] = defaultSchema;
212
+ }
213
+ }
214
+ for (const [family, schema] of families) {
215
+ const first = family * 100;
216
+ for (let status = first;status < first + 100; status++)
217
+ expanded[status] = schema;
218
+ }
219
+ for (const [status, schema] of exact)
220
+ expanded[status] = schema;
221
+ return expanded;
222
+ }
223
+ function toElysiaResponseSchema(response) {
224
+ if (typeof response === "string" || isSchema(response) || isJsonSchemaObject(response))
225
+ return response;
226
+ if (response === null || typeof response !== "object" || Array.isArray(response))
227
+ return response;
228
+ return toElysiaResponseMap(response);
229
+ }
230
+ function toElysiaRouteSchema(contract) {
231
+ if (contract.response !== undefined && contract.responses !== undefined) {
232
+ conflictingResponseSchemas();
233
+ }
234
+ const schema = {};
235
+ if (contract.body !== undefined)
236
+ schema.body = contract.body;
237
+ if (contract.params !== undefined)
238
+ schema.params = contract.params;
239
+ if (contract.query !== undefined)
240
+ schema.query = contract.query;
241
+ if (contract.headers !== undefined)
242
+ schema.headers = contract.headers;
243
+ if (contract.cookie !== undefined)
244
+ schema.cookie = contract.cookie;
245
+ if (contract.responses !== undefined)
246
+ schema.response = toElysiaResponseMap(contract.responses);
247
+ else if (contract.response !== undefined)
248
+ schema.response = toElysiaResponseSchema(contract.response);
249
+ return schema;
250
+ }
251
+ function defineElysiaRoute(method, path, contract, handler) {
252
+ toElysiaRouteSchema(contract);
253
+ const stableContract = Object.isFrozen(contract) ? contract : Object.freeze(contract);
254
+ return Object.freeze({
255
+ method,
256
+ path,
257
+ contract: stableContract,
258
+ handler
259
+ });
260
+ }
261
+ function registerElysiaRoute(app, route) {
262
+ const handler = async (context) => {
263
+ const value = await route.handler(context);
264
+ if (route.contract.responses !== undefined) {
265
+ const status = responseStatusOf(value, context.set.status);
266
+ assertResponseStatusDeclared(route.contract.responses, status);
267
+ }
268
+ return value;
269
+ };
270
+ app.route(route.method, route.path, handler, toElysiaRouteSchema(route.contract));
271
+ return app;
272
+ }
273
+
274
+ class SchemaContractError extends Error {
275
+ code = "SCHEMA_CONTRACT_INVALID";
276
+ constructor() {
277
+ super("Schema contract validation failed");
278
+ this.name = "SchemaContractError";
279
+ }
280
+ }
281
+ function createSchemaDecoder(schema, options = {}) {
282
+ const validator = getSchemaValidator(schema, {
283
+ coerce: options.coerce ?? false,
284
+ normalize: options.normalize ?? true
285
+ });
286
+ return (value) => {
287
+ try {
288
+ return validator.parse(value);
289
+ } catch {
290
+ throw new SchemaContractError;
291
+ }
292
+ };
293
+ }
294
+ function defineJsonContract(schemas, request, options = {}) {
295
+ return Object.freeze({
296
+ ...schemas,
297
+ input: createSchemaDecoder(schemas.body, options),
298
+ result: createSchemaDecoder(schemas.response, options),
299
+ request
300
+ });
301
+ }
302
+
303
+ // src/documentation.ts
304
+ import { Elysia } from "elysia";
305
+ function normalizePath(value, fallback) {
306
+ const path = value ?? fallback;
307
+ if (!path.trim() || !path.startsWith("/") || path.includes("?") || path.includes("#")) {
308
+ throw new Error(`Documentation path must be an absolute path without query or fragment: ${path}`);
309
+ }
310
+ return path.length > 1 ? path.replace(/\/+$/, "") : path;
311
+ }
312
+ function resolveSource(source) {
313
+ if (typeof source === "function") {
314
+ return Promise.resolve(source());
315
+ }
316
+ return Promise.resolve(source);
317
+ }
318
+ function jsonResponse(document) {
319
+ if (document === null || typeof document !== "object" || Array.isArray(document)) {
320
+ throw new Error("OpenAPI documentation must be a JSON object");
321
+ }
322
+ const serialized = JSON.stringify(document);
323
+ if (serialized === undefined)
324
+ throw new Error("OpenAPI documentation must be JSON-serializable");
325
+ return new Response(serialized, {
326
+ headers: { "content-type": "application/json; charset=utf-8" }
327
+ });
328
+ }
329
+ function textResponse(schema) {
330
+ return new Response(schema, {
331
+ headers: { "content-type": "text/plain; charset=utf-8" }
332
+ });
333
+ }
334
+ function escapeHtml(value) {
335
+ return value.replace(/[&<>"']/g, (character) => ({
336
+ "&": "&amp;",
337
+ "<": "&lt;",
338
+ ">": "&gt;",
339
+ '"': "&quot;",
340
+ "'": "&#39;"
341
+ })[character] ?? character);
342
+ }
343
+ function scriptString(value) {
344
+ return JSON.stringify(value).replace(/</g, "\\u003c");
345
+ }
346
+ function renderViewerPage(title, resourcePath, mode) {
347
+ const parse = mode === "json" ? "const value = await response.json(); output.textContent = JSON.stringify(value, null, 2);" : "output.textContent = await response.text();";
348
+ const safeTitle = escapeHtml(title);
349
+ const safeResourcePath = escapeHtml(resourcePath);
350
+ return `<!doctype html>
351
+ <html lang="en">
352
+ <head>
353
+ <meta charset="utf-8">
354
+ <meta name="viewport" content="width=device-width, initial-scale=1">
355
+ <title>${safeTitle}</title>
356
+ <style>
357
+ :root { color-scheme: light dark; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
358
+ body { margin: 0; padding: 24px; background: Canvas; color: CanvasText; }
359
+ header { display: flex; gap: 16px; align-items: baseline; flex-wrap: wrap; }
360
+ h1 { font: 600 20px system-ui, sans-serif; margin: 0; }
361
+ a { color: LinkText; }
362
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.5; }
363
+ </style>
364
+ </head>
365
+ <body>
366
+ <header><h1>${safeTitle}</h1><a href="${safeResourcePath}">${safeResourcePath}</a></header>
367
+ <pre id="output">Loading...</pre>
368
+ <script>
369
+ const output = document.getElementById("output");
370
+ fetch(${scriptString(resourcePath)}, { headers: { "accept": "${mode === "json" ? "application/json" : "text/plain"}" } })
371
+ .then(async (response) => {
372
+ if (!response.ok) throw new Error("HTTP " + response.status);
373
+ ${parse}
374
+ })
375
+ .catch((error) => { output.textContent = "Unable to load documentation: " + error.message; });
376
+ </script>
377
+ </body>
378
+ </html>`;
379
+ }
380
+ function createDocumentationPlugin(options = {}) {
381
+ const app = new Elysia({ name: "supacloud:documentation" });
382
+ const paths = new Set;
383
+ const reserve = (path) => {
384
+ if (paths.has(path))
385
+ throw new Error(`Duplicate documentation path: ${path}`);
386
+ paths.add(path);
387
+ };
388
+ if (options.openApi) {
389
+ const specPath = normalizePath(options.openApi.specPath, "/openapi.json");
390
+ const uiPath = normalizePath(options.openApi.uiPath, "/docs");
391
+ reserve(specPath);
392
+ reserve(uiPath);
393
+ const title = options.openApi.title ?? "OpenAPI Documentation";
394
+ app.get(specPath, async () => jsonResponse(await resolveSource(options.openApi.document)), {
395
+ detail: { hide: true }
396
+ });
397
+ app.get(uiPath, () => new Response(renderViewerPage(title, specPath, "json"), {
398
+ headers: { "content-type": "text/html; charset=utf-8" }
399
+ }), { detail: { hide: true } });
400
+ }
401
+ if (options.graphql) {
402
+ const schemaPath = normalizePath(options.graphql.schemaPath, "/graphql/schema.graphql");
403
+ const uiPath = normalizePath(options.graphql.uiPath, "/graphql/docs");
404
+ reserve(schemaPath);
405
+ reserve(uiPath);
406
+ const title = options.graphql.title ?? "GraphQL Schema";
407
+ app.get(schemaPath, async () => textResponse(await resolveSource(options.graphql.schema)), {
408
+ detail: { hide: true }
409
+ });
410
+ app.get(uiPath, () => new Response(renderViewerPage(title, schemaPath, "text"), {
411
+ headers: { "content-type": "text/html; charset=utf-8" }
412
+ }), { detail: { hide: true } });
413
+ }
414
+ return app;
415
+ }
416
+ // src/persistent-command.ts
417
+ import {
418
+ CommandError,
419
+ decodeCommandIdentity
420
+ } from "@supacloud/contracts";
421
+ function createPersistentCommandAdapter(command, options) {
422
+ return {
423
+ capabilities: {
424
+ audit: true,
425
+ idempotency: true,
426
+ transaction: command.kind === "transactional",
427
+ boundary: command.kind === "transactional" ? "database" : "external"
428
+ },
429
+ async execute(invocation) {
430
+ try {
431
+ const identity = decodeCommandIdentity(await options.identity(invocation));
432
+ return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation));
433
+ } catch (error) {
434
+ if (!(error instanceof CommandError))
435
+ throw error;
436
+ const status = commandErrorStatus(error.code);
437
+ throw new ApplicationError(error.code, { code: error.code, status });
438
+ }
439
+ }
440
+ };
441
+ }
31
442
  // src/identity.ts
32
443
  import { createRemoteJWKSet, jwtVerify, errors } from "jose";
33
444
  function httpsUrl(value) {
@@ -449,6 +860,401 @@ function cloneTables(tables) {
449
860
  function cloneValue(value) {
450
861
  return structuredClone(value);
451
862
  }
863
+ // src/worker.ts
864
+ function workerFailureMessage(error) {
865
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Worker job failed";
866
+ const normalized = message.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
867
+ return (normalized || "Worker job failed").slice(0, 4096);
868
+ }
869
+ function createQueueWorkerTransport(options) {
870
+ if (!options.queue || typeof options.queue.receive !== "function" || typeof options.queue.ack !== "function" || typeof options.queue.fail !== "function" || typeof options.decodeClaim !== "function" || typeof options.messageId !== "function") {
871
+ throw new TypeError("Queue worker transport requires queue, decodeClaim and messageId");
872
+ }
873
+ const receiveOptions = options.receive === undefined ? undefined : { ...options.receive };
874
+ return {
875
+ claim: async (signal) => {
876
+ signal.throwIfAborted();
877
+ const message = await options.queue.receive(receiveOptions);
878
+ signal.throwIfAborted();
879
+ return message === null ? null : options.decodeClaim(message);
880
+ },
881
+ ack: (claim) => options.queue.ack(options.messageId(claim)),
882
+ fail: (claim, error) => options.queue.fail(options.messageId(claim), {
883
+ error: workerFailureMessage(error)
884
+ })
885
+ };
886
+ }
887
+
888
+ class WorkerRegistrationError extends Error {
889
+ code;
890
+ constructor(code, message) {
891
+ super(message);
892
+ this.name = "WorkerRegistrationError";
893
+ this.code = code;
894
+ }
895
+ }
896
+
897
+ class WorkerReceiptUnconfirmedError extends Error {
898
+ operation;
899
+ claimId;
900
+ code = "WORKER_RECEIPT_UNCONFIRMED";
901
+ mutationMayHaveApplied = true;
902
+ constructor(operation, claimId) {
903
+ super(`Worker ${operation} receipt could not be confirmed for claim "${claimId}"`);
904
+ this.operation = operation;
905
+ this.claimId = claimId;
906
+ this.name = "WorkerReceiptUnconfirmedError";
907
+ }
908
+ }
909
+ function safeWorkerId(value) {
910
+ const workerId = value ?? `worker-${crypto.randomUUID()}`;
911
+ if (workerId.length === 0 || workerId.length > 256 || /[\u0000-\u001f\u007f]/.test(workerId)) {
912
+ throw new TypeError("Worker workerId must be a safe non-empty string");
913
+ }
914
+ return workerId;
915
+ }
916
+ function safeDescriptorText(value, maxLength) {
917
+ return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength && !/[\u0000-\u001f\u007f]/.test(value);
918
+ }
919
+ function validJobScope(value) {
920
+ return value === "application" || value === "request" || value === "job";
921
+ }
922
+ function validJobMode(value) {
923
+ return value === "task" || value === "workflow";
924
+ }
925
+ function validJobIdempotency(value) {
926
+ return value === "required" || value === "none";
927
+ }
928
+ function validPositiveInteger(value) {
929
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
930
+ }
931
+ function safeClaim(claim) {
932
+ if (!claim || typeof claim !== "object") {
933
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim is invalid");
934
+ }
935
+ if (typeof claim.id !== "string" || claim.id.length === 0 || claim.id.length > 512 || /[\u0000-\u001f\u007f]/.test(claim.id) || typeof claim.jobName !== "string" || claim.jobName.length === 0 || claim.jobName.length > 256 || /[\u0000-\u001f\u007f]/.test(claim.jobName)) {
936
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim identity is invalid");
937
+ }
938
+ const attempt = claim.attempt ?? 1;
939
+ if (!Number.isSafeInteger(attempt) || attempt < 1) {
940
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim attempt is invalid");
941
+ }
942
+ return { ...claim, attempt };
943
+ }
944
+ function delay(milliseconds, signal) {
945
+ if (milliseconds <= 0)
946
+ return Promise.resolve();
947
+ return new Promise((resolve, reject) => {
948
+ let settled = false;
949
+ const finish = (callback) => {
950
+ if (settled)
951
+ return;
952
+ settled = true;
953
+ clearTimeout(timer);
954
+ signal.removeEventListener("abort", onAbort);
955
+ callback();
956
+ };
957
+ const onAbort = () => finish(() => reject(signal.reason ?? new DOMException("Aborted", "AbortError")));
958
+ const timer = setTimeout(() => finish(resolve), milliseconds);
959
+ signal.addEventListener("abort", onAbort, { once: true });
960
+ if (signal.aborted)
961
+ onAbort();
962
+ });
963
+ }
964
+ async function destroyServiceValues(services) {
965
+ const errors = [];
966
+ const seen = new Set;
967
+ for (const value of Object.values(services).reverse()) {
968
+ if (seen.has(value) || !value || typeof value !== "object")
969
+ continue;
970
+ seen.add(value);
971
+ const candidate = value;
972
+ const hook = typeof candidate.onDestroy === "function" ? candidate.onDestroy : typeof candidate.ngOnDestroy === "function" ? candidate.ngOnDestroy : undefined;
973
+ if (!hook)
974
+ continue;
975
+ try {
976
+ await Reflect.apply(hook, value, []);
977
+ } catch (error) {
978
+ errors.push(error);
979
+ }
980
+ }
981
+ if (errors.length > 0)
982
+ throw new AggregateError(errors, "Worker service destruction failed");
983
+ }
984
+
985
+ class SupaCloudWorker {
986
+ registry = new Map;
987
+ modules = [];
988
+ moduleServices = new Map;
989
+ deps;
990
+ transport;
991
+ mapClaim;
992
+ requestContextFactory;
993
+ workerId;
994
+ concurrency;
995
+ pollIntervalMs;
996
+ executor;
997
+ observer;
998
+ onError;
999
+ destroyServices;
1000
+ stateValue = "idle";
1001
+ claimController;
1002
+ loopPromise;
1003
+ startPromise;
1004
+ stopPromise;
1005
+ stopRequested = false;
1006
+ inFlight = new Set;
1007
+ services = {};
1008
+ imported = {};
1009
+ constructor(options) {
1010
+ if (!options.transport || typeof options.transport.claim !== "function" || typeof options.transport.fail !== "function" || typeof options.transport.ack !== "function" && typeof options.transport.acknowledge !== "function") {
1011
+ throw new WorkerRegistrationError("WORKER_TRANSPORT_INVALID", "Worker transport must provide claim, ack and fail");
1012
+ }
1013
+ this.transport = options.transport;
1014
+ this.deps = options.deps ?? {};
1015
+ this.workerId = safeWorkerId(options.workerId);
1016
+ this.mapClaim = options.mapClaim ?? ((claim) => claim);
1017
+ this.requestContextFactory = options.requestContext ?? ((claim, signal) => ({
1018
+ workerId: this.workerId,
1019
+ jobId: claim.id,
1020
+ jobName: claim.jobName,
1021
+ attempt: claim.attempt,
1022
+ signal
1023
+ }));
1024
+ this.concurrency = this.capturePositiveInteger(options.concurrency, 1, 1000, "concurrency");
1025
+ this.pollIntervalMs = this.captureNonNegativeInteger(options.pollIntervalMs, 1000, 300000, "pollIntervalMs");
1026
+ this.executor = options.executor;
1027
+ this.observer = options.onExecution;
1028
+ this.onError = options.onError;
1029
+ this.destroyServices = options.destroyServices;
1030
+ for (const module of options.modules ?? [])
1031
+ this.registerModule(module);
1032
+ }
1033
+ get state() {
1034
+ return this.stateValue;
1035
+ }
1036
+ get id() {
1037
+ return this.workerId;
1038
+ }
1039
+ get jobNames() {
1040
+ return Object.freeze([...this.registry.keys()]);
1041
+ }
1042
+ get jobs() {
1043
+ return Object.freeze([...this.registry.values()].map((entry) => entry.job));
1044
+ }
1045
+ registerModule(module) {
1046
+ if (this.stateValue !== "idle" && this.stateValue !== "stopped") {
1047
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Jobs cannot be registered while the worker is active");
1048
+ }
1049
+ if (!module || typeof module !== "object" || typeof module.name !== "string" || module.name.length === 0 || module.name.length > 256 || /[\u0000-\u001f\u007f]/.test(module.name) || typeof module.createServices !== "function" || module.jobs !== undefined && !Array.isArray(module.jobs)) {
1050
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "A registered module is invalid");
1051
+ }
1052
+ if (this.modules.some((candidate) => candidate.name === module.name)) {
1053
+ throw new WorkerRegistrationError("WORKER_DUPLICATE_MODULE", `Module "${module.name}" is registered more than once`);
1054
+ }
1055
+ const jobs = module.jobs ?? [];
1056
+ const names = new Set;
1057
+ for (const job of jobs) {
1058
+ if (!job || typeof job !== "object" || !safeDescriptorText(job.className, 256) || !safeDescriptorText(job.name, 256) || !safeDescriptorText(job.serviceKey, 256) || !validJobScope(job.scope) || job.mode !== undefined && !validJobMode(job.mode) || job.idempotency !== undefined && !validJobIdempotency(job.idempotency) || job.timeoutSec !== undefined && !validPositiveInteger(job.timeoutSec) || job.maxAttempts !== undefined && !validPositiveInteger(job.maxAttempts)) {
1059
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "A registered Job has invalid identity or execution metadata");
1060
+ }
1061
+ if (this.registry.has(job.name) || names.has(job.name)) {
1062
+ throw new WorkerRegistrationError("WORKER_DUPLICATE_JOB", `Job "${job.name}" is registered more than once`);
1063
+ }
1064
+ names.add(job.name);
1065
+ }
1066
+ this.modules.push(module);
1067
+ for (const job of jobs)
1068
+ this.registry.set(job.name, { module, job });
1069
+ }
1070
+ register(module) {
1071
+ this.registerModule(module);
1072
+ }
1073
+ async start() {
1074
+ if (this.stateValue === "running")
1075
+ return;
1076
+ if (this.stateValue === "starting" && this.startPromise)
1077
+ return this.startPromise;
1078
+ if (this.stateValue === "stopping" && this.stopPromise)
1079
+ await this.stopPromise;
1080
+ this.stopRequested = false;
1081
+ this.stateValue = "starting";
1082
+ const startPromise = Promise.resolve().then(() => this.initialize()).then(async () => {
1083
+ if (this.stopRequested || this.stateValue !== "starting") {
1084
+ await this.releaseServices();
1085
+ return;
1086
+ }
1087
+ this.claimController = new AbortController;
1088
+ this.stateValue = "running";
1089
+ this.loopPromise = this.pollLoop(this.claimController.signal);
1090
+ }).catch(async (error) => {
1091
+ this.stateValue = "stopped";
1092
+ await this.releaseServices();
1093
+ throw error;
1094
+ });
1095
+ this.startPromise = startPromise;
1096
+ return startPromise;
1097
+ }
1098
+ async stop() {
1099
+ if (this.stateValue === "idle" || this.stateValue === "stopped")
1100
+ return;
1101
+ if (this.stopPromise)
1102
+ return this.stopPromise;
1103
+ this.stopRequested = true;
1104
+ this.stateValue = "stopping";
1105
+ this.claimController?.abort(new DOMException("Worker stopped", "AbortError"));
1106
+ const startPromise = this.startPromise;
1107
+ this.stopPromise = (async () => {
1108
+ await startPromise?.catch(() => {
1109
+ return;
1110
+ });
1111
+ await this.loopPromise?.catch((error) => this.reportError(error));
1112
+ await Promise.allSettled([...this.inFlight]);
1113
+ await this.releaseServices();
1114
+ this.stateValue = "stopped";
1115
+ this.claimController = undefined;
1116
+ this.loopPromise = undefined;
1117
+ this.startPromise = undefined;
1118
+ this.stopPromise = undefined;
1119
+ })();
1120
+ return this.stopPromise;
1121
+ }
1122
+ async runOnce() {
1123
+ if (this.stateValue !== "running") {
1124
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker must be started before runOnce");
1125
+ }
1126
+ const controller = this.claimController;
1127
+ if (!controller)
1128
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker claim lifecycle is unavailable");
1129
+ controller.signal.throwIfAborted();
1130
+ const claim = await this.transport.claim(controller.signal);
1131
+ if (claim === null)
1132
+ return null;
1133
+ return this.processClaim(claim);
1134
+ }
1135
+ async processClaim(rawClaim) {
1136
+ const claim = safeClaim(this.mapClaim(rawClaim));
1137
+ const entry = this.registry.get(claim.jobName);
1138
+ const controller = new AbortController;
1139
+ const context = Object.freeze({
1140
+ jobId: claim.id,
1141
+ jobName: claim.jobName,
1142
+ attempt: claim.attempt ?? 1,
1143
+ signal: controller.signal
1144
+ });
1145
+ if (!entry) {
1146
+ const receipt = await this.failClaim(rawClaim, claim, new WorkerRegistrationError("WORKER_INVALID_JOB", `Job "${claim.jobName}" is not registered`), context);
1147
+ return { claimId: claim.id, jobName: claim.jobName, status: "failed", receipt };
1148
+ }
1149
+ let output;
1150
+ try {
1151
+ if (this.stateValue !== "running" && this.stateValue !== "stopping") {
1152
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker is not active");
1153
+ }
1154
+ const requestContext = claim.requestContext ?? await this.requestContextFactory(claim, controller.signal);
1155
+ const moduleServices = this.moduleServices.get(entry.module.name);
1156
+ if (!moduleServices)
1157
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker services are unavailable");
1158
+ output = await executeJob(entry.module, moduleServices, entry.job, claim.input, requestContext, this.imported, this.executor, this.observer);
1159
+ } catch (error) {
1160
+ const receipt = await this.failClaim(rawClaim, claim, error, context);
1161
+ return { claimId: claim.id, jobName: claim.jobName, status: "failed", receipt };
1162
+ }
1163
+ const acknowledge = this.transport.ack ?? this.transport.acknowledge;
1164
+ if (!acknowledge)
1165
+ throw new WorkerRegistrationError("WORKER_TRANSPORT_INVALID", "Worker acknowledgement is unavailable");
1166
+ try {
1167
+ const receipt = await acknowledge(rawClaim, output, context);
1168
+ return { claimId: claim.id, jobName: claim.jobName, status: "acknowledged", receipt };
1169
+ } catch {
1170
+ throw new WorkerReceiptUnconfirmedError("ack", claim.id);
1171
+ }
1172
+ }
1173
+ async failClaim(rawClaim, claim, error, context) {
1174
+ try {
1175
+ return await this.transport.fail(rawClaim, error, context);
1176
+ } catch {
1177
+ throw new WorkerReceiptUnconfirmedError("fail", claim.id);
1178
+ }
1179
+ }
1180
+ async initialize() {
1181
+ this.services = {};
1182
+ this.imported = {};
1183
+ this.moduleServices.clear();
1184
+ for (const module of this.modules) {
1185
+ const services = module.createServices(this.deps, this.imported);
1186
+ this.moduleServices.set(module.name, services);
1187
+ this.services = { ...this.services, ...services };
1188
+ this.imported[module.name] = services;
1189
+ }
1190
+ }
1191
+ async releaseServices() {
1192
+ const services = this.services;
1193
+ const imported = this.imported;
1194
+ this.services = {};
1195
+ this.imported = {};
1196
+ this.moduleServices.clear();
1197
+ if (Object.keys(services).length === 0)
1198
+ return;
1199
+ try {
1200
+ if (this.destroyServices)
1201
+ await this.destroyServices(services, imported);
1202
+ else
1203
+ await destroyServiceValues(services);
1204
+ } catch (error) {
1205
+ await this.reportError(error);
1206
+ }
1207
+ }
1208
+ async pollLoop(signal) {
1209
+ while (!signal.aborted && this.stateValue === "running") {
1210
+ if (this.inFlight.size >= this.concurrency) {
1211
+ await Promise.race(this.inFlight).catch((error) => this.reportError(error));
1212
+ continue;
1213
+ }
1214
+ let claim;
1215
+ try {
1216
+ claim = await this.transport.claim(signal);
1217
+ } catch (error) {
1218
+ if (signal.aborted)
1219
+ break;
1220
+ await this.reportError(error);
1221
+ await delay(this.pollIntervalMs, signal).catch(() => {
1222
+ return;
1223
+ });
1224
+ continue;
1225
+ }
1226
+ if (claim === null) {
1227
+ await delay(this.pollIntervalMs, signal).catch(() => {
1228
+ return;
1229
+ });
1230
+ continue;
1231
+ }
1232
+ const execution = this.processClaim(claim).catch((error) => this.reportError(error));
1233
+ this.inFlight.add(execution);
1234
+ execution.finally(() => this.inFlight.delete(execution));
1235
+ }
1236
+ }
1237
+ async reportError(error) {
1238
+ try {
1239
+ await this.onError?.(error);
1240
+ } catch {}
1241
+ }
1242
+ capturePositiveInteger(value, fallback, max, name) {
1243
+ const result = value ?? fallback;
1244
+ if (!Number.isSafeInteger(result) || result < 1 || result > max)
1245
+ throw new TypeError(`Worker ${name} is invalid`);
1246
+ return result;
1247
+ }
1248
+ captureNonNegativeInteger(value, fallback, max, name) {
1249
+ const result = value ?? fallback;
1250
+ if (!Number.isSafeInteger(result) || result < 0 || result > max)
1251
+ throw new TypeError(`Worker ${name} is invalid`);
1252
+ return result;
1253
+ }
1254
+ }
1255
+ function createWorker(options) {
1256
+ return new SupaCloudWorker(options);
1257
+ }
452
1258
 
453
1259
  // src/index.ts
454
1260
  var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
@@ -463,9 +1269,10 @@ function composeAspects(...aspects) {
463
1269
  return Promise.reject(new Error("next() called multiple times"));
464
1270
  }
465
1271
  index = current;
466
- if (current === active.length)
1272
+ const aspect = active[current];
1273
+ if (!aspect)
467
1274
  return Promise.resolve(next());
468
- return Promise.resolve(active[current](context, () => dispatch(current + 1)));
1275
+ return Promise.resolve(aspect(context, () => dispatch(current + 1)));
469
1276
  };
470
1277
  return dispatch(0);
471
1278
  };
@@ -475,7 +1282,7 @@ function observedAspects(aspects, boundary, observer) {
475
1282
  kind: context.kind,
476
1283
  operation: context.name,
477
1284
  stage: `${boundary}.aspect[${index}]:${aspect.name || "anonymous"}`,
478
- requestId: executionRequestId(context.requestContext)
1285
+ ...executionTrace(context.requestContext)
479
1286
  }, () => aspect(context, next))));
480
1287
  }
481
1288
  function composeCommandExecutors(...executors) {
@@ -537,6 +1344,19 @@ function validatedJsonResponse(validate, value, init) {
537
1344
  headers.set("content-type", "application/json");
538
1345
  return new Response(body, { ...init, headers });
539
1346
  }
1347
+ function decodeJobContract(schema, value, boundary) {
1348
+ if (schema === undefined)
1349
+ return value;
1350
+ try {
1351
+ return createSchemaDecoder(schema)(value);
1352
+ } catch {
1353
+ const input = boundary === "input";
1354
+ throw new ApplicationError(input ? "Job input contract validation failed" : "Job output contract validation failed", {
1355
+ status: input ? 422 : 500,
1356
+ code: input ? "JOB_INPUT_VALIDATION_ERROR" : "JOB_OUTPUT_VALIDATION_ERROR"
1357
+ });
1358
+ }
1359
+ }
540
1360
  function safeHeaderValue(value, maxLength) {
541
1361
  if (!value || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
542
1362
  return;
@@ -555,7 +1375,7 @@ function isAuthenticatedTrustedIdentity(value) {
555
1375
  function bearerToken(request) {
556
1376
  const authorization = request.headers.get("authorization");
557
1377
  const match = authorization?.match(/^Bearer\s+(.+)$/i);
558
- return match ? safeHeaderValue(match[1], 16384) : undefined;
1378
+ return match?.[1] ? safeHeaderValue(match[1], 16384) : undefined;
559
1379
  }
560
1380
  var createSupaCloudRequestContext = (request) => {
561
1381
  const subject = safeHeaderValue(request.headers.get(VERIFIED_JWT_SUBJECT_HEADER), 1024);
@@ -612,7 +1432,7 @@ function createCommandExecutor(governance, observer) {
612
1432
  const event = {
613
1433
  kind: "command",
614
1434
  operation: command.name,
615
- requestId: executionRequestId(invocation.requestContext)
1435
+ ...executionTrace(invocation.requestContext)
616
1436
  };
617
1437
  await observeExecution(observer, { ...event, stage: "authorize" }, () => governance.authorize(invocation));
618
1438
  return observeExecution(observer, { ...event, stage: `rpc:${command.rpc}` }, () => adapter.execute(invocation, once(next)));
@@ -630,12 +1450,12 @@ function createCommandExecutor(governance, observer) {
630
1450
  throw missingGovernanceAdapter(command, "idempotency");
631
1451
  }
632
1452
  try {
633
- const observe = (stage, next2) => observeExecution(observer, {
1453
+ const observe = (stage, next) => observeExecution(observer, {
634
1454
  kind: "command",
635
1455
  operation: command.name,
636
1456
  stage,
637
- requestId: executionRequestId(invocation.requestContext)
638
- }, next2);
1457
+ ...executionTrace(invocation.requestContext)
1458
+ }, next);
639
1459
  await observe("authorize", () => governance.authorize(invocation));
640
1460
  let execute = async () => {
641
1461
  const result = await observe("handler", invokeOnce);
@@ -669,13 +1489,15 @@ async function executeCompiledCommand(options) {
669
1489
  if (matches.length !== 1)
670
1490
  throw new ApplicationError("Command descriptor missing or ambiguous", { code: "COMMAND_NOT_REGISTERED" });
671
1491
  const command = matches[0];
1492
+ if (!command)
1493
+ throw new Error(`Command not found: ${options.command}`);
672
1494
  const invocation = {
673
1495
  command,
674
1496
  input: { body: options.input, params: {}, query: {} },
675
1497
  request: options.request,
676
1498
  requestContext: options.requestContext,
677
1499
  services: options.services ?? {},
678
- scope: options.scope
1500
+ ...options.scope ? { scope: options.scope } : {}
679
1501
  };
680
1502
  const aspects = composeAspects(observedAspects(options.module.aspects ?? [], `module:${options.module.name}`, options.observer), observedAspects(command.aspects ?? [], "command", options.observer));
681
1503
  const value = await createCommandExecutor(options.governance, options.observer)(invocation, once(() => aspects({
@@ -685,11 +1507,44 @@ async function executeCompiledCommand(options) {
685
1507
  request: options.request,
686
1508
  requestContext: options.requestContext,
687
1509
  services: invocation.services,
688
- scope: invocation.scope,
1510
+ ...invocation.scope ? { scope: invocation.scope } : {},
689
1511
  metadata: command
690
1512
  }, once(() => options.handler(options.input)))));
691
1513
  return options.decode(value);
692
1514
  }
1515
+ async function previewCompiledCommand(options) {
1516
+ const matches = options.module.commands?.filter((item) => item.className === options.command) ?? [];
1517
+ if (matches.length !== 1)
1518
+ throw new ApplicationError("Command descriptor missing or ambiguous", { code: "COMMAND_NOT_REGISTERED" });
1519
+ const command = matches[0];
1520
+ if (!command)
1521
+ throw new Error(`Command not found: ${options.command}`);
1522
+ const invocation = {
1523
+ command,
1524
+ input: { body: options.input, params: {}, query: {} },
1525
+ request: options.request,
1526
+ requestContext: options.requestContext,
1527
+ services: options.services ?? {},
1528
+ ...options.scope ? { scope: options.scope } : {}
1529
+ };
1530
+ try {
1531
+ await options.governance.authorize(invocation);
1532
+ } catch (error) {
1533
+ if (error instanceof ApplicationError && (error.status === 401 || error.status === 403)) {
1534
+ return decodeCommandPreview({
1535
+ command: command.name,
1536
+ allowed: false,
1537
+ blockers: [{ code: error.code, message: error.message }]
1538
+ });
1539
+ }
1540
+ throw error;
1541
+ }
1542
+ const preview = decodeCommandPreview(await options.preview(options.input));
1543
+ if (preview.command !== command.name && preview.command !== command.className) {
1544
+ throw new TypeError("Mismatched command identity");
1545
+ }
1546
+ return preview;
1547
+ }
693
1548
  function once(next) {
694
1549
  let called = false;
695
1550
  return async () => {
@@ -731,7 +1586,63 @@ function joinPaths(prefix, path) {
731
1586
  function controllerInstance(value) {
732
1587
  return isRecord(value) ? value : undefined;
733
1588
  }
1589
+ function cookieValues(value) {
1590
+ if (!isRecord(value))
1591
+ return;
1592
+ const result = {};
1593
+ for (const [name, entry] of Object.entries(value)) {
1594
+ result[name] = isRecord(entry) && "value" in entry ? entry.value : entry;
1595
+ }
1596
+ return result;
1597
+ }
1598
+ function assertDeclaredResponseStatus(route, value, configuredStatus) {
1599
+ if (route.responses === undefined)
1600
+ return;
1601
+ const status = responseStatusOf(value, configuredStatus);
1602
+ if (responseStatusDeclared(route.responses, status))
1603
+ return;
1604
+ const transport = route.contract?.response;
1605
+ if ((transport === "binary" || transport === "stream") && status >= 200 && status < 300)
1606
+ return;
1607
+ throw new ApplicationError("Response validation failed", { status: 500, code: "RESPONSE_CONTRACT_UNDECLARED" });
1608
+ }
734
1609
  function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}) {
1610
+ const supportedMethods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
1611
+ const supportedFields = new Set([
1612
+ "method",
1613
+ "path",
1614
+ "handler",
1615
+ "body",
1616
+ "params",
1617
+ "query",
1618
+ "headers",
1619
+ "cookie",
1620
+ "response",
1621
+ "responses",
1622
+ "contract",
1623
+ "schemaKinds",
1624
+ "nativeResponse",
1625
+ "invoker",
1626
+ "command",
1627
+ "aspects",
1628
+ "paramTransforms",
1629
+ "paramDefaults",
1630
+ "queryTransforms",
1631
+ "queryDefaults",
1632
+ "title",
1633
+ "data",
1634
+ "input",
1635
+ "result",
1636
+ "request"
1637
+ ]);
1638
+ for (const controller of compiled.controllers) {
1639
+ for (const route of controller.routes) {
1640
+ const unsupported = Object.keys(route).filter((field) => !supportedFields.has(field));
1641
+ if (!supportedMethods.has(route.method) || unsupported.length > 0) {
1642
+ throw new ApplicationError(`Unsupported compiled route ${route.method} ${controller.path}${route.path}` + (unsupported.length > 0 ? `: ${unsupported.join(", ")}` : ""), { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
1643
+ }
1644
+ }
1645
+ }
735
1646
  const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
736
1647
  if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
737
1648
  throw new ApplicationError(`Module "${compiled.name}" has command routes but no commandGovernance`, { code: "COMMAND_GOVERNANCE_UNCONFIGURED" });
@@ -749,7 +1660,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
749
1660
  const requestContexts = new WeakMap;
750
1661
  const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
751
1662
  const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
752
- const plugin = new Elysia({ name: `supacloud:${compiled.name}` }).decorate("services", services);
1663
+ const plugin = new Elysia2({ name: `supacloud:${compiled.name}`, normalize: options.normalize ?? true }).decorate("services", services);
753
1664
  const createRequestScope = compiled.createRequestScope;
754
1665
  const requestScopes = new WeakMap;
755
1666
  if (compiled.destroyRequestScope) {
@@ -783,99 +1694,98 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
783
1694
  for (const controller of compiled.controllers) {
784
1695
  for (const route of controller.routes) {
785
1696
  const path = joinPaths(controller.path, route.path);
786
- const schema = {};
787
- if (route.body !== undefined)
788
- schema.body = route.body;
789
- if (route.params !== undefined)
790
- schema.params = route.params;
791
- if (route.query !== undefined)
792
- schema.query = route.query;
793
- if (route.response !== undefined)
794
- schema.response = route.response;
1697
+ const schema = toElysiaRouteSchema(route);
795
1698
  const handler = async (ctx) => {
796
1699
  const requestContext = ctx.requestContext ?? await ctxFactory(ctx.request);
797
- const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
798
- if (requestScope && compiled.destroyRequestScope)
799
- requestScopes.set(ctx.request, requestScope);
800
- const source = controller.scope === "request" ? requestScope : services;
801
- const instance = controllerInstance(source?.[controller.serviceKey]);
802
- const method = instance?.[route.handler];
803
- if (typeof method !== "function") {
804
- throw new Error(`supacloud: controller "${controller.serviceKey}" has no handler "${route.handler}" in scope "${controller.scope}"`);
805
- }
806
- const input = {
807
- body: ctx.body,
808
- params: ctx.params,
809
- query: ctx.query,
810
- headers: Object.fromEntries(ctx.request.headers.entries()),
811
- context: requestContext,
812
- request: ctx.request,
813
- scope: requestScope,
814
- requestContext
815
- };
816
- const handlerCall = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
817
- const invoke = once(() => route.command && options.commandGovernance ? handlerCall() : observeExecution(options.onExecution, {
818
- kind: route.command ? "command" : "route",
819
- operation: route.command ?? `${route.method} ${path}`,
820
- stage: "handler",
821
- requestId: executionRequestId(requestContext)
822
- }, handlerCall));
823
- const routeContext = {
824
- kind: "route",
825
- name: `${route.method} ${path}`,
826
- input,
827
- request: ctx.request,
828
- requestContext,
829
- scope: requestScope,
830
- services,
831
- metadata: route
832
- };
833
- const command = route.command ? commandsByClassName.get(route.command) : undefined;
834
- const commandContext = {
835
- kind: "command",
836
- name: command?.name ?? route.command ?? `${route.method} ${path}`,
837
- input,
838
- request: ctx.request,
839
- requestContext,
840
- scope: requestScope,
841
- services,
842
- metadata: command ?? route
843
- };
844
- const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
845
- const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
846
- const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
847
- const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
848
- const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
849
- const invokeCommand = () => {
850
- if (!route.command)
851
- return invoke();
852
- if (!command) {
853
- throw new ApplicationError(`Command "${route.command}" is not registered`, {
854
- code: "COMMAND_NOT_REGISTERED"
855
- });
1700
+ const execute = async () => {
1701
+ const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
1702
+ if (requestScope && compiled.destroyRequestScope)
1703
+ requestScopes.set(ctx.request, requestScope);
1704
+ const source = controller.scope === "request" ? requestScope : services;
1705
+ const instance = controllerInstance(source?.[controller.serviceKey]);
1706
+ const method = instance?.[route.handler];
1707
+ if (typeof method !== "function") {
1708
+ throw new Error(`supacloud: controller "${controller.serviceKey}" has no handler "${route.handler}" in scope "${controller.scope}"`);
856
1709
  }
857
- if (!commandExecutor) {
858
- throw new ApplicationError(`Command "${command.name}" has no executor`, {
859
- status: 501,
860
- code: "COMMAND_EXECUTOR_UNAVAILABLE"
861
- });
862
- }
863
- const invocation = {
864
- command,
1710
+ const normalizedCookie = ctx.cookie === undefined ? undefined : cookieValues(ctx.cookie);
1711
+ const input = {
1712
+ body: ctx.body,
1713
+ params: ctx.params,
1714
+ query: ctx.query,
1715
+ headers: ctx.headers ?? Object.fromEntries(ctx.request.headers.entries()),
1716
+ ...normalizedCookie === undefined ? {} : { cookie: normalizedCookie },
1717
+ context: requestContext,
1718
+ request: ctx.request,
1719
+ ...requestScope ? { scope: requestScope } : {},
1720
+ requestContext
1721
+ };
1722
+ const handlerCall = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
1723
+ const invoke = once(() => route.command && options.commandGovernance ? handlerCall() : observeExecution(options.onExecution, {
1724
+ kind: route.command ? "command" : "route",
1725
+ operation: route.command ?? `${route.method} ${path}`,
1726
+ stage: "handler",
1727
+ ...executionTrace(requestContext)
1728
+ }, handlerCall));
1729
+ const routeContext = {
1730
+ kind: "route",
1731
+ name: `${route.method} ${path}`,
865
1732
  input,
866
1733
  request: ctx.request,
867
1734
  requestContext,
868
- scope: requestScope,
869
- services
1735
+ ...requestScope ? { scope: requestScope } : {},
1736
+ services,
1737
+ metadata: route
870
1738
  };
871
- return observeExecution(options.onExecution, {
1739
+ const command = route.command ? commandsByClassName.get(route.command) : undefined;
1740
+ const commandContext = {
872
1741
  kind: "command",
873
- operation: command.name,
874
- stage: "commandExecutor",
875
- requestId: executionRequestId(requestContext)
876
- }, () => commandExecutor(invocation, invoke));
1742
+ name: command?.name ?? route.command ?? `${route.method} ${path}`,
1743
+ input,
1744
+ request: ctx.request,
1745
+ requestContext,
1746
+ ...requestScope ? { scope: requestScope } : {},
1747
+ services,
1748
+ metadata: command ?? route
1749
+ };
1750
+ const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
1751
+ const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
1752
+ const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
1753
+ const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
1754
+ const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
1755
+ const invokeCommand = () => {
1756
+ if (!route.command)
1757
+ return invoke();
1758
+ if (!command) {
1759
+ throw new ApplicationError(`Command "${route.command}" is not registered`, {
1760
+ code: "COMMAND_NOT_REGISTERED"
1761
+ });
1762
+ }
1763
+ if (!commandExecutor) {
1764
+ throw new ApplicationError(`Command "${command.name}" has no executor`, {
1765
+ status: 501,
1766
+ code: "COMMAND_EXECUTOR_UNAVAILABLE"
1767
+ });
1768
+ }
1769
+ const invocation = {
1770
+ command,
1771
+ input,
1772
+ request: ctx.request,
1773
+ requestContext,
1774
+ ...requestScope ? { scope: requestScope } : {},
1775
+ services
1776
+ };
1777
+ return observeExecution(options.onExecution, {
1778
+ kind: "command",
1779
+ operation: command.name,
1780
+ stage: "commandExecutor",
1781
+ ...executionTrace(requestContext)
1782
+ }, () => commandExecutor(invocation, invoke));
1783
+ };
1784
+ return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
877
1785
  };
878
- return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
1786
+ const result = options.injector ? await runInRequestContext(options.injector, [provideToken(REQUEST_CONTEXT, requestContext)], execute) : await execute();
1787
+ assertDeclaredResponseStatus(route, result, ctx.set.status);
1788
+ return result;
879
1789
  };
880
1790
  switch (route.method) {
881
1791
  case "GET":
@@ -905,6 +1815,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
905
1815
  return plugin;
906
1816
  }
907
1817
  async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor, observer) {
1818
+ const decodedInput = decodeJobContract(job.input, input, "input");
908
1819
  const jobScope = job.scope === "job" && compiled.createJobScope ? await compiled.createJobScope(services, requestContext, imported) : undefined;
909
1820
  try {
910
1821
  const source = job.scope === "job" ? jobScope : services;
@@ -915,17 +1826,17 @@ async function executeJob(compiled, services, job, input, requestContext, import
915
1826
  }
916
1827
  const invocation = {
917
1828
  job,
918
- input,
1829
+ input: decodedInput,
919
1830
  requestContext,
920
- scope: jobScope,
1831
+ ...jobScope ? { scope: jobScope } : {},
921
1832
  services
922
1833
  };
923
1834
  const context = {
924
1835
  kind: "job",
925
1836
  name: job.name,
926
- input,
1837
+ input: decodedInput,
927
1838
  requestContext,
928
- scope: jobScope,
1839
+ ...jobScope ? { scope: jobScope } : {},
929
1840
  services,
930
1841
  metadata: job
931
1842
  };
@@ -933,16 +1844,17 @@ async function executeJob(compiled, services, job, input, requestContext, import
933
1844
  kind: "job",
934
1845
  operation: job.name,
935
1846
  stage: "handler",
936
- requestId: executionRequestId(requestContext)
937
- }, () => Reflect.apply(method, instance, [input])));
1847
+ ...executionTrace(requestContext)
1848
+ }, () => Reflect.apply(method, instance, [decodedInput])));
938
1849
  const pipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, observer);
939
1850
  const jobPipeline = observedAspects(job.aspects ?? [], "job", observer);
940
- return await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
1851
+ const result = await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
941
1852
  kind: "job",
942
1853
  operation: job.name,
943
1854
  stage: "jobExecutor",
944
- requestId: executionRequestId(requestContext)
1855
+ ...executionTrace(requestContext)
945
1856
  }, executor ? () => executor(invocation, invoke) : invoke)));
1857
+ return decodeJobContract(job.output, result, "output");
946
1858
  } finally {
947
1859
  if (jobScope && compiled.destroyJobScope) {
948
1860
  await compiled.destroyJobScope(jobScope);
@@ -950,6 +1862,12 @@ async function executeJob(compiled, services, job, input, requestContext, import
950
1862
  }
951
1863
  }
952
1864
  function defaultErrorResponse(error, frameworkCode) {
1865
+ const protocolCode = commandErrorCode(error);
1866
+ if (protocolCode) {
1867
+ return Response.json({ ok: false, code: protocolCode, message: protocolCode }, {
1868
+ status: commandErrorStatus(protocolCode)
1869
+ });
1870
+ }
953
1871
  if (isPublicApplicationError(error)) {
954
1872
  return Response.json({
955
1873
  ok: false,
@@ -958,6 +1876,13 @@ function defaultErrorResponse(error, frameworkCode) {
958
1876
  ...error.details === undefined ? {} : { details: error.details }
959
1877
  }, { status: error.status });
960
1878
  }
1879
+ if (frameworkCode === "PARSE") {
1880
+ return Response.json({
1881
+ ok: false,
1882
+ code: "PARSE_ERROR",
1883
+ message: "Request body could not be parsed"
1884
+ }, { status: 400 });
1885
+ }
961
1886
  if (frameworkCode === "VALIDATION") {
962
1887
  if (isRecord(error) && error.type === "response") {
963
1888
  return Response.json({
@@ -984,7 +1909,9 @@ function isPublicApplicationError(error) {
984
1909
  return error.expose === true && typeof error.status === "number" && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 && typeof error.code === "string" && error.code.length > 0;
985
1910
  }
986
1911
  function createApplication(options) {
987
- const app = new Elysia({ name: options.name ?? "supacloud:app" });
1912
+ const app = new Elysia2({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true });
1913
+ if (options.documentation)
1914
+ app.use(createDocumentationPlugin(options.documentation));
988
1915
  const configuredContextFactory = options.requestContext ?? defaultRequestContext;
989
1916
  const contextCache = new WeakMap;
990
1917
  const ctxFactory = (request) => {
@@ -999,12 +1926,7 @@ function createApplication(options) {
999
1926
  for (const module of options.modules ?? []) {
1000
1927
  const services = module.createServices(options.deps ?? {}, imported);
1001
1928
  imported[module.name] = services;
1002
- app.use(createModulePlugin(module, services, ctxFactory, {
1003
- commandGovernance: options.commandGovernance,
1004
- commandExecutor: options.commandExecutor,
1005
- errorMapper: options.errorMapper,
1006
- onExecution: options.onExecution
1007
- }, imported));
1929
+ app.use(createModulePlugin(module, services, ctxFactory, options, imported));
1008
1930
  }
1009
1931
  return app;
1010
1932
  }
@@ -1015,22 +1937,41 @@ export {
1015
1937
  ApplicationError,
1016
1938
  EXECUTION_ID_HEADER,
1017
1939
  IDEMPOTENCY_KEY_HEADER,
1940
+ SchemaContractError,
1941
+ SupaCloudWorker,
1018
1942
  VERIFIED_JWT_SUBJECT_HEADER,
1943
+ WorkerReceiptUnconfirmedError,
1944
+ WorkerRegistrationError,
1019
1945
  assertFeatureTransition,
1946
+ assertResponseStatusDeclared,
1020
1947
  composeAspects,
1021
1948
  composeCommandExecutors,
1022
1949
  createApplication,
1023
1950
  createCommandExecutor,
1951
+ createDocumentationPlugin,
1024
1952
  createMemoryPolicy,
1025
1953
  createMemorySandbox,
1026
1954
  createModulePlugin,
1955
+ createPersistentCommandAdapter,
1956
+ createQueueWorkerTransport,
1957
+ createSchemaDecoder,
1027
1958
  createSupAuthRequestContext,
1028
1959
  createSupaCloudRequestContext,
1029
1960
  createTestApp,
1961
+ createWorker,
1030
1962
  defaultErrorResponse,
1963
+ defineElysiaRoute,
1964
+ defineJsonContract,
1965
+ defineRouteContract,
1031
1966
  executeCompiledCommand,
1032
1967
  executeJob,
1968
+ previewCompiledCommand,
1969
+ registerElysiaRoute,
1033
1970
  requireIdempotencyKey,
1034
1971
  requireTrustedIdentity,
1972
+ responseSchemaForStatus,
1973
+ responseStatusDeclared,
1974
+ responseStatusOf,
1975
+ toElysiaRouteSchema,
1035
1976
  validatedJsonResponse
1036
1977
  };