@auersperg/flare 0.3.7 → 0.3.9

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 (21) hide show
  1. package/dist/cli/{chunk-r3ceakrn.js → chunk-0s0vrjtw.js} +213 -24
  2. package/dist/cli/flare.js +15 -4
  3. package/dist/platform/admin.js +80 -47
  4. package/dist/platform/dashboard/_astro/auth-view.EASEF9BN.js +1 -0
  5. package/dist/platform/dashboard/_astro/{cli-approval.C4-RBXQh.js → cli-approval.DwZVHdyo.js} +1 -1
  6. package/dist/platform/dashboard/_astro/common.TAClb6G9.js +1 -0
  7. package/dist/platform/dashboard/_astro/{data-view.B2afgdXn.js → data-view.BaHEU4Xa.js} +1 -1
  8. package/dist/platform/dashboard/_astro/{functions-view.B0jPBNNy.js → functions-view.BGOYuDwu.js} +1 -1
  9. package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.Blg5hU9g.js +3 -0
  10. package/dist/platform/dashboard/_astro/{input-group.BziAwKdZ.js → input-group.DUp-h0xx.js} +1 -1
  11. package/dist/platform/dashboard/_astro/{sdk-view.DAd5aPbd.js → sdk-view.2ErPn4cO.js} +1 -1
  12. package/dist/platform/dashboard/_astro/{services-view.BvkS7FHr.js → services-view.iZujitxl.js} +1 -1
  13. package/dist/platform/dashboard/_astro/settings-view.CqK-mrlB.js +3 -0
  14. package/dist/platform/dashboard/index.html +1 -1
  15. package/dist/platform/manifest.json +33 -33
  16. package/dist/platform/runtime.js +214 -35
  17. package/package.json +1 -1
  18. package/dist/platform/dashboard/_astro/auth-view.DxyBDw7Z.js +0 -1
  19. package/dist/platform/dashboard/_astro/common.D6YHScsr.js +0 -1
  20. package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.BQ7pqT75.js +0 -3
  21. package/dist/platform/dashboard/_astro/settings-view.BXt0dexJ.js +0 -3
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // packages/flare/src/cli/platform.mjs
3
- import { readFile as readFile3, mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, cp } from "fs/promises";
3
+ import { readFile as readFile4, mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, cp } from "fs/promises";
4
4
  import { join as join3 } from "path";
5
5
  import { tmpdir } from "os";
6
6
  import { randomBytes, randomUUID as randomUUID2 } from "crypto";
@@ -17,7 +17,7 @@ import { join } from "path";
17
17
  import { fileURLToPath } from "url";
18
18
  import { createHash } from "crypto";
19
19
  var PLATFORM_FORMAT = 1;
20
- var WRANGLER_VERSION = "4.149.0";
20
+ var WRANGLER_VERSION = "4.150.0";
21
21
  var sha256 = (value) => createHash("sha256").update(value).digest("hex");
22
22
  var manifestDigest = ({ digest: _digest, ...manifest }) => sha256(JSON.stringify(manifest));
