@auersperg/flare 0.3.7 → 0.3.8

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-y6e2h40n.js} +187 -20
  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,63 @@ 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
+ async function platformHealth(cf, state, fetcher) {
598
+ const checkedAt = Date.now();
599
+ const probe = async (fn) => {
600
+ try {
601
+ return await fn();
602
+ } catch {
603
+ return { status: "unavailable" };
604
+ }
605
+ };
606
+ const [runtime, dashboard, access] = await Promise.all([
607
+ probe(async () => {
608
+ const r = await fetcher(`https://${state.addresses.runtimeHost}/health?ready=1`, { redirect: "error", signal: AbortSignal.timeout(5000) });
609
+ if (!r.ok) {
610
+ await r.body?.cancel();
611
+ return { status: "unhealthy", httpStatus: r.status };
612
+ }
613
+ const d = await r.json();
614
+ const matches = d.readiness === "control-plane-ready" && d.installationId === state.id && d.platformPayload === state.installed?.digest && d.version === state.installed?.version;
615
+ return {
616
+ status: matches ? "healthy" : "mismatch",
617
+ version: d.version,
618
+ installationId: d.installationId
619
+ };
620
+ }),
621
+ probe(async () => {
622
+ const r = await fetcher(`https://${state.addresses.adminHost}/api/session`, { redirect: "manual", signal: AbortSignal.timeout(5000) });
623
+ const location = r.headers.get("location");
624
+ await r.body?.cancel();
625
+ 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;
626
+ return {
627
+ status: closed ? "access-protected" : "not-verified-closed",
628
+ httpStatus: r.status,
629
+ applicationHealth: "not-checked-without-owner-session"
630
+ };
631
+ }),
632
+ probe(async () => {
633
+ if (!state.access)
634
+ return { status: "not-configured" };
635
+ const { app, digest } = await accessSnapshot(cf, state.access.id);
636
+ const host = app.domain === state.addresses.adminHost || app.destinations?.some((d) => d.type === "public" && d.uri === state.addresses.adminHost);
637
+ const matches = !state.access.snapshot || (state.access.snapshotFormat === 2 ? digest : configurationDigest(app)) === state.access.snapshot;
638
+ return {
639
+ status: host && app.aud === state.access.aud && matches ? "verified" : "drift",
640
+ snapshot: digest,
641
+ snapshotFormat: 2
642
+ };
643
+ })
644
+ ]);
645
+ return {
646
+ status: state.operation ? "incomplete" : runtime.status === "healthy" && dashboard.status === "access-protected" && access.status === "verified" ? "ready" : "not-verified",
647
+ checkedAt,
648
+ runtime,
649
+ dashboard,
650
+ access
515
651
  };
516
- return sha256(JSON.stringify(canonical(value)));
517
652
  }
518
653
 
519
654
  // packages/flare/src/cli/platform.mjs
@@ -681,6 +816,7 @@ function workerConfiguration(state, release, role, bootstrap = false) {
681
816
  return {
682
817
  ...common,
683
818
  worker_loaders: [{ binding: "LOADER" }],
819
+ ...state.config ? { secrets_store_secrets: validatePlatformConfig(state.config).secrets } : {},
684
820
  durable_objects: {
685
821
  bindings: [{ name: "SUPERVISORS", class_name: "EnvironmentSupervisor" }]
686
822
  },
@@ -720,7 +856,10 @@ function workerConfiguration(state, release, role, bootstrap = false) {
720
856
  ],
721
857
  vars: {
722
858
  ...common.vars,
723
- DEV_TOKEN_ISSUER: "https://" + state.addresses.runtimeHost
859
+ DEV_TOKEN_ISSUER: "https://" + state.addresses.runtimeHost,
860
+ ...state.config ? {
861
+ APP_CAPABILITIES: JSON.stringify(validatePlatformConfig(state.config).capabilities)
862
+ } : {}
724
863
  }
725
864
  };
726
865
  }
@@ -731,10 +870,15 @@ async function confirm(options, state, plan, ask, log) {
731
870
  throw Error("Account confirmation required; no cloud changes were made");
732
871
  }
733
872
  async function assertAccess(cf, state) {
734
- const app = await cf.api(`access/apps/${state.access.id}`);
873
+ const { app, digest } = await accessSnapshot(cf, state.access.id);
735
874
  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");
875
+ if (!coversHost || app.aud !== state.access.aud)
876
+ throw Error("Access identity/host drift; explicit snapshot acceptance cannot change identities");
877
+ const expected = state.access.snapshot;
878
+ if (expected && (state.access.snapshotFormat === 2 ? digest : configurationDigest(app)) !== expected)
879
+ throw Error(`Access application/policy drift. Review Cloudflare settings, then explicitly accept snapshot ${digest} using --accept-access; no policies were changed.`);
880
+ state.access.snapshot = digest;
881
+ state.access.snapshotFormat = 2;
738
882
  }
