@splitin/verification-server 0.1.0-beta.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/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +28 -0
- package/dist/express.cjs +721 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +30 -0
- package/dist/express.d.ts +30 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +717 -0
- package/dist/express.js.map +1 -0
- package/dist/hono.cjs +679 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +21 -0
- package/dist/hono.d.ts +21 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +676 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +1009 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +996 -0
- package/dist/index.js.map +1 -0
- package/dist/supabase.cjs +678 -0
- package/dist/supabase.cjs.map +1 -0
- package/dist/supabase.d.cts +19 -0
- package/dist/supabase.d.ts +19 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +676 -0
- package/dist/supabase.js.map +1 -0
- package/dist/types-MNGUwN4G.d.cts +231 -0
- package/dist/types-MNGUwN4G.d.ts +231 -0
- package/dist/types-MNGUwN4G.d.ts.map +1 -0
- package/openapi/openapi.v1.json +2876 -0
- package/package.json +69 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var verificationAdapterSdk = require('@splitin/verification-adapter-sdk');
|
|
4
|
+
|
|
5
|
+
// src/handler.ts
|
|
6
|
+
var VerificationHttpError = class extends Error {
|
|
7
|
+
constructor(status, code, message, retryAfterSeconds = null) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
12
|
+
this.name = "VerificationHttpError";
|
|
13
|
+
}
|
|
14
|
+
status;
|
|
15
|
+
code;
|
|
16
|
+
retryAfterSeconds;
|
|
17
|
+
};
|
|
18
|
+
var SECRET_PATTERN = /sk_|rk_|whsec_|client_secret|ephemeral|password|bearer\s+[a-z0-9._-]+/i;
|
|
19
|
+
function scrubUnsafeMessage(value) {
|
|
20
|
+
if (SECRET_PATTERN.test(value) || /provider\s+body|raw\s+webhook/i.test(value)) {
|
|
21
|
+
return "The verification request could not be completed.";
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function toSafeErrorEnvelope(error, supportPath) {
|
|
26
|
+
if (error instanceof VerificationHttpError) {
|
|
27
|
+
return {
|
|
28
|
+
status: error.status,
|
|
29
|
+
retryAfterSeconds: error.retryAfterSeconds,
|
|
30
|
+
body: envelope(error.code, scrubUnsafeMessage(error.message), error.retryAfterSeconds, supportPath)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (error instanceof verificationAdapterSdk.ProviderError) {
|
|
34
|
+
const status = error.retryable ? error.code === "RATE_LIMITED" ? 429 : 503 : 409;
|
|
35
|
+
return {
|
|
36
|
+
status,
|
|
37
|
+
retryAfterSeconds: error.retryAfterSeconds ?? null,
|
|
38
|
+
body: envelope(error.safeCode, scrubUnsafeMessage(error.message), error.retryAfterSeconds ?? null, supportPath)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const safe = verificationAdapterSdk.toSafeProviderFailure(error);
|
|
42
|
+
return {
|
|
43
|
+
status: 503,
|
|
44
|
+
retryAfterSeconds: safe.retryAfterSeconds ?? null,
|
|
45
|
+
body: envelope(safe.safeCode, "The verification request could not be completed.", safe.retryAfterSeconds ?? null, supportPath)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function envelope(code, message, retryAfterSeconds, supportPath) {
|
|
49
|
+
return {
|
|
50
|
+
contractVersion: "1.0.0",
|
|
51
|
+
error: {
|
|
52
|
+
code,
|
|
53
|
+
message,
|
|
54
|
+
retryable: retryAfterSeconds !== null || /retry|unavailable|rate/i.test(code)
|
|
55
|
+
},
|
|
56
|
+
safeErrorCode: code,
|
|
57
|
+
retryAfter: retryAfterSeconds,
|
|
58
|
+
supportPath
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/auth.ts
|
|
63
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9._:~-]{8,128}$/;
|
|
64
|
+
var OPERATOR_ROLES = /* @__PURE__ */ new Set(["operator", "admin", "reviewer", "approver"]);
|
|
65
|
+
var SYSTEM_ROLES = /* @__PURE__ */ new Set(["system", "worker"]);
|
|
66
|
+
function isCookieAuthenticated(request, override) {
|
|
67
|
+
if (override) return override(request);
|
|
68
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
69
|
+
if (/^bearer\s+/i.test(authorization)) return false;
|
|
70
|
+
return Boolean(request.headers.get("cookie"));
|
|
71
|
+
}
|
|
72
|
+
function requireCsrf(request, route, allowedOrigins) {
|
|
73
|
+
if (!route.csrf) return;
|
|
74
|
+
const origin = request.headers.get("origin") ?? originFromReferer(request.headers.get("referer"));
|
|
75
|
+
if (!origin) {
|
|
76
|
+
throw new VerificationHttpError(403, "CSRF_REJECTED", "Cookie-authenticated mutations require a trusted Origin.");
|
|
77
|
+
}
|
|
78
|
+
if (!allowedOrigins.includes(origin)) {
|
|
79
|
+
throw new VerificationHttpError(403, "ORIGIN_NOT_ALLOWED", "Request origin is not allowed.");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function requireOriginWhenPresent(request, allowedOrigins) {
|
|
83
|
+
const origin = request.headers.get("origin");
|
|
84
|
+
if (!origin) return;
|
|
85
|
+
if (!allowedOrigins.includes(origin)) {
|
|
86
|
+
throw new VerificationHttpError(403, "ORIGIN_NOT_ALLOWED", "Request origin is not allowed.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function requireIdempotencyKey(request, route) {
|
|
90
|
+
if (!route.idempotency) return void 0;
|
|
91
|
+
const key = request.headers.get("idempotency-key")?.trim();
|
|
92
|
+
if (!key) {
|
|
93
|
+
throw new VerificationHttpError(400, "IDEMPOTENCY_KEY_REQUIRED", "Mutations require an Idempotency-Key header.");
|
|
94
|
+
}
|
|
95
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
96
|
+
throw new VerificationHttpError(400, "INVALID_IDEMPOTENCY_KEY", "Idempotency-Key is invalid.");
|
|
97
|
+
}
|
|
98
|
+
return key;
|
|
99
|
+
}
|
|
100
|
+
function authorizeActor(actor, auth) {
|
|
101
|
+
if (auth === "none") return;
|
|
102
|
+
if (!actor.actorId || !actor.tenantKey) {
|
|
103
|
+
throw new VerificationHttpError(401, "UNAUTHENTICATED", "Authentication is required.");
|
|
104
|
+
}
|
|
105
|
+
if (auth === "actor") {
|
|
106
|
+
if (actor.actorType === "system" && !hasRole(actor, SYSTEM_ROLES) && !hasRole(actor, OPERATOR_ROLES)) {
|
|
107
|
+
throw new VerificationHttpError(403, "FORBIDDEN", "This actor cannot access subject verification sessions.");
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (auth === "operator") {
|
|
112
|
+
if (actor.actorType !== "operator" && !hasRole(actor, OPERATOR_ROLES)) {
|
|
113
|
+
throw new VerificationHttpError(403, "FORBIDDEN", "Operator authorization is required.");
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (auth === "system") {
|
|
118
|
+
if (actor.actorType !== "system" && !hasRole(actor, SYSTEM_ROLES)) {
|
|
119
|
+
throw new VerificationHttpError(403, "FORBIDDEN", "Worker authorization is required.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function authorizeSubjectScope(actor, subjectReference) {
|
|
124
|
+
if (!subjectReference) return;
|
|
125
|
+
if (actor.actorType === "operator" || actor.actorType === "system") return;
|
|
126
|
+
if (subjectReference === actor.actorId) return;
|
|
127
|
+
if (actor.authorizedSubjectScope.includes("*")) return;
|
|
128
|
+
if (actor.authorizedSubjectScope.includes(subjectReference)) return;
|
|
129
|
+
throw new VerificationHttpError(403, "FORBIDDEN", "The actor is not authorized for this subject.");
|
|
130
|
+
}
|
|
131
|
+
function assertSeparationOfDuties(actor, otherActorId, action) {
|
|
132
|
+
if (!otherActorId) return;
|
|
133
|
+
if (otherActorId === actor.actorId) {
|
|
134
|
+
throw new VerificationHttpError(
|
|
135
|
+
403,
|
|
136
|
+
"SEPARATION_OF_DUTIES",
|
|
137
|
+
`A different authorized operator must ${action}.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function hasRole(actor, allowed) {
|
|
142
|
+
return actor.roles.some((role) => allowed.has(role));
|
|
143
|
+
}
|
|
144
|
+
function originFromReferer(referer) {
|
|
145
|
+
if (!referer) return null;
|
|
146
|
+
try {
|
|
147
|
+
return new URL(referer).origin;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function corsHeaders(request, allowedOrigins) {
|
|
153
|
+
const origin = request.headers.get("origin");
|
|
154
|
+
const allowOrigin = origin && allowedOrigins.includes(origin) ? origin : allowedOrigins[0] ?? "";
|
|
155
|
+
return {
|
|
156
|
+
Vary: "Origin",
|
|
157
|
+
"Access-Control-Allow-Origin": allowOrigin,
|
|
158
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type, Idempotency-Key, X-CSRF-Token",
|
|
159
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
160
|
+
"Access-Control-Allow-Credentials": "true"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/catalog.ts
|
|
165
|
+
var V1_ROUTES = Object.freeze([
|
|
166
|
+
{ id: "sessions.create", method: "POST", path: "/v1/sessions", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
167
|
+
{ id: "sessions.resume", method: "POST", path: "/v1/sessions/:id/resume", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
168
|
+
{ id: "sessions.get", method: "GET", path: "/v1/sessions/:id", auth: "actor", csrf: false, idempotency: false, mutation: false },
|
|
169
|
+
{ id: "sessions.retry", method: "POST", path: "/v1/sessions/:id/retry", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
170
|
+
{ id: "sessions.pause", method: "POST", path: "/v1/sessions/:id/pause", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
171
|
+
{ id: "sessions.cancel", method: "POST", path: "/v1/sessions/:id/cancel", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
172
|
+
{ id: "appeals.create", method: "POST", path: "/v1/appeals", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
173
|
+
{ id: "support.escalations", method: "POST", path: "/v1/support/escalations", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
174
|
+
{ id: "webhooks.ingest", method: "POST", path: "/v1/webhooks/:provider", auth: "none", csrf: false, idempotency: false, mutation: true },
|
|
175
|
+
{ id: "protectedActions.evaluate", method: "POST", path: "/v1/protected-actions/evaluate", auth: "actor", csrf: true, idempotency: true, mutation: true },
|
|
176
|
+
{ id: "admin.health", method: "GET", path: "/v1/admin/health", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
177
|
+
{ id: "admin.routes.get", method: "GET", path: "/v1/admin/routes", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
178
|
+
{ id: "admin.routes.mutate", method: "POST", path: "/v1/admin/routes", auth: "operator", csrf: true, idempotency: true, mutation: true },
|
|
179
|
+
{ id: "admin.circuits.get", method: "GET", path: "/v1/admin/circuits", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
180
|
+
{ id: "admin.circuits.mutate", method: "POST", path: "/v1/admin/circuits", auth: "operator", csrf: true, idempotency: true, mutation: true },
|
|
181
|
+
{ id: "admin.attempts", method: "GET", path: "/v1/admin/attempts", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
182
|
+
{ id: "admin.review.get", method: "GET", path: "/v1/admin/review", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
183
|
+
{ id: "admin.review.mutate", method: "POST", path: "/v1/admin/review", auth: "operator", csrf: true, idempotency: true, mutation: true },
|
|
184
|
+
{ id: "admin.audit", method: "GET", path: "/v1/admin/audit", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
185
|
+
{ id: "admin.reconciliation.get", method: "GET", path: "/v1/admin/reconciliation", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
186
|
+
{ id: "admin.reconciliation.mutate", method: "POST", path: "/v1/admin/reconciliation", auth: "operator", csrf: true, idempotency: true, mutation: true },
|
|
187
|
+
{ id: "admin.redaction.get", method: "GET", path: "/v1/admin/redaction", auth: "operator", csrf: false, idempotency: false, mutation: false },
|
|
188
|
+
{ id: "admin.redaction.mutate", method: "POST", path: "/v1/admin/redaction", auth: "operator", csrf: true, idempotency: true, mutation: true },
|
|
189
|
+
{ id: "workers.claim", method: "POST", path: "/v1/workers/claim", auth: "system", csrf: false, idempotency: true, mutation: true },
|
|
190
|
+
{ id: "workers.process", method: "POST", path: "/v1/workers/process", auth: "system", csrf: false, idempotency: true, mutation: true }
|
|
191
|
+
]);
|
|
192
|
+
function matchV1Route(method, pathname, basePath = "") {
|
|
193
|
+
const relative = stripBasePath(pathname, basePath);
|
|
194
|
+
const normalizedMethod = method.toUpperCase();
|
|
195
|
+
if (normalizedMethod === "OPTIONS") {
|
|
196
|
+
const found = V1_ROUTES.find((route) => matchPath(route.path, relative));
|
|
197
|
+
return found ? { route: { ...found, method: "OPTIONS", csrf: false, idempotency: false, mutation: false }, params: matchPath(found.path, relative) ?? {} } : null;
|
|
198
|
+
}
|
|
199
|
+
for (const route of V1_ROUTES) {
|
|
200
|
+
if (route.method !== normalizedMethod) continue;
|
|
201
|
+
const params = matchPath(route.path, relative);
|
|
202
|
+
if (params) return { route, params };
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
function stripBasePath(pathname, basePath) {
|
|
207
|
+
const normalized = normalizePath(pathname);
|
|
208
|
+
const base = normalizePath(basePath);
|
|
209
|
+
if (!base || base === "/") return normalized;
|
|
210
|
+
if (normalized === base) return "/";
|
|
211
|
+
if (normalized.startsWith(`${base}/`)) return normalized.slice(base.length) || "/";
|
|
212
|
+
return normalized;
|
|
213
|
+
}
|
|
214
|
+
function normalizePath(value) {
|
|
215
|
+
const path = value.split("?")[0] ?? "/";
|
|
216
|
+
if (!path.startsWith("/")) return `/${path}`;
|
|
217
|
+
return path.replace(/\/+$/, "") || "/";
|
|
218
|
+
}
|
|
219
|
+
function matchPath(pattern, pathname) {
|
|
220
|
+
const patternParts = normalizePath(pattern).split("/").filter(Boolean);
|
|
221
|
+
const pathParts = normalizePath(pathname).split("/").filter(Boolean);
|
|
222
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
223
|
+
const params = {};
|
|
224
|
+
for (let index = 0; index < patternParts.length; index += 1) {
|
|
225
|
+
const expected = patternParts[index];
|
|
226
|
+
const actual = pathParts[index];
|
|
227
|
+
if (expected.startsWith(":")) {
|
|
228
|
+
params[expected.slice(1)] = decodeURIComponent(actual);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (expected !== actual) return null;
|
|
232
|
+
}
|
|
233
|
+
return params;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/types.ts
|
|
237
|
+
var FORBIDDEN_CLIENT_FIELDS = [
|
|
238
|
+
"provider",
|
|
239
|
+
"launcherKey",
|
|
240
|
+
"templateId",
|
|
241
|
+
"workflowId",
|
|
242
|
+
"apiOrigin",
|
|
243
|
+
"environment",
|
|
244
|
+
"configurationRevision",
|
|
245
|
+
"adapter",
|
|
246
|
+
"adapterVersion",
|
|
247
|
+
"routePolicyId"
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
// src/json.ts
|
|
251
|
+
function jsonResponse(body, status, extra = {}) {
|
|
252
|
+
const headers = new Headers({
|
|
253
|
+
"Cache-Control": "no-store",
|
|
254
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
255
|
+
...extra
|
|
256
|
+
});
|
|
257
|
+
if (typeof extra["Retry-After"] === "string") headers.set("Retry-After", extra["Retry-After"]);
|
|
258
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
259
|
+
}
|
|
260
|
+
async function readLimitedBody(request, maxBodyBytes) {
|
|
261
|
+
if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS") {
|
|
262
|
+
return request;
|
|
263
|
+
}
|
|
264
|
+
const raw = new Uint8Array(await request.arrayBuffer());
|
|
265
|
+
if (raw.byteLength > maxBodyBytes) {
|
|
266
|
+
throw new VerificationHttpError(413, "PAYLOAD_TOO_LARGE", "Request body exceeds the allowed size.");
|
|
267
|
+
}
|
|
268
|
+
return new Request(request.url, {
|
|
269
|
+
method: request.method,
|
|
270
|
+
headers: request.headers,
|
|
271
|
+
body: raw.byteLength > 0 ? raw : void 0
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async function parseJsonObject(request, allowedKeys, options = {}) {
|
|
275
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
276
|
+
if (contentType && !contentType.toLowerCase().includes("application/json")) {
|
|
277
|
+
throw new VerificationHttpError(415, "INVALID_REQUEST", "Content-Type must be application/json.");
|
|
278
|
+
}
|
|
279
|
+
let body;
|
|
280
|
+
try {
|
|
281
|
+
const text = await request.text();
|
|
282
|
+
body = text.trim() ? JSON.parse(text) : {};
|
|
283
|
+
} catch {
|
|
284
|
+
throw new VerificationHttpError(400, "INVALID_JSON", "Request body must be valid JSON.");
|
|
285
|
+
}
|
|
286
|
+
if (!isPlainRecord(body)) {
|
|
287
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", "Request body must be an object.");
|
|
288
|
+
}
|
|
289
|
+
for (const key of Object.keys(body)) {
|
|
290
|
+
if (options.rejectRoutingFields && FORBIDDEN_CLIENT_FIELDS.includes(key)) {
|
|
291
|
+
throw new VerificationHttpError(400, "UNSUPPORTED_REQUEST_FIELD", `Unsupported request field: ${key}.`);
|
|
292
|
+
}
|
|
293
|
+
if (!allowedKeys.includes(key)) {
|
|
294
|
+
throw new VerificationHttpError(400, "UNSUPPORTED_REQUEST_FIELD", `Unsupported request field: ${key}.`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return body;
|
|
298
|
+
}
|
|
299
|
+
function requiredString(body, field, pattern, maxLength) {
|
|
300
|
+
const value = body[field];
|
|
301
|
+
if (typeof value !== "string" || !pattern.test(value) || value.length > maxLength) {
|
|
302
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", `Invalid ${field}.`);
|
|
303
|
+
}
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
function optionalString(body, field, pattern, maxLength) {
|
|
307
|
+
const value = body[field];
|
|
308
|
+
if (value === void 0 || value === null) return void 0;
|
|
309
|
+
if (typeof value !== "string" || !pattern.test(value) || value.length > maxLength) {
|
|
310
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", `Invalid ${field}.`);
|
|
311
|
+
}
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
function optionalBoolean(body, field) {
|
|
315
|
+
const value = body[field];
|
|
316
|
+
if (value === void 0) return void 0;
|
|
317
|
+
if (typeof value !== "boolean") {
|
|
318
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", `Invalid ${field}.`);
|
|
319
|
+
}
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
function isPlainRecord(value) {
|
|
323
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/handler.ts
|
|
327
|
+
var PACKAGE = /^[a-z0-9][a-z0-9_.-]{1,80}$/;
|
|
328
|
+
var ACTION = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/;
|
|
329
|
+
var IDENTIFIER = /^[A-Za-z0-9._:~-]{4,128}$/;
|
|
330
|
+
var REASON = /^[a-z][a-z0-9_.-]{2,127}$/;
|
|
331
|
+
var COUNTRY = /^[A-Z]{2}$/;
|
|
332
|
+
var HASH = /^[a-f0-9]{64}$/;
|
|
333
|
+
var ATTEMPT_ID = /^[A-Za-z0-9._:~-]{8,128}$/;
|
|
334
|
+
var SESSION_KEYS = [
|
|
335
|
+
"packageCode",
|
|
336
|
+
"countryCode",
|
|
337
|
+
"action",
|
|
338
|
+
"resourceType",
|
|
339
|
+
"resourceReference",
|
|
340
|
+
"subjectReference",
|
|
341
|
+
"continuationToken",
|
|
342
|
+
"continuationKey",
|
|
343
|
+
"legalFirstName",
|
|
344
|
+
"legalLastName",
|
|
345
|
+
"email",
|
|
346
|
+
"organization",
|
|
347
|
+
"relationship",
|
|
348
|
+
"associatedPerson",
|
|
349
|
+
"evidenceReferences",
|
|
350
|
+
"metadata"
|
|
351
|
+
];
|
|
352
|
+
var APPEAL_KEYS = ["attemptId", "reasonCode", "nonBiometricPathRequested", "details"];
|
|
353
|
+
var SUPPORT_KEYS = ["attemptId", "appealId", "reasonCode", "message"];
|
|
354
|
+
var PROTECTED_KEYS = ["action", "resourceType", "resourceReference"];
|
|
355
|
+
var ROUTE_KEYS = ["operation", "targetRoutePolicyId", "requestId", "action", "reasonCode"];
|
|
356
|
+
var CIRCUIT_KEYS = ["provider", "providerEnvironment", "status", "errorCode", "circuitOpenUntil"];
|
|
357
|
+
var REVIEW_KEYS = [
|
|
358
|
+
"operation",
|
|
359
|
+
"appealId",
|
|
360
|
+
"requestId",
|
|
361
|
+
"packageCode",
|
|
362
|
+
"decision",
|
|
363
|
+
"reasonCode",
|
|
364
|
+
"policyVersionId",
|
|
365
|
+
"evidenceReferenceHash",
|
|
366
|
+
"expiresAt"
|
|
367
|
+
];
|
|
368
|
+
var RECONCILE_KEYS = ["attemptId"];
|
|
369
|
+
var REDACT_KEYS = ["privacyRequestId"];
|
|
370
|
+
var WORKER_CLAIM_KEYS = ["queue", "limit"];
|
|
371
|
+
var WORKER_PROCESS_KEYS = ["leaseId", "jobId"];
|
|
372
|
+
function createVerificationFetchHandler(platform, options) {
|
|
373
|
+
const allowedOrigins = options.allowedOrigins ?? [];
|
|
374
|
+
const supportPath = options.supportPath ?? null;
|
|
375
|
+
const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
|
|
376
|
+
return async (incoming) => {
|
|
377
|
+
const extra = corsHeaders(incoming, allowedOrigins);
|
|
378
|
+
try {
|
|
379
|
+
requireOriginWhenPresent(incoming, allowedOrigins.length > 0 ? allowedOrigins : [incoming.headers.get("origin") ?? ""]);
|
|
380
|
+
const request = await readLimitedBody(incoming, maxBodyBytes);
|
|
381
|
+
const url = new URL(request.url);
|
|
382
|
+
const matched = matchV1Route(request.method, url.pathname, options.basePath);
|
|
383
|
+
if (!matched) {
|
|
384
|
+
if (request.method === "OPTIONS") {
|
|
385
|
+
return new Response(null, { status: 204, headers: extra });
|
|
386
|
+
}
|
|
387
|
+
throw new VerificationHttpError(404, "NOT_FOUND", "The verification endpoint was not found.");
|
|
388
|
+
}
|
|
389
|
+
const { route, params } = matched;
|
|
390
|
+
if (route.method === "OPTIONS") {
|
|
391
|
+
return new Response(null, { status: 204, headers: extra });
|
|
392
|
+
}
|
|
393
|
+
if (route.id === "webhooks.ingest") {
|
|
394
|
+
const provider = params.provider ?? "";
|
|
395
|
+
if (!verificationAdapterSdk.isProviderCode(provider)) {
|
|
396
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", "Invalid provider.");
|
|
397
|
+
}
|
|
398
|
+
const result = await platform.ingestWebhook(provider, request);
|
|
399
|
+
return jsonResponse(result, result.accepted ? 202 : 400, extra);
|
|
400
|
+
}
|
|
401
|
+
if (isCookieAuthenticated(request, options.isCookieAuthenticated) && route.csrf) {
|
|
402
|
+
if (allowedOrigins.length === 0) {
|
|
403
|
+
throw new VerificationHttpError(403, "CSRF_REJECTED", "Cookie-authenticated mutations require an origin allowlist.");
|
|
404
|
+
}
|
|
405
|
+
requireCsrf(request, route, allowedOrigins);
|
|
406
|
+
}
|
|
407
|
+
const actor = await options.resolveActor(request);
|
|
408
|
+
authorizeActor(actor, route.auth);
|
|
409
|
+
const idempotencyKey = requireIdempotencyKey(request, route) ?? "";
|
|
410
|
+
if (options.rateLimit) {
|
|
411
|
+
const budget = await options.rateLimit(actor, route.id);
|
|
412
|
+
if (!budget.allowed) {
|
|
413
|
+
throw new VerificationHttpError(429, "RATE_LIMITED", "Please wait before retrying.", budget.retryAfterSeconds ?? 30);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const body = route.mutation && route.method === "POST" && route.id !== "sessions.get" ? await parseBodyForRoute(route.id, request) : {};
|
|
417
|
+
const payload = await dispatch(platform, route.id, {
|
|
418
|
+
actor,
|
|
419
|
+
params,
|
|
420
|
+
body,
|
|
421
|
+
idempotencyKey,
|
|
422
|
+
request
|
|
423
|
+
});
|
|
424
|
+
return jsonResponse(payload, route.mutation && route.method === "POST" ? 200 : 200, extra);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
const mapped = toSafeErrorEnvelope(error, supportPath);
|
|
427
|
+
const headers = { ...extra };
|
|
428
|
+
if (mapped.retryAfterSeconds !== null) headers["Retry-After"] = String(Math.max(1, Math.ceil(mapped.retryAfterSeconds)));
|
|
429
|
+
return jsonResponse(mapped.body, mapped.status, headers);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async function parseBodyForRoute(routeId, request) {
|
|
434
|
+
switch (routeId) {
|
|
435
|
+
case "sessions.create":
|
|
436
|
+
return parseJsonObject(request, SESSION_KEYS, { rejectRoutingFields: true });
|
|
437
|
+
case "sessions.resume":
|
|
438
|
+
case "sessions.retry":
|
|
439
|
+
case "sessions.pause":
|
|
440
|
+
case "sessions.cancel":
|
|
441
|
+
return parseJsonObject(request, []);
|
|
442
|
+
case "appeals.create":
|
|
443
|
+
return parseJsonObject(request, APPEAL_KEYS);
|
|
444
|
+
case "support.escalations":
|
|
445
|
+
return parseJsonObject(request, SUPPORT_KEYS);
|
|
446
|
+
case "protectedActions.evaluate":
|
|
447
|
+
return parseJsonObject(request, PROTECTED_KEYS);
|
|
448
|
+
case "admin.routes.mutate":
|
|
449
|
+
return parseJsonObject(request, ROUTE_KEYS, { rejectRoutingFields: false });
|
|
450
|
+
case "admin.circuits.mutate":
|
|
451
|
+
return parseJsonObject(request, CIRCUIT_KEYS, { rejectRoutingFields: false });
|
|
452
|
+
case "admin.review.mutate":
|
|
453
|
+
return parseJsonObject(request, REVIEW_KEYS, { rejectRoutingFields: false });
|
|
454
|
+
case "admin.reconciliation.mutate":
|
|
455
|
+
return parseJsonObject(request, RECONCILE_KEYS, { rejectRoutingFields: false });
|
|
456
|
+
case "admin.redaction.mutate":
|
|
457
|
+
return parseJsonObject(request, REDACT_KEYS, { rejectRoutingFields: false });
|
|
458
|
+
case "workers.claim":
|
|
459
|
+
return parseJsonObject(request, WORKER_CLAIM_KEYS, { rejectRoutingFields: false });
|
|
460
|
+
case "workers.process":
|
|
461
|
+
return parseJsonObject(request, WORKER_PROCESS_KEYS, { rejectRoutingFields: false });
|
|
462
|
+
default:
|
|
463
|
+
return {};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function dispatch(platform, routeId, ctx) {
|
|
467
|
+
const attemptId = ctx.params.id;
|
|
468
|
+
switch (routeId) {
|
|
469
|
+
case "sessions.create": {
|
|
470
|
+
const input = parseStartInput(ctx.body);
|
|
471
|
+
authorizeSubjectScope(ctx.actor, input.subjectReference);
|
|
472
|
+
return platform.startSession(ctx.actor, input, ctx.idempotencyKey, ctx.request);
|
|
473
|
+
}
|
|
474
|
+
case "sessions.resume":
|
|
475
|
+
return platform.resumeSession(ctx.actor, { attemptId: requireAttemptId(attemptId), idempotencyKey: ctx.idempotencyKey }, ctx.request);
|
|
476
|
+
case "sessions.get":
|
|
477
|
+
return platform.getSession(ctx.actor, requireAttemptId(attemptId));
|
|
478
|
+
case "sessions.retry":
|
|
479
|
+
return platform.retrySession(ctx.actor, { attemptId: requireAttemptId(attemptId), idempotencyKey: ctx.idempotencyKey }, ctx.request);
|
|
480
|
+
case "sessions.pause":
|
|
481
|
+
return platform.pauseSession(ctx.actor, { attemptId: requireAttemptId(attemptId), idempotencyKey: ctx.idempotencyKey });
|
|
482
|
+
case "sessions.cancel":
|
|
483
|
+
return platform.cancelSession(ctx.actor, { attemptId: requireAttemptId(attemptId), idempotencyKey: ctx.idempotencyKey });
|
|
484
|
+
case "appeals.create":
|
|
485
|
+
return platform.createAppeal(ctx.actor, {
|
|
486
|
+
attemptId: optionalString(ctx.body, "attemptId", ATTEMPT_ID, 128),
|
|
487
|
+
reasonCode: requiredString(ctx.body, "reasonCode", REASON, 128),
|
|
488
|
+
nonBiometricPathRequested: optionalBoolean(ctx.body, "nonBiometricPathRequested"),
|
|
489
|
+
details: optionalString(ctx.body, "details", /^[\s\S]{1,2000}$/, 2e3)
|
|
490
|
+
}, ctx.idempotencyKey);
|
|
491
|
+
case "support.escalations":
|
|
492
|
+
return platform.createSupportEscalation(ctx.actor, {
|
|
493
|
+
attemptId: optionalString(ctx.body, "attemptId", ATTEMPT_ID, 128),
|
|
494
|
+
appealId: optionalString(ctx.body, "appealId", ATTEMPT_ID, 128),
|
|
495
|
+
reasonCode: requiredString(ctx.body, "reasonCode", REASON, 128),
|
|
496
|
+
message: optionalString(ctx.body, "message", /^[\s\S]{1,2000}$/, 2e3)
|
|
497
|
+
}, ctx.idempotencyKey);
|
|
498
|
+
case "protectedActions.evaluate":
|
|
499
|
+
return platform.evaluateProtectedAction(ctx.actor, parseProtectedAction(ctx.body), ctx.idempotencyKey);
|
|
500
|
+
case "admin.health":
|
|
501
|
+
return platform.admin.health(ctx.actor);
|
|
502
|
+
case "admin.routes.get":
|
|
503
|
+
return platform.admin.routes(ctx.actor);
|
|
504
|
+
case "admin.routes.mutate":
|
|
505
|
+
return mutateRoute(platform, ctx.actor, ctx.body, ctx.idempotencyKey);
|
|
506
|
+
case "admin.circuits.get":
|
|
507
|
+
return platform.admin.circuits(ctx.actor);
|
|
508
|
+
case "admin.circuits.mutate":
|
|
509
|
+
return platform.admin.setCircuit(ctx.actor, parseCircuit(ctx.body), ctx.idempotencyKey);
|
|
510
|
+
case "admin.attempts":
|
|
511
|
+
return platform.admin.attempts(ctx.actor);
|
|
512
|
+
case "admin.review.get":
|
|
513
|
+
return platform.admin.review(ctx.actor);
|
|
514
|
+
case "admin.review.mutate":
|
|
515
|
+
return mutateReview(platform, ctx.actor, ctx.body, ctx.idempotencyKey);
|
|
516
|
+
case "admin.audit":
|
|
517
|
+
return platform.admin.audit(ctx.actor);
|
|
518
|
+
case "admin.reconciliation.get":
|
|
519
|
+
return platform.admin.reconciliation(ctx.actor);
|
|
520
|
+
case "admin.reconciliation.mutate":
|
|
521
|
+
return platform.admin.enqueueReconciliation(ctx.actor, {
|
|
522
|
+
attemptId: requiredString(ctx.body, "attemptId", ATTEMPT_ID, 128)
|
|
523
|
+
}, ctx.idempotencyKey);
|
|
524
|
+
case "admin.redaction.get":
|
|
525
|
+
return platform.admin.redaction(ctx.actor);
|
|
526
|
+
case "admin.redaction.mutate":
|
|
527
|
+
return platform.admin.approveRedaction(ctx.actor, {
|
|
528
|
+
privacyRequestId: requiredString(ctx.body, "privacyRequestId", IDENTIFIER, 128)
|
|
529
|
+
}, ctx.idempotencyKey);
|
|
530
|
+
case "workers.claim":
|
|
531
|
+
return platform.workers.claim(ctx.actor, {
|
|
532
|
+
queue: parseWorkerQueue(ctx.body.queue),
|
|
533
|
+
limit: typeof ctx.body.limit === "number" ? ctx.body.limit : void 0
|
|
534
|
+
}, ctx.idempotencyKey);
|
|
535
|
+
case "workers.process":
|
|
536
|
+
return platform.workers.process(ctx.actor, {
|
|
537
|
+
leaseId: requiredString(ctx.body, "leaseId", IDENTIFIER, 128),
|
|
538
|
+
jobId: requiredString(ctx.body, "jobId", IDENTIFIER, 128)
|
|
539
|
+
}, ctx.idempotencyKey);
|
|
540
|
+
default:
|
|
541
|
+
throw new VerificationHttpError(404, "NOT_FOUND", "The verification endpoint was not found.");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function parseStartInput(body) {
|
|
545
|
+
const packageCode = requiredString(body, "packageCode", PACKAGE, 80);
|
|
546
|
+
if (!verificationAdapterSdk.isPackageCode(packageCode)) {
|
|
547
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", "Invalid packageCode.");
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
packageCode,
|
|
551
|
+
countryCode: optionalString(body, "countryCode", COUNTRY, 2),
|
|
552
|
+
action: optionalString(body, "action", ACTION, 160),
|
|
553
|
+
resourceType: optionalString(body, "resourceType", /^[a-z][a-z0-9_]{1,63}$/, 64),
|
|
554
|
+
resourceReference: optionalString(body, "resourceReference", IDENTIFIER, 128),
|
|
555
|
+
subjectReference: optionalString(body, "subjectReference", IDENTIFIER, 128),
|
|
556
|
+
continuationToken: optionalString(body, "continuationToken", HASH, 64),
|
|
557
|
+
continuationKey: optionalString(body, "continuationKey", /^[a-z][a-z0-9_.-]{2,127}$/, 128),
|
|
558
|
+
legalFirstName: optionalString(body, "legalFirstName", /^[\p{L}\p{M}\s'.-]{1,80}$/u, 80) ?? null,
|
|
559
|
+
legalLastName: optionalString(body, "legalLastName", /^[\p{L}\p{M}\s'.-]{1,80}$/u, 80) ?? null,
|
|
560
|
+
email: optionalString(body, "email", /^[^\s@]+@[^\s@]+\.[^\s@]+$/, 254) ?? null,
|
|
561
|
+
organization: isRecord(body.organization) ? body.organization : void 0,
|
|
562
|
+
relationship: isRecord(body.relationship) ? body.relationship : void 0,
|
|
563
|
+
associatedPerson: isRecord(body.associatedPerson) ? body.associatedPerson : void 0,
|
|
564
|
+
evidenceReferences: Array.isArray(body.evidenceReferences) ? body.evidenceReferences.filter((value) => typeof value === "string") : void 0,
|
|
565
|
+
metadata: isRecord(body.metadata) ? sanitizeMetadata(body.metadata) : void 0
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function parseProtectedAction(body) {
|
|
569
|
+
return {
|
|
570
|
+
action: requiredString(body, "action", ACTION, 160),
|
|
571
|
+
resourceType: optionalString(body, "resourceType", /^[a-z][a-z0-9_]{1,63}$/, 64),
|
|
572
|
+
resourceReference: optionalString(body, "resourceReference", IDENTIFIER, 128)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function parseCircuit(body) {
|
|
576
|
+
const status = requiredString(body, "status", /^(healthy|degraded|unavailable|circuit_open)$/, 32);
|
|
577
|
+
const environment = requiredString(body, "providerEnvironment", /^(sandbox|production)$/, 16);
|
|
578
|
+
return {
|
|
579
|
+
provider: requiredString(body, "provider", /^[a-z][a-z0-9_]{1,63}$/, 64),
|
|
580
|
+
providerEnvironment: environment,
|
|
581
|
+
status,
|
|
582
|
+
errorCode: optionalString(body, "errorCode", /^[A-Z][A-Z0-9_]{2,127}$/, 128),
|
|
583
|
+
circuitOpenUntil: optionalString(body, "circuitOpenUntil", /^\d{4}-\d{2}-\d{2}T/, 40)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function mutateRoute(platform, actor, body, idempotencyKey) {
|
|
587
|
+
const operation = requiredString(body, "operation", /^(propose|approve|apply)$/, 16);
|
|
588
|
+
const input = {
|
|
589
|
+
operation,
|
|
590
|
+
targetRoutePolicyId: optionalString(body, "targetRoutePolicyId", IDENTIFIER, 128),
|
|
591
|
+
requestId: optionalString(body, "requestId", IDENTIFIER, 128),
|
|
592
|
+
action: optionalString(body, "action", /^(activate|drain|rollback)$/, 16),
|
|
593
|
+
reasonCode: optionalString(body, "reasonCode", REASON, 128)
|
|
594
|
+
};
|
|
595
|
+
if (operation !== "propose") {
|
|
596
|
+
const snapshot = await platform.admin.routes(actor);
|
|
597
|
+
const change = snapshot.routeChanges.find((row) => row.id === input.requestId);
|
|
598
|
+
if (operation === "approve") assertSeparationOfDuties(actor, stringField(change, "proposedBy"), "approve this route change");
|
|
599
|
+
if (operation === "apply") assertSeparationOfDuties(actor, stringField(change, "approvedBy"), "apply this route change");
|
|
600
|
+
}
|
|
601
|
+
return platform.admin.mutateRoute(actor, input, idempotencyKey);
|
|
602
|
+
}
|
|
603
|
+
async function mutateReview(platform, actor, body, idempotencyKey) {
|
|
604
|
+
const operation = requiredString(
|
|
605
|
+
body,
|
|
606
|
+
"operation",
|
|
607
|
+
/^(begin_appeal_review|propose_manual_exception|approve_manual_exception|apply_manual_exception)$/,
|
|
608
|
+
40
|
|
609
|
+
);
|
|
610
|
+
const input = {
|
|
611
|
+
operation,
|
|
612
|
+
appealId: optionalString(body, "appealId", IDENTIFIER, 128),
|
|
613
|
+
requestId: optionalString(body, "requestId", IDENTIFIER, 128),
|
|
614
|
+
packageCode: optionalString(body, "packageCode", PACKAGE, 80),
|
|
615
|
+
decision: optionalString(body, "decision", /^(approve|deny|revoke|request_more_information)$/, 40),
|
|
616
|
+
reasonCode: optionalString(body, "reasonCode", REASON, 128),
|
|
617
|
+
policyVersionId: optionalString(body, "policyVersionId", IDENTIFIER, 128),
|
|
618
|
+
evidenceReferenceHash: optionalString(body, "evidenceReferenceHash", HASH, 64),
|
|
619
|
+
expiresAt: optionalString(body, "expiresAt", /^\d{4}-\d{2}-\d{2}T/, 40)
|
|
620
|
+
};
|
|
621
|
+
if (operation === "approve_manual_exception" || operation === "apply_manual_exception") {
|
|
622
|
+
const snapshot = await platform.admin.review(actor);
|
|
623
|
+
const exception = snapshot.manualExceptions.find((row) => row.id === input.requestId);
|
|
624
|
+
if (operation === "approve_manual_exception") {
|
|
625
|
+
assertSeparationOfDuties(actor, stringField(exception, "proposedBy"), "approve this manual exception");
|
|
626
|
+
}
|
|
627
|
+
if (operation === "apply_manual_exception") {
|
|
628
|
+
assertSeparationOfDuties(actor, stringField(exception, "approvedBy"), "apply this manual exception");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return platform.admin.mutateReview(actor, input, idempotencyKey);
|
|
632
|
+
}
|
|
633
|
+
function parseWorkerQueue(value) {
|
|
634
|
+
if (value === "webhook" || value === "reconciliation" || value === "redaction" || value === "dead_letter") return value;
|
|
635
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", "Invalid queue.");
|
|
636
|
+
}
|
|
637
|
+
function requireAttemptId(value) {
|
|
638
|
+
if (!value || !ATTEMPT_ID.test(value)) {
|
|
639
|
+
throw new VerificationHttpError(400, "INVALID_REQUEST", "Invalid attempt id.");
|
|
640
|
+
}
|
|
641
|
+
return value;
|
|
642
|
+
}
|
|
643
|
+
function sanitizeMetadata(value) {
|
|
644
|
+
const result = {};
|
|
645
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
646
|
+
if (typeof nested === "string" || typeof nested === "number" || typeof nested === "boolean" || nested === null) {
|
|
647
|
+
result[key] = nested;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
652
|
+
function isRecord(value) {
|
|
653
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
654
|
+
}
|
|
655
|
+
function stringField(record, key) {
|
|
656
|
+
const value = record?.[key];
|
|
657
|
+
return typeof value === "string" ? value : null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/supabase.ts
|
|
661
|
+
function createVerificationSupabaseHandler(platform, options) {
|
|
662
|
+
return createVerificationFetchHandler(platform, {
|
|
663
|
+
allowedOrigins: options.allowedOrigins,
|
|
664
|
+
isCookieAuthenticated: options.isCookieAuthenticated,
|
|
665
|
+
basePath: options.basePath,
|
|
666
|
+
maxBodyBytes: options.maxBodyBytes,
|
|
667
|
+
supportPath: options.supportPath,
|
|
668
|
+
rateLimit: options.rateLimit,
|
|
669
|
+
resolveActor: async (request) => {
|
|
670
|
+
const user = await options.getUser(request);
|
|
671
|
+
return options.resolveActor(request, user);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
exports.createVerificationSupabaseHandler = createVerificationSupabaseHandler;
|
|
677
|
+
//# sourceMappingURL=supabase.cjs.map
|
|
678
|
+
//# sourceMappingURL=supabase.cjs.map
|