flowdular 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Flowdular: Build your business platform](https://cdn.jsdelivr.net/npm/flowdular@latest/assets/flowdular-banner.png)](https://flowdular.com)
1
+ [![Flowdular: Build your business platform](https://cdn.jsdelivr.net/npm/flowdular@latest/assets/flowdular-banner.webp)](https://flowdular.com)
2
2
 
3
3
  # Flowdular CLI
4
4
 
Binary file
@@ -3115,7 +3115,7 @@ var moduleCatalogSchema = module_catalog_schema_default;
3115
3115
  var moduleArtifactSchema = module_artifact_schema_default;
3116
3116
 
3117
3117
  // ../contracts/src/index.ts
3118
- var PLATFORM_API_VERSION = "0.1.6";
3118
+ var PLATFORM_API_VERSION = "0.1.9";
3119
3119
 
3120
3120
  // ../kernel/src/module-compatibility.ts
3121
3121
  function satisfiesModuleVersion(version, range) {
@@ -3796,7 +3796,7 @@ import { dirname as dirname6, join as join6, relative as relative5 } from "node:
3796
3796
  import { createRequire } from "node:module";
3797
3797
  import { readFile as readFile5 } from "node:fs/promises";
3798
3798
  import { dirname as dirname4, join as join3 } from "node:path";
3799
- var SDK_VERSION = "0.3.0";
3799
+ var SDK_VERSION = "0.3.2";
3800
3800
  function npmPackage(specifier) {
3801
3801
  return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
3802
3802
  }
@@ -4710,8 +4710,8 @@ import type { WebMount } from '${serverPackage}';
4710
4710
  ` + capabilityScope(module) + ` metrics: createModuleMetrics('${module.id}'),
4711
4711
  }), moduleId: '${module.id}' },`
4712
4712
  ).join("\n");
4713
- return GENERATED_HEADER + typeImport + `import { createModuleMetrics } from '${serverPackage}';
4714
- ` + imports + "\n\nexport function composeModuleServer(\n context: PlatformServerContext,\n): readonly PlatformServerComposition[] {\n return [\n" + calls + "\n ];\n}\n\nexport const moduleWebMounts: readonly WebMount[] = " + JSON.stringify(mounts) + " as const;\nexport const applicationBasePath = " + JSON.stringify(applicationPath) + ";\n";
4713
+ return GENERATED_HEADER + typeImport + `import { bindModuleCompositions, createModuleMetrics } from '${serverPackage}';
4714
+ ` + imports + "\n\nexport function composeModuleServer(\n context: PlatformServerContext,\n): readonly PlatformServerComposition[] {\n return bindModuleCompositions([\n" + calls + "\n ]);\n}\n\nexport const moduleWebMounts: readonly WebMount[] = " + JSON.stringify(mounts) + " as const;\nexport const applicationBasePath = " + JSON.stringify(applicationPath) + ";\n";
4715
4715
  }
4716
4716
  function generateClientComposition(modules, sdk = false, applicationPath = "/app") {
4717
4717
  const client = modules.filter((module) => module.client);
package/dist/index.js CHANGED
@@ -2022,6 +2022,10 @@ function stringFlag(arguments_, name) {
2022
2022
  const value = arguments_.flags.get(name);
2023
2023
  return typeof value === "string" ? value : void 0;
2024
2024
  }
2025
+ function grantFlag(arguments_) {
2026
+ const value = stringFlag(arguments_, "grant");
2027
+ return value === void 0 || value.length === 0 ? void 0 : value;
2028
+ }
2025
2029
 
2026
2030
  // src/output.ts
2027
2031
  import { styleText } from "node:util";
@@ -3283,7 +3287,7 @@ var moduleCatalogSchema = module_catalog_schema_default;
3283
3287
  var moduleArtifactSchema = module_artifact_schema_default;
3284
3288
 
3285
3289
  // ../contracts/src/index.ts
3286
- var PLATFORM_API_VERSION = "0.1.6";
3290
+ var PLATFORM_API_VERSION = "0.1.9";
3287
3291
 
3288
3292
  // src/module-templates.ts
3289
3293
  function packageSuffix(id) {
@@ -5386,6 +5390,178 @@ var RegistryError = class extends Error {
5386
5390
  var ALLOWED = Object.freeze({ allowed: true });
5387
5391
  var NO_POLICIES = Object.freeze([]);
5388
5392
 
5393
+ // ../kernel/src/approval-grant.ts
5394
+ import { createHash as createHash2, createHmac, timingSafeEqual } from "node:crypto";
5395
+
5396
+ // ../kernel/src/keyring.ts
5397
+ import {
5398
+ createCipheriv,
5399
+ createDecipheriv,
5400
+ createHash,
5401
+ randomBytes
5402
+ } from "node:crypto";
5403
+ var KEYRING_KEY_BYTES = 32;
5404
+ var KEYRING_MAX_PREVIOUS_KEYS = 8;
5405
+ function keyFingerprint(key) {
5406
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
5407
+ }
5408
+ function parsePreviousKeys(value, decode) {
5409
+ if (!value) return [];
5410
+ return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map(decode);
5411
+ }
5412
+
5413
+ // ../kernel/src/approval-grant.ts
5414
+ var APPROVAL_GRANT_KEY_VARIABLE = "FD_APPROVAL_GRANT_KEY";
5415
+ var APPROVAL_GRANT_PREVIOUS_KEY_VARIABLE = "FD_APPROVAL_GRANT_KEY_PREVIOUS";
5416
+ var APPROVAL_GRANT_MAX_TOKEN_LENGTH = 2048;
5417
+ var APPROVAL_GRANT_INPUT_DIGEST_LENGTH = 64;
5418
+ var TOKEN_PREFIX = "ag1";
5419
+ var MAX_CLAIM_LENGTH = 128;
5420
+ function createApprovalGrantKeyring(options) {
5421
+ const previous = options.previous ?? [];
5422
+ if (previous.length > KEYRING_MAX_PREVIOUS_KEYS) {
5423
+ throw new Error(
5424
+ `An approval grant keyring accepts at most ${KEYRING_MAX_PREVIOUS_KEYS} previous keys; ${previous.length} were given.`
5425
+ );
5426
+ }
5427
+ const keys = /* @__PURE__ */ new Map();
5428
+ const order = [];
5429
+ for (const key of [options.current, ...previous]) {
5430
+ if (key.byteLength !== KEYRING_KEY_BYTES) {
5431
+ throw new Error(
5432
+ `An approval grant key must be exactly ${KEYRING_KEY_BYTES} bytes.`
5433
+ );
5434
+ }
5435
+ const id = keyFingerprint(key);
5436
+ if (keys.has(id)) continue;
5437
+ keys.set(id, Buffer.from(key));
5438
+ order.push(id);
5439
+ }
5440
+ return {
5441
+ keyId: order[0],
5442
+ previousKeyIds: order.slice(1),
5443
+ key: (keyId) => keys.get(keyId)
5444
+ };
5445
+ }
5446
+ function decodeKey(value, variable) {
5447
+ const key = Buffer.from(value.trim(), "base64");
5448
+ if (key.byteLength !== KEYRING_KEY_BYTES) {
5449
+ key.fill(0);
5450
+ throw new Error(
5451
+ `${variable} must be a base64-encoded ${KEYRING_KEY_BYTES}-byte key.`
5452
+ );
5453
+ }
5454
+ return key;
5455
+ }
5456
+ function approvalGrantKeyringFromEnvironment(environment = process.env) {
5457
+ const current = environment[APPROVAL_GRANT_KEY_VARIABLE];
5458
+ if (!current) return void 0;
5459
+ return createApprovalGrantKeyring({
5460
+ current: decodeKey(current, APPROVAL_GRANT_KEY_VARIABLE),
5461
+ previous: parsePreviousKeys(
5462
+ environment[APPROVAL_GRANT_PREVIOUS_KEY_VARIABLE],
5463
+ (entry) => decodeKey(entry, APPROVAL_GRANT_PREVIOUS_KEY_VARIABLE)
5464
+ )
5465
+ });
5466
+ }
5467
+ function canonical(value) {
5468
+ if (value === null || typeof value !== "object") {
5469
+ return JSON.stringify(value === void 0 ? null : value);
5470
+ }
5471
+ if (Array.isArray(value)) {
5472
+ return `[${value.map((entry) => canonical(entry)).join(",")}]`;
5473
+ }
5474
+ const entries = Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
5475
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`).join(",")}}`;
5476
+ }
5477
+ function approvalInputDigest(input) {
5478
+ return createHash2("sha256").update(canonical(input), "utf8").digest("hex");
5479
+ }
5480
+ function claimString(value, maximum = MAX_CLAIM_LENGTH) {
5481
+ return typeof value === "string" && value.length >= 1 && value.length <= maximum && !/[\s\u0000]/.test(value) ? value : void 0;
5482
+ }
5483
+ function claimInteger(value) {
5484
+ return Number.isSafeInteger(value) && value >= 0 ? value : void 0;
5485
+ }
5486
+ function readClaims(value) {
5487
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5488
+ return void 0;
5489
+ }
5490
+ const raw = value;
5491
+ const tenantId = claimString(raw.tenantId);
5492
+ const capabilityId = claimString(raw.capabilityId);
5493
+ const inputDigest = claimString(
5494
+ raw.inputDigest,
5495
+ APPROVAL_GRANT_INPUT_DIGEST_LENGTH
5496
+ );
5497
+ const requestId = claimString(raw.requestId);
5498
+ const nonce = claimString(raw.nonce);
5499
+ const issuedAt = claimInteger(raw.issuedAt);
5500
+ const expiresAt = claimInteger(raw.expiresAt);
5501
+ if (raw.v !== 1 || tenantId === void 0 || capabilityId === void 0 || inputDigest === void 0 || requestId === void 0 || nonce === void 0 || issuedAt === void 0 || expiresAt === void 0 || expiresAt <= issuedAt) {
5502
+ return void 0;
5503
+ }
5504
+ return {
5505
+ v: 1,
5506
+ tenantId,
5507
+ capabilityId,
5508
+ inputDigest,
5509
+ requestId,
5510
+ issuedAt,
5511
+ expiresAt,
5512
+ nonce
5513
+ };
5514
+ }
5515
+ function signature(key, signed) {
5516
+ return createHmac("sha256", key).update(signed, "utf8").digest();
5517
+ }
5518
+ function invalid(message) {
5519
+ return { ok: false, reason: "APPROVAL_GRANT_INVALID", message };
5520
+ }
5521
+ function verifyApprovalGrant(keyring, token, expected, now = Date.now()) {
5522
+ if (typeof token !== "string" || token.length < 32 || token.length > APPROVAL_GRANT_MAX_TOKEN_LENGTH) {
5523
+ return invalid("The approval grant is not a token this platform issued.");
5524
+ }
5525
+ const [prefix, keyId, payload, supplied, extra] = token.split(".");
5526
+ if (prefix !== TOKEN_PREFIX || !keyId || !payload || !supplied || extra !== void 0) {
5527
+ return invalid("The approval grant is not a token this platform issued.");
5528
+ }
5529
+ const key = keyring.key(keyId);
5530
+ if (!key) {
5531
+ return invalid(
5532
+ "The approval grant names a key this platform does not hold."
5533
+ );
5534
+ }
5535
+ const expectedSignature = signature(key, `${prefix}.${keyId}.${payload}`);
5536
+ const suppliedSignature = Buffer.from(supplied, "base64url");
5537
+ if (suppliedSignature.byteLength !== expectedSignature.byteLength || !timingSafeEqual(suppliedSignature, expectedSignature)) {
5538
+ return invalid("The approval grant signature does not verify.");
5539
+ }
5540
+ let parsed;
5541
+ try {
5542
+ parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
5543
+ } catch {
5544
+ return invalid("The approval grant payload is unreadable.");
5545
+ }
5546
+ const claims = readClaims(parsed);
5547
+ if (!claims) return invalid("The approval grant payload is out of bounds.");
5548
+ if (claims.expiresAt <= now) {
5549
+ return {
5550
+ ok: false,
5551
+ reason: "APPROVAL_GRANT_EXPIRED",
5552
+ message: "The approval grant has expired."
5553
+ };
5554
+ }
5555
+ if (claims.tenantId !== expected.tenantId || claims.capabilityId !== expected.capabilityId || expected.inputDigest !== void 0 && claims.inputDigest !== expected.inputDigest) {
5556
+ return {
5557
+ ok: false,
5558
+ reason: "APPROVAL_GRANT_MISMATCH",
5559
+ message: "The approval grant was issued for another workspace, capability or input."
5560
+ };
5561
+ }
5562
+ return { ok: true, claims };
5563
+ }
5564
+
5389
5565
  // ../kernel/src/module-compatibility.ts
5390
5566
  var import_semver = __toESM(require_semver2(), 1);
5391
5567
  function incrementModuleVersion(version, level) {
@@ -5571,11 +5747,11 @@ import { dirname as dirname3, resolve as resolve2 } from "node:path";
5571
5747
 
5572
5748
  // src/module-artifact.ts
5573
5749
  import { parse as parseYaml2 } from "yaml";
5574
- import { createHash } from "node:crypto";
5750
+ import { createHash as createHash3 } from "node:crypto";
5575
5751
  var MAX_ARTIFACT_BYTES = 48 * 1024 * 1024;
5576
5752
  var MAX_SOURCE_BYTES = 32 * 1024 * 1024;
5577
5753
  var MAX_FILES = 4096;
5578
- var hashBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
5754
+ var hashBytes = (bytes) => createHash3("sha256").update(bytes).digest("hex");
5579
5755
  var HASH = /^[a-f0-9]{64}$/;
5580
5756
  var ROOTS = /* @__PURE__ */ new Set(["src", "tests", "migrations", "translations", "spec"]);
5581
5757
  var ROOT_FILES = /* @__PURE__ */ new Set([
@@ -5961,7 +6137,7 @@ import { dirname as dirname6, join as join5, relative as relative5 } from "node:
5961
6137
  import { createRequire } from "node:module";
5962
6138
  import { readFile as readFile4 } from "node:fs/promises";
5963
6139
  import { dirname as dirname4, join as join2 } from "node:path";
5964
- var SDK_VERSION = "0.3.0";
6140
+ var SDK_VERSION = "0.3.2";
5965
6141
  var LIBRARIES = /* @__PURE__ */ new Set([
5966
6142
  "ai-provider",
5967
6143
  "cli-protocol",
@@ -7142,11 +7318,12 @@ $ledger_privileges$;`;
7142
7318
  }