23
23
  async function payloadFiles(directory, prefix = "") {
@@ -46,7 +46,7 @@ async function loadPlatformRelease(directory = fileURLToPath(new URL("../platfor
46
46
  } catch {
47
47
  throw Error("Platform payload missing or invalid. Install a complete Flare npm release.");
48
48
  }
49
- if (manifest.format !== PLATFORM_FORMAT || !/^\d+\.\d+\.\d+$/.test(manifest.version ?? "") || manifest.protocol !== 1 || manifest.runtimeAbi !== 4 || manifest.stateFormat !== 1 || manifest.wranglerVersion !== WRANGLER_VERSION || !/^\d{4}-\d{2}-\d{2}$/.test(manifest.compatibilityDate ?? "") || !Array.isArray(manifest.upgradeFrom) || manifest.upgradeFrom.some((version) => !/^\d+\.\d+\.\d+$/.test(version) || Bun.semver.order(version, manifest.version) >= 0) || !Array.isArray(manifest.files) || !manifest.files.length || manifest.files.length > 1000 || !/^[a-f0-9]{64}$/.test(manifest.digest ?? "") || manifest.digest !== manifestDigest(manifest))
49
+ if (manifest.format !== PLATFORM_FORMAT || !/^\d+\.\d+\.\d+$/.test(manifest.version ?? "") || manifest.protocol !== 1 || manifest.runtimeAbi !== 4 || manifest.stateFormat !== 1 || ![WRANGLER_VERSION, "4.149.0"].includes(manifest.wranglerVersion) || !/^\d{4}-\d{2}-\d{2}$/.test(manifest.compatibilityDate ?? "") || !Array.isArray(manifest.upgradeFrom) || manifest.upgradeFrom.some((version) => !/^\d+\.\d+\.\d+$/.test(version) || Bun.semver.order(version, manifest.version) >= 0) || !Array.isArray(manifest.files) || !manifest.files.length || manifest.files.length > 1000 || !/^[a-f0-9]{64}$/.test(manifest.digest ?? "") || manifest.digest !== manifestDigest(manifest))
50
50
  throw Error("Unsupported or damaged platform manifest");
51
51
  const paths = new Set;
52
52
  let size = 0;
@@ -84,6 +84,16 @@ function assertUpgrade(installed, target) {
84
84
  if (installed.version === target.version && installed.digest !== target.digest)
85
85
  throw Error("Same platform version has different bytes. Publish a new version; refusing an in-place replacement.");
86
86
  }
87
+ function configurationDigest(value) {
88
+ const canonical = (item) => {
89
+ if (Array.isArray(item))
90
+ return item.map(canonical);
91
+ if (item && typeof item === "object")
92
+ return Object.fromEntries(Object.keys(item).sort().map((key) => [key, canonical(item[key])]));
93
+ return item;
94
+ };
95
+ return sha256(JSON.stringify(canonical(value)));
96
+ }
87
97
 
88
98
  // packages/flare/src/cli/cloudflare-account.mjs
89
99
  class CloudflareError extends Error {
@@ -271,10 +281,85 @@ class CloudflareAccount {
271
281
  }
272
282
 
273
283
  // packages/flare/src/cli/platform-state.mjs
274
- import { mkdir, readFile as readFile2, writeFile, rename, chmod } from "fs/promises";
284
+ import { mkdir, readFile as readFile3, writeFile, rename, chmod } from "fs/promises";
275
285
  import { join as join2 } from "path";
276
286
  import { homedir } from "os";
277
287
  import { randomUUID } from "crypto";
288
+
289
+ // packages/flare/src/cli/platform-config.mjs
290
+ import { readFile as readFile2 } from "fs/promises";
291
+ function validatePlatformConfig(value = {}) {
292
+ const object = (v) => v && typeof v === "object" && !Array.isArray(v);
293
+ if (!object(value) || Object.keys(value).some((k) => !["capabilities", "secrets"].includes(k)))
294
+ throw Error("Platform config accepts only capabilities and secrets references");
295
+ const capabilities = value.capabilities ?? {}, secrets = value.secrets ?? [];
296
+ if (!object(capabilities) || !Array.isArray(secrets) || secrets.length > 64)
297
+ throw Error("Invalid platform capabilities/secrets references");
298
+ const bindings = new Set;
299
+ const references = secrets.map((r) => {
300
+ if (!object(r) || Object.keys(r).sort().join() !== "binding,secret_name,store_id" || !/^APP_SECRET_[A-Z0-9_]{1,64}$/.test(r.binding) || !/^[a-f0-9]{32}$/.test(r.store_id) || typeof r.secret_name !== "string" || !/^[A-Za-z0-9_.-]{1,128}$/.test(r.secret_name) || bindings.has(r.binding))
301
+ throw Error("Invalid or duplicate native Secrets Store reference");
302
+ bindings.add(r.binding);
303
+ return {
304
+ binding: r.binding,
305
+ store_id: r.store_id,
306
+ secret_name: r.secret_name
307
+ };
308
+ }).sort((a, b) => a.binding.localeCompare(b.binding));
309
+ const result = {};
310
+ const used = new Set;
311
+ for (const scope of Object.keys(capabilities).sort()) {
312
+ const p = capabilities[scope];
313
+ if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\/(dev|prod)$/.test(scope) || !object(p) || Object.keys(p).some((k) => !["outbound", "secrets"].includes(k)))
314
+ throw Error("Invalid capability scope");
315
+ const hosts = p.outbound ?? [], mapping = p.secrets ?? {};
316
+ if (!Array.isArray(hosts) || hosts.length > 20 || !object(mapping) || Object.keys(mapping).length > 16)
317
+ throw Error("Capability limits exceeded");
318
+ for (const host of hosts)
319
+ if (typeof host !== "string" || host.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,63}$/.test(host) || /\.(localhost|local|internal)$/.test(host))
320
+ throw Error("Only exact public HTTPS hostnames are allowed");
321
+ for (const [name, binding] of Object.entries(mapping)) {
322
+ if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(name) || !bindings.has(binding))
323
+ throw Error("Secret grant must reference an explicit APP_SECRET binding");
324
+ used.add(binding);
325
+ }
326
+ result[scope] = {
327
+ outbound: [...new Set(hosts)].sort(),
328
+ secrets: Object.fromEntries(Object.entries(mapping).sort())
329
+ };
330
+ }
331
+ if (references.some((r) => !used.has(r.binding)))
332
+ throw Error("Unused secret binding; grant it explicitly or remove it");
333
+ if (JSON.stringify(result).length > 16384)
334
+ throw Error("Capabilities exceed 16 KiB");
335
+ return { capabilities: result, secrets: references };
336
+ }
337
+ async function readPlatformConfig(path) {
338
+ const bytes = await readFile2(path);
339
+ if (bytes.length > 32768)
340
+ throw Error("Platform config exceeds 32 KiB");
341
+ return validatePlatformConfig(JSON.parse(bytes));
342
+ }
343
+ function accessConfiguration(app, policies) {
344
+ const {
345
+ created_at: _created,
346
+ updated_at: _updated,
347
+ policies: _embedded,
348
+ ...config
349
+ } = app;
350
+ const clean = policies.map(({ created_at: _c, updated_at: _u, ...p }) => p).sort((a, b) => String(a.id).localeCompare(String(b.id)));
351
+ return { app: config, policies: clean };
352
+ }
353
+ async function accessSnapshot(cf, id) {
354
+ const app = await cf.api(`access/apps/${id}`);
355
+ const policies = await cf.list(`access/apps/${id}/policies`);
356
+ return {
357
+ app,
358
+ digest: configurationDigest(accessConfiguration(app, policies))
359
+ };
360
+ }
361
+
362
+ // packages/flare/src/cli/platform-state.mjs
278
363
  var leaseMilliseconds = 15 * 60 * 1000;
279
364
  var identifier = /^[a-z][a-z0-9-]{0,19}$/;
280
365
  var resourceName = /^[a-z][a-z0-9-]{0,62}$/;
@@ -288,6 +373,8 @@ function hostname(value) {
288
373
  function validateState(state, account, name) {
289
374
  if (!state || state.format !== 1 || state.account !== account || state.name !== name || !uuid.test(state.id ?? "") || !validName(name) || typeof state.owner !== "string" || !/^\S+@\S+\.\S+$/.test(state.owner) || state.owner.length > 254)
290
375
  throw Error("Unsupported installation state or account/name mismatch");
376
+ if (state.config !== undefined)
377
+ validatePlatformConfig(state.config);
291
378
  const prefix = `flare-${name}-${state.id.replaceAll("-", "").slice(0, 12)}`;
292
379
  const operator = state.origin === "operator";
293
380
  if (state.origin !== undefined && !operator)
@@ -407,7 +494,7 @@ class InstallationStore {
407
494
  await chmod(this.file, 384);
408
495
  }
409
496
  async local() {
410
- const source = await readFile2(this.file, "utf8").catch((error) => {
497
+ const source = await readFile3(this.file, "utf8").catch((error) => {
411
498
  if (error.code !== "ENOENT")
412
499
  throw error;
413
500
  return null;
@@ -505,15 +592,75 @@ function settingsDigest(settings) {
505
592
  stable.bindings = [...settings.bindings ?? []].sort((a, b) => a.name.localeCompare(b.name));
506
593
  return configurationDigest(stable);
507
594
  }
508
- function configurationDigest(value) {
509
- const canonical = (item) => {
510
- if (Array.isArray(item))
511
- return item.map(canonical);
512
- if (item && typeof item === "object")
513
- return Object.fromEntries(Object.keys(item).sort().map((key) => [key, canonical(item[key])]));
514
- return item;
595
+
596
+ // packages/flare/src/cli/platform-health.mjs
597
+ function runtimeReadinessFailures(data, state, release = state.installed) {
598
+ const expected = {
599
+ readiness: "control-plane-ready",
600
+ installationId: state.id,
601
+ platformPayload: release?.digest,
602
+ version: release?.version,
603
+ runtimeAbi: release?.runtimeAbi ?? 4,
604
+ protocol: release?.protocol ?? 1
605
+ };
606
+ return Object.keys(expected).filter((key) => expected[key] === undefined || data?.[key] !== expected[key]);
607
+ }
608
+ async function platformHealth(cf, state, fetcher) {
609
+ const checkedAt = Date.now();
610
+ const probe = async (fn) => {
611
+ try {
612
+ return await fn();
613
+ } catch {
614
+ return { status: "unavailable" };
615
+ }
616
+ };
617
+ const [runtime, dashboard, access] = await Promise.all([
618
+ probe(async () => {
619
+ const r = await fetcher(`https://${state.addresses.runtimeHost}/health?ready=1`, { redirect: "error", signal: AbortSignal.timeout(5000) });
620
+ if (!r.ok) {
621
+ await r.body?.cancel();
622
+ return { status: "unhealthy", httpStatus: r.status };
623
+ }
624
+ const d = await r.json();
625
+ const mismatches = runtimeReadinessFailures(d, state);
626
+ return {
627
+ status: mismatches.length ? "mismatch" : "healthy",
628
+ ...mismatches.length ? { mismatches } : {},
629
+ version: d.version,
630
+ installationId: d.installationId
631
+ };
632
+ }),
633
+ probe(async () => {
634
+ const r = await fetcher(`https://${state.addresses.adminHost}/api/session`, { redirect: "manual", signal: AbortSignal.timeout(5000) });
635
+ const location = r.headers.get("location");
636
+ await r.body?.cancel();
637
+ const closed = [401, 403].includes(r.status) || [301, 302, 303, 307, 308].includes(r.status) && location && new URL(location, `https://${state.addresses.adminHost}`).origin === state.access?.issuer;
638
+ return {
639
+ status: closed ? "access-protected" : "not-verified-closed",
640
+ httpStatus: r.status,
641
+ applicationHealth: "not-checked-without-owner-session"
642
+ };
643
+ }),
644
+ probe(async () => {
645
+ if (!state.access)
646
+ return { status: "not-configured" };
647
+ const { app, digest } = await accessSnapshot(cf, state.access.id);
648
+ const host = app.domain === state.addresses.adminHost || app.destinations?.some((d) => d.type === "public" && d.uri === state.addresses.adminHost);
649
+ const matches = !state.access.snapshot || (state.access.snapshotFormat === 2 ? digest : configurationDigest(app)) === state.access.snapshot;
650
+ return {
651
+ status: host && app.aud === state.access.aud && matches ? "verified" : "drift",
652
+ snapshot: digest,
653
+ snapshotFormat: 2
654
+ };
655
+ })
656
+ ]);
657
+ return {
658
+ status: state.operation ? "incomplete" : runtime.status === "healthy" && dashboard.status === "access-protected" && access.status === "verified" ? "ready" : "not-verified",
659
+ checkedAt,
660
+ runtime,
661
+ dashboard,
662
+ access
515
663
  };
516
- return sha256(JSON.stringify(canonical(value)));
517
664
  }
518
665
 
519
666
  // packages/flare/src/cli/platform.mjs
@@ -681,6 +828,7 @@ function workerConfiguration(state, release, role, bootstrap = false) {
681
828
  return {
682
829
  ...common,
683
830
  worker_loaders: [{ binding: "LOADER" }],
831
+ ...state.config ? { secrets_store_secrets: validatePlatformConfig(state.config).secrets } : {},
684
832
  durable_objects: {
685
833
  bindings: [{ name: "SUPERVISORS", class_name: "EnvironmentSupervisor" }]
686
834
  },
@@ -720,7 +868,10 @@ function workerConfiguration(state, release, role, bootstrap = false) {
720
868
  ],
721
869
  vars: {
722
870
  ...common.vars,
723
- DEV_TOKEN_ISSUER: "https://" + state.addresses.runtimeHost
871
+ DEV_TOKEN_ISSUER: "https://" + state.addresses.runtimeHost,
872
+ ...state.config ? {
873
+ APP_CAPABILITIES: JSON.stringify(validatePlatformConfig(state.config).capabilities)
874
+ } : {}
724
875
  }
725
876
  };
726
877
  }
@@ -731,10 +882,15 @@ async function confirm(options, state, plan, ask, log) {
731
882
  throw Error("Account confirmation required; no cloud changes were made");
732
883
  }
733
884
  async function assertAccess(cf, state) {
734
- const app = await cf.api(`access/apps/${state.access.id}`);
885
+ const { app, digest } = await accessSnapshot(cf, state.access.id);
735
886
  const coversHost = app.domain === state.addresses.adminHost || app.destinations?.some((d) => d.type === "public" && d.uri === state.addresses.adminHost);
736
- if (!coversHost || app.aud !== state.access.aud || state.access.snapshot && configurationDigest(app) !== state.access.snapshot)
737
- throw Error("Access application drift; refusing to change policies or audiences");
887
+ if (!coversHost || app.aud !== state.access.aud)
888
+ throw Error("Access identity/host drift; explicit snapshot acceptance cannot change identities");
889
+ const expected = state.access.snapshot;
890
+ if (expected && (state.access.snapshotFormat === 2 ? digest : configurationDigest(app)) !== expected)
891
+ throw Error(`Access application/policy drift. Review Cloudflare settings, then explicitly accept snapshot ${digest} using --accept-access; no policies were changed.`);
892
+ state.access.snapshot = digest;
893
+ state.access.snapshotFormat = 2;
738
894
  }
739
895
  async function remoteWorker(cf, name) {
740
896
  try {
@@ -880,19 +1036,29 @@ async function verify(cf, state, release, fetcher, wait) {
880
1036
  await assertAccess(cf, state);
881
1037
  await supervisorNamespace(cf, state);
882
1038
  let healthy = false;
1039
+ let lastProbe = "unavailable";
883
1040
  for (let attempt = 0;attempt < 5; attempt++) {
884
1041
  try {
885
1042
  const response = await fetcher(`https://${state.addresses.runtimeHost}/health?ready=1`, { redirect: "error", signal: AbortSignal.timeout(15000) });
886
- const data = await response.json();
887
- healthy = response.ok && data.readiness === "control-plane-ready" && data.version === release.manifest.version && data.installationId === state.id && data.platformPayload === release.manifest.digest && data.runtimeAbi === 4 && data.protocol === 1;
888
- } catch {}
1043
+ lastProbe = `HTTP ${response.status}`;
1044
+ if (!response.ok)
1045
+ await response.body?.cancel();
1046
+ else {
1047
+ const failures = runtimeReadinessFailures(await response.json(), state, release.manifest);
1048
+ healthy = failures.length === 0;
1049
+ if (failures.length)
1050
+ lastProbe += "; mismatches: " + failures.join(",");
1051
+ }
1052
+ } catch {
1053
+ lastProbe = "network/timeout or invalid health response";
1054
+ }
889
1055
  if (healthy)
890
1056
  break;
891
1057
  if (attempt < 4)
892
1058
  await wait(2000);
893
1059
  }
894
1060
  if (!healthy)
895
- throw Error("Runtime health does not match the installed platform payload; operation remains resumable");
1061
+ throw Error("Runtime health does not match the installed platform payload; operation remains resumable. Last probe: " + lastProbe);
896
1062
  for (const path of ["/", "/api/session"]) {
897
1063
  const response = await fetcher(`https://${state.addresses.adminHost}${path}`, { redirect: "manual", signal: AbortSignal.timeout(15000) });
898
1064
  if ([301, 302, 303, 307, 308].includes(response.status)) {
@@ -1140,12 +1306,16 @@ async function runPlatform(options, dependencies = {}) {
1140
1306
  const discovered = adoption ? await discoverOperator(cf, store, name) : await store.discover();
1141
1307
  let state = discovered.state;
1142
1308
  if (operation === "status")
1143
- return state ? publicState(state, release.manifest, discovered.row) : {
1309
+ return state ? {
1310
+ ...publicState(state, release.manifest, discovered.row),
1311
+ health: await platformHealth(cf, state, dependencies.fetch ?? globalThis.fetch)
1312
+ } : {
1144
1313
  account,
1145
1314
  installation: name,
1146
1315
  status: "not-installed",
1147
1316
  target: release.manifest.version
1148
1317
  };
1318
+ const requestedConfig = options.config ? await readPlatformConfig(options.config) : null;
1149
1319
  const inventory = await preflight(cf);
1150
1320
  if (adoption && !state)
1151
1321
  state = await prepareOperator(cf, inventory, release, options, dependencies);
@@ -1164,6 +1334,20 @@ async function runPlatform(options, dependencies = {}) {
1164
1334
  runtimeHost: options["runtime-host"]
1165
1335
  });
1166
1336
  }
1337
+ const configChanged = requestedConfig && configurationDigest(requestedConfig) !== configurationDigest(validatePlatformConfig(state.config));
1338
+ if (state.operation && requestedConfig && configurationDigest(requestedConfig) !== state.operation.configDigest)
1339
+ throw Error("Pending config differs; resume with the original config or omit --config");
1340
+ let accessChanged = false;
1341
+ if (options["accept-access"]) {
1342
+ if (!/^[a-f0-9]{64}$/.test(options["accept-access"]) || operation !== "update" || state.operation)
1343
+ throw Error("Access snapshot acceptance requires an idle managed update and exact SHA256");
1344
+ const actual = await accessSnapshot(cf, state.access.id);
1345
+ if (actual.digest !== options["accept-access"])
1346
+ throw Error("Access changed since review; read platform status and review the new snapshot");
1347
+ accessChanged = state.access.snapshotFormat !== 2 || state.access.snapshot !== actual.digest;
1348
+ state.access.snapshot = actual.digest;
1349
+ state.access.snapshotFormat = 2;
1350
+ }
1167
1351
  if (state.operation && (state.operation.kind !== operation || state.operation.targetDigest !== release.manifest.digest))
1168
1352
  throw Error("A different platform operation is incomplete. Resume it with the original CLI release before changing versions.");
1169
1353
  if (state.installed) {
@@ -1172,7 +1356,7 @@ async function runPlatform(options, dependencies = {}) {
1172
1356
  if (!state.operation) {
1173
1357
  await assertResources(cf, state, inventory);
1174
1358
  await assertNoDrift(cf, state);
1175
- if (state.installed.digest === release.manifest.digest)
1359
+ if (state.installed.digest === release.manifest.digest && !configChanged && !accessChanged)
1176
1360
  return { ...publicState(state, release.manifest), unchanged: true };
1177
1361
  if (operation === "install" || adoption)
1178
1362
  throw Error("Already installed. Use flare platform update to change the platform version");
@@ -1186,6 +1370,8 @@ async function runPlatform(options, dependencies = {}) {
1186
1370
  from: state.installed?.version ?? state.adoptedFrom?.version ?? null,
1187
1371
  to: release.manifest.version,
1188
1372
  digest: release.manifest.digest,
1373
+ configuration: requestedConfig ?? state.config ?? null,
1374
+ acceptedAccessSnapshot: accessChanged ? state.access.snapshot : null,
1189
1375
  dashboard: "https://" + state.addresses.adminHost,
1190
1376
  runtime: "https://" + state.addresses.runtimeHost,
1191
1377
  steps: state.installed || adoption ? ["migrations", "runtime", "admin", "verify"] : steps,
@@ -1208,10 +1394,13 @@ async function runPlatform(options, dependencies = {}) {
1208
1394
  if (options["dry-run"])
1209
1395
  return plan;
1210
1396
  await confirm(options, state, plan, ask, log);
1397
+ if (!state.operation && requestedConfig)
1398
+ state.config = requestedConfig;
1211
1399
  state.operation ??= {
1212
1400
  id: randomUUID2(),
1213
1401
  kind: operation,
1214
1402
  targetDigest: release.manifest.digest,
1403
+ configDigest: configurationDigest(validatePlatformConfig(state.config)),
1215
1404
  completed: [],
1216
1405
  phase: "catalog",
1217
1406
  previousVersions: { ...state.versions },
@@ -1235,7 +1424,7 @@ async function runPlatform(options, dependencies = {}) {
1235
1424
  await assertResources(cf, state, inventory);
1236
1425
  await assertNoDrift(cf, state);
1237
1426
  }
1238
- await store.initialize(state, await readFile3(join3(release.directory, "migrations/0006_platform_installation.sql"), "utf8"));
1427
+ await store.initialize(state, await readFile4(join3(release.directory, "migrations/0006_platform_installation.sql"), "utf8"));
1239
1428
  }
1240
1429
  await store.acquire(state, options.resume === true);
1241
1430
  const scratch = await mkdtemp(join3(tmpdir(), "flare-platform-"));
package/dist/cli/flare.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // packages/flare/package.json
4
4
  var package_default = {
5
5
  name: "@auersperg/flare",
6
- version: "0.3.7",
6
+ version: "0.3.9",
7
7
  license: "MIT",
8
8
  type: "module",
9
9
  description: "Flare client, server declarations and development CLI",
@@ -107,6 +107,8 @@ else if (command === "platform") {
107
107
  console.log(`flare platform install|status|update|adopt [--account <ID>] [--name <name>]
108
108
  install: [--owner <email>] [--dashboard-host <host> --runtime-host <host>] [--subdomain <workers.dev prefix>]
109
109
  adopt: --owner <email> (existing standard flare-admin/flare-runtime/flare-catalog; default installation only)
110
+ install/update: [--config <operator JSON>]
111
+ update: [--accept-access <reviewed snapshot SHA256>]
110
112
  install/update/adopt: [--dry-run] [--confirm <account ID>] [--resume]
111
113
  Uses the platform payload bundled with this CLI release. Never updates applications or runs a local dashboard. Requires separate Cloudflare account permissions; FLARE_DEPLOY_TOKEN is not an account token.`);
112
114
  } else {
@@ -122,8 +124,17 @@ Uses the platform payload bundled with this CLI release. Never updates applicati
122
124
  "subdomain",
123
125
  "confirm",
124
126
  "dry-run",
125
- "resume"
126
- ] : operation === "update" ? ["account", "name", "confirm", "dry-run", "resume"] : ["account", "name"];
127
+ "resume",
128
+ "config"
129
+ ] : operation === "update" ? [
130
+ "account",
131
+ "name",
132
+ "confirm",
133
+ "dry-run",
134
+ "resume",
135
+ "config",
136
+ "accept-access"
137
+ ] : ["account", "name"];
127
138
  for (let i = 0;i < args.length; i++) {
128
139
  const key = args[i]?.slice(2);
129
140
  if (!args[i]?.startsWith("--") || !allowed.includes(key) || key in options)
@@ -136,7 +147,7 @@ Uses the platform payload bundled with this CLI release. Never updates applicati
136
147
  options[key] = args[++i];
137
148
  }
138
149
  }
139
- const { platform } = await import("./chunk-r3ceakrn.js");
150
+ const { platform } = await import("./chunk-0s0vrjtw.js");
140
151
  console.log(JSON.stringify(await platform(options), null, 2));
141
152
  }
142
153
  } catch (error) {
@@ -1,49 +1,3 @@
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.7",
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
1
  // packages/flare/src/sdk/protocol.ts
48
2
  var LIMITS = Object.freeze({
49
3
  inputBytes: 65536,
@@ -305,6 +259,69 @@ function projectCustomer(value) {
305
259
  return value.trim();
306
260
  }
307
261
 
262
+ // packages/core/src/maintenance.ts
263
+ function scopeForDeletion(value) {
264
+ if (typeof value !== "string")
265
+ fail("INVALID_INPUT");
266
+ const parts = value.split("/");
267
+ if (parts.length !== 2)
268
+ fail("INVALID_INPUT");
269
+ return `${uuid(parts[0])}/${environment(parts[1])}`;
270
+ }
271
+
272
+ // packages/core/src/dashboard-live.ts
273
+ async function dashboardAllowed(catalog, scope, email) {
274
+ const project = scopeForDeletion(scope).split("/")[0];
275
+ const row = await catalog.prepare("SELECT m.role FROM members m JOIN projects p ON p.id=? JOIN environments e ON e.projectId=p.id AND e.env=? WHERE m.email=? AND NOT EXISTS (SELECT 1 FROM project_deletions d WHERE d.projectId=p.id)").bind(project, scope.split("/")[1], email).first();
276
+ return !!row && ["owner", "editor", "viewer"].includes(row.role);
277
+ }
278
+
279
+ // packages/core/src/retention.ts
280
+ var jobRetentionMs = 30 * 86400000;
281
+ var catalogDeadlines = new WeakMap;
282
+ async function collectCatalog(database, now = Date.now()) {
283
+ if (now < (catalogDeadlines.get(database) ?? 0))
284
+ return;
285
+ const next = now + 86400000;
286
+ const claim = await database.prepare("UPDATE maintenance SET next=? WHERE id=1 AND next<=? RETURNING id").bind(next, now).first();
287
+ catalogDeadlines.set(database, claim ? next : now + 60000);
288
+ if (!claim)
289
+ return;
290
+ const statements = [
291
+ database.prepare("DELETE FROM audit WHERE id IN (SELECT id FROM audit WHERE at<? ORDER BY at LIMIT 1000)").bind(now - 90 * 86400000),
292
+ database.prepare("DELETE FROM credentials WHERE hash IN (SELECT hash FROM credentials WHERE expiresAt<? ORDER BY expiresAt LIMIT 1000)").bind(now - 14 * 86400000),
293
+ database.prepare("DELETE FROM cli_logins WHERE id IN (SELECT id FROM cli_logins WHERE expiresAt<? ORDER BY expiresAt LIMIT 1000)").bind(now)
294
+ ];
295
+ const result = await database.batch(statements);
296
+ if (result.some((value) => Number(value.meta.changes) >= 1000)) {
297
+ catalogDeadlines.set(database, now + 60000);
298
+ await database.prepare("UPDATE maintenance SET next=? WHERE id=1").bind(now + 60000).run();
299
+ }
300
+ }
301
+ // packages/core/package.json
302
+ var package_default = {
303
+ name: "@flare/platform",
304
+ version: "0.3.9",
305
+ type: "module",
306
+ license: "MIT",
307
+ private: true,
308
+ files: [
309
+ "dist",
310
+ "src",
311
+ "bootstrap",
312
+ "migrations",
313
+ "README.md"
314
+ ],
315
+ dependencies: {
316
+ agents: "0.28.0"
317
+ },
318
+ repository: {
319
+ type: "git",
320
+ url: "https://github.com/auersperg/flare.git",
321
+ directory: "packages/core"
322
+ }
323
+ };
324
+
308
325
  // packages/core/src/project-deletion.ts
309
326
  function authorizeProjectDeletion(actor, project, input) {
310
327
  if (actor.role !== "owner" || actor.projectId)
@@ -599,6 +616,19 @@ async function api(request, env, a) {
599
616
  });
600
617
  const which = environment(match[2]), scope = `${projectId}/${which}`, op = match[3] ?? "health";
601
618
  if (request.method === "GET") {
619
+ if (op === "live") {
620
+ if (a.projectId || request.headers.get("origin") !== url.origin || request.headers.get("upgrade")?.toLowerCase() !== "websocket")
621
+ fail("FORBIDDEN");
622
+ return env.RUNTIME.fetch(new Request(`https://dashboard.internal/${scope}`, {
623
+ headers: {
624
+ upgrade: "websocket",
625
+ "x-flared-dashboard": JSON.stringify({
626
+ email: a.email,
627
+ expires: Date.now() + 600000
628
+ })
629
+ }
630
+ }));
631
+ }
602
632
  if (["release", "release-bundle"].includes(op)) {
603
633
  const hash = url.searchParams.get("hash");
604
634
  if (!hash || !/^[a-f0-9]{64}$/.test(hash))
@@ -749,6 +779,9 @@ async function api(request, env, a) {
749
779
  }
750
780
 
751
781
  class AdminControl extends WorkerEntrypoint {
782
+ dashboardAllowed(scope, email) {
783
+ return dashboardAllowed(this.env.CATALOG, scope, email);
784
+ }
752
785
  async readiness() {
753
786
  const row = await this.env.CATALOG.prepare("SELECT installation_id FROM flare_installation WHERE singleton=1").first();
754
787
  const owner = await this.env.CATALOG.prepare("SELECT email FROM members WHERE role='owner' LIMIT 1").first();
@@ -785,7 +818,7 @@ var admin_default = {
785
818
  if (u.pathname.startsWith("/bootstrap/") || u.pathname.startsWith("/starter/"))
786
819
  return new Response("Not found", { status: 404 });
787
820
  const response = await env.ASSETS.fetch(request), headers = new Headers(response.headers);
788
- 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'");
821
+ 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.DASHBOARD_URL.replace("https:", "wss:") + " " + env.RUNTIME_URL + " " + env.RUNTIME_URL.replace("https:", "wss:") + "; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
789
822
  headers.set("referrer-policy", "no-referrer");
790
823
  headers.set("x-content-type-options", "nosniff");
791
824
  headers.set("cache-control", response.ok && u.pathname.startsWith("/_astro/") ? "private, max-age=31536000, immutable" : "private, no-store");
@@ -0,0 +1 @@
1
+ import{a as e,r as t}from"./client.BeKNsjPN.js";import{At as n,S as r,_ as i,a,b as o,f as s,g as c,n as l,p as u,pt as d,s as f,u as p,v as m,x as h,y as g}from"./common.TAClb6G9.js";import{o as _}from"./index.astro_astro_type_script_index_0_lang.Blg5hU9g.js";var v=e(t(),1),y=n();function b(e){let t=c(e.project,e.env),[n,b]=(0,v.useState)(``),[x,S]=(0,v.useState)(),C=p(t+`/auth-users`+(n?`?cursor=`+encodeURIComponent(n):``)),w=p(t+`/auth-settings`),T=e.session.role===`owner`,E=(n,r,i,a=[])=>{S({title:r,description:`Passwords are never displayed. Changes revoke existing sessions.`,productionId:e.env===`prod`?e.project.id:void 0,fields:a,submit:async r=>{try{await u(t+`/`+n,{...i,...s(a,e=>new FormData(r).get(e)),confirm:e.project.id})}catch(e){throw e instanceof Error&&e.message===`INVALID_PASSWORD`?Error(`Password must contain 6–128 characters. Spaces are allowed; the password is not trimmed.`):e instanceof Error&&e.message===`REGISTRATION_UNAVAILABLE`?Error(`This login is already registered. Choose another login or manage the existing user.`):e}C.refresh(),w.refresh()}})},D={name:`password`,type:`password`,required:!0,description:`6–128 characters.`};return(0,y.jsxs)(`div`,{className:`space-y-4 p-4`,children:[(0,y.jsx)(`h1`,{className:`page-title`,children:`Auth`}),(0,y.jsx)(f,{loading:C.loading||w.loading,error:C.error||w.error}),w.data&&(0,y.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,y.jsx)(_,{status:w.data.enabled?`Enabled`:`Disabled`}),(0,y.jsx)(_,{status:w.data.registration?`Public registration`:`Registration closed`}),T&&(0,y.jsx)(d,{variant:`outline`,onClick:()=>E(`auth-configure`,`Configure authentication`,{},[{name:`enabled`,type:`boolean`,value:w.data.enabled},{name:`registration`,type:`boolean`,value:w.data.registration}]),children:`Configure`}),T&&(0,y.jsx)(d,{onClick:()=>E(`auth-create`,`Create user`,{},[{name:`login`,type:`string`,required:!0},D]),children:`Create user`})]}),C.data&&(0,y.jsxs)(y.Fragment,{children:[(0,y.jsxs)(i,{children:[(0,y.jsx)(h,{children:(0,y.jsxs)(r,{children:[(0,y.jsx)(o,{children:`Login`}),(0,y.jsx)(o,{children:`User ID`}),(0,y.jsx)(o,{children:`Status`}),(0,y.jsx)(o,{children:`Created`}),(0,y.jsx)(o,{children:`Actions`})]})}),(0,y.jsx)(m,{children:C.data.users.map(e=>(0,y.jsxs)(r,{children:[(0,y.jsx)(g,{children:e.login}),(0,y.jsx)(g,{children:(0,y.jsx)(`code`,{children:e.id})}),(0,y.jsx)(g,{children:(0,y.jsx)(_,{status:e.disabled?`Blocked`:`Active`})}),(0,y.jsx)(g,{children:new Date(e.createdAt).toLocaleString()}),(0,y.jsx)(g,{children:(0,y.jsx)(`div`,{className:`flex flex-wrap gap-2`,children:T&&(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(d,{variant:`outline`,size:`sm`,onClick:()=>E(`auth-password`,`Change user password`,{id:e.id},[D]),children:`Change password`}),(0,y.jsx)(d,{variant:`outline`,size:`sm`,onClick:()=>E(`auth-revoke`,`Revoke all sessions`,{id:e.id}),children:`Revoke sessions`}),(0,y.jsx)(d,{variant:`outline`,size:`sm`,onClick:()=>E(`auth-disable`,e.disabled?`Unblock user`:`Block user`,{id:e.id,disabled:!e.disabled}),children:e.disabled?`Unblock`:`Block`})]})})})]},e.id))})]}),!C.data.users.length&&(0,y.jsx)(l,{title:`No users`}),(0,y.jsxs)(`div`,{className:`flex gap-2`,children:[(0,y.jsx)(d,{variant:`outline`,disabled:!n,onClick:()=>b(``),children:`First page`}),(0,y.jsx)(d,{variant:`outline`,disabled:!C.data.next,onClick:()=>b(C.data.next),children:`Next page`})]})]}),x&&(0,y.jsx)(a,{modal:x,onClose:()=>S(void 0)})]})}export{b as default};
@@ -1 +1 @@
1
- import{a as e,r as t}from"./client.BeKNsjPN.js";import{A as n,O as r,k as i,m as a,mt as o,p as s,pt as c,q as l,r as u,t as d}from"./common.D6YHScsr.js";var f=e(t(),1),p=o();function m(){let e=new URLSearchParams(location.search).get(`cli`)??``,[t,o]=(0,f.useState)(``),[m,h]=(0,f.useState)([]),[g,_]=(0,f.useState)(`new`),[v,y]=(0,f.useState)(``),[b,x]=(0,f.useState)(``),[S,C]=(0,f.useState)(!1),[w,T]=(0,f.useState)(!1);(0,f.useEffect)(()=>{let t=new AbortController;return Promise.all([s(`cli/approval?id=`+encodeURIComponent(e),void 0,t.signal),s(`projects`,void 0,t.signal)]).then(([e,t])=>{o(e.code),h(t),_(`new`)}).catch(e=>{t.signal.aborted||x(a(e))}),()=>t.abort()},[e]);async function E(){C(!0),x(``);try{let t=g;if(t===`new`){let e=await s(`projects`,{name:v,customer:`Applications`,origins:[]});t=e.id,h(t=>[...t,{id:e.id,name:v,customer:`Applications`}]),_(t)}await s(`cli/approval`,{id:e,project:t}),T(!0)}catch(e){x(a(e))}finally{C(!1)}}return(0,p.jsxs)(`main`,{className:`mx-auto max-w-lg p-8`,children:[(0,p.jsx)(`h1`,{className:`mb-4 text-xl font-semibold`,children:`Connect Flare CLI`}),w?(0,p.jsx)(`p`,{children:`Connected. Return to your terminal. You can close this tab.`}):(0,p.jsxs)(i,{children:[(0,p.jsx)(`p`,{children:`Approve only a login you just started. Compare this code with your terminal:`}),(0,p.jsx)(`strong`,{className:`text-2xl font-mono`,children:t||`Loading…`}),(0,p.jsx)(`p`,{children:`Grants this computer deployment access to the selected project, including production. The credential expires in 30 days and can be revoked in Settings.`}),(0,p.jsxs)(r,{children:[(0,p.jsx)(n,{children:`Project`}),(0,p.jsx)(d,{label:`CLI project`,value:g,onChange:_,items:[...m.map(e=>({value:e.id,label:e.name})),{value:`new`,label:`Create new project`}]})]}),g===`new`&&(0,p.jsxs)(r,{children:[(0,p.jsx)(n,{children:`New project name`}),(0,p.jsx)(l,{value:v,onChange:e=>y(e.target.value),maxLength:100})]}),b&&(0,p.jsx)(u,{children:b}),(0,p.jsx)(c,{disabled:S||!t||!g||g===`new`&&!v.trim(),onClick:E,children:`Approve connection`})]})]})}export{m as default};
1
+ import{a as e,r as t}from"./client.BeKNsjPN.js";import{A as n,At as r,O as i,k as a,m as o,p as s,pt as c,q as l,r as u,t as d}from"./common.TAClb6G9.js";var f=e(t(),1),p=r();function m(){let e=new URLSearchParams(location.search).get(`cli`)??``,[t,r]=(0,f.useState)(``),[m,h]=(0,f.useState)([]),[g,_]=(0,f.useState)(`new`),[v,y]=(0,f.useState)(``),[b,x]=(0,f.useState)(``),[S,C]=(0,f.useState)(!1),[w,T]=(0,f.useState)(!1);(0,f.useEffect)(()=>{let t=new AbortController;return Promise.all([s(`cli/approval?id=`+encodeURIComponent(e),void 0,t.signal),s(`projects`,void 0,t.signal)]).then(([e,t])=>{r(e.code),h(t),_(`new`)}).catch(e=>{t.signal.aborted||x(o(e))}),()=>t.abort()},[e]);async function E(){C(!0),x(``);try{let t=g;if(t===`new`){let e=await s(`projects`,{name:v,customer:`Applications`,origins:[]});t=e.id,h(t=>[...t,{id:e.id,name:v,customer:`Applications`}]),_(t)}await s(`cli/approval`,{id:e,project:t}),T(!0)}catch(e){x(o(e))}finally{C(!1)}}return(0,p.jsxs)(`main`,{className:`mx-auto max-w-lg p-8`,children:[(0,p.jsx)(`h1`,{className:`mb-4 text-xl font-semibold`,children:`Connect Flare CLI`}),w?(0,p.jsx)(`p`,{children:`Connected. Return to your terminal. You can close this tab.`}):(0,p.jsxs)(a,{children:[(0,p.jsx)(`p`,{children:`Approve only a login you just started. Compare this code with your terminal:`}),(0,p.jsx)(`strong`,{className:`text-2xl font-mono`,children:t||`Loading…`}),(0,p.jsx)(`p`,{children:`Grants this computer deployment access to the selected project, including production. The credential expires in 30 days and can be revoked in Settings.`}),(0,p.jsxs)(i,{children:[(0,p.jsx)(n,{children:`Project`}),(0,p.jsx)(d,{label:`CLI project`,value:g,onChange:_,items:[...m.map(e=>({value:e.id,label:e.name})),{value:`new`,label:`Create new project`}]})]}),g===`new`&&(0,p.jsxs)(i,{children:[(0,p.jsx)(n,{children:`New project name`}),(0,p.jsx)(l,{value:v,onChange:e=>y(e.target.value),maxLength:100})]}),b&&(0,p.jsx)(u,{children:b}),(0,p.jsx)(c,{disabled:S||!t||!g||g===`new`&&!v.trim(),onClick:E,children:`Approve connection`})]})]})}export{m as default};