@supacloud/elysia 0.18.0 → 0.20.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,5 @@
1
1
  // src/index.ts
2
- import { Elysia as Elysia2 } from "elysia";
2
+ import { Elysia as Elysia3, setupTypebox, Validator as Validator2 } from "elysia";
3
3
  import { decodeCommandPreview } from "@supacloud/contracts";
4
4
 
5
5
  // src/command-errors.ts
@@ -36,6 +36,631 @@ function commandErrorStatus(code) {
36
36
  }
37
37
  }
38
38
 
39
+ // src/http-policy.ts
40
+ class HttpPolicyConfigurationError extends Error {
41
+ code = "HTTP_POLICY_CONFIGURATION_INVALID";
42
+ }
43
+ function compileHttpPolicies(route, path, registry = {}) {
44
+ const declarations = route.data?.httpPolicies;
45
+ if (declarations === undefined)
46
+ return [];
47
+ const fail = () => {
48
+ throw new HttpPolicyConfigurationError(`Invalid HTTP policy configuration for ${route.method} ${path}`);
49
+ };
50
+ if (!Array.isArray(declarations))
51
+ return fail();
52
+ return Array.from(declarations, (value, index) => {
53
+ if (!value || typeof value !== "object" || Array.isArray(value))
54
+ return fail();
55
+ const declaration = value;
56
+ if (typeof declaration.name !== "string" || Object.keys(declaration).some((key) => key !== "name" && key !== "options") || !Object.hasOwn(registry, declaration.name))
57
+ return fail();
58
+ const factory = registry[declaration.name];
59
+ if (typeof factory !== "function")
60
+ return fail();
61
+ const policy = factory(declaration.options, Object.freeze({
62
+ method: route.method,
63
+ path,
64
+ ...route.command === undefined ? {} : { command: route.command }
65
+ }));
66
+ if (typeof policy !== "function")
67
+ return fail();
68
+ if (policy.terminal && index !== declarations.length - 1)
69
+ return fail();
70
+ if (policy.afterResponse !== undefined && typeof policy.afterResponse !== "function")
71
+ return fail();
72
+ if (policy.mapResponse !== undefined && typeof policy.mapResponse !== "function")
73
+ return fail();
74
+ return policy;
75
+ });
76
+ }
77
+
78
+ // src/http-telemetry.ts
79
+ import { Elysia, StatusMap } from "elysia";
80
+ var requestIds = new WeakMap;
81
+ function httpRequestId(request) {
82
+ return requestIds.get(request);
83
+ }
84
+ function createHttpTelemetry(observe) {
85
+ const starts = new WeakMap;
86
+ return new Elysia().request(({ request, set }) => {
87
+ const supplied = request.headers.get("x-request-id");
88
+ const requestId = supplied && /^[A-Za-z0-9._:-]{1,128}$/.test(supplied) ? supplied : crypto.randomUUID();
89
+ starts.set(request, { time: performance.now(), requestId });
90
+ requestIds.set(request, requestId);
91
+ set.headers["x-request-id"] = requestId;
92
+ }).error(() => {
93
+ return;
94
+ }).afterResponse(async ({ request, responseValue: response, set, route }) => {
95
+ const start = starts.get(request);
96
+ if (!start)
97
+ return;
98
+ starts.delete(request);
99
+ const status = response instanceof Response ? response.status : typeof set.status === "number" ? set.status : StatusMap[set.status ?? "OK"];
100
+ try {
101
+ await observe(Object.freeze({
102
+ requestId: start.requestId,
103
+ method: request.method,
104
+ route: route ?? "<unmatched>",
105
+ status,
106
+ durationMs: performance.now() - start.time
107
+ }));
108
+ } catch {}
109
+ }).as("global");
110
+ }
111
+ // src/identity.ts
112
+ import { createRemoteJWKSet, jwtVerify, errors } from "jose";
113
+ function httpsUrl(value) {
114
+ const url = new URL(value);
115
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) {
116
+ throw new TypeError("Identity endpoints must be HTTPS URLs without credentials or fragments");
117
+ }
118
+ return url;
119
+ }
120
+ function createSupAuthRequestContext(options) {
121
+ httpsUrl(options.issuer);
122
+ const jwksUrl = httpsUrl(options.jwksUrl);
123
+ if (!options.audience.trim() || !options.projectId.trim() || !options.clientId?.trim()) {
124
+ throw new TypeError("SupAuth audience, clientId and projectId are required");
125
+ }
126
+ const algorithms = [...options.algorithms ?? ["ES256", "RS256"]];
127
+ if (!algorithms.length || algorithms.some((value) => value !== "ES256" && value !== "RS256")) {
128
+ throw new TypeError("SupAuth requires ES256 or RS256");
129
+ }
130
+ const { issuer, audience, clientId, projectId, resolveAccess } = options;
131
+ const keyResolver = options.keyResolver ?? createRemoteJWKSet(jwksUrl, { timeoutDuration: 5000 });
132
+ return async (request) => {
133
+ const match = request.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/i);
134
+ const token = match?.[1];
135
+ if (!token || token.length > 16384)
136
+ throw unauthenticated();
137
+ let subject;
138
+ try {
139
+ const { payload } = await jwtVerify(token, keyResolver, {
140
+ issuer,
141
+ audience,
142
+ algorithms,
143
+ requiredClaims: ["sub", "exp", "iat"]
144
+ });
145
+ if (typeof payload.sub !== "string" || !payload.sub.trim() || payload.sub.length > 1024 || /[\u0000-\u001f\u007f]/.test(payload.sub)) {
146
+ throw unauthenticated();
147
+ }
148
+ const application = payload.client_id ?? payload.azp;
149
+ if (application !== clientId || payload.role !== "authenticated" || payload.client_id !== undefined && payload.client_id !== clientId || payload.azp !== undefined && payload.azp !== clientId) {
150
+ throw unauthenticated();
151
+ }
152
+ subject = payload.sub;
153
+ } catch (error) {
154
+ if (error instanceof ApplicationError || error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid || error instanceof errors.JWSInvalid || error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JOSEAlgNotAllowed || error instanceof errors.JOSENotSupported || error instanceof errors.JWKSNoMatchingKey)
155
+ throw unauthenticated();
156
+ throw new ApplicationError("Identity verification service unavailable", {
157
+ status: 503,
158
+ code: "AUTHENTICATION_UNAVAILABLE"
159
+ });
160
+ }
161
+ const identity = { authenticated: true, subject, issuer, clientId };
162
+ Object.defineProperty(identity, "accessToken", { value: token, enumerable: false });
163
+ Object.freeze(identity);
164
+ let access;
165
+ try {
166
+ access = await resolveAccess(identity, request);
167
+ } catch {
168
+ throw new ApplicationError("Application access service unavailable", {
169
+ status: 503,
170
+ code: "APPLICATION_ACCESS_UNAVAILABLE"
171
+ });
172
+ }
173
+ if (!access || access.projectId !== projectId || typeof access.tenantId !== "string" || !access.tenantId.trim() || !Array.isArray(access.permissions) || access.permissions.some((permission) => typeof permission !== "string" || !permission.trim())) {
174
+ throw new ApplicationError("Application access denied", { status: 403, code: "APPLICATION_ACCESS_DENIED" });
175
+ }
176
+ const context = createSupaCloudRequestContext(request);
177
+ return {
178
+ ...context,
179
+ identity,
180
+ access: Object.freeze({
181
+ projectId,
182
+ tenantId: access.tenantId,
183
+ permissions: Object.freeze([...new Set(access.permissions)])
184
+ })
185
+ };
186
+ };
187
+ }
188
+ function unauthenticated() {
189
+ return new ApplicationError("Authenticated user context is required", {
190
+ status: 401,
191
+ code: "AUTHENTICATION_REQUIRED"
192
+ });
193
+ }
194
+
195
+ // src/http-response-cache.ts
196
+ import { WebStandardAdapter } from "elysia/adapter/web-standard";
197
+ function mayStore({ http, response }) {
198
+ if (response === null || typeof response !== "object" || response instanceof Response || !Array.isArray(response) && Object.getPrototypeOf(response) !== Object.prototype)
199
+ return false;
200
+ if (http.set.status !== undefined && http.set.status !== 200 && http.set.status !== "OK")
201
+ return false;
202
+ if (http.set.cookie && Object.keys(http.set.cookie).length)
203
+ return false;
204
+ const headers = new Headers(http.set.headers);
205
+ for (const name of headers.keys()) {
206
+ if (!["content-type", "x-request-id", "ratelimit-limit", "ratelimit-remaining"].includes(name))
207
+ return false;
208
+ }
209
+ return !headers.has("content-type") || headers.get("content-type").split(";")[0].trim() === "application/json";
210
+ }
211
+ async function boundedBody(response, maxBytes) {
212
+ const reader = response.body?.getReader();
213
+ if (!reader)
214
+ return;
215
+ const chunks = [];
216
+ let size = 0;
217
+ try {
218
+ while (true) {
219
+ const next = await reader.read();
220
+ if (next.done)
221
+ break;
222
+ size += next.value.byteLength;
223
+ if (size > maxBytes) {
224
+ reader.cancel().catch(() => {});
225
+ return;
226
+ }
227
+ chunks.push(next.value);
228
+ }
229
+ } finally {
230
+ reader.releaseLock();
231
+ }
232
+ const bytes = new Uint8Array(size);
233
+ let offset = 0;
234
+ for (const chunk of chunks) {
235
+ bytes.set(chunk, offset);
236
+ offset += chunk.byteLength;
237
+ }
238
+ return new TextDecoder().decode(bytes);
239
+ }
240
+ function createCachePolicy(options) {
241
+ const misses = new WeakMap;
242
+ const snapshots = new WeakMap;
243
+ const policy = async ({ http }) => {
244
+ const context = options.requireAccess(http.request);
245
+ if (http.request.headers.has("range") || http.request.headers.has("if-none-match") || http.request.headers.has("if-modified-since") || /(?:no-cache|no-store|max-age\s*=\s*0)/i.test(http.request.headers.get("cache-control") ?? ""))
246
+ return;
247
+ const headers = [...http.request.headers].filter(([name]) => name !== "x-request-id").sort(([a], [b]) => a.localeCompare(b));
248
+ const key = await policyKey([
249
+ "cache",
250
+ options.namespace,
251
+ options.route,
252
+ ...principalKey(context),
253
+ [...context.access.permissions].sort(),
254
+ http.request.url,
255
+ headers
256
+ ]);
257
+ let entry, generation;
258
+ try {
259
+ generation = await options.store.generation();
260
+ if (typeof generation !== "string" || !generation.length || generation.length > 128)
261
+ throw new Error("Invalid cache generation");
262
+ entry = await options.store.get(key, generation);
263
+ } catch {
264
+ throw new ApplicationError("Cache service unavailable", { status: 503, code: "HTTP_CACHE_UNAVAILABLE" });
265
+ }
266
+ if (entry && entry.expiresAt > Date.now()) {
267
+ if (typeof entry.body !== "string" || entry.contentType !== "application/json" || new TextEncoder().encode(entry.body).byteLength > options.maxBodyBytes) {
268
+ throw new ApplicationError("Cache service unavailable", { status: 503, code: "HTTP_CACHE_UNAVAILABLE" });
269
+ }
270
+ return new Response(entry.body, {
271
+ headers: { "content-type": "application/json", "cache-control": "private, no-store" }
272
+ });
273
+ }
274
+ misses.set(http.request, { key, generation });
275
+ };
276
+ policy.terminal = true;
277
+ policy.mapResponse = async (context) => {
278
+ if (!misses.has(context.http.request) || !mayStore(context))
279
+ return;
280
+ const value = context.encodeResponse ? await context.encodeResponse(context.response) : context.response;
281
+ const response = await WebStandardAdapter.response.map(value, context.http.set, context.http.request);
282
+ if (!(response instanceof Response))
283
+ throw new Error("HTTP response mapping did not return a Response");
284
+ const body = boundedBody(response.clone(), options.maxBodyBytes).catch(() => {
285
+ try {
286
+ options.onWriteError();
287
+ } catch {}
288
+ return;
289
+ });
290
+ snapshots.set(context.http.request, { response, body });
291
+ return response;
292
+ };
293
+ policy.afterResponse = async (context) => {
294
+ const miss = misses.get(context.http.request);
295
+ const snapshot = snapshots.get(context.http.request);
296
+ misses.delete(context.http.request);
297
+ snapshots.delete(context.http.request);
298
+ if (!miss || !snapshot || context.response !== snapshot.response || snapshot.response.status !== 200)
299
+ return;
300
+ const headers = snapshot.response.headers;
301
+ if ([...headers.keys()].some((name) => !["content-type", "x-request-id", "ratelimit-limit", "ratelimit-remaining"].includes(name)))
302
+ return;
303
+ try {
304
+ const body = await snapshot.body;
305
+ if (body === undefined)
306
+ return;
307
+ await options.store.set(miss.key, { body, contentType: "application/json", expiresAt: Date.now() + options.ttlMs }, miss.generation);
308
+ } catch {
309
+ try {
310
+ options.onWriteError();
311
+ } catch {}
312
+ }
313
+ };
314
+ return policy;
315
+ }
316
+
317
+ // src/http-policy-suite.ts
318
+ function policyOptions(value, allowed) {
319
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !allowed.includes(key))) {
320
+ throw new HttpPolicyConfigurationError("Invalid built-in HTTP policy options");
321
+ }
322
+ return value;
323
+ }
324
+ function positiveInteger(value, max) {
325
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > max) {
326
+ throw new HttpPolicyConfigurationError("HTTP policy numeric option is out of range");
327
+ }
328
+ return value;
329
+ }
330
+ async function policyKey(parts) {
331
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(parts)));
332
+ return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
333
+ }
334
+ function principalKey(context) {
335
+ return [
336
+ context.identity.issuer,
337
+ context.identity.clientId,
338
+ context.identity.subject,
339
+ context.access.projectId,
340
+ context.access.tenantId
341
+ ];
342
+ }
343
+ function createHttpPolicySuite(options) {
344
+ const verify = createSupAuthRequestContext(options.auth);
345
+ const verified = new WeakMap;
346
+ const requestContext = async (request) => {
347
+ verified.delete(request);
348
+ if (!request.headers.has("authorization")) {
349
+ const base = createSupaCloudRequestContext(request);
350
+ return { ...base, identity: Object.freeze({ authenticated: false }) };
351
+ }
352
+ const context = await verify(request);
353
+ verified.set(request, context);
354
+ return context;
355
+ };
356
+ const requireAccess = (request) => {
357
+ const context = verified.get(request);
358
+ if (!context)
359
+ throw new ApplicationError("Authentication required", {
360
+ status: 401,
361
+ code: "AUTHENTICATION_REQUIRED"
362
+ });
363
+ return context;
364
+ };
365
+ const registry = {
366
+ authenticated: (value) => {
367
+ if (value !== undefined)
368
+ throw new HttpPolicyConfigurationError("authenticated does not accept options");
369
+ return ({ http }) => {
370
+ requireAccess(http.request);
371
+ };
372
+ },
373
+ tenant: (value) => {
374
+ const { param } = policyOptions(value, ["param"]);
375
+ if (typeof param !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(param)) {
376
+ throw new HttpPolicyConfigurationError("tenant requires a route parameter name");
377
+ }
378
+ return ({ http }) => {
379
+ const context = requireAccess(http.request);
380
+ if (http.params[param] !== context.access.tenantId) {
381
+ throw new ApplicationError("Tenant access denied", { status: 403, code: "TENANT_ACCESS_DENIED" });
382
+ }
383
+ };
384
+ },
385
+ permission: (value) => {
386
+ const { allOf } = policyOptions(value, ["allOf"]);
387
+ if (!Array.isArray(allOf) || !allOf.length || Array.from(allOf).some((permission) => typeof permission !== "string" || !permission.trim())) {
388
+ throw new HttpPolicyConfigurationError("permission requires non-empty allOf permissions");
389
+ }
390
+ const required = [...new Set(allOf)];
391
+ return ({ http }) => {
392
+ const access = requireAccess(http.request).access;
393
+ if (!required.every((permission) => access.permissions.includes(permission))) {
394
+ throw new ApplicationError("Permission denied", { status: 403, code: "PERMISSION_DENIED" });
395
+ }
396
+ };
397
+ },
398
+ rateLimit: (value, route) => {
399
+ const config = policyOptions(value, ["limit", "windowMs"]);
400
+ const limit = positiveInteger(config.limit, 1e9);
401
+ const windowMs = positiveInteger(config.windowMs, 86400000);
402
+ const store = options.rateLimitStore;
403
+ if (!store)
404
+ throw new HttpPolicyConfigurationError("rateLimit requires an explicit store");
405
+ return async ({ http }) => {
406
+ const context = requireAccess(http.request);
407
+ const key = await policyKey(["rate", route.method, route.path, ...principalKey(context), limit, windowMs]);
408
+ let result;
409
+ try {
410
+ result = await store.consume(key, limit, windowMs);
411
+ } catch {
412
+ throw new ApplicationError("Rate limit service unavailable", { status: 503, code: "RATE_LIMIT_UNAVAILABLE" });
413
+ }
414
+ if (typeof result?.allowed !== "boolean" || !Number.isSafeInteger(result.remaining) || result.remaining < 0 || result.remaining > limit || !Number.isFinite(result.resetAt)) {
415
+ throw new ApplicationError("Rate limit service unavailable", { status: 503, code: "RATE_LIMIT_UNAVAILABLE" });
416
+ }
417
+ http.set.headers["ratelimit-limit"] = String(limit);
418
+ http.set.headers["ratelimit-remaining"] = String(result.remaining);
419
+ if (!result.allowed) {
420
+ const retryAfter = Math.max(1, Math.ceil((result.resetAt - Date.now()) / 1000));
421
+ return new Response("Too Many Requests", { status: 429, headers: { "retry-after": String(retryAfter) } });
422
+ }
423
+ };
424
+ },
425
+ cache: (value, route) => {
426
+ if (route.method !== "GET" || route.command) {
427
+ throw new HttpPolicyConfigurationError("cache is restricted to GET queries, never commands");
428
+ }
429
+ const config = policyOptions(value, ["ttlMs", "maxBodyBytes"]);
430
+ const ttlMs = positiveInteger(config.ttlMs, 86400000);
431
+ const maxBodyBytes = positiveInteger(config.maxBodyBytes ?? 262144, 16777216);
432
+ if (!options.cacheStore)
433
+ throw new HttpPolicyConfigurationError("cache requires an explicit store");
434
+ if (typeof options.cacheNamespace !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(options.cacheNamespace)) {
435
+ throw new HttpPolicyConfigurationError("cache requires an explicit deployment cacheNamespace");
436
+ }
437
+ return createCachePolicy({
438
+ store: options.cacheStore,
439
+ requireAccess,
440
+ ttlMs,
441
+ maxBodyBytes,
442
+ route: `${route.method} ${route.path}`,
443
+ namespace: options.cacheNamespace,
444
+ onWriteError: options.onCacheWriteError ?? (() => console.warn("supacloud: HTTP cache write failed"))
445
+ });
446
+ }
447
+ };
448
+ return { requestContext, httpPolicies: registry };
449
+ }
450
+ // src/http-policy-stores.ts
451
+ function capacity(value) {
452
+ if (!Number.isSafeInteger(value) || value <= 0)
453
+ throw new TypeError("Store capacity must be a positive integer");
454
+ }
455
+ function createMemoryHttpRateLimitStore(options = {}) {
456
+ const maxEntries = options.maxEntries ?? 1e4;
457
+ capacity(maxEntries);
458
+ const now = options.now ?? Date.now;
459
+ const entries = new Map;
460
+ return {
461
+ consume(key, limit, windowMs) {
462
+ capacity(limit);
463
+ capacity(windowMs);
464
+ const time = now();
465
+ let entry = entries.get(key);
466
+ if (!entry || entry.resetAt <= time) {
467
+ if (!entry && entries.size >= maxEntries) {
468
+ for (const [expiredKey, value] of entries) {
469
+ if (value.resetAt <= time)
470
+ entries.delete(expiredKey);
471
+ }
472
+ if (entries.size >= maxEntries)
473
+ throw new Error("Rate limit store capacity exhausted");
474
+ }
475
+ entry = { used: 0, resetAt: time + windowMs };
476
+ entries.set(key, entry);
477
+ }
478
+ const allowed = entry.used < limit;
479
+ if (allowed)
480
+ entry.used++;
481
+ return { allowed, remaining: Math.max(0, limit - entry.used), resetAt: entry.resetAt };
482
+ }
483
+ };
484
+ }
485
+ function createMemoryHttpCacheStore(options = {}) {
486
+ const maxEntries = options.maxEntries ?? 1000, maxBytes = options.maxBytes ?? 8 * 1024 * 1024;
487
+ capacity(maxEntries);
488
+ capacity(maxBytes);
489
+ const now = options.now ?? Date.now;
490
+ const entries = new Map;
491
+ let generation = crypto.randomUUID();
492
+ let bytes = 0;
493
+ const remove = (key) => {
494
+ bytes -= entries.get(key)?.bytes ?? 0;
495
+ entries.delete(key);
496
+ };
497
+ return {
498
+ generation: () => generation,
499
+ get(key, expected) {
500
+ if (expected !== generation)
501
+ return;
502
+ const entry = entries.get(key);
503
+ if (!entry)
504
+ return;
505
+ if (entry.value.expiresAt <= now()) {
506
+ remove(key);
507
+ return;
508
+ }
509
+ entries.delete(key);
510
+ entries.set(key, entry);
511
+ return { ...entry.value };
512
+ },
513
+ set(key, value, expected) {
514
+ if (expected !== generation)
515
+ return;
516
+ const size = new TextEncoder().encode(key + value.body + value.contentType).byteLength;
517
+ if (size > maxBytes || value.expiresAt <= now())
518
+ return;
519
+ remove(key);
520
+ while (entries.size >= maxEntries || bytes + size > maxBytes) {
521
+ const oldest = entries.keys().next().value;
522
+ if (oldest === undefined)
523
+ break;
524
+ remove(oldest);
525
+ }
526
+ entries.set(key, { value: { ...value }, bytes: size });
527
+ bytes += size;
528
+ },
529
+ clear() {
530
+ generation = crypto.randomUUID();
531
+ entries.clear();
532
+ bytes = 0;
533
+ }
534
+ };
535
+ }
536
+ // src/http-policy-postgres.ts
537
+ var HTTP_POLICY_STORE_SQL = `
538
+ CREATE SCHEMA IF NOT EXISTS supacloud_http;
539
+ REVOKE ALL ON SCHEMA supacloud_http FROM PUBLIC;
540
+ CREATE TABLE IF NOT EXISTS supacloud_http.rate_limits (
541
+ key text PRIMARY KEY CHECK (key ~ '^[a-f0-9]{64}$'),
542
+ used bigint NOT NULL CHECK (used > 0),
543
+ reset_at timestamptz NOT NULL
544
+ );
545
+ CREATE INDEX IF NOT EXISTS http_rate_limit_expiry ON supacloud_http.rate_limits(reset_at);
546
+ CREATE TABLE IF NOT EXISTS supacloud_http.response_cache (
547
+ key text PRIMARY KEY CHECK (key ~ '^[a-f0-9]{64}$'),
548
+ body text NOT NULL CHECK (octet_length(body) <= 16777216),
549
+ expires_at timestamptz NOT NULL,
550
+ generation uuid NOT NULL
551
+ );
552
+ ALTER TABLE supacloud_http.response_cache ADD COLUMN IF NOT EXISTS generation uuid
553
+ NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';
554
+ CREATE TABLE IF NOT EXISTS supacloud_http.cache_generation (
555
+ singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
556
+ generation uuid NOT NULL DEFAULT gen_random_uuid()
557
+ );
558
+ INSERT INTO supacloud_http.cache_generation(singleton) VALUES (true) ON CONFLICT DO NOTHING;
559
+ CREATE INDEX IF NOT EXISTS http_response_cache_expiry ON supacloud_http.response_cache(expires_at);
560
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_http FROM PUBLIC;
561
+ CREATE OR REPLACE FUNCTION supacloud_http.consume_rate_limit(p_key text, p_limit bigint, p_window_ms integer)
562
+ RETURNS TABLE(allowed boolean, remaining bigint, reset_at_ms numeric)
563
+ LANGUAGE plpgsql SET search_path = pg_catalog, supacloud_http AS $rate$
564
+ DECLARE
565
+ entry supacloud_http.rate_limits%ROWTYPE;
566
+ observed_at timestamptz;
567
+ BEGIN
568
+ IF p_limit < 1 OR p_limit > 1000000000 OR p_window_ms < 1 OR p_window_ms > 86400000 THEN
569
+ RAISE EXCEPTION 'Invalid rate limit window';
570
+ END IF;
571
+ LOOP
572
+ INSERT INTO supacloud_http.rate_limits(key, used, reset_at)
573
+ VALUES (p_key, 1, '-infinity') ON CONFLICT DO NOTHING;
574
+ SELECT * INTO entry FROM supacloud_http.rate_limits WHERE key=p_key FOR UPDATE;
575
+ EXIT WHEN FOUND;
576
+ -- Expiry pruning may remove a conflicting row before we acquire its lock.
577
+ END LOOP;
578
+ observed_at := clock_timestamp();
579
+ IF entry.reset_at <= observed_at THEN
580
+ entry.used := 1;
581
+ entry.reset_at := observed_at + p_window_ms * interval '1 millisecond';
582
+ ELSE
583
+ entry.used := least(entry.used + 1, p_limit + 1);
584
+ END IF;
585
+ UPDATE supacloud_http.rate_limits SET used=entry.used, reset_at=entry.reset_at WHERE key=p_key;
586
+ RETURN QUERY SELECT entry.used <= p_limit, greatest(p_limit-entry.used,0),
587
+ extract(epoch FROM entry.reset_at)*1000;
588
+ END
589
+ $rate$;
590
+ REVOKE ALL ON FUNCTION supacloud_http.consume_rate_limit(text,bigint,integer) FROM PUBLIC;
591
+ `;
592
+ function validKey(key) {
593
+ if (!/^[a-f0-9]{64}$/.test(key))
594
+ throw new TypeError("HTTP store keys must be SHA-256 digests");
595
+ }
596
+ function createPostgresHttpPolicyStores(database) {
597
+ const rateLimitStore = {
598
+ async consume(key, limit, windowMs) {
599
+ validKey(key);
600
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 1e9 || !Number.isSafeInteger(windowMs) || windowMs <= 0 || windowMs > 86400000) {
601
+ throw new TypeError("Invalid rate limit window");
602
+ }
603
+ const [row] = await database.query("SELECT * FROM supacloud_http.consume_rate_limit($1,$2,$3)", [key, limit, windowMs]);
604
+ if (!row || typeof row.allowed !== "boolean")
605
+ throw new Error("Invalid rate limit storage result");
606
+ return { allowed: row.allowed, remaining: Number(row.remaining), resetAt: Number(row.reset_at_ms) };
607
+ }
608
+ };
609
+ const cacheStore = {
610
+ async generation() {
611
+ const [row] = await database.query("SELECT generation FROM supacloud_http.cache_generation WHERE singleton", []);
612
+ if (!row || typeof row.generation !== "string")
613
+ throw new Error("Invalid cache generation");
614
+ return row.generation;
615
+ },
616
+ async get(key, generation) {
617
+ validKey(key);
618
+ const [row] = await database.query(`
619
+ SELECT body, extract(epoch FROM expires_at) * 1000 AS expires_at_ms
620
+ FROM supacloud_http.response_cache AS cache
621
+ JOIN supacloud_http.cache_generation AS current ON cache.generation = current.generation
622
+ WHERE key = $1 AND cache.generation = $2::uuid AND expires_at > clock_timestamp()
623
+ `, [key, generation]);
624
+ if (!row)
625
+ return;
626
+ if (typeof row.body !== "string")
627
+ throw new Error("Invalid cache storage result");
628
+ return { body: row.body, contentType: "application/json", expiresAt: Number(row.expires_at_ms) };
629
+ },
630
+ async set(key, value, generation) {
631
+ validKey(key);
632
+ if (value.contentType !== "application/json" || typeof value.body !== "string" || new TextEncoder().encode(value.body).byteLength > 16777216 || !Number.isSafeInteger(value.expiresAt) || value.expiresAt <= 0) {
633
+ throw new TypeError("Invalid cache entry");
634
+ }
635
+ await database.query(`
636
+ WITH current AS MATERIALIZED (
637
+ SELECT generation FROM supacloud_http.cache_generation
638
+ WHERE singleton AND generation=$4::uuid FOR SHARE
639
+ )
640
+ INSERT INTO supacloud_http.response_cache (key, body, expires_at, generation)
641
+ SELECT $1, $2, to_timestamp($3 / 1000.0), generation FROM current
642
+ ON CONFLICT (key) DO UPDATE SET body = excluded.body,
643
+ expires_at = excluded.expires_at, generation = excluded.generation
644
+ `, [key, value.body, value.expiresAt, generation]);
645
+ },
646
+ async clear() {
647
+ await database.query(`
648
+ WITH advanced AS (
649
+ UPDATE supacloud_http.cache_generation SET generation=gen_random_uuid() WHERE singleton RETURNING generation
650
+ )
651
+ DELETE FROM supacloud_http.response_cache WHERE EXISTS(SELECT 1 FROM advanced)
652
+ `, []);
653
+ }
654
+ };
655
+ return {
656
+ rateLimitStore,
657
+ cacheStore,
658
+ async prune() {
659
+ await database.query("DELETE FROM supacloud_http.rate_limits WHERE reset_at <= clock_timestamp()", []);
660
+ await database.query("DELETE FROM supacloud_http.response_cache WHERE expires_at <= clock_timestamp()", []);
661
+ }
662
+ };
663
+ }
39
664
  // src/execution.ts