739
883
  async function remoteWorker(cf, name) {
740
884
  try {
@@ -1140,12 +1284,16 @@ async function runPlatform(options, dependencies = {}) {
1140
1284
  const discovered = adoption ? await discoverOperator(cf, store, name) : await store.discover();
1141
1285
  let state = discovered.state;
1142
1286
  if (operation === "status")
1143
- return state ? publicState(state, release.manifest, discovered.row) : {
1287
+ return state ? {
1288
+ ...publicState(state, release.manifest, discovered.row),
1289
+ health: await platformHealth(cf, state, dependencies.fetch ?? globalThis.fetch)
1290
+ } : {
1144
1291
  account,
1145
1292
  installation: name,
1146
1293
  status: "not-installed",
1147
1294
  target: release.manifest.version
1148
1295
  };
1296
+ const requestedConfig = options.config ? await readPlatformConfig(options.config) : null;
1149
1297
  const inventory = await preflight(cf);
1150
1298
  if (adoption && !state)
1151
1299
  state = await prepareOperator(cf, inventory, release, options, dependencies);
@@ -1164,6 +1312,20 @@ async function runPlatform(options, dependencies = {}) {
1164
1312
  runtimeHost: options["runtime-host"]
1165
1313
  });
1166
1314
  }
1315
+ const configChanged = requestedConfig && configurationDigest(requestedConfig) !== configurationDigest(validatePlatformConfig(state.config));
1316
+ if (state.operation && requestedConfig && configurationDigest(requestedConfig) !== state.operation.configDigest)
1317
+ throw Error("Pending config differs; resume with the original config or omit --config");
1318
+ let accessChanged = false;
1319
+ if (options["accept-access"]) {
1320
+ if (!/^[a-f0-9]{64}$/.test(options["accept-access"]) || operation !== "update" || state.operation)
1321
+ throw Error("Access snapshot acceptance requires an idle managed update and exact SHA256");
1322
+ const actual = await accessSnapshot(cf, state.access.id);
1323
+ if (actual.digest !== options["accept-access"])
1324
+ throw Error("Access changed since review; read platform status and review the new snapshot");
1325
+ accessChanged = state.access.snapshotFormat !== 2 || state.access.snapshot !== actual.digest;
1326
+ state.access.snapshot = actual.digest;
1327
+ state.access.snapshotFormat = 2;
1328
+ }
1167
1329
  if (state.operation && (state.operation.kind !== operation || state.operation.targetDigest !== release.manifest.digest))
1168
1330
  throw Error("A different platform operation is incomplete. Resume it with the original CLI release before changing versions.");
1169
1331
  if (state.installed) {
@@ -1172,7 +1334,7 @@ async function runPlatform(options, dependencies = {}) {
1172
1334
  if (!state.operation) {
1173
1335
  await assertResources(cf, state, inventory);
1174
1336
  await assertNoDrift(cf, state);
1175
- if (state.installed.digest === release.manifest.digest)
1337
+ if (state.installed.digest === release.manifest.digest && !configChanged && !accessChanged)
1176
1338
  return { ...publicState(state, release.manifest), unchanged: true };
1177
1339
  if (operation === "install" || adoption)
1178
1340
  throw Error("Already installed. Use flare platform update to change the platform version");
@@ -1186,6 +1348,8 @@ async function runPlatform(options, dependencies = {}) {
1186
1348
  from: state.installed?.version ?? state.adoptedFrom?.version ?? null,
1187
1349
  to: release.manifest.version,
1188
1350
  digest: release.manifest.digest,
1351
+ configuration: requestedConfig ?? state.config ?? null,
1352
+ acceptedAccessSnapshot: accessChanged ? state.access.snapshot : null,
1189
1353
  dashboard: "https://" + state.addresses.adminHost,
1190
1354
  runtime: "https://" + state.addresses.runtimeHost,
1191
1355
  steps: state.installed || adoption ? ["migrations", "runtime", "admin", "verify"] : steps,
@@ -1208,10 +1372,13 @@ async function runPlatform(options, dependencies = {}) {
1208
1372
  if (options["dry-run"])
1209
1373
  return plan;
1210
1374
  await confirm(options, state, plan, ask, log);
1375
+ if (!state.operation && requestedConfig)
1376
+ state.config = requestedConfig;
1211
1377
  state.operation ??= {
1212
1378
  id: randomUUID2(),
1213
1379
  kind: operation,
1214
1380
  targetDigest: release.manifest.digest,
1381
+ configDigest: configurationDigest(validatePlatformConfig(state.config)),
1215
1382
  completed: [],
1216
1383
  phase: "catalog",
1217
1384
  previousVersions: { ...state.versions },
@@ -1235,7 +1402,7 @@ async function runPlatform(options, dependencies = {}) {
1235
1402
  await assertResources(cf, state, inventory);
1236
1403
  await assertNoDrift(cf, state);
1237
1404
  }
1238
- await store.initialize(state, await readFile3(join3(release.directory, "migrations/0006_platform_installation.sql"), "utf8"));
1405
+ await store.initialize(state, await readFile4(join3(release.directory, "migrations/0006_platform_installation.sql"), "utf8"));
1239
1406
  }
1240
1407
  await store.acquire(state, options.resume === true);
1241
1408
  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.8",
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-y6e2h40n.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.8",
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};