7143
7319
 
7144
7320
  // ../database/src/migrations.ts
7145
- import { createHash as createHash2 } from "node:crypto";
7321
+ import { createHash as createHash4 } from "node:crypto";
7146
7322
  var DATABASE_MIGRATION_LEDGER = "_coreloom_migrations_v2";
7323
+ var DATABASE_MIGRATION_LEDGER_LOCK = "coreloom.migrations";
7147
7324
  function moduleMigrationChecksum(statements) {
7148
7325
  const normalized = statements.replace(/\r\n?/g, "\n").trim();
7149
- return `sha256:${createHash2("sha256").update(normalized, "utf8").digest("hex")}`;
7326
+ return `sha256:${createHash4("sha256").update(normalized, "utf8").digest("hex")}`;
7150
7327
  }
7151
7328
  var DatabaseMigrationError = class extends Error {
7152
7329
  constructor(code, migrationId, message, options) {
@@ -7338,6 +7515,7 @@ async function runDatabaseMigrations(database, namespace, migrations, options =
7338
7515
  }
7339
7516
  return database.transaction(
7340
7517
  async (transaction) => {
7518
+ await transaction.acquireMigrationLock(DATABASE_MIGRATION_LEDGER_LOCK);
7341
7519
  await transaction.acquireMigrationLock(namespace);
7342
7520
  await transaction.executeScript(
7343
7521
  database.capabilities.sql.migrationLedgerDdl(DATABASE_MIGRATION_LEDGER)
@@ -8278,12 +8456,13 @@ function createDatabaseProvider(config, factories = {}) {
8278
8456
  }
8279
8457
 
8280
8458
  // ../database/src/backup.ts
8281
- import { createHash as createHash3 } from "node:crypto";
8459
+ import { createHash as createHash5 } from "node:crypto";
8282
8460
  var BACKUP_MANIFEST_FILE = "backup.json";
8283
8461
  var BACKUP_MANIFEST_VERSION = 1;
8284
8462
  var BACKUP_KEY_VARIABLES = Object.freeze([
8285
8463
  "FD_AGENT_CREDENTIAL_KEY",
8286
8464
  "FD_AGENT_RUN_GRANT_KEY",
8465
+ "FD_APPROVAL_GRANT_KEY",
8287
8466
  "FD_WORKFLOWS_PAYLOAD_KEY",
8288
8467
  "FD_WORKFLOWS_CURSOR_KEY",
8289
8468
  "FD_AUTH_MFA_KEY",
@@ -8302,7 +8481,7 @@ function backupKeyFingerprint(value) {
8302
8481
  const trimmed = value?.trim();
8303
8482
  if (!trimmed) return null;
8304
8483
  const material = keyMaterial(trimmed);
8305
- const digest2 = createHash3("sha256").update(material).digest("hex");
8484
+ const digest2 = createHash5("sha256").update(material).digest("hex");
8306
8485
  material.fill(0);
8307
8486
  return `sha256:${digest2}`;
8308
8487
  }
@@ -8339,7 +8518,7 @@ function createBackupManifest(input) {
8339
8518
  keys: backupKeyFingerprints(input.environment)
8340
8519
  };
8341
8520
  }
8342
- function invalid(reason2) {
8521
+ function invalid2(reason2) {
8343
8522
  throw new DatabaseError(
8344
8523
  "INVALID_ARGUMENT",
8345
8524
  `The backup manifest is invalid: ${reason2}`
@@ -8347,27 +8526,27 @@ function invalid(reason2) {
8347
8526
  }
8348
8527
  function boundedString(value, field) {
8349
8528
  if (typeof value !== "string" || value.length < 1 || value.length > 256) {
8350
- invalid(`${field} must be a string of 1 to 256 characters.`);
8529
+ invalid2(`${field} must be a string of 1 to 256 characters.`);
8351
8530
  }
8352
8531
  return value;
8353
8532
  }
8354
8533
  function parseBackupManifest(value) {
8355
8534
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
8356
- invalid("it is not an object.");
8535
+ invalid2("it is not an object.");
8357
8536
  }
8358
8537
  const record = value;
8359
8538
  if (record.schemaVersion !== BACKUP_MANIFEST_VERSION) {
8360
- invalid(`schemaVersion must be ${BACKUP_MANIFEST_VERSION}.`);
8539
+ invalid2(`schemaVersion must be ${BACKUP_MANIFEST_VERSION}.`);
8361
8540
  }
8362
8541
  const adapter = record.adapter;
8363
8542
  if (adapter !== "pglite" && adapter !== "postgresql") {
8364
- invalid('adapter must be "pglite" or "postgresql".');
8543
+ invalid2('adapter must be "pglite" or "postgresql".');
8365
8544
  }
8366
8545
  if (!Array.isArray(record.modules) || record.modules.length > 512) {
8367
- invalid("modules must be a list of at most 512 module ids.");
8546
+ invalid2("modules must be a list of at most 512 module ids.");
8368
8547
  }
8369
8548
  if (!Array.isArray(record.keys) || record.keys.length > 64) {
8370
- invalid("keys must be a list of at most 64 fingerprints.");
8549
+ invalid2("keys must be a list of at most 64 fingerprints.");
8371
8550
  }
8372
8551
  return {
8373
8552
  schemaVersion: BACKUP_MANIFEST_VERSION,
@@ -8377,12 +8556,12 @@ function parseBackupManifest(value) {
8377
8556
  modules: record.modules.map((entry) => boundedString(entry, "modules[]")),
8378
8557
  keys: record.keys.map((entry) => {
8379
8558
  if (typeof entry !== "object" || entry === null) {
8380
- invalid("keys[] must be an object.");
8559
+ invalid2("keys[] must be an object.");
8381
8560
  }
8382
8561
  const key = entry;
8383
8562
  const fingerprint = key.fingerprint;
8384
8563
  if (fingerprint !== null && typeof fingerprint !== "string") {
8385
- invalid("keys[].fingerprint must be a string or null.");
8564
+ invalid2("keys[].fingerprint must be a string or null.");
8386
8565
  }
8387
8566
  return {
8388
8567
  variable: boundedString(key.variable, "keys[].variable"),
@@ -8564,6 +8743,15 @@ var capabilities = [
8564
8743
  localOnly: true,
8565
8744
  confirmation: "restore-database"
8566
8745
  },
8746
+ {
8747
+ id: "database.restore.production",
8748
+ version: 1,
8749
+ summary: "Replace a deployed database with the contents of a backup directory under a verified approval grant.",
8750
+ risk: "destructive",
8751
+ requiresApprovedSpec: false,
8752
+ supportsDryRun: true,
8753
+ confirmation: "restore-database"
8754
+ },
8567
8755
  {
8568
8756
  id: "migration.scaffold",
8569
8757
  version: 1,
@@ -8829,6 +9017,8 @@ import { promisify } from "node:util";
8829
9017
  var POSTGRES_DUMP_FILE = "database.dump";
8830
9018
  var PGLITE_PAYLOAD_DIRECTORY = "pglite";
8831
9019
  var TOOL_OUTPUT_LIMIT = 4e3;
9020
+ var PLATFORM_PROBE_TIMEOUT_MS = 3e3;
9021
+ var PLATFORM_URL_LIMIT = 2048;
8832
9022
  var runFile = promisify(execFile);
8833
9023
  function reason(error) {
8834
9024
  return error instanceof Error ? error.message : String(error);
@@ -9060,11 +9250,97 @@ async function restrictTree(root) {
9060
9250
  else if (entry.isFile()) await chmod(path, 384);
9061
9251
  }
9062
9252
  }
9063
- async function databaseRestore(workspace, input, apply) {
9253
+ function restoreTarget(config) {
9254
+ if (config.adapter === "postgresql") {
9255
+ const url = new URL(config.postgresql.migrator.connectionString);
9256
+ return decodeURIComponent(url.pathname.slice(1));
9257
+ }
9258
+ const directory = config.pglite?.dataDirectory;
9259
+ return directory === void 0 ? void 0 : resolve5(config.workspaceRoot, directory);
9260
+ }
9261
+ function describeKeyMismatch(divergent) {
9262
+ return divergent.map((key) => `${key.variable} (${key.status})`).join(", ");
9263
+ }
9264
+ function platformEndpoint(options, environment) {
9265
+ if (options.platformUrl !== void 0) {
9266
+ return `${options.platformUrl.replace(/\/+$/, "")}/api/health`;
9267
+ }
9268
+ const port = environment.FD_PORT?.trim();
9269
+ return port && /^[1-9]\d{0,4}$/.test(port) ? `http://127.0.0.1:${port}/api/health` : void 0;
9270
+ }
9271
+ function connectionRefused(cause) {
9272
+ const failure_ = cause;
9273
+ if (failure_?.code === "ECONNREFUSED") return true;
9274
+ return Array.isArray(failure_?.errors) && failure_.errors.length > 0 && failure_.errors.every((entry) => connectionRefused(entry));
9275
+ }
9276
+ async function platformState(endpoint) {
9277
+ try {
9278
+ await fetch(endpoint, {
9279
+ redirect: "manual",
9280
+ signal: AbortSignal.timeout(PLATFORM_PROBE_TIMEOUT_MS)
9281
+ });
9282
+ return "running";
9283
+ } catch (error) {
9284
+ return connectionRefused(error.cause) ? "stopped" : "unknown";
9285
+ }
9286
+ }
9287
+ async function platformRefusal(options, environment) {
9288
+ const endpoint = platformEndpoint(options, environment);
9289
+ if (!endpoint) {
9290
+ return options.platformStopped ? void 0 : failure(
9291
+ "PLATFORM_STATE_UNKNOWN",
9292
+ "Nothing names the platform to probe. Pass --platform-url <origin>, set FD_PORT, or pass --platform-stopped to attest that it is not running."
9293
+ );
9294
+ }
9295
+ const state = await platformState(endpoint);
9296
+ if (state === "running") {
9297
+ return failure(
9298
+ "PLATFORM_RUNNING",
9299
+ `The platform still answers at ${endpoint}. Stop it or take it out of the load balancer before restoring.`
9300
+ );
9301
+ }
9302
+ if (state === "unknown" && !options.platformStopped) {
9303
+ return failure(
9304
+ "PLATFORM_STATE_UNKNOWN",
9305
+ `${endpoint} could not be probed (no answer within ${PLATFORM_PROBE_TIMEOUT_MS}ms, an unknown host or an untrusted certificate), so the platform may still be running. Pass --platform-stopped to attest that it is not.`
9306
+ );
9307
+ }
9308
+ return void 0;
9309
+ }
9310
+ function productionRefusal(options, config) {
9311
+ if (!options.target) {
9312
+ return failure(
9313
+ "INPUT_REQUIRED",
9314
+ "--target <database name> is required: it must repeat the database the migrator DSN names."
9315
+ );
9316
+ }
9317
+ const expected = restoreTarget(config);
9318
+ const target = config.adapter === "pglite" ? resolve5(config.workspaceRoot, options.target) : options.target;
9319
+ if (target !== expected) {
9320
+ return failure(
9321
+ "RESTORE_TARGET_MISMATCH",
9322
+ `--target names "${options.target}", but the migrator connection points at "${expected ?? ""}".`
9323
+ );
9324
+ }
9325
+ if (options.platformUrl !== void 0 && (options.platformUrl.length > PLATFORM_URL_LIMIT || !/^https?:\/\//.test(options.platformUrl) || !URL.canParse(options.platformUrl))) {
9326
+ return failure(
9327
+ "INVALID_ARGUMENT",
9328
+ "--platform-url must be an http or https origin."
9329
+ );
9330
+ }
9331
+ if (config.adapter === "postgresql" && new URL(config.postgresql.migrator.connectionString).username === new URL(config.postgresql.runtime.connectionString).username) {
9332
+ return failure(
9333
+ "MIGRATOR_ROLE_REQUIRED",
9334
+ "A production restore runs under the migrator role only. Set FD_DATABASE_MIGRATOR_URL to a connection whose user differs from the one in FD_DATABASE_URL."
9335
+ );
9336
+ }
9337
+ return void 0;
9338
+ }
9339
+ async function databaseRestore(workspace, input, apply, production) {
9064
9340
  if (!input) {
9065
9341
  return failure(
9066
9342
  "INPUT_REQUIRED",
9067
- "Use database restore --input <dir> --apply --confirm restore-database."
9343
+ production ? "Use database restore-production --input <dir> --target <database> --grant <token> --tenant <id> --apply --confirm restore-database." : "Use database restore --input <dir> --apply --confirm restore-database."
9068
9344
  );
9069
9345
  }
9070
9346
  let config;
@@ -9092,16 +9368,29 @@ async function databaseRestore(workspace, input, apply) {
9092
9368
  `The backup was taken from the ${manifest2.adapter} adapter, but this workspace is configured for ${config.adapter}.`
9093
9369
  );
9094
9370
  }
9371
+ if (production) {
9372
+ const refused = productionRefusal(production, config);
9373
+ if (refused) return refused;
9374
+ }
9095
9375
  const keys = compareBackupKeys(manifest2, config.environment);
9096
9376
  const divergent = keys.filter((key) => key.status !== "match");
9377
+ if (production && divergent.length > 0 && !production.allowKeyMismatch) {
9378
+ return failure(
9379
+ "BACKUP_KEY_MISMATCH",
9380
+ `The running environment holds keys this backup was not taken with: ${describeKeyMismatch(divergent)}. Restore the keys first, or pass --allow-key-mismatch to restore rows that stay unreadable.`,
9381
+ { keys }
9382
+ );
9383
+ }
9097
9384
  const warnings = [
9098
9385
  ...divergent.length === 0 ? [] : [
9099
- `BACKUP_KEY_MISMATCH: ${divergent.map((key) => `${key.variable} (${key.status})`).join(
9100
- ", "
9101
- )}. Restore the keys this backup was taken with, or the credentials, MFA secrets and workflow payloads it carries stay unreadable.`
9386
+ `BACKUP_KEY_MISMATCH: ${describeKeyMismatch(divergent)}. Restore the keys this backup was taken with, or the credentials, MFA secrets and workflow payloads it carries stay unreadable.`
9102
9387
  ],
9103
9388
  ...apply ? [] : ["Dry run only. Pass --apply to restore the database."]
9104
9389
  ];
9390
+ if (production && apply) {
9391
+ const refused = await platformRefusal(production, config.environment);
9392
+ if (refused) return refused;
9393
+ }
9105
9394
  if (config.adapter === "postgresql") {
9106
9395
  const tool = await resolveTool("pg_restore", process.env);
9107
9396
  if (!tool) {
@@ -9124,7 +9413,8 @@ async function databaseRestore(workspace, input, apply) {
9124
9413
  payload: payload2,
9125
9414
  tool,
9126
9415
  manifest: manifest2,
9127
- keys
9416
+ keys,
9417
+ ...production ? { target: production.target } : {}
9128
9418
  };
9129
9419
  if (!apply) return success(plan2, { warnings });
9130
9420
  const result2 = await withPostgresConnection(
@@ -9395,8 +9685,8 @@ import type { WebMount } from '${serverPackage}';
9395
9685
  ` + capabilityScope(module) + ` metrics: createModuleMetrics('${module.id}'),
9396
9686
  }), moduleId: '${module.id}' },`
9397
9687
  ).join("\n");
9398
- return GENERATED_HEADER + typeImport + `import { createModuleMetrics } from '${serverPackage}';
9399
- ` + imports + "\n\nexport function composeModuleServer(\n context: PlatformServerContext,\n): readonly PlatformServerComposition[] {\n return [\n" + calls + "\n ];\n}\n\nexport const moduleWebMounts: readonly WebMount[] = " + JSON.stringify(mounts) + " as const;\nexport const applicationBasePath = " + JSON.stringify(applicationPath) + ";\n";
9688
+ return GENERATED_HEADER + typeImport + `import { bindModuleCompositions, createModuleMetrics } from '${serverPackage}';
9689
+ ` + imports + "\n\nexport function composeModuleServer(\n context: PlatformServerContext,\n): readonly PlatformServerComposition[] {\n return bindModuleCompositions([\n" + calls + "\n ]);\n}\n\nexport const moduleWebMounts: readonly WebMount[] = " + JSON.stringify(mounts) + " as const;\nexport const applicationBasePath = " + JSON.stringify(applicationPath) + ";\n";
9400
9690
  }
9401
9691
  function generateClientComposition(modules, sdk = false, applicationPath = "/app") {
9402
9692
  const client = modules.filter((module) => module.client);
@@ -10607,7 +10897,7 @@ import {
10607
10897
  stat as stat3
10608
10898
  } from "node:fs/promises";
10609
10899
  import { dirname as dirname14, join as join14, relative as relative9 } from "node:path";
10610
- import { createHash as createHash4 } from "node:crypto";
10900
+ import { createHash as createHash6 } from "node:crypto";
10611
10901
 
10612
10902
  // ../kernel/src/legacy-local-state.ts
10613
10903
  var LEGACY_DATA_DIRECTORY = ".octane-erp";
@@ -10703,7 +10993,7 @@ async function migrationPlan(workspace) {
10703
10993
  };
10704
10994
  }
10705
10995
  async function digest(path) {
10706
- return createHash4("sha256").update(await readFile16(path)).digest("hex");
10996
+ return createHash6("sha256").update(await readFile16(path)).digest("hex");
10707
10997
  }
10708
10998
  async function ensurePrivateDirectory(path) {
10709
10999
  try {
@@ -11037,19 +11327,35 @@ function relativeReports2(root, reports) {
11037
11327
  file: relative11(root, report.file)
11038
11328
  }));
11039
11329
  }
11040
- function environmentRefusal(descriptor) {
11041
- if (descriptor.risk === "external") {
11042
- return failure(
11043
- "APPROVAL_VERIFIER_REQUIRED",
11044
- `Capability "${descriptor.id}" is disabled until a signed approval verifier is configured.`
11045
- );
11046
- }
11047
- if (descriptor.risk === "destructive" && !descriptor.localOnly) {
11330
+ var RUNNER_FLAGS = /* @__PURE__ */ new Set(["confirm", "grant", "json", "root", "tenant"]);
11331
+ function invocationDigest(arguments_, commandArguments) {
11332
+ const flags = [...arguments_.flags].filter(([name]) => !RUNNER_FLAGS.has(name)).sort(([left], [right]) => left.localeCompare(right));
11333
+ return approvalInputDigest({
11334
+ arguments: commandArguments,
11335
+ flags: Object.fromEntries(flags)
11336
+ });
11337
+ }
11338
+ function approvalRefusal(descriptor, arguments_, commandArguments) {
11339
+ const gated = descriptor.risk === "external" || descriptor.risk === "destructive" && !descriptor.localOnly;
11340
+ if (!gated) return void 0;
11341
+ const token = grantFlag(arguments_);
11342
+ const keyring = approvalGrantKeyringFromEnvironment(process.env);
11343
+ if (!token || !keyring) {
11048
11344
  return failure(
11049
11345
  "APPROVAL_VERIFIER_REQUIRED",
11050
- `Destructive capability "${descriptor.id}" is disabled until a signed approval verifier is configured.`
11346
+ `Capability "${descriptor.id}" runs only with --grant <token> from an approved approvals.core request, verified under FD_APPROVAL_GRANT_KEY.`
11051
11347
  );
11052
11348
  }
11349
+ const verification = verifyApprovalGrant(keyring, token, {
11350
+ tenantId: stringFlag(arguments_, "tenant") ?? "",
11351
+ capabilityId: descriptor.id,
11352
+ inputDigest: invocationDigest(arguments_, commandArguments)
11353
+ });
11354
+ return verification.ok ? void 0 : failure(verification.reason, verification.message);
11355
+ }
11356
+ function environmentRefusal(descriptor, arguments_, commandArguments = []) {
11357
+ const approval = approvalRefusal(descriptor, arguments_, commandArguments);
11358
+ if (approval) return approval;
11053
11359
  const environment = process.env.FD_ENV ?? process.env.NODE_ENV ?? "development";
11054
11360
  if (descriptor.localOnly && environment !== "development" && environment !== "test") {
11055
11361
  return failure(
@@ -11080,7 +11386,11 @@ function writeRefusal(descriptor, arguments_) {
11080
11386
  }
11081
11387
  async function runExtensionCommand(workspace, extension, arguments_) {
11082
11388
  const descriptor = extension.command.capability;
11083
- const refused = environmentRefusal(descriptor);
11389
+ const invokedByCapability = arguments_.positionals[0] === "capability" && arguments_.positionals[1] === "run";
11390
+ const commandArguments = arguments_.positionals.slice(
11391
+ invokedByCapability ? 3 : extension.command.path.length
11392
+ );
11393
+ const refused = environmentRefusal(descriptor, arguments_, commandArguments);
11084
11394
  if (refused) return refused;
11085
11395
  if (descriptor.requiresApprovedSpec) {
11086
11396
  const specFlag = stringFlag(arguments_, "spec");
@@ -11111,7 +11421,6 @@ async function runExtensionCommand(workspace, extension, arguments_) {
11111
11421
  if (writeRefused) return writeRefused;
11112
11422
  const apply = arguments_.flags.has("apply");
11113
11423
  const command = await loadCliCommand(extension);
11114
- const invokedByCapability = arguments_.positionals[0] === "capability" && arguments_.positionals[1] === "run";
11115
11424
  let databases;
11116
11425
  try {
11117
11426
  const result2 = await command.execute({
@@ -11119,9 +11428,7 @@ async function runExtensionCommand(workspace, extension, arguments_) {
11119
11428
  moduleRoot: extension.moduleRoot,
11120
11429
  apply,
11121
11430
  flags: arguments_.flags,
11122
- arguments: arguments_.positionals.slice(
11123
- invokedByCapability ? 3 : extension.command.path.length
11124
- ),
11431
+ arguments: commandArguments,
11125
11432
  /* Built on first read, so a command that touches no database starts no
11126
11433
  embedded PostgreSQL and opens no pool. The runner owns it for the
11127
11434
  length of the command: an extension releases the leases it takes and
@@ -11168,6 +11475,7 @@ async function runCommand(arguments_) {
11168
11475
  "database reset [--module <id>] [--apply --confirm reset-database]",
11169
11476
  "database backup --output <dir> [--apply]",
11170
11477
  "database restore --input <dir> --apply --confirm restore-database",
11478
+ "database restore-production --input <dir> --target <database> --grant <token> --tenant <id> [--allow-key-mismatch] [--platform-url <origin>|--platform-stopped] --apply --confirm restore-database",
11171
11479
  "setup (interactive)|check|quick [--apply --confirm reset-local-auth]|migrate-state [--apply --confirm migrate-legacy-state]",
11172
11480
  ...extensionCommands.map((entry) => entry.command.path.join(" "))
11173
11481
  ],
@@ -11217,7 +11525,7 @@ async function runCommand(arguments_) {
11217
11525
  }
11218
11526
  if (group === "setup" && action === "migrate-state") {
11219
11527
  const descriptor = capability("workspace.state.migrate");
11220
- const refused = environmentRefusal(descriptor) ?? writeRefusal(descriptor, arguments_);
11528
+ const refused = environmentRefusal(descriptor, arguments_) ?? writeRefusal(descriptor, arguments_);
11221
11529
  if (refused) return refused;
11222
11530
  return migrateLegacyState(workspace, arguments_.flags.has("apply"));
11223
11531
  }
@@ -11261,7 +11569,8 @@ async function runCommand(arguments_) {
11261
11569
  "workspace.state.migrate": ["setup", "migrate-state"],
11262
11570
  "database.reset.local": ["database", "reset"],
11263
11571
  "database.backup": ["database", "backup"],
11264
- "database.restore": ["database", "restore"]
11572
+ "database.restore": ["database", "restore"],
11573
+ "database.restore.production": ["database", "restore-production"]
11265
11574
  };
11266
11575
  const alias = aliases[target];
11267
11576
  if (alias)
@@ -11336,7 +11645,7 @@ async function runCommand(arguments_) {
11336
11645
  if (group === "database") {
11337
11646
  if (action === "reset") {
11338
11647
  const descriptor = capability("database.reset.local");
11339
- const refused = environmentRefusal(descriptor) ?? writeRefusal(descriptor, arguments_);
11648
+ const refused = environmentRefusal(descriptor, arguments_) ?? writeRefusal(descriptor, arguments_);
11340
11649
  if (refused) return refused;
11341
11650
  return databaseReset(
11342
11651
  workspace,
@@ -11346,7 +11655,7 @@ async function runCommand(arguments_) {
11346
11655
  }
11347
11656
  if (action === "backup") {
11348
11657
  const descriptor = capability("database.backup");
11349
- const refused = environmentRefusal(descriptor) ?? writeRefusal(descriptor, arguments_);
11658
+ const refused = environmentRefusal(descriptor, arguments_) ?? writeRefusal(descriptor, arguments_);
11350
11659
  if (refused) return refused;
11351
11660
  return await databaseBackup(
11352
11661
  workspace,
@@ -11356,7 +11665,7 @@ async function runCommand(arguments_) {
11356
11665
  }
11357
11666
  if (action === "restore") {
11358
11667
  const descriptor = capability("database.restore");
11359
- const refused = environmentRefusal(descriptor) ?? writeRefusal(descriptor, arguments_);
11668
+ const refused = environmentRefusal(descriptor, arguments_) ?? writeRefusal(descriptor, arguments_);
11360
11669
  if (refused) return refused;
11361
11670
  return await databaseRestore(
11362
11671
  workspace,
@@ -11364,9 +11673,25 @@ async function runCommand(arguments_) {
11364
11673
  arguments_.flags.has("apply")
11365
11674
  );
11366
11675
  }
11676
+ if (action === "restore-production") {
11677
+ const descriptor = capability("database.restore.production");
11678
+ const refused = environmentRefusal(descriptor, arguments_) ?? writeRefusal(descriptor, arguments_);
11679
+ if (refused) return refused;
11680
+ return await databaseRestore(
11681
+ workspace,
11682
+ stringFlag(arguments_, "input"),
11683
+ arguments_.flags.has("apply"),
11684
+ {
11685
+ target: stringFlag(arguments_, "target"),
11686
+ allowKeyMismatch: arguments_.flags.has("allow-key-mismatch"),
11687
+ platformUrl: stringFlag(arguments_, "platform-url"),
11688
+ platformStopped: arguments_.flags.has("platform-stopped")
11689
+ }
11690
+ );
11691
+ }
11367
11692
  return failure(
11368
11693
  "USAGE_ERROR",
11369
- "Use database reset [--module <id>] [--apply --confirm reset-database], database backup --output <dir> [--apply], or database restore --input <dir> --apply --confirm restore-database."
11694
+ "Use database reset [--module <id>] [--apply --confirm reset-database], database backup --output <dir> [--apply], database restore --input <dir> --apply --confirm restore-database, or database restore-production --input <dir> --target <database> --grant <token> --tenant <id> --apply --confirm restore-database."
11370
11695
  );
11371
11696
  }
11372
11697
  if (group === "migration") {
@@ -11400,7 +11725,7 @@ async function runCommand(arguments_) {
11400
11725
  );
11401
11726
  }
11402
11727
  const descriptor = capability("migration.apply.local");
11403
- const refused = environmentRefusal(descriptor) ?? writeRefusal(descriptor, arguments_);
11728
+ const refused = environmentRefusal(descriptor, arguments_) ?? writeRefusal(descriptor, arguments_);
11404
11729
  if (refused) return refused;
11405
11730
  return migrationApply(
11406
11731
  workspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowdular",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "flowdular": "./dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "ajv": "8.20.0",
17
17
  "pg": "8.23.0",
18
18
  "yaml": "2.9.0",
19
- "@flowdular/sdk": "0.3.0"
19
+ "@flowdular/sdk": "0.3.2"
20
20
  },
21
21
  "license": "MIT",
22
22
  "repository": {
Binary file