@auersperg/flare 0.3.2 → 0.3.4

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.
Files changed (52) hide show
  1. package/README.md +10 -4
  2. package/dist/cli/{chunk-x14smx1y.js → chunk-5s6zv1h9.js} +1 -1
  3. package/dist/cli/chunk-5tjevcsj.js +1174 -0
  4. package/dist/cli/{chunk-5cq8wgte.js → chunk-5yg1zj5n.js} +48 -29
  5. package/dist/cli/{chunk-786w6vrj.js → chunk-fv98dfg5.js} +21 -1
  6. package/dist/cli/flare.js +120 -4
  7. package/dist/platform/admin.js +790 -0
  8. package/dist/platform/dashboard/_astro/auth-view.CuxQ12Fa.js +1 -0
  9. package/dist/platform/dashboard/_astro/badge.DwKWvVfO.js +1 -0
  10. package/dist/platform/dashboard/_astro/card.Bdqmram9.js +1 -0
  11. package/dist/platform/dashboard/_astro/cli-approval.VKPc8wYS.js +1 -0
  12. package/dist/platform/dashboard/_astro/client.BUUZgEFj.js +1 -0
  13. package/dist/platform/dashboard/_astro/client.DYdPL2z4.js +9 -0
  14. package/dist/platform/dashboard/_astro/common.HrRZXwD9.js +1 -0
  15. package/dist/platform/dashboard/_astro/dashboard.CH594XTw.js +3 -0
  16. package/dist/platform/dashboard/_astro/data-view.CcyUYvId.js +1 -0
  17. package/dist/platform/dashboard/_astro/function-badge.MlTRYkqM.js +1 -0
  18. package/dist/platform/dashboard/_astro/functions-view.C8JWdNAh.js +1 -0
  19. package/dist/platform/dashboard/_astro/geist-cyrillic-ext-wght-normal.DjL33-gN.woff2 +0 -0
  20. package/dist/platform/dashboard/_astro/geist-cyrillic-wght-normal.BEAKL7Jp.woff2 +0 -0
  21. package/dist/platform/dashboard/_astro/geist-latin-ext-wght-normal.DC-KSUi6.woff2 +0 -0
  22. package/dist/platform/dashboard/_astro/geist-latin-wght-normal.BgDaEnEv.woff2 +0 -0
  23. package/dist/platform/dashboard/_astro/geist-vietnamese-wght-normal.6IgcOCM7.woff2 +0 -0
  24. package/dist/platform/dashboard/_astro/index.DiiMDa7R.css +2 -0
  25. package/dist/platform/dashboard/_astro/input-group.3twGpQ77.js +1 -0
  26. package/dist/platform/dashboard/_astro/react-dom.DwX89IcG.js +1 -0
  27. package/dist/platform/dashboard/_astro/refresh-cw.B0Cj85jJ.js +1 -0
  28. package/dist/platform/dashboard/_astro/sdk-view.CRFkbEyS.js +1 -0
  29. package/dist/platform/dashboard/_astro/services-view.B2suYnaG.js +1 -0
  30. package/dist/platform/dashboard/_astro/settings-view.BFP47ySK.js +3 -0
  31. package/dist/platform/dashboard/_astro/status-badge.BV1dzbX9.js +1 -0
  32. package/dist/platform/dashboard/_astro/table-guide.DzBDP6fC.js +2 -0
  33. package/dist/platform/dashboard/_astro/tabs.GRO5rCrK.js +1 -0
  34. package/dist/platform/dashboard/_headers +3 -0
  35. package/dist/platform/dashboard/bootstrap/action.js +263 -0
  36. package/dist/platform/dashboard/bootstrap/api.ts +5 -0
  37. package/dist/platform/dashboard/bootstrap/facet.js +914 -0
  38. package/dist/platform/dashboard/bootstrap/manifest.json +5 -0
  39. package/dist/platform/dashboard/bootstrap/openapi.json +18 -0
  40. package/dist/platform/dashboard/bootstrap/release.json +11 -0
  41. package/dist/platform/dashboard/bootstrap/schema.json +1 -0
  42. package/dist/platform/dashboard/index.html +1 -0
  43. package/dist/platform/manifest.json +230 -0
  44. package/dist/platform/migrations/0001.sql +8 -0
  45. package/dist/platform/migrations/0002_project_aliases.sql +6 -0
  46. package/dist/platform/migrations/0003_cli_login.sql +9 -0
  47. package/dist/platform/migrations/0004_retention.sql +4 -0
  48. package/dist/platform/migrations/0005_project_deletion.sql +11 -0
  49. package/dist/platform/migrations/0006_platform_installation.sql +8 -0
  50. package/dist/platform/runtime.js +9392 -0
  51. package/dist/sdk/client.js +10 -2
  52. package/package.json +1 -1
