flowdular 0.3.0 → 0.3.1
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/dist/distribution.js +2 -2
- package/dist/index.js +371 -46
- package/package.json +2 -2
package/dist/distribution.js
CHANGED
|
@@ -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.
|
|
3118
|
+
var PLATFORM_API_VERSION = "0.1.8";
|
|
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.
|
|
3799
|
+
var SDK_VERSION = "0.3.1";
|
|
3800
3800
|
function npmPackage(specifier) {
|
|
3801
3801
|
return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
3802
3802
|
}
|
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.
|
|
3290
|
+
var PLATFORM_API_VERSION = "0.1.8";
|
|
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) =>
|
|
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.
|
|
6140
|
+
var SDK_VERSION = "0.3.1";
|
|
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
|
|
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:${
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8535
|
+
invalid2("it is not an object.");
|
|
8357
8536
|
}
|
|
8358
8537
|
const record = value;
|
|
8359
8538
|
if (record.schemaVersion !== BACKUP_MANIFEST_VERSION) {
|
|
8360
|
-
|
|
8539
|
+
invalid2(`schemaVersion must be ${BACKUP_MANIFEST_VERSION}.`);
|
|
8361
8540
|
}
|
|
8362
8541
|
const adapter = record.adapter;
|
|
8363
8542
|
if (adapter !== "pglite" && adapter !== "postgresql") {
|
|
8364
|
-
|
|
8543
|
+
invalid2('adapter must be "pglite" or "postgresql".');
|
|
8365
8544
|
}
|
|
8366
8545
|
if (!Array.isArray(record.modules) || record.modules.length > 512) {
|
|
8367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
)
|
|
11046
|
-
}
|
|
11047
|
-
|
|
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
|
-
`
|
|
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
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
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.
|
|
19
|
+
"@flowdular/sdk": "0.3.1"
|
|
20
20
|
},
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"repository": {
|