40
665
  async function observeExecution(observer, event, next) {
41
666
  const emit = (phase, durationMs) => {
@@ -73,9 +698,10 @@ function executionTrace(context) {
73
698
 
74
699
  // src/schema_contract.ts
75
700
  import {
76
- getSchemaValidator,
77
- StatusMap,
78
- ElysiaCustomStatusResponse
701
+ Validator,
702
+ t,
703
+ StatusMap as StatusMap2,
704
+ ElysiaStatus
79
705
  } from "elysia";
80
706
  function defineRouteContract(schemas) {
81
707
  return Object.freeze({ ...schemas });
@@ -86,7 +712,7 @@ var RESPONSE_STATUS_MAX = 599;
86
712
  var RESPONSE_FAMILY_PATTERN = /^([1-5])(?:xx|XX)$/;
87
713
  var RESPONSE_STATUS_PATTERN = /^[1-5]\d{2}$/;
88
714
  function isSchema(value) {
89
- return value !== null && typeof value === "object" && ((TYPEBOX_KIND in value) || ("~standard" in value));
715
+ return value !== null && typeof value === "object" && ((TYPEBOX_KIND in value) || ("~kind" in value) || ("~standard" in value));
90
716
  }
91
717
  function isJsonSchemaObject(value) {
92
718
  if (value === null || typeof value !== "object" || Array.isArray(value))
@@ -147,8 +773,8 @@ function responseStatusDeclared(responses, status) {
147
773
  function responseStatusOf(value, configuredStatus) {
148
774
  if (value instanceof Response)
149
775
  return value.status;
150
- if (value instanceof ElysiaCustomStatusResponse) {
151
- const code = value.code;
776
+ if (value instanceof ElysiaStatus) {
777
+ const code = value.status;
152
778
  if (typeof code === "number" && Number.isInteger(code) && code >= RESPONSE_STATUS_MIN && code <= RESPONSE_STATUS_MAX) {
153
779
  return code;
154
780
  }
@@ -156,7 +782,7 @@ function responseStatusOf(value, configuredStatus) {
156
782
  if (typeof configuredStatus === "number")
157
783
  return configuredStatus;
158
784
  if (typeof configuredStatus === "string") {
159
- const mapped = StatusMap[configuredStatus];
785
+ const mapped = StatusMap2[configuredStatus];
160
786
  if (typeof mapped === "number")
161
787
  return mapped;
162
788
  }
@@ -262,7 +888,7 @@ function registerElysiaRoute(app, route) {
262
888
  }
263
889
  return value;
264
890
  };
265
- app.route(route.method, route.path, handler, toElysiaRouteSchema(route.contract));
891
+ app.method(route.method, route.path, toElysiaRouteSchema(route.contract), handler);
266
892
  return app;
267
893
  }
268
894
 
@@ -274,13 +900,19 @@ class SchemaContractError extends Error {
274
900
  }
275
901
  }
276
902
  function createSchemaDecoder(schema, options = {}) {
277
- const validator = getSchemaValidator(schema, {
278
- coerce: options.coerce ?? false,
903
+ const validator = Validator.create(schema, {
904
+ ...options.coerce ? { coerces: [[[
905
+ ["number", (value) => t.Numeric(value)],
906
+ ["integer", (value) => t.IntegerString(value)],
907
+ ["boolean", (value) => t.BooleanString(value)]
908
+ ]]] } : {},
279
909
  normalize: options.normalize ?? true
280
910
  });
281
911
  return (value) => {
282
912
  try {
283
- return validator.parse(value);
913
+ if (validator.isAsync)
914
+ throw new SchemaContractError;
915
+ return validator.FromSync(value);
284
916
  } catch {
285
917
  throw new SchemaContractError;
286
918
  }
@@ -296,7 +928,7 @@ function defineJsonContract(schemas, request, options = {}) {
296
928
  }
297
929
 
298
930
  // src/documentation.ts
299
- import { Elysia } from "elysia";
931
+ import { Elysia as Elysia2 } from "elysia";
300
932
  function normalizePath(value, fallback) {
301
933
  const path = value ?? fallback;
302
934
  if (!path.trim() || !path.startsWith("/") || path.includes("?") || path.includes("#")) {
@@ -373,7 +1005,7 @@ function renderViewerPage(title, resourcePath, mode) {
373
1005
  </html>`;
374
1006
  }
375
1007
  function createDocumentationPlugin(options = {}) {
376
- const app = new Elysia({ name: "supacloud:documentation" });
1008
+ const app = new Elysia2({ name: "supacloud:documentation" });
377
1009
  const paths = new Set;
378
1010
  const reserve = (path) => {
379
1011
  if (paths.has(path))
@@ -386,12 +1018,12 @@ function createDocumentationPlugin(options = {}) {
386
1018
  reserve(specPath);
387
1019
  reserve(uiPath);
388
1020
  const title = options.openApi.title ?? "OpenAPI Documentation";
389
- app.get(specPath, async () => jsonResponse(await resolveSource(options.openApi.document)), {
1021
+ app.get(specPath, {
390
1022
  detail: { hide: true }
391
- });
392
- app.get(uiPath, () => new Response(renderViewerPage(title, specPath, "json"), {
1023
+ }, async () => jsonResponse(await resolveSource(options.openApi.document)));
1024
+ app.get(uiPath, { detail: { hide: true } }, () => new Response(renderViewerPage(title, specPath, "json"), {
393
1025
  headers: { "content-type": "text/html; charset=utf-8" }
394
- }), { detail: { hide: true } });
1026
+ }));
395
1027
  }
396
1028
  if (options.graphql) {
397
1029
  const schemaPath = normalizePath(options.graphql.schemaPath, "/graphql/schema.graphql");
@@ -399,15 +1031,48 @@ function createDocumentationPlugin(options = {}) {
399
1031
  reserve(schemaPath);
400
1032
  reserve(uiPath);
401
1033
  const title = options.graphql.title ?? "GraphQL Schema";
402
- app.get(schemaPath, async () => textResponse(await resolveSource(options.graphql.schema)), {
1034
+ app.get(schemaPath, {
403
1035
  detail: { hide: true }
404
- });
405
- app.get(uiPath, () => new Response(renderViewerPage(title, schemaPath, "text"), {
1036
+ }, async () => textResponse(await resolveSource(options.graphql.schema)));
1037
+ app.get(uiPath, { detail: { hide: true } }, () => new Response(renderViewerPage(title, schemaPath, "text"), {
406
1038
  headers: { "content-type": "text/html; charset=utf-8" }
407
- }), { detail: { hide: true } });
1039
+ }));
408
1040
  }
409
1041
  return app;
410
1042
  }
1043
+
1044
+ // src/command-binding.ts
1045
+ function bindCompiledCommand(options) {
1046
+ const { module, command, governance, observer, handler, decode, preview } = options;
1047
+ return {
1048
+ execute: (input, context) => executeCompiledCommand({
1049
+ module,
1050
+ command,
1051
+ governance,
1052
+ observer,
1053
+ decode,
1054
+ input,
1055
+ request: context.request,
1056
+ requestContext: context.requestContext,
1057
+ services: context.services,
1058
+ scope: context.scope,
1059
+ handler: (value) => handler(value, context)
1060
+ }),
1061
+ ...preview ? {
1062
+ preview: (input, context) => previewCompiledCommand({
1063
+ module,
1064
+ command,
1065
+ governance,
1066
+ input,
1067
+ request: context.request,
1068
+ requestContext: context.requestContext,
1069
+ services: context.services,
1070
+ scope: context.scope,
1071
+ preview: (value) => preview(value, context)
1072
+ })
1073
+ } : {}
1074
+ };
1075
+ }
411
1076
  // src/persistent-command.ts
412
1077
  import {
413
1078
  CommandError,
@@ -434,81 +1099,6 @@ function createPersistentCommandAdapter(command, options) {
434
1099
  }
435
1100
  };
436
1101
  }
437
- // src/identity.ts
438
- import { createRemoteJWKSet, jwtVerify, errors } from "jose";
439
- function httpsUrl(value) {
440
- const url = new URL(value);
441
- if (url.protocol !== "https:" || url.username || url.password || url.hash) {
442
- throw new TypeError("Identity endpoints must be HTTPS URLs without credentials or fragments");
443
- }
444
- return url;
445
- }
446
- function createSupAuthRequestContext(options) {
447
- httpsUrl(options.issuer);
448
- const jwksUrl = httpsUrl(options.jwksUrl);
449
- if (!options.audience.trim() || !options.projectId.trim() || !options.clientId?.trim()) {
450
- throw new TypeError("SupAuth audience, clientId and projectId are required");
451
- }
452
- const algorithms = [...options.algorithms ?? ["ES256", "RS256"]];
453
- if (!algorithms.length || algorithms.some((value) => value !== "ES256" && value !== "RS256")) {
454
- throw new TypeError("SupAuth requires ES256 or RS256");
455
- }
456
- const { issuer, audience, clientId, projectId, resolveAccess } = options;
457
- const keyResolver = options.keyResolver ?? createRemoteJWKSet(jwksUrl, { timeoutDuration: 5000 });
458
- return async (request) => {
459
- const match = request.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/i);
460
- const token = match?.[1];
461
- if (!token || token.length > 16384)
462
- throw unauthenticated();
463
- let subject;
464
- try {
465
- const { payload } = await jwtVerify(token, keyResolver, {
466
- issuer,
467
- audience,
468
- algorithms,
469
- requiredClaims: ["sub", "exp", "iat"]
470
- });
471
- if (typeof payload.sub !== "string" || !payload.sub.trim() || payload.sub.length > 1024 || /[\u0000-\u001f\u007f]/.test(payload.sub)) {
472
- throw unauthenticated();
473
- }
474
- const application = payload.client_id ?? payload.azp;
475
- if (application !== clientId || payload.role !== "authenticated" || payload.client_id !== undefined && payload.client_id !== clientId || payload.azp !== undefined && payload.azp !== clientId) {
476
- throw unauthenticated();
477
- }
478
- subject = payload.sub;
479
- } catch (error) {
480
- if (error instanceof ApplicationError || error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid || error instanceof errors.JWSInvalid || error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JOSEAlgNotAllowed || error instanceof errors.JOSENotSupported || error instanceof errors.JWKSNoMatchingKey)
481
- throw unauthenticated();
482
- throw new ApplicationError("Identity verification service unavailable", {
483
- status: 503,
484
- code: "AUTHENTICATION_UNAVAILABLE"
485
- });
486
- }
487
- const identity = { authenticated: true, subject, issuer, clientId };
488
- Object.defineProperty(identity, "accessToken", { value: token, enumerable: false });
489
- Object.freeze(identity);
490
- const access = await resolveAccess(identity, request);
491
- if (!access || access.projectId !== projectId || typeof access.tenantId !== "string" || !access.tenantId.trim() || !Array.isArray(access.permissions) || access.permissions.some((permission) => typeof permission !== "string" || !permission.trim())) {
492
- throw new ApplicationError("Application access denied", { status: 403, code: "APPLICATION_ACCESS_DENIED" });
493
- }
494
- const context = createSupaCloudRequestContext(request);
495
- return {
496
- ...context,
497
- identity,
498
- access: Object.freeze({
499
- projectId,
500
- tenantId: access.tenantId,
501
- permissions: Object.freeze([...new Set(access.permissions)])
502
- })
503
- };
504
- };
505
- }
506
- function unauthenticated() {
507
- return new ApplicationError("Authenticated user context is required", {
508
- status: 401,
509
- code: "AUTHENTICATION_REQUIRED"
510
- });
511
- }
512
1102
  // src/feature.ts
513
1103
  function assertFeatureTransition(spec, state, event) {
514
1104
  const transition = Object.hasOwn(spec.transitions, event) ? spec.transitions[event] : undefined;
@@ -1252,6 +1842,7 @@ function createWorker(options) {
1252
1842
  }
1253
1843
 
1254
1844
  // src/index.ts
1845
+ setupTypebox();
1255
1846
  var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
1256
1847
  var EXECUTION_ID_HEADER = "x-sb-execution-id";
1257
1848
  var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
@@ -1386,6 +1977,19 @@ function validatedJsonResponse(validate, value, init) {
1386
1977
  headers.set("content-type", "application/json");
1387
1978
  return new Response(body, { ...init, headers });
1388
1979
  }
1980
+ var httpPluginIds = new WeakMap;
1981
+ var nextHttpPluginId = 0;
1982
+ function contextPlugin(http) {
1983
+ let seed = http ? httpPluginIds.get(http) : 0;
1984
+ if (http && seed === undefined) {
1985
+ seed = ++nextHttpPluginId;
1986
+ httpPluginIds.set(http, seed);
1987
+ }
1988
+ return new Elysia3({ name: "supacloud:http-context", seed }).use(http ?? new Elysia3).as("plugin");
1989
+ }
1990
+ function mountHttp(http, name, normalize = true) {
1991
+ return new Elysia3({ name, normalize }).use(contextPlugin(http));
1992
+ }
1389
1993
  function decodeJobContract(schema, value, boundary) {
1390
1994
  if (schema === undefined)
1391
1995
  return value;
@@ -1422,7 +2026,7 @@ function bearerToken(request) {
1422
2026
  var createSupaCloudRequestContext = (request) => {
1423
2027
  const subject = safeHeaderValue(request.headers.get(VERIFIED_JWT_SUBJECT_HEADER), 1024);
1424
2028
  const accessToken = subject ? bearerToken(request) : undefined;
1425
- const requestId = safeHeaderValue(request.headers.get(EXECUTION_ID_HEADER), 256) ?? safeHeaderValue(request.headers.get("x-request-id"), 256) ?? crypto.randomUUID();
2029
+ const requestId = httpRequestId(request) ?? safeHeaderValue(request.headers.get(EXECUTION_ID_HEADER), 256) ?? safeHeaderValue(request.headers.get("x-request-id"), 256) ?? crypto.randomUUID();
1426
2030
  const idempotencyKey = safeHeaderValue(request.headers.get(IDEMPOTENCY_KEY_HEADER), 512);
1427
2031
  const identity = {
1428
2032
  authenticated: subject !== undefined,
@@ -1737,7 +2341,9 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1737
2341
  "data",
1738
2342
  "input",
1739
2343
  "result",
1740
- "request"
2344
+ "request",
2345
+ "parse",
2346
+ "allowDeleteBody"
1741
2347
  ]);
1742
2348
  for (const controller of compiled.controllers) {
1743
2349
  for (const route of controller.routes) {
@@ -1745,6 +2351,9 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1745
2351
  if (!supportedMethods.has(route.method) || unsupported.length > 0) {
1746
2352
  throw new ApplicationError(`Unsupported compiled route ${route.method} ${controller.path}${route.path}` + (unsupported.length > 0 ? `: ${unsupported.join(", ")}` : ""), { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
1747
2353
  }
2354
+ if (route.parse !== undefined && route.parse !== "none" || route.parse === "none" && (route.body !== undefined || route.contract?.body !== "domain" || !route.contract.evidence?.trim()) || route.allowDeleteBody !== undefined && (route.allowDeleteBody !== true || route.method !== "DELETE" || route.body === undefined)) {
2355
+ throw new ApplicationError(`Invalid compiled body policy on ${route.method} ${controller.path}${route.path}`, { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
2356
+ }
1748
2357
  }
1749
2358
  }
1750
2359
  const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
@@ -1765,12 +2374,16 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1765
2374
  const requestContexts = new WeakMap;
1766
2375
  const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
1767
2376
  const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
1768
- const plugin = new Elysia2({ name: `supacloud:${compiled.name}`, normalize: options.normalize ?? true }).decorate("services", services);
2377
+ const plugin = mountHttp(options.http, `supacloud:${compiled.name}`, options.normalize ?? true).derive(async (context) => {
2378
+ const requestContext = await ctxFactory(context.request, context);
2379
+ requestContexts.set(context.request, requestContext);
2380
+ return { services, requestContext };
2381
+ });
1769
2382
  const createRequestScope = compiled.createRequestScope;
1770
2383
  const requestScopes = new WeakMap;
1771
2384
  if (compiled.destroyRequestScope) {
1772
2385
  const destroyRequestScope = compiled.destroyRequestScope;
1773
- plugin.onAfterResponse(async ({ request }) => {
2386
+ plugin.afterResponse(async ({ request }) => {
1774
2387
  const scope = requestScopes.get(request);
1775
2388
  if (!scope)
1776
2389
  return;
@@ -1782,12 +2395,8 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1782
2395
  }
1783
2396
  });
1784
2397
  }
1785
- plugin.resolve(async ({ request }) => {
1786
- const requestContext = await ctxFactory(request);
1787
- requestContexts.set(request, requestContext);
1788
- return { requestContext };
1789
- });
1790
- plugin.onError(async ({ code, error, request }) => {
2398
+ plugin.error(async ({ error, request }) => {
2399
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code.toUpperCase().replaceAll("-", "_") : "UNKNOWN";
1791
2400
  const context = {
1792
2401
  request,
1793
2402
  requestContext: requestContexts.get(request),
@@ -1796,12 +2405,92 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1796
2405
  const mapped = await options.errorMapper?.(error, context);
1797
2406
  return mapped ?? defaultErrorResponse(error, code);
1798
2407
  });
2408
+ const routeRegistrar = plugin;
1799
2409
  for (const controller of compiled.controllers) {
1800
2410
  for (const route of controller.routes) {
1801
2411
  const path = joinPaths(controller.path, route.path);
1802
- const schema = toElysiaRouteSchema(route);
2412
+ const documentation = route.data?.openapi;
2413
+ const hidden = documentation !== null && typeof documentation === "object" && "hide" in documentation && documentation.hide === true;
2414
+ const tags = documentation !== null && typeof documentation === "object" && "tags" in documentation && Array.isArray(documentation.tags) ? documentation.tags.filter((tag) => typeof tag === "string") : undefined;
2415
+ const schema = {
2416
+ ...toElysiaRouteSchema(route),
2417
+ ...route.parse === "none" ? { parse: "none" } : {},
2418
+ ...route.title || hidden || tags ? { detail: {
2419
+ ...route.title ? { summary: route.title } : {},
2420
+ ...hidden ? { hide: true } : {},
2421
+ ...tags ? { tags } : {}
2422
+ } } : {}
2423
+ };
2424
+ const policies = compileHttpPolicies(route, path, options.httpPolicies);
2425
+ if (policies.length > 0) {
2426
+ Object.assign(schema, {
2427
+ beforeHandle: async (context) => {
2428
+ for (const policy of policies) {
2429
+ const result = await policy({
2430
+ http: context,
2431
+ requestContext: requestContexts.get(context.request)
2432
+ });
2433
+ if (result instanceof Response)
2434
+ return result;
2435
+ if (result !== undefined) {
2436
+ throw new Error("HTTP policies must return Response or undefined");
2437
+ }
2438
+ }
2439
+ }
2440
+ });
2441
+ }
2442
+ const responsePolicies = policies.filter((policy) => policy.afterResponse);
2443
+ const mappingPolicies = policies.filter((policy) => policy.mapResponse);
2444
+ if (mappingPolicies.length > 0) {
2445
+ const responseValidators = Validator2.response(schema.response, { normalize: options.normalize ?? true });
2446
+ Object.assign(schema, {
2447
+ mapResponse: async (context) => {
2448
+ let response = context.responseValue;
2449
+ let mapped;
2450
+ for (const policy of mappingPolicies) {
2451
+ const result = await policy.mapResponse({
2452
+ http: context,
2453
+ requestContext: requestContexts.get(context.request),
2454
+ response,
2455
+ encodeResponse: async (value) => {
2456
+ const validator = responseValidators?.[responseStatusOf(value, context.set.status)];
2457
+ if (!validator)
2458
+ return value;
2459
+ if (!("EncodeFrom" in validator) || typeof validator.EncodeFrom !== "function") {
2460
+ throw new ApplicationError("Response validation failed", { code: "RESPONSE_VALIDATION_FAILED" });
2461
+ }
2462
+ if ("mayReturnPromise" in validator && validator.mayReturnPromise && "From" in validator && typeof validator.From === "function") {
2463
+ return await Reflect.apply(validator.From, validator, [value, "response", true]);
2464
+ }
2465
+ return await Reflect.apply(validator.EncodeFrom, validator, [value, "response"]);
2466
+ }
2467
+ });
2468
+ if (result !== undefined) {
2469
+ if (!(result instanceof Response))
2470
+ throw new Error("HTTP response policies must return Response or undefined");
2471
+ mapped = result;
2472
+ response = result;
2473
+ }
2474
+ }
2475
+ return mapped;
2476
+ }
2477
+ });
2478
+ }
2479
+ if (responsePolicies.length > 0) {
2480
+ Object.assign(schema, {
2481
+ afterResponse: async (context) => {
2482
+ for (const policy of responsePolicies) {
2483
+ await policy.afterResponse({
2484
+ http: context,
2485
+ requestContext: requestContexts.get(context.request),
2486
+ response: context.responseValue
2487
+ });
2488
+ }
2489
+ }
2490
+ });
2491
+ }
1803
2492
  const handler = async (ctx) => {
1804
- const requestContext = ctx.requestContext ?? await ctxFactory(ctx.request);
2493
+ const requestContext = requestContexts.get(ctx.request);
1805
2494
  const execute = async () => {
1806
2495
  const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
1807
2496
  if (requestScope && compiled.destroyRequestScope)
@@ -1893,25 +2582,25 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1893
2582
  };
1894
2583
  switch (route.method) {
1895
2584
  case "GET":
1896
- plugin.get(path, handler, schema);
2585
+ routeRegistrar.get(path, schema, handler);
1897
2586
  break;
1898
2587
  case "POST":
1899
- plugin.post(path, handler, schema);
2588
+ routeRegistrar.post(path, schema, handler);
1900
2589
  break;
1901
2590
  case "PUT":
1902
- plugin.put(path, handler, schema);
2591
+ routeRegistrar.put(path, schema, handler);
1903
2592
  break;
1904
2593
  case "PATCH":
1905
- plugin.patch(path, handler, schema);
2594
+ routeRegistrar.patch(path, schema, handler);
1906
2595
  break;
1907
2596
  case "DELETE":
1908
- plugin.delete(path, handler, schema);
2597
+ routeRegistrar.delete(path, schema, handler);
1909
2598
  break;
1910
2599
  case "HEAD":
1911
- plugin.head(path, handler, schema);
2600
+ routeRegistrar.head(path, schema, handler);
1912
2601
  break;
1913
2602
  case "OPTIONS":
1914
- plugin.options(path, handler, schema);
2603
+ routeRegistrar.options(path, schema, handler);
1915
2604
  break;
1916
2605
  }
1917
2606
  }
@@ -2013,16 +2702,16 @@ function isPublicApplicationError(error) {
2013
2702
  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;
2014
2703
  }
2015
2704
  function createApplication(options) {
2016
- const app = new Elysia2({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true });
2705
+ const app = new Elysia3({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true }).use(contextPlugin(options.http).as("global"));
2017
2706
  if (options.documentation)
2018
2707
  app.use(createDocumentationPlugin(options.documentation));
2019
2708
  const configuredContextFactory = options.requestContext ?? defaultRequestContext;
2020
2709
  const contextCache = new WeakMap;
2021
- const ctxFactory = (request) => {
2710
+ const ctxFactory = (request, context) => {
2022
2711
  const cached = contextCache.get(request);
2023
2712
  if (cached)
2024
2713
  return cached;
2025
- const pending = Promise.resolve(configuredContextFactory(request));
2714
+ const pending = Promise.resolve(configuredContextFactory(request, context));
2026
2715
  contextCache.set(request, pending);
2027
2716
  return pending;
2028
2717
  };
@@ -2030,7 +2719,7 @@ function createApplication(options) {
2030
2719
  for (const module of options.modules ?? []) {
2031
2720
  const services = module.createServices(options.deps ?? {}, imported);
2032
2721
  imported[module.name] = services;
2033
- app.use(createModulePlugin(module, services, ctxFactory, options, imported));
2722
+ app.use(createModulePlugin(module, services, ctxFactory, { ...options, http: undefined }, imported));
2034
2723
  }
2035
2724
  return app;
2036
2725
  }
@@ -2040,6 +2729,8 @@ function createTestApp(options) {
2040
2729
  export {
2041
2730
  ApplicationError,
2042
2731
  EXECUTION_ID_HEADER,
2732
+ HTTP_POLICY_STORE_SQL,
2733
+ HttpPolicyConfigurationError,
2043
2734
  IDEMPOTENCY_KEY_HEADER,
2044
2735
  SchemaContractError,
2045
2736
  SupaCloudWorker,
@@ -2048,16 +2739,22 @@ export {
2048
2739
  WorkerRegistrationError,
2049
2740
  assertFeatureTransition,
2050
2741
  assertResponseStatusDeclared,
2742
+ bindCompiledCommand,
2051
2743
  composeAspects,
2052
2744
  composeCommandExecutors,
2053
2745
  createApplication,
2054
2746
  createCommandAuthorizationAdapter,
2055
2747
  createCommandExecutor,
2056
2748
  createDocumentationPlugin,
2749
+ createHttpPolicySuite,
2750
+ createHttpTelemetry,
2751
+ createMemoryHttpCacheStore,
2752
+ createMemoryHttpRateLimitStore,
2057
2753
  createMemoryPolicy,
2058
2754
  createMemorySandbox,
2059
2755
  createModulePlugin,
2060
2756
  createPersistentCommandAdapter,
2757
+ createPostgresHttpPolicyStores,
2061
2758
  createQueueWorkerTransport,
2062
2759
  createSchemaDecoder,
2063
2760
  createSupAuthRequestContext,