@@ -0,0 +1,790 @@
1
+ // packages/core/src/retention.ts
2
+ var jobRetentionMs = 30 * 86400000;
3
+ var catalogDeadlines = new WeakMap;
4
+ async function collectCatalog(database, now = Date.now()) {
5
+ if (now < (catalogDeadlines.get(database) ?? 0))
6
+ return;
7
+ const next = now + 86400000;
8
+ const claim = await database.prepare("UPDATE maintenance SET next=? WHERE id=1 AND next<=? RETURNING id").bind(next, now).first();
9
+ catalogDeadlines.set(database, claim ? next : now + 60000);
10
+ if (!claim)
11
+ return;
12
+ const statements = [
13
+ database.prepare("DELETE FROM audit WHERE id IN (SELECT id FROM audit WHERE at<? ORDER BY at LIMIT 1000)").bind(now - 90 * 86400000),
14
+ database.prepare("DELETE FROM credentials WHERE hash IN (SELECT hash FROM credentials WHERE expiresAt<? ORDER BY expiresAt LIMIT 1000)").bind(now - 14 * 86400000),
15
+ database.prepare("DELETE FROM cli_logins WHERE id IN (SELECT id FROM cli_logins WHERE expiresAt<? ORDER BY expiresAt LIMIT 1000)").bind(now)
16
+ ];
17
+ const result = await database.batch(statements);
18
+ if (result.some((value) => Number(value.meta.changes) >= 1000)) {
19
+ catalogDeadlines.set(database, now + 60000);
20
+ await database.prepare("UPDATE maintenance SET next=? WHERE id=1").bind(now + 60000).run();
21
+ }
22
+ }
23
+ // packages/core/package.json
24
+ var package_default = {
25
+ name: "@flare/platform",
26
+ version: "0.3.4",
27
+ type: "module",
28
+ license: "MIT",
29
+ private: true,
30
+ files: [
31
+ "dist",
32
+ "src",
33
+ "bootstrap",
34
+ "migrations",
35
+ "README.md"
36
+ ],
37
+ dependencies: {
38
+ agents: "0.28.0"
39
+ },
40
+ repository: {
41
+ type: "git",
42
+ url: "https://github.com/auersperg/flare.git",
43
+ directory: "packages/core"
44
+ }
45
+ };
46
+
47
+ // packages/flare/src/sdk/protocol.ts
48
+ var LIMITS = Object.freeze({
49
+ inputBytes: 65536,
50
+ outputBytes: 262144,
51
+ frameBytes: 8192,
52
+ subscriptions: 64,
53
+ fileBytes: 10485760
54
+ });
55
+
56
+ class FlareError extends Error {
57
+ code;
58
+ ambiguous;
59
+ constructor(code, message, ambiguous = false) {
60
+ super(message);
61
+ this.code = code;
62
+ this.ambiguous = ambiguous;
63
+ this.name = "FlareError";
64
+ }
65
+ }
66
+ function stableJson(value) {
67
+ const ancestors = new Set;
68
+ function visit(v, depth) {
69
+ if (depth > 40)
70
+ throw new FlareError("INVALID_JSON", "JSON nesting limit exceeded");
71
+ if (v === null || typeof v === "string" || typeof v === "boolean")
72
+ return v;
73
+ if (typeof v === "number" && Number.isFinite(v))
74
+ return v;
75
+ if (typeof v !== "object" || v === null || ancestors.has(v))
76
+ throw new FlareError("INVALID_JSON", "Expected lossless JSON");
77
+ if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) {
78
+ throw new FlareError("INVALID_JSON", "Expected plain JSON object");
79
+ }
80
+ if (Object.getOwnPropertySymbols(v).length)
81
+ throw new FlareError("INVALID_JSON", "Symbol keys are unsupported");
82
+ ancestors.add(v);
83
+ let result;
84
+ if (Array.isArray(v)) {
85
+ result = Array.from(v, (item) => visit(item, depth + 1));
86
+ } else {
87
+ const out = Object.create(null);
88
+ for (const key of Object.keys(v).sort()) {
89
+ const descriptor = Object.getOwnPropertyDescriptor(v, key);
90
+ if (!("value" in descriptor))
91
+ throw new FlareError("INVALID_JSON", "Getters are unsupported");
92
+ out[key] = visit(descriptor.value, depth + 1);
93
+ }
94
+ result = out;
95
+ }
96
+ ancestors.delete(v);
97
+ return result;
98
+ }
99
+ return JSON.stringify(visit(value, 0));
100
+ }
101
+ function boundedJson(value, maxBytes) {
102
+ const json = stableJson(value);
103
+ if (new TextEncoder().encode(json).byteLength > maxBytes)
104
+ throw new FlareError("TOO_LARGE", "JSON payload exceeds limit");
105
+ return json;
106
+ }
107
+
108
+ // packages/core/src/security.ts
109
+ function fail(code) {
110
+ throw new FlareError(code, code);
111
+ }
112
+ var uuid = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value) ? value : fail("INVALID_ID");
113
+ var environment = (value) => value === "dev" || value === "prod" ? value : fail("INVALID_ENV");
114
+ async function digest(value) {
115
+ return [
116
+ ...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)))
117
+ ].map((x) => x.toString(16).padStart(2, "0")).join("");
118
+ }
119
+ async function jsonBody(request, limit = LIMITS.inputBytes) {
120
+ if (!request.headers.get("content-type")?.startsWith("application/json"))
121
+ fail("INVALID_CONTENT_TYPE");
122
+ if (!request.body)
123
+ fail("INVALID_INPUT");
124
+ const reader = request.body.getReader();
125
+ const chunks = [];
126
+ let size = 0;
127
+ try {
128
+ for (;; ) {
129
+ const { done, value } = await reader.read();
130
+ if (done)
131
+ break;
132
+ size += value.byteLength;
133
+ if (size > limit)
134
+ fail("TOO_LARGE");
135
+ chunks.push(value);
136
+ }
137
+ } finally {
138
+ await reader.cancel().catch(() => {});
139
+ }
140
+ const bytes = new Uint8Array(size);
141
+ let offset = 0;
142
+ for (const c of chunks) {
143
+ bytes.set(c, offset);
144
+ offset += c.byteLength;
145
+ }
146
+ try {
147
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
148
+ boundedJson(parsed, limit);
149
+ return parsed;
150
+ } catch {
151
+ fail("INVALID_INPUT");
152
+ }
153
+ }
154
+ var decode = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
155
+ var encoder = new TextEncoder;
156
+ var jwksCache = new Map;
157
+ var jwksPending = new Map;
158
+ function loadJwks(url, rotation) {
159
+ const existing = jwksPending.get(url);
160
+ if (existing)
161
+ return existing;
162
+ const pending = (async () => {
163
+ const response = await fetch(url, {
164
+ redirect: "manual",
165
+ signal: AbortSignal.timeout(5000)
166
+ });
167
+ if (!response.ok)
168
+ fail("AUTH_UNAVAILABLE");
169
+ const raw = await jsonBody(response, 65536);
170
+ if (!Array.isArray(raw.keys) || raw.keys.length > 16)
171
+ fail("AUTH_UNAVAILABLE");
172
+ const cached = {
173
+ until: Date.now() + 300000,
174
+ refreshAfter: rotation ? Date.now() + 60000 : 0,
175
+ keys: raw.keys,
176
+ imported: new Map
177
+ };
178
+ if (jwksCache.size >= 32 && !jwksCache.has(url))
179
+ jwksCache.delete(jwksCache.keys().next().value);
180
+ jwksCache.set(url, cached);
181
+ return cached;
182
+ })();
183
+ jwksPending.set(url, pending);
184
+ pending.finally(() => jwksPending.delete(url)).catch(() => {
185
+ return;
186
+ });
187
+ return pending;
188
+ }
189
+ function validateOidc(value) {
190
+ if (!value || typeof value.issuer !== "string" || typeof value.audience !== "string" || value.audience.length < 1 || value.audience.length > 256)
191
+ fail("INVALID_AUTH_CONFIG");
192
+ for (const field of ["issuer", "jwks"]) {
193
+ const u = new URL(value[field]);
194
+ if (u.protocol !== "https:" || u.username || u.password || u.port || u.hash || u.search || u.hostname === "localhost" || !u.hostname.includes(".") || /^(\d+\.){3}\d+$/.test(u.hostname) || u.hostname.includes(":"))
195
+ fail("INVALID_AUTH_CONFIG");
196
+ }
197
+ if (new URL(value.issuer).origin !== new URL(value.jwks).origin)
198
+ fail("INVALID_AUTH_CONFIG");
199
+ return value;
200
+ }
201
+ function claimsAuth(claims, issuer, audience) {
202
+ const now = Date.now() / 1000;
203
+ if (claims.iss !== issuer || !(claims.aud === audience || Array.isArray(claims.aud) && claims.aud.includes(audience)) || typeof claims.sub !== "string" || !claims.sub || !Number.isFinite(claims.exp) || claims.exp <= now || claims.nbf !== undefined && (!Number.isFinite(claims.nbf) || claims.nbf > now))
204
+ fail("UNAUTHORIZED");
205
+ const auth = {
206
+ subject: claims.sub,
207
+ issuer,
208
+ expiresAt: claims.exp * 1000,
209
+ claims
210
+ };
211
+ boundedJson(auth, 1536);
212
+ return auth;
213
+ }
214
+ async function verifyJwt(token, config) {
215
+ validateOidc(config);
216
+ if (token.length > 8192)
217
+ fail("UNAUTHORIZED");
218
+ try {
219
+ const parts = token.split(".");
220
+ if (parts.length !== 3)
221
+ fail("UNAUTHORIZED");
222
+ const header = JSON.parse(new TextDecoder().decode(decode(parts[0])));
223
+ if (header.alg !== "RS256" || typeof header.kid !== "string" || header.crit)
224
+ fail("UNAUTHORIZED");
225
+ let cached = jwksCache.get(config.jwks);
226
+ if (!cached || cached.until < Date.now())
227
+ cached = await loadJwks(config.jwks, false);
228
+ const match = () => cached.keys.find((k) => k.kid === header.kid && k.kty === "RSA" && (!k.alg || k.alg === "RS256") && (!k.use || k.use === "sig"));
229
+ let jwk = match();
230
+ if (!jwk && cached.refreshAfter <= Date.now()) {
231
+ cached.refreshAfter = Date.now() + 60000;
232
+ cached = await loadJwks(config.jwks, true);
233
+ jwk = match();
234
+ }
235
+ if (!jwk)
236
+ fail("UNAUTHORIZED");
237
+ let imported = cached.imported.get(header.kid);
238
+ if (!imported) {
239
+ imported = crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
240
+ cached.imported.set(header.kid, imported);
241
+ }
242
+ const key = await imported;
243
+ if (!await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, decode(parts[2]), encoder.encode(parts[0] + "." + parts[1])))
244
+ fail("UNAUTHORIZED");
245
+ return claimsAuth(JSON.parse(new TextDecoder().decode(decode(parts[1]))), config.issuer, config.audience);
246
+ } catch (e) {
247
+ if (e instanceof FlareError)
248
+ throw e;
249
+ fail("UNAUTHORIZED");
250
+ }
251
+ }
252
+ function origins(value) {
253
+ if (!Array.isArray(value) || value.length > 10)
254
+ fail("INVALID_ORIGIN");
255
+ return value.map((x) => {
256
+ if (typeof x !== "string")
257
+ fail("INVALID_ORIGIN");
258
+ const u = new URL(x);
259
+ if (u.origin !== x || u.username || u.password || !(u.protocol === "https:" || u.protocol === "http:" && ["localhost", "127.0.0.1"].includes(u.hostname)))
260
+ fail("INVALID_ORIGIN");
261
+ return x;
262
+ });
263
+ }
264
+ function status(error) {
265
+ const code = error instanceof FlareError ? error.code : "OPERATION_FAILED";
266
+ return {
267
+ code,
268
+ status: code === "RATE_LIMITED" ? 429 : ["AUTH_DISABLED", "REGISTRATION_DISABLED"].includes(code) ? 403 : code === "REGISTRATION_UNAVAILABLE" ? 409 : code === "UNAUTHORIZED" || code === "AUTH_EXPIRED" ? 401 : code === "FORBIDDEN" ? 403 : code === "NOT_FOUND" || code === "FUNCTION_NOT_FOUND" ? 404 : ["TOO_LARGE", "BACKUP_TOO_LARGE"].includes(code) ? 413 : ["CONFLICT", "BACKUP_BUSY", "RESTORE_NOT_EMPTY"].includes(code) ? 409 : code.startsWith("INVALID") || code === "READ_ONLY" ? 400 : [
269
+ "NOT_READY",
270
+ "UNSUPPORTED",
271
+ "RESTORE_IN_PROGRESS"
272
+ ].includes(code) ? 503 : 500
273
+ };
274
+ }
275
+ function unwrap(result) {
276
+ if (!result.ok)
277
+ fail(result.code);
278
+ return result.value;
279
+ }
280
+ function accessAssertion(request) {
281
+ const assertion = request.headers.get("CF-Access-Jwt-Assertion");
282
+ if (assertion)
283
+ return assertion;
284
+ const matches = (request.headers.get("cookie") ?? "").split(";").map((x) => x.trim()).filter((x) => x.startsWith("CF_Authorization="));
285
+ if (matches.length !== 1)
286
+ fail("UNAUTHORIZED");
287
+ const token = matches[0].slice("CF_Authorization=".length);
288
+ if (!token)
289
+ fail("UNAUTHORIZED");
290
+ return token;
291
+ }
292
+ async function accessEmail(access, audience) {
293
+ if (!access || !audience || access.aud !== audience)
294
+ fail("UNAUTHORIZED");
295
+ const identity = await access.getIdentity();
296
+ if (!identity || typeof identity.email !== "string" || !identity.email)
297
+ fail("UNAUTHORIZED");
298
+ return identity.email.toLowerCase();
299
+ }
300
+
301
+ // packages/core/src/project-deletion.ts
302
+ function authorizeProjectDeletion(actor, project, input) {
303
+ if (actor.role !== "owner" || actor.projectId)
304
+ fail("FORBIDDEN");
305
+ if (input.confirm !== uuid(project.id) || input.name !== project.name)
306
+ fail("INVALID_CONFIRMATION");
307
+ }
308
+ async function deleteProject(catalog, runtime, projectId) {
309
+ const id = uuid(projectId);
310
+ await catalog.batch([
311
+ catalog.prepare("INSERT OR IGNORE INTO project_deletions VALUES (?,?)").bind(id, Date.now()),
312
+ catalog.prepare("UPDATE credentials SET revoked=1 WHERE projectId=?").bind(id)
313
+ ]);
314
+ for (const env of ["dev", "prod"])
315
+ await runtime.prepareDestroy(`${id}/${env}`);
316
+ for (const env of ["dev", "prod"])
317
+ await runtime.destroy(`${id}/${env}`);
318
+ await catalog.batch([
319
+ catalog.prepare("DELETE FROM credentials WHERE projectId=?").bind(id),
320
+ catalog.prepare("DELETE FROM environments WHERE projectId=?").bind(id),
321
+ catalog.prepare("UPDATE project_aliases SET published=0 WHERE projectId=?").bind(id),
322
+ catalog.prepare("DELETE FROM projects WHERE id=?").bind(id)
323
+ ]);
324
+ return { deleted: true, id };
325
+ }
326
+
327
+ // packages/core/src/cli-login.ts
328
+ var json = (value, status = 200) => Response.json(value, { status, headers: { "cache-control": "no-store" } });
329
+ async function publicLogin(request, env) {
330
+ if (request.method !== "POST")
331
+ return json({ error: "METHOD_NOT_ALLOWED" }, 405);
332
+ const input = await jsonBody(request, 8192), path = new URL(request.url).pathname;
333
+ if (path === "/api/cli/start") {
334
+ if (!/^[a-f0-9]{64}$/.test(input.secretHash) || typeof input.publicKey !== "object" || input.publicKey?.kty !== "RSA")
335
+ fail("INVALID_INPUT");
336
+ await crypto.subtle.importKey("jwk", input.publicKey, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
337
+ await env.CATALOG.prepare("DELETE FROM cli_logins WHERE expiresAt<?").bind(Date.now()).run();
338
+ const id = crypto.randomUUID(), code = crypto.randomUUID().slice(0, 8).toUpperCase();
339
+ await env.CATALOG.prepare("INSERT INTO cli_logins (id,secretHash,publicKey,code,expiresAt) VALUES (?,?,?,?,?)").bind(id, input.secretHash, JSON.stringify(input.publicKey), code, Date.now() + 600000).run();
340
+ return json({
341
+ id,
342
+ code,
343
+ dashboard: env.DASHBOARD_URL,
344
+ expiresIn: 600,
345
+ interval: 3
346
+ });
347
+ }
348
+ if (path !== "/api/cli/poll")
349
+ fail("NOT_FOUND");
350
+ const id = uuid(input.id);
351
+ if (typeof input.secret !== "string" || !/^[a-f0-9]{64}$/.test(input.secret))
352
+ fail("UNAUTHORIZED");
353
+ const hash = await digest(input.secret);
354
+ const result = await env.CATALOG.prepare("DELETE FROM cli_logins WHERE id=? AND secretHash=? AND expiresAt>? AND payload IS NOT NULL RETURNING payload").bind(id, hash, Date.now()).first();
355
+ if (result)
356
+ return json(JSON.parse(result.payload));
357
+ const pending = await env.CATALOG.prepare("SELECT id FROM cli_logins WHERE id=? AND secretHash=? AND expiresAt>?").bind(id, hash, Date.now()).first();
358
+ if (!pending)
359
+ fail("UNAUTHORIZED");
360
+ return json({ pending: true }, 202);
361
+ }
362
+ async function loginInfo(env, id) {
363
+ const row = await env.CATALOG.prepare("SELECT code,expiresAt,payload FROM cli_logins WHERE id=? AND expiresAt>?").bind(uuid(id), Date.now()).first();
364
+ if (!row || row.payload)
365
+ fail("NOT_FOUND");
366
+ return { code: row.code, expiresAt: row.expiresAt };
367
+ }
368
+ async function approveLogin(env, id, project, email, platform) {
369
+ const row = await env.CATALOG.prepare("SELECT publicKey FROM cli_logins WHERE id=? AND expiresAt>? AND payload IS NULL").bind(uuid(id), Date.now()).first();
370
+ if (!row)
371
+ fail("NOT_FOUND");
372
+ const token = crypto.randomUUID() + crypto.randomUUID(), expiresAt = Date.now() + 30 * 86400000;
373
+ const key = await crypto.subtle.importKey("jwk", JSON.parse(row.publicKey), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
374
+ const cipher = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, key, new TextEncoder().encode(token));
375
+ const encrypted = btoa(String.fromCharCode(...new Uint8Array(cipher)));
376
+ const results = await env.CATALOG.batch([
377
+ env.CATALOG.prepare("INSERT INTO credentials (hash,projectId,actor,expiresAt,revoked) SELECT ?,?,?,?,0 WHERE EXISTS (SELECT 1 FROM cli_logins WHERE id=? AND expiresAt>? AND payload IS NULL)").bind(await digest(token), uuid(project), email, expiresAt, id, Date.now()),
378
+ env.CATALOG.prepare("UPDATE cli_logins SET payload=? WHERE id=? AND expiresAt>? AND payload IS NULL").bind(JSON.stringify({ encrypted, platform, project, expiresAt }), id, Date.now())
379
+ ]);
380
+ if (!results[1].meta.changes)
381
+ fail("CONFLICT");
382
+ return { ok: true };
383
+ }
384
+
385
+ // packages/core/src/admin.ts
386
+ import { WorkerEntrypoint } from "cloudflare:workers";
387
+
388
+ // packages/core/src/routing.ts
389
+ var reserved = new Set(["p", "api", "control", "health", "v1", "cdn-cgi"]);
390
+ function projectAlias(value) {
391
+ if (typeof value !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value) || value.length > 48 || reserved.has(value))
392
+ fail("INVALID_ALIAS");
393
+ return value;
394
+ }
395
+ function defaultAlias(name, id) {
396
+ const stem = name.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32).replace(/-+$/g, "");
397
+ return projectAlias((/^[a-z]/.test(stem) ? stem : "app") + "-" + uuid(id).slice(0, 8));
398
+ }
399
+ function applicationUrl(base, id, env, alias) {
400
+ return base.replace(/\/$/, "") + (alias ? "/" + projectAlias(alias) : "/p/" + uuid(id)) + "/" + environment(env);
401
+ }
402
+
403
+ // packages/core/src/admin.ts
404
+ function runtime(env) {
405
+ return new Proxy({}, {
406
+ get: (_target, key) => (...args) => env.RUNTIME.command(String(key), args).then(unwrap)
407
+ });
408
+ }
409
+ var json2 = (data, status = 200) => Response.json(data, {
410
+ status,
411
+ headers: {
412
+ "cache-control": "no-store",
413
+ "x-content-type-options": "nosniff"
414
+ }
415
+ });
416
+ async function actor(request, env, credentialsOnly = false, ctx) {
417
+ if (credentialsOnly) {
418
+ const bearer = request.headers.get("authorization");
419
+ if (!bearer?.startsWith("Bearer ") || bearer.length > 256)
420
+ fail("UNAUTHORIZED");
421
+ const row = await env.CATALOG.prepare("SELECT c.actor,c.projectId,m.role FROM credentials c JOIN members m ON m.email=c.actor WHERE c.hash=? AND c.revoked=0 AND c.expiresAt>? AND NOT EXISTS (SELECT 1 FROM project_deletions d WHERE d.projectId=c.projectId)").bind(await digest(bearer.slice(7)), Date.now()).first();
422
+ if (!row || row.role === "viewer")
423
+ fail("UNAUTHORIZED");
424
+ return { email: row.actor, role: row.role, projectId: row.projectId };
425
+ }
426
+ const email = ctx?.access ? await accessEmail(ctx.access, env.ACCESS_AUD) : String((await verifyJwt(accessAssertion(request), {
427
+ issuer: env.ACCESS_ISSUER,
428
+ audience: env.ACCESS_AUD,
429
+ jwks: env.ACCESS_ISSUER + "/cdn-cgi/access/certs"
430
+ })).claims.email ?? "").toLowerCase();
431
+ if (!email || email.length > 254)
432
+ fail("UNAUTHORIZED");
433
+ const member = await env.CATALOG.prepare("SELECT role FROM members WHERE email=?").bind(email).first();
434
+ if (!member)
435
+ fail("FORBIDDEN");
436
+ return { email: email.toLowerCase(), role: member.role, projectId: null };
437
+ }
438
+ var writable = (a) => {
439
+ if (a.role === "viewer")
440
+ fail("FORBIDDEN");
441
+ };
442
+ async function audit(env, a, projectId, which, operation, work) {
443
+ await collectCatalog(env.CATALOG).catch((error) => console.warn({
444
+ source: "flare-catalog-retention",
445
+ code: status(error).code
446
+ }));
447
+ const id = crypto.randomUUID();
448
+ await env.CATALOG.prepare("INSERT INTO audit VALUES (?,?,?,?,?,?,?,?)").bind(id, Date.now(), a.email, projectId, which, operation, "pending", "{}").run();
449
+ try {
450
+ const result = await work();
451
+ await env.CATALOG.prepare("UPDATE audit SET status='ok' WHERE id=?").bind(id).run().catch((error) => console.warn({
452
+ source: "flare-audit-finalize",
453
+ code: status(error).code
454
+ }));
455
+ return result;
456
+ } catch (e) {
457
+ await env.CATALOG.prepare("UPDATE audit SET status='error',detail=? WHERE id=?").bind(JSON.stringify({ code: status(e).code }), id).run().catch((error) => console.warn({
458
+ source: "flare-audit-finalize",
459
+ code: status(error).code
460
+ }));
461
+ throw e;
462
+ }
463
+ }
464
+ async function bootstrap(env) {
465
+ const get = async (path) => (await env.ASSETS.fetch(new Request("https://assets.internal/bootstrap/" + path))).text();
466
+ return {
467
+ release: JSON.parse(await get("release.json")),
468
+ manifest: JSON.parse(await get("manifest.json")),
469
+ schema: JSON.parse(await get("schema.json")),
470
+ facet: await get("facet.js"),
471
+ action: await get("action.js")
472
+ };
473
+ }
474
+ async function publishAlias(env, id, alias) {
475
+ projectAlias(alias);
476
+ await env.CATALOG.prepare("INSERT INTO project_aliases (projectId,alias) VALUES (?,?) ON CONFLICT DO NOTHING").bind(id, alias).run();
477
+ const reserved = await env.CATALOG.prepare("SELECT alias FROM project_aliases WHERE projectId=?").bind(id).first();
478
+ if (reserved?.alias !== alias)
479
+ fail("CONFLICT");
480
+ await runtime(env).publishAlias(alias, id);
481
+ await env.CATALOG.prepare("UPDATE project_aliases SET published=1 WHERE projectId=? AND alias=?").bind(id, alias).run();
482
+ return { alias };
483
+ }
484
+ async function api(request, env, a) {
485
+ const url = new URL(request.url), path = url.pathname;
486
+ if (path === "/api/cli/approval") {
487
+ if (a.projectId || a.role !== "owner")
488
+ fail("FORBIDDEN");
489
+ if (request.method === "GET")
490
+ return json2(await loginInfo(env, url.searchParams.get("id") ?? ""));
491
+ if (request.method !== "POST")
492
+ return json2({ error: "METHOD_NOT_ALLOWED" }, 405);
493
+ const input = await jsonBody(request);
494
+ const selected = uuid(input.project);
495
+ if (!await env.CATALOG.prepare("SELECT id FROM projects WHERE id=? AND NOT EXISTS (SELECT 1 FROM project_deletions WHERE projectId=projects.id)").bind(selected).first())
496
+ fail("NOT_FOUND");
497
+ return json2(await audit(env, a, selected, "dev", "cli.login", () => approveLogin(env, input.id, selected, a.email, env.RUNTIME_URL + "/control")));
498
+ }
499
+ if (path === "/api/session" && request.method === "GET")
500
+ return json2({
501
+ email: a.email,
502
+ role: a.role,
503
+ projectId: a.projectId,
504
+ runtime: env.RUNTIME_URL,
505
+ version: package_default.version,
506
+ stage: "trusted-team-preview"
507
+ });
508
+ if (path === "/api/projects" && request.method === "GET") {
509
+ const rows = await env.CATALOG.prepare("SELECT p.*,c.name customer,r.alias,EXISTS(SELECT 1 FROM project_deletions d WHERE d.projectId=p.id) deleting FROM projects p JOIN customers c ON c.id=p.customerId LEFT JOIN project_aliases r ON r.projectId=p.id AND r.published=1 WHERE (? IS NULL OR p.id=?) ORDER BY p.createdAt DESC LIMIT 100").bind(a.projectId, a.projectId).all();
510
+ return json2(rows.results);
511
+ }
512
+ if (path === "/api/projects" && request.method === "POST") {
513
+ if (a.projectId)
514
+ fail("FORBIDDEN");
515
+ writable(a);
516
+ const input = await jsonBody(request);
517
+ for (const field of ["name", "customer"])
518
+ if (typeof input[field] !== "string" || input[field].trim().length < 1 || input[field].length > 100)
519
+ fail("INVALID_INPUT");
520
+ const customer = await env.CATALOG.prepare("SELECT id FROM customers WHERE name=? COLLATE NOCASE LIMIT 1").bind(input.customer.trim()).first();
521
+ const id = crypto.randomUUID(), customerId = customer?.id ?? crypto.randomUUID(), at = Date.now();
522
+ const alias = input.alias ? projectAlias(input.alias) : defaultAlias(input.name, id);
523
+ if (await env.CATALOG.prepare("SELECT projectId FROM project_aliases WHERE alias=?").bind(alias).first())
524
+ fail("CONFLICT");
525
+ const allowed = origins(input.origins ?? []);
526
+ const result = await audit(env, a, id, null, "project.create", async () => {
527
+ await env.CATALOG.batch([
528
+ ...customer ? [] : [
529
+ env.CATALOG.prepare("INSERT INTO customers VALUES (?,?,?)").bind(customerId, input.customer.trim(), at)
530
+ ],
531
+ env.CATALOG.prepare("INSERT INTO projects VALUES (?,?,?,?)").bind(id, customerId, input.name.trim(), at),
532
+ env.CATALOG.prepare("INSERT INTO project_aliases (projectId,alias) VALUES (?,?)").bind(id, alias),
533
+ ...["dev", "prod"].map((e) => env.CATALOG.prepare("INSERT INTO environments (projectId,env,origins) VALUES (?,?,?)").bind(id, e, JSON.stringify(allowed)))
534
+ ]);
535
+ const bundle = await bootstrap(env);
536
+ for (const e of ["dev", "prod"]) {
537
+ await runtime(env).configure({
538
+ scope: `${id}/${e}`,
539
+ origins: allowed,
540
+ oidc: null
541
+ });
542
+ await runtime(env).deploy(`${id}/${e}`, bundle);
543
+ }
544
+ await publishAlias(env, id, alias);
545
+ return { id, alias };
546
+ });
547
+ return json2(result, 201);
548
+ }
549
+ const match = path.match(/^\/api\/projects\/([^/]+)(?:\/(alias|delete|dev|prod)(?:\/([a-z-]+))?)?$/);
550
+ if (!match)
551
+ fail("NOT_FOUND");
552
+ const projectId = uuid(match[1]);
553
+ if (a.projectId && a.projectId !== projectId)
554
+ fail("FORBIDDEN");
555
+ const project = await env.CATALOG.prepare("SELECT p.*,r.alias,EXISTS(SELECT 1 FROM project_deletions d WHERE d.projectId=p.id) deleting FROM projects p LEFT JOIN project_aliases r ON r.projectId=p.id AND r.published=1 WHERE p.id=?").bind(projectId).first();
556
+ if (!project)
557
+ fail("NOT_FOUND");
558
+ if (match[2] === "delete" && !match[3]) {
559
+ if (request.method !== "POST")
560
+ return json2({ error: "METHOD_NOT_ALLOWED" }, 405);
561
+ const input = await jsonBody(request);
562
+ authorizeProjectDeletion(a, project, input);
563
+ return json2(await audit(env, a, projectId, null, "project.delete", () => deleteProject(env.CATALOG, runtime(env), projectId)));
564
+ }
565
+ if (project.deleting && match[2])
566
+ fail("NOT_READY");
567
+ if (match[2] === "alias") {
568
+ if (request.method !== "POST")
569
+ return json2({ error: "METHOD_NOT_ALLOWED" }, 405);
570
+ if (a.role !== "owner" || a.projectId)
571
+ fail("FORBIDDEN");
572
+ const input = await jsonBody(request);
573
+ const alias = projectAlias(input.alias);
574
+ return json2(await audit(env, a, projectId, null, "project.alias", () => publishAlias(env, projectId, alias)));
575
+ }
576
+ if (!match[2])
577
+ return json2({
578
+ project,
579
+ environments: (await env.CATALOG.prepare("SELECT * FROM environments WHERE projectId=?").bind(projectId).all()).results,
580
+ runtime: env.RUNTIME_URL,
581
+ urls: Object.fromEntries(["dev", "prod"].map((e) => [
582
+ e,
583
+ applicationUrl(env.RUNTIME_URL, projectId, e, project.alias)
584
+ ]))
585
+ });
586
+ const which = environment(match[2]), scope = `${projectId}/${which}`, op = match[3] ?? "health";
587
+ if (request.method === "GET") {
588
+ if (["release", "release-bundle"].includes(op)) {
589
+ const hash = url.searchParams.get("hash");
590
+ if (!hash || !/^[a-f0-9]{64}$/.test(hash))
591
+ fail("INVALID_INPUT");
592
+ if (op === "release-bundle" && a.role === "viewer")
593
+ fail("FORBIDDEN");
594
+ return json2(await runtime(env).admin(scope, op === "release" ? "release-details" : "release-export", { hash }));
595
+ }
596
+ if (op === "settings") {
597
+ const row = await env.CATALOG.prepare("SELECT * FROM environments WHERE projectId=? AND env=?").bind(projectId, which).first();
598
+ return json2({
599
+ ...row,
600
+ origins: JSON.parse(row.origins),
601
+ oidc: row.oidc ? JSON.parse(row.oidc) : null,
602
+ url: applicationUrl(env.RUNTIME_URL, projectId, which, project.alias),
603
+ alias: project.alias
604
+ });
605
+ }
606
+ if (op === "file-download")
607
+ return json2(await runtime(env).admin(scope, "file-download", {
608
+ id: url.searchParams.get("id")
609
+ }));
610
+ if (op === "audit")
611
+ return json2((await env.CATALOG.prepare("SELECT * FROM audit WHERE projectId=? ORDER BY at DESC LIMIT 100").bind(projectId).all()).results);
612
+ if (![
613
+ "auth-users",
614
+ "auth-settings",
615
+ "health",
616
+ "schema",
617
+ "data",
618
+ "functions",
619
+ "logs",
620
+ "history",
621
+ "jobs",
622
+ "files",
623
+ "crons",
624
+ "backups",
625
+ "backup-status"
626
+ ].includes(op))
627
+ fail("INVALID_OPERATION");
628
+ const input = {};
629
+ if (url.searchParams.has("table"))
630
+ input.table = url.searchParams.get("table");
631
+ if (url.searchParams.has("cursor")) {
632
+ input.cursor = url.searchParams.get("cursor");
633
+ if (!input.cursor || input.cursor.length > 8192)
634
+ fail("INVALID_CURSOR");
635
+ }
636
+ if (url.searchParams.has("limit")) {
637
+ const value = url.searchParams.get("limit");
638
+ if (!/^\d{1,3}$/.test(value) || Number(value) < 1 || Number(value) > 100)
639
+ fail("INVALID_INPUT");
640
+ input.limit = Number(value);
641
+ }
642
+ return json2(await runtime(env).admin(scope, op === "backups" ? "backup-list" : op, input));
643
+ }
644
+ if (request.method !== "POST")
645
+ return json2({ error: "METHOD_NOT_ALLOWED" }, 405);
646
+ writable(a);
647
+ const input = await jsonBody(request, op === "deploy" ? 4 * 1048576 : 65536);
648
+ if (which === "prod" && input.confirm !== projectId)
649
+ fail("INVALID_CONFIRMATION");
650
+ if ([
651
+ "auth-create",
652
+ "auth-password",
653
+ "auth-disable",
654
+ "auth-revoke",
655
+ "auth-configure"
656
+ ].includes(op)) {
657
+ if (a.role !== "owner" || a.projectId)
658
+ fail("FORBIDDEN");
659
+ return json2(await audit(env, a, projectId, which, op, () => runtime(env).admin(scope, op, input)));
660
+ }
661
+ if (op === "deploy") {
662
+ if (input.expected !== undefined && !/^[a-f0-9]{64}$/.test(input.expected))
663
+ fail("INVALID_INPUT");
664
+ const result = await audit(env, a, projectId, which, "release.deploy", () => runtime(env).deploy(scope, input.bundle, input.expected));
665
+ return json2({
666
+ ...result,
667
+ url: applicationUrl(env.RUNTIME_URL, projectId, which, project.alias)
668
+ });
669
+ }
670
+ if (["release-promote", "release-rollback"].includes(op)) {
671
+ if (!/^[a-f0-9]{64}$/.test(input.hash ?? "") || !/^[a-f0-9]{64}$/.test(input.expected ?? ""))
672
+ fail("INVALID_INPUT");
673
+ if (op === "release-promote" && which !== "prod")
674
+ fail("FORBIDDEN");
675
+ return json2(await audit(env, a, projectId, which, op, () => runtime(env).admin(scope, op, input)));
676
+ }
677
+ if (["backup-create", "backup-delete", "backup-restore"].includes(op)) {
678
+ if (a.role !== "owner")
679
+ fail("FORBIDDEN");
680
+ uuid(input.id);
681
+ if (op === "backup-restore")
682
+ input.source = `${projectId}/${environment(input.sourceEnv)}`;
683
+ return json2(await audit(env, a, projectId, which, op, () => runtime(env).admin(scope, op, input)));
684
+ }
685
+ if (op === "edit")
686
+ return json2(await audit(env, a, projectId, which, "data." + input.operation, () => runtime(env).admin(scope, "edit", input)));
687
+ if ([
688
+ "file-upload",
689
+ "file-delete",
690
+ "job-cancel",
691
+ "cron-set",
692
+ "cron-cancel"
693
+ ].includes(op))
694
+ return json2(await audit(env, a, projectId, which, op, () => runtime(env).admin(scope, op, input)));
695
+ if (op === "settings") {
696
+ const allowed = origins(input.origins), oidc = input.oidc ? validateOidc(input.oidc) : null;
697
+ return json2(await audit(env, a, projectId, which, "settings.update", async () => {
698
+ await runtime(env).configure({ scope, origins: allowed, oidc });
699
+ await env.CATALOG.prepare("UPDATE environments SET origins=?,oidc=? WHERE projectId=? AND env=?").bind(JSON.stringify(allowed), oidc ? JSON.stringify(oidc) : null, projectId, which).run();
700
+ return { ok: true };
701
+ }));
702
+ }
703
+ if (op === "dev-token") {
704
+ if (which !== "dev" || a.projectId)
705
+ fail("FORBIDDEN");
706
+ return json2({
707
+ token: await runtime(env).devIdentity(scope, a.email),
708
+ expiresIn: 900,
709
+ url: applicationUrl(env.RUNTIME_URL, projectId, which, project.alias)
710
+ });
711
+ }
712
+ if (op === "credential") {
713
+ if (a.role !== "owner" || a.projectId)
714
+ fail("FORBIDDEN");
715
+ const token = crypto.randomUUID() + crypto.randomUUID();
716
+ await audit(env, a, projectId, which, "credential.issue", async () => {
717
+ await env.CATALOG.prepare("INSERT INTO credentials VALUES (?,?,?,?,0)").bind(await digest(token), projectId, a.email, Date.now() + 30 * 86400000).run();
718
+ return { ok: true };
719
+ });
720
+ return json2({
721
+ token,
722
+ expiresIn: 30 * 86400,
723
+ platform: env.RUNTIME_URL + "/control"
724
+ });
725
+ }
726
+ if (op === "revoke-credentials") {
727
+ if (a.role !== "owner")
728
+ fail("FORBIDDEN");
729
+ return json2(await audit(env, a, projectId, which, "credential.revoke", async () => {
730
+ await env.CATALOG.prepare("UPDATE credentials SET revoked=1 WHERE projectId=?").bind(projectId).run();
731
+ return { ok: true };
732
+ }));
733
+ }
734
+ fail("INVALID_OPERATION");
735
+ }
736
+
737
+ class AdminControl extends WorkerEntrypoint {
738
+ async readiness() {
739
+ const row = await this.env.CATALOG.prepare("SELECT installation_id FROM flare_installation WHERE singleton=1").first();
740
+ const owner = await this.env.CATALOG.prepare("SELECT email FROM members WHERE role='owner' LIMIT 1").first();
741
+ if (!row || row.installation_id !== this.env.FLARE_INSTALLATION_ID || !owner)
742
+ fail("NOT_READY");
743
+ return {
744
+ version: package_default.version,
745
+ installationId: this.env.FLARE_INSTALLATION_ID,
746
+ platformPayload: this.env.FLARE_PLATFORM_PAYLOAD,
747
+ runtime: await this.env.RUNTIME.command("readiness", []).then(unwrap)
748
+ };
749
+ }
750
+ async request(request) {
751
+ try {
752
+ if (["/api/cli/start", "/api/cli/poll"].includes(new URL(request.url).pathname))
753
+ return await publicLogin(request, this.env);
754
+ return await api(request, this.env, await actor(request, this.env, true));
755
+ } catch (e) {
756
+ const out = status(e);
757
+ return json2({ error: out.code }, out.status);
758
+ }
759
+ }
760
+ }
761
+ var admin_default = {
762
+ async fetch(request, env, ctx) {
763
+ try {
764
+ const a = await actor(request, env, false, ctx);
765
+ const u = new URL(request.url);
766
+ if (u.pathname.startsWith("/api/")) {
767
+ if (!["GET", "HEAD", "OPTIONS"].includes(request.method) && request.headers.get("origin") !== u.origin)
768
+ fail("FORBIDDEN");
769
+ return await api(request, env, a);
770
+ }
771
+ if (u.pathname.startsWith("/bootstrap/") || u.pathname.startsWith("/starter/"))
772
+ return new Response("Not found", { status: 404 });
773
+ const response = await env.ASSETS.fetch(request), headers = new Headers(response.headers);
774
+ headers.set("content-security-policy", "default-src 'self'; script-src 'self' " + { scripts: ["'sha256-xIb/Pa7FUshAu4ihwjchYcQpyCI8eS8kt40GpOAYCz8='", "'sha256-1BvDVWLr3QCC20uHhUDi27eDFVJpk1uMQJGIWKUks2I='", "'sha256-kmZE/W8I1iuTnl1b3yqzv9/DQZenPngd3NjSKMQl7y0='", "'sha256-Y8t0ntteBwH+PqGKUyjWW3CAk+BNJKSoQc0xZ68FjhM='", "'sha256-0O7iB6zr9x0r6X6eWYITC9V5q+yWCGAoFvcysBGbY9Q='", "'sha256-tQIkdAw2vqfQ1PjgUE4ML2A4pIV8SByQ1Seeehy6UIc='", "'sha256-4f86GpVBksx63uHEB4f15jGyhnVEOKjwqYxwnfDVOik='"], styles: ["'sha256-vv9IoKo7BSLbWcUHr3tNmfNVmm5L/9Cfn2H6LMk7/ow='", "'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"] }.scripts.join(" ") + "; style-src 'self' " + { scripts: ["'sha256-xIb/Pa7FUshAu4ihwjchYcQpyCI8eS8kt40GpOAYCz8='", "'sha256-1BvDVWLr3QCC20uHhUDi27eDFVJpk1uMQJGIWKUks2I='", "'sha256-kmZE/W8I1iuTnl1b3yqzv9/DQZenPngd3NjSKMQl7y0='", "'sha256-Y8t0ntteBwH+PqGKUyjWW3CAk+BNJKSoQc0xZ68FjhM='", "'sha256-0O7iB6zr9x0r6X6eWYITC9V5q+yWCGAoFvcysBGbY9Q='", "'sha256-tQIkdAw2vqfQ1PjgUE4ML2A4pIV8SByQ1Seeehy6UIc='", "'sha256-4f86GpVBksx63uHEB4f15jGyhnVEOKjwqYxwnfDVOik='"], styles: ["'sha256-vv9IoKo7BSLbWcUHr3tNmfNVmm5L/9Cfn2H6LMk7/ow='", "'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"] }.styles.join(" ") + "; connect-src 'self' " + env.RUNTIME_URL + " " + env.RUNTIME_URL.replace("https:", "wss:") + "; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
775
+ headers.set("referrer-policy", "no-referrer");
776
+ headers.set("x-content-type-options", "nosniff");
777
+ headers.set("cache-control", response.ok && u.pathname.startsWith("/_astro/") ? "private, max-age=31536000, immutable" : "private, no-store");
778
+ return new Response(response.body, { status: response.status, headers });
779
+ } catch (e) {
780
+ const out = status(e);
781
+ if (out.status >= 500)
782
+ console.error({ source: "flare-admin", code: out.code });
783
+ return json2({ error: out.code }, out.status);
784
+ }
785
+ }
786
+ };
787
+ export {
788
+ AdminControl,
789
+ admin_default as default
790
+ };