@supacloud/elysia 0.17.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/README.md +319 -0
- package/dist/command-binding.d.ts +33 -0
- package/dist/examples/fulfillment.d.ts +38 -0
- package/dist/fixtures/webhook-generated/application.d.ts +11 -1
- package/dist/http-policy-postgres.d.ts +12 -0
- package/dist/http-policy-stores.d.ts +33 -0
- package/dist/http-policy-suite.d.ts +75 -0
- package/dist/http-policy.d.ts +25 -0
- package/dist/http-response-cache.d.ts +14 -0
- package/dist/http-telemetry.d.ts +40 -0
- package/dist/index.d.ts +345 -44
- package/dist/index.js +870 -101
- package/dist/memory.d.ts +4 -3
- package/dist/schema_contract.d.ts +2 -2
- package/dist/testing.d.ts +3 -2
- package/dist/webhook-migration-example.d.ts +17 -2
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { Elysia as
|
|
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 =
|
|
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
|
|
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;
|
|
@@ -1291,6 +1871,42 @@ function descriptorPipeline(descriptor, boundary, observer) {
|
|
|
1291
1871
|
...executionTrace(context.requestContext)
|
|
1292
1872
|
}, run) : undefined);
|
|
1293
1873
|
}
|
|
1874
|
+
function createCommandAuthorizationAdapter(options) {
|
|
1875
|
+
return async (invocation) => {
|
|
1876
|
+
const permission = invocation.command.permission;
|
|
1877
|
+
if (!permission || !/^[a-z][a-z0-9._-]*:[a-z][a-z0-9._-]*$/.test(permission)) {
|
|
1878
|
+
throw new ApplicationError("Command permission must use resource:action syntax", {
|
|
1879
|
+
status: 500,
|
|
1880
|
+
code: "COMMAND_PERMISSION_INVALID"
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
const context = invocation.requestContext;
|
|
1884
|
+
const identity = isRecord(context) && isTrustedIdentity(context.identity) ? context.identity : undefined;
|
|
1885
|
+
if (!isAuthenticatedTrustedIdentity(identity)) {
|
|
1886
|
+
throw new ApplicationError("Authentication required", { status: 401, code: "AUTHENTICATION_REQUIRED" });
|
|
1887
|
+
}
|
|
1888
|
+
const request = {
|
|
1889
|
+
principal: { kind: "user", issuer: options.issuer, subject: identity.subject },
|
|
1890
|
+
applicationId: options.applicationId,
|
|
1891
|
+
domain: options.domain(invocation)
|
|
1892
|
+
};
|
|
1893
|
+
let resolved;
|
|
1894
|
+
try {
|
|
1895
|
+
resolved = await options.resolve(request);
|
|
1896
|
+
} catch {
|
|
1897
|
+
throw new ApplicationError("Authorization is unavailable", { status: 503, code: "AUTHORIZATION_UNAVAILABLE" });
|
|
1898
|
+
}
|
|
1899
|
+
if (!isRecord(resolved) || !Array.isArray(resolved.permissions) || !resolved.permissions.every((value) => typeof value === "string") || resolved.permissionCatalogVersion !== undefined && typeof resolved.permissionCatalogVersion !== "string" || resolved.permissionCatalogDigest !== undefined && typeof resolved.permissionCatalogDigest !== "string" || resolved.applicationId !== options.applicationId || options.catalog !== undefined && (resolved.permissionCatalogVersion !== options.catalog.version || options.catalog.digest !== undefined && resolved.permissionCatalogDigest !== options.catalog.digest)) {
|
|
1900
|
+
throw new ApplicationError("Authorization context is not bound to this application", {
|
|
1901
|
+
status: 503,
|
|
1902
|
+
code: "AUTHORIZATION_CONTEXT_INVALID"
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
if (!resolved.permissions.includes(permission)) {
|
|
1906
|
+
throw new ApplicationError("Permission denied", { status: 403, code: "PERMISSION_DENIED" });
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1294
1910
|
function composeCommandExecutors(...executors) {
|
|
1295
1911
|
const active = executors.filter((e) => typeof e === "function");
|
|
1296
1912
|
if (active.length === 0)
|
|
@@ -1350,6 +1966,19 @@ function validatedJsonResponse(validate, value, init) {
|
|
|
1350
1966
|
headers.set("content-type", "application/json");
|
|
1351
1967
|
return new Response(body, { ...init, headers });
|
|
1352
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
|
+
}
|
|
1353
1982
|
function decodeJobContract(schema, value, boundary) {
|
|
1354
1983
|
if (schema === undefined)
|
|
1355
1984
|
return value;
|
|
@@ -1386,7 +2015,7 @@ function bearerToken(request) {
|
|
|
1386
2015
|
var createSupaCloudRequestContext = (request) => {
|
|
1387
2016
|
const subject = safeHeaderValue(request.headers.get(VERIFIED_JWT_SUBJECT_HEADER), 1024);
|
|
1388
2017
|
const accessToken = subject ? bearerToken(request) : undefined;
|
|
1389
|
-
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();
|
|
1390
2019
|
const idempotencyKey = safeHeaderValue(request.headers.get(IDEMPOTENCY_KEY_HEADER), 512);
|
|
1391
2020
|
const identity = {
|
|
1392
2021
|
authenticated: subject !== undefined,
|
|
@@ -1585,6 +2214,67 @@ function requireIdempotencyKey(invocation) {
|
|
|
1585
2214
|
}
|
|
1586
2215
|
return key;
|
|
1587
2216
|
}
|
|
2217
|
+
function assertCommandGovernanceReady(compiled, governance, executor) {
|
|
2218
|
+
if ((compiled.commands ?? []).length === 0)
|
|
2219
|
+
return;
|
|
2220
|
+
if (executor !== undefined && typeof executor !== "function") {
|
|
2221
|
+
throw new ApplicationError(`Module "${compiled.name}" has no callable command executor`, {
|
|
2222
|
+
code: "COMMAND_EXECUTOR_UNCONFIGURED"
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
if (!governance && !executor) {
|
|
2226
|
+
throw new ApplicationError(`Module "${compiled.name}" has commands but no command governance`, {
|
|
2227
|
+
code: "COMMAND_GOVERNANCE_UNCONFIGURED"
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
if (!governance)
|
|
2231
|
+
return;
|
|
2232
|
+
if (typeof governance.authorize !== "function") {
|
|
2233
|
+
throw new ApplicationError(`Module "${compiled.name}" has no authorization adapter`, {
|
|
2234
|
+
code: "COMMAND_AUTHORIZATION_UNCONFIGURED"
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
for (const command of compiled.commands ?? []) {
|
|
2238
|
+
if (command.audit && command.rpc === undefined && (typeof governance.audit?.succeeded !== "function" || typeof governance.audit?.failed !== "function")) {
|
|
2239
|
+
throw new ApplicationError(`Command "${command.name}" has no audit adapter`, {
|
|
2240
|
+
code: "COMMAND_AUDIT_UNCONFIGURED"
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
if (command.idempotency === "required" && typeof governance.idempotency !== "function" && command.rpc === undefined) {
|
|
2244
|
+
throw new ApplicationError(`Command "${command.name}" has no idempotency adapter`, {
|
|
2245
|
+
code: "COMMAND_IDEMPOTENCY_UNCONFIGURED"
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
if (command.transaction === "required" && typeof governance.transaction !== "function" && command.rpc === undefined) {
|
|
2249
|
+
throw new ApplicationError(`Command "${command.name}" has no transaction adapter`, {
|
|
2250
|
+
code: "COMMAND_TRANSACTION_UNCONFIGURED"
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
if (command.rpc !== undefined) {
|
|
2254
|
+
const adapter = Object.hasOwn(governance.rpc ?? {}, command.rpc) ? governance.rpc?.[command.rpc] : undefined;
|
|
2255
|
+
if (!adapter || typeof adapter.execute !== "function" || !isRecord(adapter.capabilities)) {
|
|
2256
|
+
throw new ApplicationError(`Command "${command.name}" has no RPC governance adapter`, {
|
|
2257
|
+
code: "COMMAND_RPC_UNCONFIGURED"
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
if (command.audit && adapter.capabilities.audit !== true) {
|
|
2261
|
+
throw new ApplicationError(`Command "${command.name}" RPC adapter has no audit capability`, {
|
|
2262
|
+
code: "COMMAND_AUDIT_UNCONFIGURED"
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
if (command.idempotency === "required" && adapter.capabilities.idempotency !== true) {
|
|
2266
|
+
throw new ApplicationError(`Command "${command.name}" RPC adapter has no idempotency capability`, {
|
|
2267
|
+
code: "COMMAND_IDEMPOTENCY_UNCONFIGURED"
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
if (command.transaction === "required" && adapter.capabilities.transaction !== true) {
|
|
2271
|
+
throw new ApplicationError(`Command "${command.name}" RPC adapter has no transaction capability`, {
|
|
2272
|
+
code: "COMMAND_TRANSACTION_UNCONFIGURED"
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
1588
2278
|
function joinPaths(prefix, path) {
|
|
1589
2279
|
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1590
2280
|
return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
@@ -1640,7 +2330,9 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1640
2330
|
"data",
|
|
1641
2331
|
"input",
|
|
1642
2332
|
"result",
|
|
1643
|
-
"request"
|
|
2333
|
+
"request",
|
|
2334
|
+
"parse",
|
|
2335
|
+
"allowDeleteBody"
|
|
1644
2336
|
]);
|
|
1645
2337
|
for (const controller of compiled.controllers) {
|
|
1646
2338
|
for (const route of controller.routes) {
|
|
@@ -1648,12 +2340,16 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1648
2340
|
if (!supportedMethods.has(route.method) || unsupported.length > 0) {
|
|
1649
2341
|
throw new ApplicationError(`Unsupported compiled route ${route.method} ${controller.path}${route.path}` + (unsupported.length > 0 ? `: ${unsupported.join(", ")}` : ""), { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
|
|
1650
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
|
+
}
|
|
1651
2346
|
}
|
|
1652
2347
|
}
|
|
1653
2348
|
const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
|
|
1654
2349
|
if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
|
|
1655
2350
|
throw new ApplicationError(`Module "${compiled.name}" has command routes but no commandGovernance`, { code: "COMMAND_GOVERNANCE_UNCONFIGURED" });
|
|
1656
2351
|
}
|
|
2352
|
+
assertCommandGovernanceReady(compiled, options.commandGovernance, options.commandExecutor);
|
|
1657
2353
|
const commandsByClassName = new Map((compiled.commands ?? []).map((command) => [command.className, command]));
|
|
1658
2354
|
for (const controller of compiled.controllers) {
|
|
1659
2355
|
for (const route of controller.routes) {
|
|
@@ -1667,7 +2363,11 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1667
2363
|
const requestContexts = new WeakMap;
|
|
1668
2364
|
const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
|
|
1669
2365
|
const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
|
|
1670
|
-
const plugin =
|
|
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
|
+
});
|
|
1671
2371
|
const createRequestScope = compiled.createRequestScope;
|
|
1672
2372
|
const requestScopes = new WeakMap;
|
|
1673
2373
|
if (compiled.destroyRequestScope) {
|
|
@@ -1684,11 +2384,6 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1684
2384
|
}
|
|
1685
2385
|
});
|
|
1686
2386
|
}
|
|
1687
|
-
plugin.resolve(async ({ request }) => {
|
|
1688
|
-
const requestContext = await ctxFactory(request);
|
|
1689
|
-
requestContexts.set(request, requestContext);
|
|
1690
|
-
return { requestContext };
|
|
1691
|
-
});
|
|
1692
2387
|
plugin.onError(async ({ code, error, request }) => {
|
|
1693
2388
|
const context = {
|
|
1694
2389
|
request,
|
|
@@ -1698,12 +2393,77 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1698
2393
|
const mapped = await options.errorMapper?.(error, context);
|
|
1699
2394
|
return mapped ?? defaultErrorResponse(error, code);
|
|
1700
2395
|
});
|
|
2396
|
+
const routeRegistrar = plugin;
|
|
1701
2397
|
for (const controller of compiled.controllers) {
|
|
1702
2398
|
for (const route of controller.routes) {
|
|
1703
2399
|
const path = joinPaths(controller.path, route.path);
|
|
1704
|
-
const
|
|
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
|
+
}
|
|
1705
2465
|
const handler = async (ctx) => {
|
|
1706
|
-
const requestContext =
|
|
2466
|
+
const requestContext = requestContexts.get(ctx.request);
|
|
1707
2467
|
const execute = async () => {
|
|
1708
2468
|
const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
|
|
1709
2469
|
if (requestScope && compiled.destroyRequestScope)
|
|
@@ -1795,25 +2555,25 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1795
2555
|
};
|
|
1796
2556
|
switch (route.method) {
|
|
1797
2557
|
case "GET":
|
|
1798
|
-
|
|
2558
|
+
routeRegistrar.get(path, handler, schema);
|
|
1799
2559
|
break;
|
|
1800
2560
|
case "POST":
|
|
1801
|
-
|
|
2561
|
+
routeRegistrar.post(path, handler, schema);
|
|
1802
2562
|
break;
|
|
1803
2563
|
case "PUT":
|
|
1804
|
-
|
|
2564
|
+
routeRegistrar.put(path, handler, schema);
|
|
1805
2565
|
break;
|
|
1806
2566
|
case "PATCH":
|
|
1807
|
-
|
|
2567
|
+
routeRegistrar.patch(path, handler, schema);
|
|
1808
2568
|
break;
|
|
1809
2569
|
case "DELETE":
|
|
1810
|
-
|
|
2570
|
+
routeRegistrar.delete(path, handler, schema);
|
|
1811
2571
|
break;
|
|
1812
2572
|
case "HEAD":
|
|
1813
|
-
|
|
2573
|
+
routeRegistrar.head(path, handler, schema);
|
|
1814
2574
|
break;
|
|
1815
2575
|
case "OPTIONS":
|
|
1816
|
-
|
|
2576
|
+
routeRegistrar.options(path, handler, schema);
|
|
1817
2577
|
break;
|
|
1818
2578
|
}
|
|
1819
2579
|
}
|
|
@@ -1915,16 +2675,16 @@ function isPublicApplicationError(error) {
|
|
|
1915
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;
|
|
1916
2676
|
}
|
|
1917
2677
|
function createApplication(options) {
|
|
1918
|
-
const app = new
|
|
2678
|
+
const app = new Elysia3({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true });
|
|
1919
2679
|
if (options.documentation)
|
|
1920
2680
|
app.use(createDocumentationPlugin(options.documentation));
|
|
1921
2681
|
const configuredContextFactory = options.requestContext ?? defaultRequestContext;
|
|
1922
2682
|
const contextCache = new WeakMap;
|
|
1923
|
-
const ctxFactory = (request) => {
|
|
2683
|
+
const ctxFactory = (request, context) => {
|
|
1924
2684
|
const cached = contextCache.get(request);
|
|
1925
2685
|
if (cached)
|
|
1926
2686
|
return cached;
|
|
1927
|
-
const pending = Promise.resolve(configuredContextFactory(request));
|
|
2687
|
+
const pending = Promise.resolve(configuredContextFactory(request, context));
|
|
1928
2688
|
contextCache.set(request, pending);
|
|
1929
2689
|
return pending;
|
|
1930
2690
|
};
|
|
@@ -1934,7 +2694,7 @@ function createApplication(options) {
|
|
|
1934
2694
|
imported[module.name] = services;
|
|
1935
2695
|
app.use(createModulePlugin(module, services, ctxFactory, options, imported));
|
|
1936
2696
|
}
|
|
1937
|
-
return app;
|
|
2697
|
+
return app.use(contextPlugin(options.http));
|
|
1938
2698
|
}
|
|
1939
2699
|
function createTestApp(options) {
|
|
1940
2700
|
return createApplication(options);
|
|
@@ -1942,6 +2702,8 @@ function createTestApp(options) {
|
|
|
1942
2702
|
export {
|
|
1943
2703
|
ApplicationError,
|
|
1944
2704
|
EXECUTION_ID_HEADER,
|
|
2705
|
+
HTTP_POLICY_STORE_SQL,
|
|
2706
|
+
HttpPolicyConfigurationError,
|
|
1945
2707
|
IDEMPOTENCY_KEY_HEADER,
|
|
1946
2708
|
SchemaContractError,
|
|
1947
2709
|
SupaCloudWorker,
|
|
@@ -1950,15 +2712,22 @@ export {
|
|
|
1950
2712
|
WorkerRegistrationError,
|
|
1951
2713
|
assertFeatureTransition,
|
|
1952
2714
|
assertResponseStatusDeclared,
|
|
2715
|
+
bindCompiledCommand,
|
|
1953
2716
|
composeAspects,
|
|
1954
2717
|
composeCommandExecutors,
|
|
1955
2718
|
createApplication,
|
|
2719
|
+
createCommandAuthorizationAdapter,
|
|
1956
2720
|
createCommandExecutor,
|
|
1957
2721
|
createDocumentationPlugin,
|
|
2722
|
+
createHttpPolicySuite,
|
|
2723
|
+
createHttpTelemetry,
|
|
2724
|
+
createMemoryHttpCacheStore,
|
|
2725
|
+
createMemoryHttpRateLimitStore,
|
|
1958
2726
|
createMemoryPolicy,
|
|
1959
2727
|
createMemorySandbox,
|
|
1960
2728
|
createModulePlugin,
|
|
1961
2729
|
createPersistentCommandAdapter,
|
|
2730
|
+
createPostgresHttpPolicyStores,
|
|
1962
2731
|
createQueueWorkerTransport,
|
|
1963
2732
|
createSchemaDecoder,
|
|
1964
2733
|
createSupAuthRequestContext,
|