@supacloud/elysia 0.18.0 → 0.19.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 } from "elysia";
3
3
  import { decodeCommandPreview } from "@supacloud/contracts";
4
4
 
5
5
  // src/command-errors.ts
@@ -36,6 +36,628 @@ 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().onRequest(({ 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
+ }).onError(() => {
93
+ return;
94
+ }).onAfterResponse(async ({ request, 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 { mapResponse } from "elysia/adapter/web-standard/handler";
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 response = await mapResponse(context.response, context.http.set, context.http.request);
281
+ const body = boundedBody(response.clone(), options.maxBodyBytes).catch(() => {
282
+ try {
283
+ options.onWriteError();
284
+ } catch {}
285
+ return;
286
+ });
287
+ snapshots.set(context.http.request, { response, body });
288
+ return response;
289
+ };
290
+ policy.afterResponse = async (context) => {
291
+ const miss = misses.get(context.http.request);
292
+ const snapshot = snapshots.get(context.http.request);
293
+ misses.delete(context.http.request);
294
+ snapshots.delete(context.http.request);
295
+ if (!miss || !snapshot || context.response !== snapshot.response || snapshot.response.status !== 200)
296
+ return;
297
+ const headers = snapshot.response.headers;
298
+ if ([...headers.keys()].some((name) => !["content-type", "x-request-id", "ratelimit-limit", "ratelimit-remaining"].includes(name)))
299
+ return;
300
+ try {
301
+ const body = await snapshot.body;
302
+ if (body === undefined)
303
+ return;
304
+ await options.store.set(miss.key, { body, contentType: "application/json", expiresAt: Date.now() + options.ttlMs }, miss.generation);
305
+ } catch {
306
+ try {
307
+ options.onWriteError();
308
+ } catch {}
309
+ }
310
+ };
311
+ return policy;
312
+ }
313
+
314
+ // src/http-policy-suite.ts
315
+ function policyOptions(value, allowed) {
316
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !allowed.includes(key))) {
317
+ throw new HttpPolicyConfigurationError("Invalid built-in HTTP policy options");
318
+ }
319
+ return value;
320
+ }
321
+ function positiveInteger(value, max) {
322
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > max) {
323
+ throw new HttpPolicyConfigurationError("HTTP policy numeric option is out of range");
324
+ }
325
+ return value;
326
+ }
327
+ async function policyKey(parts) {
328
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(parts)));
329
+ return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
330
+ }
331
+ function principalKey(context) {
332
+ return [
333
+ context.identity.issuer,
334
+ context.identity.clientId,
335
+ context.identity.subject,
336
+ context.access.projectId,
337
+ context.access.tenantId
338
+ ];
339
+ }
340
+ function createHttpPolicySuite(options) {
341
+ const verify = createSupAuthRequestContext(options.auth);
342
+ const verified = new WeakMap;
343
+ const requestContext = async (request) => {
344
+ verified.delete(request);
345
+ if (!request.headers.has("authorization")) {
346
+ const base = createSupaCloudRequestContext(request);
347
+ return { ...base, identity: Object.freeze({ authenticated: false }) };
348
+ }
349
+ const context = await verify(request);
350
+ verified.set(request, context);
351
+ return context;
352
+ };
353
+ const requireAccess = (request) => {
354
+ const context = verified.get(request);
355
+ if (!context)
356
+ throw new ApplicationError("Authentication required", {
357
+ status: 401,
358
+ code: "AUTHENTICATION_REQUIRED"
359
+ });
360
+ return context;
361
+ };
362
+ const registry = {
363
+ authenticated: (value) => {
364
+ if (value !== undefined)
365
+ throw new HttpPolicyConfigurationError("authenticated does not accept options");
366
+ return ({ http }) => {
367
+ requireAccess(http.request);
368
+ };
369
+ },
370
+ tenant: (value) => {
371
+ const { param } = policyOptions(value, ["param"]);
372
+ if (typeof param !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(param)) {
373
+ throw new HttpPolicyConfigurationError("tenant requires a route parameter name");
374
+ }
375
+ return ({ http }) => {
376
+ const context = requireAccess(http.request);
377
+ if (http.params[param] !== context.access.tenantId) {
378
+ throw new ApplicationError("Tenant access denied", { status: 403, code: "TENANT_ACCESS_DENIED" });
379
+ }
380
+ };
381
+ },
382
+ permission: (value) => {
383
+ const { allOf } = policyOptions(value, ["allOf"]);
384
+ if (!Array.isArray(allOf) || !allOf.length || Array.from(allOf).some((permission) => typeof permission !== "string" || !permission.trim())) {
385
+ throw new HttpPolicyConfigurationError("permission requires non-empty allOf permissions");
386
+ }
387
+ const required = [...new Set(allOf)];
388
+ return ({ http }) => {
389
+ const access = requireAccess(http.request).access;
390
+ if (!required.every((permission) => access.permissions.includes(permission))) {
391
+ throw new ApplicationError("Permission denied", { status: 403, code: "PERMISSION_DENIED" });
392
+ }
393
+ };
394
+ },
395
+ rateLimit: (value, route) => {
396
+ const config = policyOptions(value, ["limit", "windowMs"]);
397
+ const limit = positiveInteger(config.limit, 1e9);
398
+ const windowMs = positiveInteger(config.windowMs, 86400000);
399
+ const store = options.rateLimitStore;
400
+ if (!store)
401
+ throw new HttpPolicyConfigurationError("rateLimit requires an explicit store");
402
+ return async ({ http }) => {
403
+ const context = requireAccess(http.request);
404
+ const key = await policyKey(["rate", route.method, route.path, ...principalKey(context), limit, windowMs]);
405
+ let result;
406
+ try {
407
+ result = await store.consume(key, limit, windowMs);
408
+ } catch {
409
+ throw new ApplicationError("Rate limit service unavailable", { status: 503, code: "RATE_LIMIT_UNAVAILABLE" });
410
+ }
411
+ if (typeof result?.allowed !== "boolean" || !Number.isSafeInteger(result.remaining) || result.remaining < 0 || result.remaining > limit || !Number.isFinite(result.resetAt)) {
412
+ throw new ApplicationError("Rate limit service unavailable", { status: 503, code: "RATE_LIMIT_UNAVAILABLE" });
413
+ }
414
+ http.set.headers["ratelimit-limit"] = String(limit);
415
+ http.set.headers["ratelimit-remaining"] = String(result.remaining);
416
+ if (!result.allowed) {
417
+ const retryAfter = Math.max(1, Math.ceil((result.resetAt - Date.now()) / 1000));
418
+ return new Response("Too Many Requests", { status: 429, headers: { "retry-after": String(retryAfter) } });
419
+ }
420
+ };
421
+ },
422
+ cache: (value, route) => {
423
+ if (route.method !== "GET" || route.command) {
424
+ throw new HttpPolicyConfigurationError("cache is restricted to GET queries, never commands");
425
+ }
426
+ const config = policyOptions(value, ["ttlMs", "maxBodyBytes"]);
427
+ const ttlMs = positiveInteger(config.ttlMs, 86400000);
428
+ const maxBodyBytes = positiveInteger(config.maxBodyBytes ?? 262144, 16777216);
429
+ if (!options.cacheStore)
430
+ throw new HttpPolicyConfigurationError("cache requires an explicit store");
431
+ if (typeof options.cacheNamespace !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(options.cacheNamespace)) {
432
+ throw new HttpPolicyConfigurationError("cache requires an explicit deployment cacheNamespace");
433
+ }
434
+ return createCachePolicy({
435
+ store: options.cacheStore,
436
+ requireAccess,
437
+ ttlMs,
438
+ maxBodyBytes,
439
+ route: `${route.method} ${route.path}`,
440
+ namespace: options.cacheNamespace,
441
+ onWriteError: options.onCacheWriteError ?? (() => console.warn("supacloud: HTTP cache write failed"))
442
+ });
443
+ }
444
+ };
445
+ return { requestContext, httpPolicies: registry };
446
+ }
447
+ // src/http-policy-stores.ts
448
+ function capacity(value) {
449
+ if (!Number.isSafeInteger(value) || value <= 0)
450
+ throw new TypeError("Store capacity must be a positive integer");
451
+ }
452
+ function createMemoryHttpRateLimitStore(options = {}) {
453
+ const maxEntries = options.maxEntries ?? 1e4;
454
+ capacity(maxEntries);
455
+ const now = options.now ?? Date.now;
456
+ const entries = new Map;
457
+ return {
458
+ consume(key, limit, windowMs) {
459
+ capacity(limit);
460
+ capacity(windowMs);
461
+ const time = now();
462
+ let entry = entries.get(key);
463
+ if (!entry || entry.resetAt <= time) {
464
+ if (!entry && entries.size >= maxEntries) {
465
+ for (const [expiredKey, value] of entries) {
466
+ if (value.resetAt <= time)
467
+ entries.delete(expiredKey);
468
+ }
469
+ if (entries.size >= maxEntries)
470
+ throw new Error("Rate limit store capacity exhausted");
471
+ }
472
+ entry = { used: 0, resetAt: time + windowMs };
473
+ entries.set(key, entry);
474
+ }
475
+ const allowed = entry.used < limit;
476
+ if (allowed)
477
+ entry.used++;
478
+ return { allowed, remaining: Math.max(0, limit - entry.used), resetAt: entry.resetAt };
479
+ }
480
+ };
481
+ }
482
+ function createMemoryHttpCacheStore(options = {}) {
483
+ const maxEntries = options.maxEntries ?? 1000, maxBytes = options.maxBytes ?? 8 * 1024 * 1024;
484
+ capacity(maxEntries);
485
+ capacity(maxBytes);
486
+ const now = options.now ?? Date.now;
487
+ const entries = new Map;
488
+ let generation = crypto.randomUUID();
489
+ let bytes = 0;
490
+ const remove = (key) => {
491
+ bytes -= entries.get(key)?.bytes ?? 0;
492
+ entries.delete(key);
493
+ };
494
+ return {
495
+ generation: () => generation,
496
+ get(key, expected) {
497
+ if (expected !== generation)
498
+ return;
499
+ const entry = entries.get(key);
500
+ if (!entry)
501
+ return;
502
+ if (entry.value.expiresAt <= now()) {
503
+ remove(key);
504
+ return;
505
+ }
506
+ entries.delete(key);
507
+ entries.set(key, entry);
508
+ return { ...entry.value };
509
+ },
510
+ set(key, value, expected) {
511
+ if (expected !== generation)
512
+ return;
513
+ const size = new TextEncoder().encode(key + value.body + value.contentType).byteLength;
514
+ if (size > maxBytes || value.expiresAt <= now())
515
+ return;
516
+ remove(key);
517
+ while (entries.size >= maxEntries || bytes + size > maxBytes) {
518
+ const oldest = entries.keys().next().value;
519
+ if (oldest === undefined)
520
+ break;
521
+ remove(oldest);
522
+ }
523
+ entries.set(key, { value: { ...value }, bytes: size });
524
+ bytes += size;
525
+ },
526
+ clear() {
527
+ generation = crypto.randomUUID();
528
+ entries.clear();
529
+ bytes = 0;
530
+ }
531
+ };
532
+ }
533
+ // src/http-policy-postgres.ts
534
+ var HTTP_POLICY_STORE_SQL = `
535
+ CREATE SCHEMA IF NOT EXISTS supacloud_http;
536
+ REVOKE ALL ON SCHEMA supacloud_http FROM PUBLIC;
537
+ CREATE TABLE IF NOT EXISTS supacloud_http.rate_limits (
538
+ key text PRIMARY KEY CHECK (key ~ '^[a-f0-9]{64}$'),
539
+ used bigint NOT NULL CHECK (used > 0),
540
+ reset_at timestamptz NOT NULL
541
+ );
542
+ CREATE INDEX IF NOT EXISTS http_rate_limit_expiry ON supacloud_http.rate_limits(reset_at);
543
+ CREATE TABLE IF NOT EXISTS supacloud_http.response_cache (
544
+ key text PRIMARY KEY CHECK (key ~ '^[a-f0-9]{64}$'),
545
+ body text NOT NULL CHECK (octet_length(body) <= 16777216),
546
+ expires_at timestamptz NOT NULL,
547
+ generation uuid NOT NULL
548
+ );
549
+ ALTER TABLE supacloud_http.response_cache ADD COLUMN IF NOT EXISTS generation uuid
550
+ NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';
551
+ CREATE TABLE IF NOT EXISTS supacloud_http.cache_generation (
552
+ singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
553
+ generation uuid NOT NULL DEFAULT gen_random_uuid()
554
+ );
555
+ INSERT INTO supacloud_http.cache_generation(singleton) VALUES (true) ON CONFLICT DO NOTHING;
556
+ CREATE INDEX IF NOT EXISTS http_response_cache_expiry ON supacloud_http.response_cache(expires_at);
557
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_http FROM PUBLIC;
558
+ CREATE OR REPLACE FUNCTION supacloud_http.consume_rate_limit(p_key text, p_limit bigint, p_window_ms integer)
559
+ RETURNS TABLE(allowed boolean, remaining bigint, reset_at_ms numeric)
560
+ LANGUAGE plpgsql SET search_path = pg_catalog, supacloud_http AS $rate$
561
+ DECLARE
562
+ entry supacloud_http.rate_limits%ROWTYPE;
563
+ observed_at timestamptz;
564
+ BEGIN
565
+ IF p_limit < 1 OR p_limit > 1000000000 OR p_window_ms < 1 OR p_window_ms > 86400000 THEN
566
+ RAISE EXCEPTION 'Invalid rate limit window';
567
+ END IF;
568
+ LOOP
569
+ INSERT INTO supacloud_http.rate_limits(key, used, reset_at)
570
+ VALUES (p_key, 1, '-infinity') ON CONFLICT DO NOTHING;
571
+ SELECT * INTO entry FROM supacloud_http.rate_limits WHERE key=p_key FOR UPDATE;
572
+ EXIT WHEN FOUND;
573
+ -- Expiry pruning may remove a conflicting row before we acquire its lock.
574
+ END LOOP;
575
+ observed_at := clock_timestamp();
576
+ IF entry.reset_at <= observed_at THEN
577
+ entry.used := 1;
578
+ entry.reset_at := observed_at + p_window_ms * interval '1 millisecond';
579
+ ELSE
580
+ entry.used := least(entry.used + 1, p_limit + 1);
581
+ END IF;
582
+ UPDATE supacloud_http.rate_limits SET used=entry.used, reset_at=entry.reset_at WHERE key=p_key;
583
+ RETURN QUERY SELECT entry.used <= p_limit, greatest(p_limit-entry.used,0),
584
+ extract(epoch FROM entry.reset_at)*1000;
585
+ END
586
+ $rate$;
587
+ REVOKE ALL ON FUNCTION supacloud_http.consume_rate_limit(text,bigint,integer) FROM PUBLIC;
588
+ `;
589
+ function validKey(key) {
590
+ if (!/^[a-f0-9]{64}$/.test(key))
591
+ throw new TypeError("HTTP store keys must be SHA-256 digests");
592
+ }
593
+ function createPostgresHttpPolicyStores(database) {
594
+ const rateLimitStore = {
595
+ async consume(key, limit, windowMs) {
596
+ validKey(key);
597
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 1e9 || !Number.isSafeInteger(windowMs) || windowMs <= 0 || windowMs > 86400000) {
598
+ throw new TypeError("Invalid rate limit window");
599
+ }
600
+ const [row] = await database.query("SELECT * FROM supacloud_http.consume_rate_limit($1,$2,$3)", [key, limit, windowMs]);
601
+ if (!row || typeof row.allowed !== "boolean")
602
+ throw new Error("Invalid rate limit storage result");
603
+ return { allowed: row.allowed, remaining: Number(row.remaining), resetAt: Number(row.reset_at_ms) };
604
+ }
605
+ };
606
+ const cacheStore = {
607
+ async generation() {
608
+ const [row] = await database.query("SELECT generation FROM supacloud_http.cache_generation WHERE singleton", []);
609
+ if (!row || typeof row.generation !== "string")
610
+ throw new Error("Invalid cache generation");
611
+ return row.generation;
612
+ },
613
+ async get(key, generation) {
614
+ validKey(key);
615
+ const [row] = await database.query(`
616
+ SELECT body, extract(epoch FROM expires_at) * 1000 AS expires_at_ms
617
+ FROM supacloud_http.response_cache AS cache
618
+ JOIN supacloud_http.cache_generation AS current ON cache.generation = current.generation
619
+ WHERE key = $1 AND cache.generation = $2::uuid AND expires_at > clock_timestamp()
620
+ `, [key, generation]);
621
+ if (!row)
622
+ return;
623
+ if (typeof row.body !== "string")
624
+ throw new Error("Invalid cache storage result");
625
+ return { body: row.body, contentType: "application/json", expiresAt: Number(row.expires_at_ms) };
626
+ },
627
+ async set(key, value, generation) {
628
+ validKey(key);
629
+ if (value.contentType !== "application/json" || typeof value.body !== "string" || new TextEncoder().encode(value.body).byteLength > 16777216 || !Number.isSafeInteger(value.expiresAt) || value.expiresAt <= 0) {
630
+ throw new TypeError("Invalid cache entry");
631
+ }
632
+ await database.query(`
633
+ WITH current AS MATERIALIZED (
634
+ SELECT generation FROM supacloud_http.cache_generation
635
+ WHERE singleton AND generation=$4::uuid FOR SHARE
636
+ )
637
+ INSERT INTO supacloud_http.response_cache (key, body, expires_at, generation)
638
+ SELECT $1, $2, to_timestamp($3 / 1000.0), generation FROM current
639
+ ON CONFLICT (key) DO UPDATE SET body = excluded.body,
640
+ expires_at = excluded.expires_at, generation = excluded.generation
641
+ `, [key, value.body, value.expiresAt, generation]);
642
+ },
643
+ async clear() {
644
+ await database.query(`
645
+ WITH advanced AS (
646
+ UPDATE supacloud_http.cache_generation SET generation=gen_random_uuid() WHERE singleton RETURNING generation
647
+ )
648
+ DELETE FROM supacloud_http.response_cache WHERE EXISTS(SELECT 1 FROM advanced)
649
+ `, []);
650
+ }
651
+ };
652
+ return {
653
+ rateLimitStore,
654
+ cacheStore,
655
+ async prune() {
656
+ await database.query("DELETE FROM supacloud_http.rate_limits WHERE reset_at <= clock_timestamp()", []);
657
+ await database.query("DELETE FROM supacloud_http.response_cache WHERE expires_at <= clock_timestamp()", []);
658
+ }
659
+ };
660
+ }
39
661
  // src/execution.ts
