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