40
662
  async function observeExecution(observer, event, next) {
41
663
  const emit = (phase, durationMs) => {
@@ -74,7 +696,7 @@ function executionTrace(context) {
74
696
  // src/schema_contract.ts
75
697
  import {
76
698
  getSchemaValidator,
77
- StatusMap,
699
+ StatusMap as StatusMap2,
78
700
  ElysiaCustomStatusResponse
79
701
  } from "elysia";
80
702
  function defineRouteContract(schemas) {
@@ -156,7 +778,7 @@ function responseStatusOf(value, configuredStatus) {
156
778
  if (typeof configuredStatus === "number")
157
779
  return configuredStatus;
158
780
  if (typeof configuredStatus === "string") {
159
- const mapped = StatusMap[configuredStatus];
781
+ const mapped = StatusMap2[configuredStatus];
160
782
  if (typeof mapped === "number")
161
783
  return mapped;
162
784
  }
@@ -296,7 +918,7 @@ function defineJsonContract(schemas, request, options = {}) {
296
918
  }
297
919
 
298
920
  // src/documentation.ts
299
- import { Elysia } from "elysia";
921
+ import { Elysia as Elysia2 } from "elysia";
300
922
  function normalizePath(value, fallback) {
301
923
  const path = value ?? fallback;
302
924
  if (!path.trim() || !path.startsWith("/") || path.includes("?") || path.includes("#")) {
@@ -373,7 +995,7 @@ function renderViewerPage(title, resourcePath, mode) {
373
995
  </html>`;
374
996
  }
375
997
  function createDocumentationPlugin(options = {}) {
376
- const app = new Elysia({ name: "supacloud:documentation" });
998
+ const app = new Elysia2({ name: "supacloud:documentation" });
377
999
  const paths = new Set;
378
1000
  const reserve = (path) => {
379
1001
  if (paths.has(path))
@@ -408,6 +1030,39 @@ function createDocumentationPlugin(options = {}) {
408
1030
  }
409
1031
  return app;
410
1032
  }
1033
+
1034
+ // src/command-binding.ts
1035
+ function bindCompiledCommand(options) {
1036
+ const { module, command, governance, observer, handler, decode, preview } = options;
1037
+ return {
1038
+ execute: (input, context) => executeCompiledCommand({
1039
+ module,
1040
+ command,
1041
+ governance,
1042
+ observer,
1043
+ decode,
1044
+ input,
1045
+ request: context.request,
1046
+ requestContext: context.requestContext,
1047
+ services: context.services,
1048
+ scope: context.scope,
1049
+ handler: (value) => handler(value, context)
1050
+ }),
1051
+ ...preview ? {
1052
+ preview: (input, context) => previewCompiledCommand({
1053
+ module,
1054
+ command,
1055
+ governance,
1056
+ input,
1057
+ request: context.request,
1058
+ requestContext: context.requestContext,
1059
+ services: context.services,
1060
+ scope: context.scope,
1061
+ preview: (value) => preview(value, context)
1062
+ })
1063
+ } : {}
1064
+ };
1065
+ }
411
1066
  // src/persistent-command.ts
412
1067
  import {
413
1068
  CommandError,
@@ -434,81 +1089,6 @@ function createPersistentCommandAdapter(command, options) {
434
1089
  }
435
1090
  };
436
1091
  }
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
1092
  // src/feature.ts
513
1093
  function assertFeatureTransition(spec, state, event) {
514
1094
  const transition = Object.hasOwn(spec.transitions, event) ? spec.transitions[event] : undefined;
@@ -1386,6 +1966,19 @@ function validatedJsonResponse(validate, value, init) {
1386
1966
  headers.set("content-type", "application/json");
1387
1967
  return new Response(body, { ...init, headers });
1388
1968
  }
1969
+ var httpPluginIds = new WeakMap;
1970
+ var nextHttpPluginId = 0;
1971
+ function contextPlugin(http) {
1972
+ let seed = http ? httpPluginIds.get(http) : 0;
1973
+ if (http && seed === undefined) {
1974
+ seed = ++nextHttpPluginId;
1975
+ httpPluginIds.set(http, seed);
1976
+ }
1977
+ return new Elysia3({ name: "supacloud:http-context", seed }).use(http ?? new Elysia3).as("scoped");
1978
+ }
1979
+ function mountHttp(http, name, normalize = true) {
1980
+ return new Elysia3({ name, normalize }).use(contextPlugin(http));
1981
+ }
1389
1982
  function decodeJobContract(schema, value, boundary) {
1390
1983
  if (schema === undefined)
1391
1984
  return value;
@@ -1422,7 +2015,7 @@ function bearerToken(request) {
1422
2015
  var createSupaCloudRequestContext = (request) => {
1423
2016
  const subject = safeHeaderValue(request.headers.get(VERIFIED_JWT_SUBJECT_HEADER), 1024);
1424
2017
  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();
2018
+ const requestId = httpRequestId(request) ?? safeHeaderValue(request.headers.get(EXECUTION_ID_HEADER), 256) ?? safeHeaderValue(request.headers.get("x-request-id"), 256) ?? crypto.randomUUID();
1426
2019
  const idempotencyKey = safeHeaderValue(request.headers.get(IDEMPOTENCY_KEY_HEADER), 512);
1427
2020
  const identity = {
1428
2021
  authenticated: subject !== undefined,
@@ -1737,7 +2330,9 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1737
2330
  "data",
1738
2331
  "input",
1739
2332
  "result",
1740
- "request"
2333
+ "request",
2334
+ "parse",
2335
+ "allowDeleteBody"
1741
2336
  ]);
1742
2337
  for (const controller of compiled.controllers) {
1743
2338
  for (const route of controller.routes) {
@@ -1745,6 +2340,9 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1745
2340
  if (!supportedMethods.has(route.method) || unsupported.length > 0) {
1746
2341
  throw new ApplicationError(`Unsupported compiled route ${route.method} ${controller.path}${route.path}` + (unsupported.length > 0 ? `: ${unsupported.join(", ")}` : ""), { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
1747
2342
  }
2343
+ 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)) {
2344
+ throw new ApplicationError(`Invalid compiled body policy on ${route.method} ${controller.path}${route.path}`, { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
2345
+ }
1748
2346
  }
1749
2347
  }
1750
2348
  const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
@@ -1765,7 +2363,11 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1765
2363
  const requestContexts = new WeakMap;
1766
2364
  const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
1767
2365
  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);
2366
+ const plugin = mountHttp(options.http, `supacloud:${compiled.name}`, options.normalize ?? true).resolve(async (context) => {
2367
+ const requestContext = await ctxFactory(context.request, context);
2368
+ requestContexts.set(context.request, requestContext);
2369
+ return { services, requestContext };
2370
+ });
1769
2371
  const createRequestScope = compiled.createRequestScope;
1770
2372
  const requestScopes = new WeakMap;
1771
2373
  if (compiled.destroyRequestScope) {
@@ -1782,11 +2384,6 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1782
2384
  }
1783
2385
  });
1784
2386
  }
1785
- plugin.resolve(async ({ request }) => {
1786
- const requestContext = await ctxFactory(request);
1787
- requestContexts.set(request, requestContext);
1788
- return { requestContext };
1789
- });
1790
2387
  plugin.onError(async ({ code, error, request }) => {
1791
2388
  const context = {
1792
2389
  request,
@@ -1796,12 +2393,77 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1796
2393
  const mapped = await options.errorMapper?.(error, context);
1797
2394
  return mapped ?? defaultErrorResponse(error, code);
1798
2395
  });
2396
+ const routeRegistrar = plugin;
1799
2397
  for (const controller of compiled.controllers) {
1800
2398
  for (const route of controller.routes) {
1801
2399
  const path = joinPaths(controller.path, route.path);
1802
- const schema = toElysiaRouteSchema(route);
2400
+ const documentation = route.data?.openapi;
2401
+ const hidden = documentation !== null && typeof documentation === "object" && "hide" in documentation && documentation.hide === true;
2402
+ const schema = {
2403
+ ...toElysiaRouteSchema(route),
2404
+ ...route.parse === "none" ? { parse: "none" } : {},
2405
+ ...route.title || hidden ? { detail: {
2406
+ ...route.title ? { summary: route.title } : {},
2407
+ ...hidden ? { hide: true } : {}
2408
+ } } : {}
2409
+ };
2410
+ const policies = compileHttpPolicies(route, path, options.httpPolicies);
2411
+ if (policies.length > 0) {
2412
+ Object.assign(schema, {
2413
+ beforeHandle: async (context) => {
2414
+ for (const policy of policies) {
2415
+ const result = await policy({
2416
+ http: context,
2417
+ requestContext: requestContexts.get(context.request)
2418
+ });
2419
+ if (result instanceof Response)
2420
+ return result;
2421
+ if (result !== undefined) {
2422
+ throw new Error("HTTP policies must return Response or undefined");
2423
+ }
2424
+ }
2425
+ }
2426
+ });
2427
+ }
2428
+ const responsePolicies = policies.filter((policy) => policy.afterResponse);
2429
+ const mappingPolicies = policies.filter((policy) => policy.mapResponse);
2430
+ if (mappingPolicies.length > 0) {
2431
+ Object.assign(schema, {
2432
+ mapResponse: async (context) => {
2433
+ let response = context.response;
2434
+ let mapped;
2435
+ for (const policy of mappingPolicies) {
2436
+ const result = await policy.mapResponse({
2437
+ http: context,
2438
+ requestContext: requestContexts.get(context.request),
2439
+ response
2440
+ });
2441
+ if (result !== undefined) {
2442
+ if (!(result instanceof Response))
2443
+ throw new Error("HTTP response policies must return Response or undefined");
2444
+ mapped = result;
2445
+ response = result;
2446
+ }
2447
+ }
2448
+ return mapped;
2449
+ }
2450
+ });
2451
+ }
2452
+ if (responsePolicies.length > 0) {
2453
+ Object.assign(schema, {
2454
+ afterResponse: async (context) => {
2455
+ for (const policy of responsePolicies) {
2456
+ await policy.afterResponse({
2457
+ http: context,
2458
+ requestContext: requestContexts.get(context.request),
2459
+ response: context.response
2460
+ });
2461
+ }
2462
+ }
2463
+ });
2464
+ }
1803
2465
  const handler = async (ctx) => {
1804
- const requestContext = ctx.requestContext ?? await ctxFactory(ctx.request);
2466
+ const requestContext = requestContexts.get(ctx.request);
1805
2467
  const execute = async () => {
1806
2468
  const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
1807
2469
  if (requestScope && compiled.destroyRequestScope)
@@ -1893,25 +2555,25 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1893
2555
  };
1894
2556
  switch (route.method) {
1895
2557
  case "GET":
1896
- plugin.get(path, handler, schema);
2558
+ routeRegistrar.get(path, handler, schema);
1897
2559
  break;
1898
2560
  case "POST":
1899
- plugin.post(path, handler, schema);
2561
+ routeRegistrar.post(path, handler, schema);
1900
2562
  break;
1901
2563
  case "PUT":
1902
- plugin.put(path, handler, schema);
2564
+ routeRegistrar.put(path, handler, schema);
1903
2565
  break;
1904
2566
  case "PATCH":
1905
- plugin.patch(path, handler, schema);
2567
+ routeRegistrar.patch(path, handler, schema);
1906
2568
  break;
1907
2569
  case "DELETE":
1908
- plugin.delete(path, handler, schema);
2570
+ routeRegistrar.delete(path, handler, schema);
1909
2571
  break;
1910
2572
  case "HEAD":
1911
- plugin.head(path, handler, schema);
2573
+ routeRegistrar.head(path, handler, schema);
1912
2574
  break;
1913
2575
  case "OPTIONS":
1914
- plugin.options(path, handler, schema);
2576
+ routeRegistrar.options(path, handler, schema);
1915
2577
  break;
1916
2578
  }
1917
2579
  }
@@ -2013,16 +2675,16 @@ function isPublicApplicationError(error) {
2013
2675
  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
2676
  }
2015
2677
  function createApplication(options) {
2016
- const app = new Elysia2({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true });
2678
+ const app = new Elysia3({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true });
2017
2679
  if (options.documentation)
2018
2680
  app.use(createDocumentationPlugin(options.documentation));
2019
2681
  const configuredContextFactory = options.requestContext ?? defaultRequestContext;
2020
2682
  const contextCache = new WeakMap;
2021
- const ctxFactory = (request) => {
2683
+ const ctxFactory = (request, context) => {
2022
2684
  const cached = contextCache.get(request);
2023
2685
  if (cached)
2024
2686
  return cached;
2025
- const pending = Promise.resolve(configuredContextFactory(request));
2687
+ const pending = Promise.resolve(configuredContextFactory(request, context));
2026
2688
  contextCache.set(request, pending);
2027
2689
  return pending;
2028
2690
  };
@@ -2032,7 +2694,7 @@ function createApplication(options) {
2032
2694
  imported[module.name] = services;
2033
2695
  app.use(createModulePlugin(module, services, ctxFactory, options, imported));
2034
2696
  }
2035
- return app;
2697
+ return app.use(contextPlugin(options.http));
2036
2698
  }
2037
2699
  function createTestApp(options) {
2038
2700
  return createApplication(options);
@@ -2040,6 +2702,8 @@ function createTestApp(options) {
2040
2702
  export {
2041
2703
  ApplicationError,
2042
2704
  EXECUTION_ID_HEADER,
2705
+ HTTP_POLICY_STORE_SQL,
2706
+ HttpPolicyConfigurationError,
2043
2707
  IDEMPOTENCY_KEY_HEADER,
2044
2708
  SchemaContractError,
2045
2709
  SupaCloudWorker,
@@ -2048,16 +2712,22 @@ export {
2048
2712
  WorkerRegistrationError,
2049
2713
  assertFeatureTransition,
2050
2714
  assertResponseStatusDeclared,
2715
+ bindCompiledCommand,
2051
2716
  composeAspects,
2052
2717
  composeCommandExecutors,
2053
2718
  createApplication,
2054
2719
  createCommandAuthorizationAdapter,
2055
2720
  createCommandExecutor,
2056
2721
  createDocumentationPlugin,
2722
+ createHttpPolicySuite,
2723
+ createHttpTelemetry,
2724
+ createMemoryHttpCacheStore,
2725
+ createMemoryHttpRateLimitStore,
2057
2726
  createMemoryPolicy,
2058
2727
  createMemorySandbox,
2059
2728
  createModulePlugin,
2060
2729
  createPersistentCommandAdapter,
2730
+ createPostgresHttpPolicyStores,
2061
2731
  createQueueWorkerTransport,
2062
2732
  createSchemaDecoder,
2063
2733
  createSupAuthRequestContext,