create-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/agent-template/.agents/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +7 -2
- package/agent-template/.ai/platform-capabilities.md +2 -2
- package/agent-template/.ai/policies/capabilities.yaml +30 -3
- package/agent-template/.ai/references/catalog/module.json +1 -1
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +1 -1
- package/agent-template/.ai/references/catalog.provenance.json +6 -6
- package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +7 -2
- package/agent-template/.claude/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +7 -2
- package/agent-template/docs/cli.md +5 -0
- package/agent-template/docs/module-distribution.md +2 -2
- package/agent-template/docs/operations.md +101 -20
- package/agent-template/platform/scripts/build.mjs +1 -0
- package/dist/bin.js +1 -0
- package/package.json +1 -1
- package/template/default/.env.example +2 -0
- package/template/default/modules/example/package.json +1 -1
- package/template/default/package.json +1 -1
- package/template/default/platform/package.json +1 -1
|
@@ -85,7 +85,7 @@ export default cliExtension;
|
|
|
85
85
|
|
|
86
86
|
## 4. What the runner does with the descriptor (`packages/cli/src/runner.ts`, `runExtensionCommand`)
|
|
87
87
|
|
|
88
|
-
- `risk: 'external'`: refused with `APPROVAL_VERIFIER_REQUIRED
|
|
88
|
+
- `risk: 'external'`, or `'destructive'` without `localOnly`: refused with `APPROVAL_VERIFIER_REQUIRED` unless `--grant <token> --tenant <id>` carries a verified approval grant for this capability and invocation digest (`APPROVAL_GRANT_INVALID`, `APPROVAL_GRANT_EXPIRED`, `APPROVAL_GRANT_MISMATCH` otherwise).
|
|
89
89
|
- `localOnly: true`: refused with `LOCAL_ONLY_CAPABILITY` unless `FD_ENV` or `NODE_ENV` is `development` or `test` (unset counts as development).
|
|
90
90
|
- `requiresApprovedSpec: true`: needs `--spec <path>` to a schema-valid spec with `status: approved`, otherwise `APPROVED_SPEC_REQUIRED`, `SPEC_VALIDATION_FAILED` or `SPEC_NOT_APPROVED`.
|
|
91
91
|
- `destructive` with `--apply`: needs `--confirm <confirmation>` (`CONFIRMATION_REQUIRED`).
|
|
@@ -76,9 +76,12 @@ Both are served by `platform/src/server/health.ts`. The shipped Docker and Kuber
|
|
|
76
76
|
pnpm flowdular database backup --output <dir> # dry run
|
|
77
77
|
pnpm flowdular database backup --output <dir> --apply
|
|
78
78
|
pnpm flowdular database restore --input <dir> --apply --confirm restore-database
|
|
79
|
+
pnpm flowdular database restore-production --input <dir> --target <db> --grant <token> --tenant <id> [--platform-url <origin>|--platform-stopped] --apply --confirm restore-database
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
Production restores run `database.restore.production`: an approval grant bound to the exact flags, `--target` equal to the migrator DSN database, a separate migrator DSN, a key mismatch refused unless the approval included `--allow-key-mismatch`, and a refusal while the health endpoint answers. PITR for the compose stack is `infra/docker/pitr.sh` (see `infra/README.md`); in Kubernetes it is the managed provider's job.
|
|
83
|
+
|
|
84
|
+
The trap: **the encryption keys live outside the database.** Agent provider credentials, agent run grants, workflow payloads and cursors, automation secrets, MFA secrets, stored objects and connector credentials are all stored as ciphertext, and the keys are environment variables (`FD_AGENT_CREDENTIAL_KEY`, `FD_AGENT_RUN_GRANT_KEY`, `FD_WORKFLOWS_PAYLOAD_KEY`, `FD_WORKFLOWS_CURSOR_KEY`, `FD_AUTOMATIONS_CREDENTIAL_KEY`, `FD_AUTH_MFA_KEY`, `FD_NOTIFICATIONS_SECRET_KEY`, `FD_STORAGE_ENCRYPTION_KEY`, `FD_CONNECTORS_SECRET_KEY`, `FD_AUDIT_ANCHOR_KEY`). A database backup without the matching keys restores rows nobody can read, and rotating a key without re-encrypting orphans everything encrypted under the old one; every sealing key has a `secrets-rotate` command in the runbook's rotation table, the storage key two (`documents` and `exports`). Back the keys up separately, restore them together with the dump, and record which key version a dump belongs to.
|
|
82
85
|
|
|
83
86
|
## 6. Rollback
|
|
84
87
|
|
|
@@ -87,6 +90,8 @@ The trap: **the encryption keys live outside the database.** Agent provider cred
|
|
|
87
90
|
3. To take one module out of service without a redeploy: `pnpm flowdular module disable <id> --apply` (it is a dry run without `--apply`), then rebuild the composition and redeploy. Its tables stay.
|
|
88
91
|
4. If the rollback is because of a key change, restore the previous key first; the image alone will not fix unreadable ciphertext.
|
|
89
92
|
|
|
93
|
+
- For data loss between two dumps use PITR (`pitr.sh restore --target-time`), never a `.down.sql`.
|
|
94
|
+
|
|
90
95
|
## 7. Production checklist
|
|
91
96
|
|
|
92
97
|
- `pnpm verify` and `pnpm build` pass on the commit being shipped (the image build runs both).
|
|
@@ -98,7 +103,7 @@ The trap: **the encryption keys live outside the database.** Agent provider cred
|
|
|
98
103
|
- Sign-up closed (`FD_AUTH_ALLOW_SIGN_UP`) unless the deployment is public; the first owner created with `flowdular auth workspace-create` (see `docs/cli.md`).
|
|
99
104
|
- Liveness on `/api/health`, readiness on `/api/ready`.
|
|
100
105
|
- `pnpm flowdular migration verify` clean after rollout, and `pnpm flowdular doctor --json` reports `status: healthy`.
|
|
101
|
-
- A restore has been rehearsed once, keys included.
|
|
106
|
+
- A restore has been rehearsed once, keys included, and a PITR restore once from the newest base backup.
|
|
102
107
|
|
|
103
108
|
## Pitfalls
|
|
104
109
|
|
|
@@ -50,7 +50,7 @@ Read this file before writing or implementing a spec. It replaces scanning `modu
|
|
|
50
50
|
|
|
51
51
|
**Automations triggers.** Two mechanisms in `automations.core`. A schedule uses a cadence string in one of two forms (`modules/automations/src/domain/cadence.ts`): `every:N`, where `N` is whole minutes from 1 to 10080 and the unit is implicit, or `cron:<minute> <hour> <day of month> <month> <day of week>`, five fields with no seconds, at most 100 characters, each field `*`, a number, a three letter month or weekday name, a list, a range, or any of those with a step (`*/15`, `9-17/4`); both day fields restricted means either matches. A cron slot is a wall clock time in the workspace zone from the shared `system.core.timeZone` setting, stored in UTC; a wall time a daylight saving change removes is skipped and one that occurs twice fires at the first of the two. An expression the module cannot honour is refused at save with `INVALID_CADENCE`. Missed slots are skipped, never replayed. An inbound webhook posts to `POST /api/automations/triggers/:id/fire`, authenticated by an HMAC signature in `x-flowdular-signature` with `x-flowdular-timestamp` inside a 5 minute window and a 16 KB body cap, answering `202 { accepted, runId }`. Targets are pluggable through `automations.targets.v1`; the shipped kinds are `agent` and `workflow`.
|
|
52
52
|
|
|
53
|
-
**CLI extensions.** A module contributes `pnpm flowdular <namespace> <action>` through `src/cli/commands.json` plus `defineCliExtension` (`packages/cli-protocol/src/index.ts`), declared in `module.json` under `cli`. The first path segment must equal the module id's first segment and may not be a reserved group (`help`, `doctor`, `capability`, `spec`, `blueprint`, `module`, `migration`, `setup`). Each command carries a `CapabilityDescriptor` with `id`, `version`, `summary`, `risk: 'read' | 'workspace-write' | 'process' | 'external' | 'destructive'`, `requiresApprovedSpec`, `supportsDryRun`, optional `localOnly` and `confirmation`. The runner refuses `external`
|
|
53
|
+
**CLI extensions.** A module contributes `pnpm flowdular <namespace> <action>` through `src/cli/commands.json` plus `defineCliExtension` (`packages/cli-protocol/src/index.ts`), declared in `module.json` under `cli`. The first path segment must equal the module id's first segment and may not be a reserved group (`help`, `doctor`, `capability`, `spec`, `blueprint`, `module`, `migration`, `setup`). Each command carries a `CapabilityDescriptor` with `id`, `version`, `summary`, `risk: 'read' | 'workspace-write' | 'process' | 'external' | 'destructive'`, `requiresApprovedSpec`, `supportsDryRun`, optional `localOnly` and `confirmation`. The runner refuses `external` and non-local `destructive` commands unless `--grant <token> --tenant <id>` carries a verified approval grant for the capability and the invocation digest, gates `localOnly` to `development` and `test`, and treats every write command as a dry run without `--apply`.
|
|
54
54
|
|
|
55
55
|
**Translations.** The shipped locales are `en` and `pl`. A module keeps a flat bundle per locale in `translations/<locale>.json`, imports them in `src/client/contribution.tsrx` and returns `translations: { en, pl }`. Keys resolve fully qualified as `<first module segment>.<key>` through `t()`. `flowdular module validate` reports `TRANSLATION_FILE_MISSING`, `TRANSLATION_PARSE_ERROR`, `TRANSLATION_KEYS_MISMATCH` and `TRANSLATION_KEY_MISSING`.
|
|
56
56
|
|
|
@@ -66,7 +66,7 @@ Read this file before writing or implementing a spec. It replaces scanning `modu
|
|
|
66
66
|
|
|
67
67
|
**Connectors.** `connectors.core` (optional) is the governed way out to an external system. A module ships a connector definition through the public capability `connectors.definitions.v1` (`modules/connectors/src/domain/definitions.ts`): `register({ key, moduleId, label, authKinds, operations: [{ key, label, method, path, inputSchema, outputSchema }], defaultAllowedHosts, allowedPorts? })` (ports default to 443 only), and the platform ships `http-json`. An owner creates an instance behind `connectors.instances.manage` with a base URL, sealed credentials under `FD_CONNECTORS_SECRET_KEY`, a host allowlist and two consent flags, `allowWorkflows` and `allowAgents`, both off. A call goes through `connectors.calls.v1`: `call({ tenantId, instanceId, operation, input, caller: 'test' | 'workflow' | 'agent', callerRef? })`, which enforces status, consent for that caller kind, the egress policy (https, no private addresses, no redirects, timeout and size caps) and logs the call without any body; `consented(tenantId, instanceId, caller)` answers admission alone. The agent tool `connectors.call` is declared `workspace-write` with the harness consent gate `connectors.instance-consent` (`AgentToolConsent` in `packages/harness/src/runtime.ts`), carries the full action contract (`idempotency: 'required'` backed by a per-call key ledger) so workflows may use it, has no HTTP route of its own, and dials only the addresses the egress policy verified, so the ceiling is raised per instance by the owner's consent, never by a declaration.
|
|
68
68
|
|
|
69
|
-
**Approvals.** `approvals.core` (optional) turns a policy's `requiresApproval` into a request people decide. A module opens one through the public capability `approvals.requests.v1` (`modules/approvals/src/domain/capability.ts`): `open({ tenantId, subjectModule, subjectRef, permission, action, title, summary?, requesterAccountId, requirement, onResolved? })`, plus `get`, `list` and `cancel`. Eligible deciders are resolved at open from the requirement's role key and scope (both, when both are named; the requester never decides), re-read at decision time, and a request needs `decisions` approvals before `expiresInDays` runs out. Open is idempotent per subject while a request is pending, and a requirement that resolves to nobody, to too few or to more than 200 deciders is refused with a stable code. Decisions are an append-only ledger behind `approvals.requests.read`, `approvals.requests.decide` and `approvals.requests.manage`; deciders and requesters are notified through the kinds `approval-requested` and `approval-decided`. The subject module learns the outcome from `onResolved`, which runs once per terminal state after the deciding transaction commits, or by reading the request back.
|
|
69
|
+
**Approvals.** `approvals.core` (optional) turns a policy's `requiresApproval` into a request people decide. A module opens one through the public capability `approvals.requests.v1` (`modules/approvals/src/domain/capability.ts`): `open({ tenantId, subjectModule, subjectRef, permission, action, title, summary?, requesterAccountId, requirement, onResolved? })`, plus `get`, `list` and `cancel`. Eligible deciders are resolved at open from the requirement's role key and scope (both, when both are named; the requester never decides), re-read at decision time, and a request needs `decisions` approvals before `expiresInDays` runs out. Open is idempotent per subject while a request is pending, and a requirement that resolves to nobody, to too few or to more than 200 deciders is refused with a stable code. Decisions are an append-only ledger behind `approvals.requests.read`, `approvals.requests.decide` and `approvals.requests.manage`; deciders and requesters are notified through the kinds `approval-requested` and `approval-decided`. The subject module learns the outcome from `onResolved`, which runs once per terminal state after the deciding transaction commits, or by reading the request back. A request whose `subjectRef` is `encodeCapabilitySubjectRef({ capabilityId, inputDigest })` yields, once approved, a signed token through `grant(tenantId, id, subjectModule)`, handed only to the module that opened it; the CLI runner takes it as `--grant` (valid until expiry) and the harness as `AgentExecutionRequest.grants` (one tool call per grant), both bound to the tenant, the capability id and `approvalInputDigest` of the input.
|
|
70
70
|
|
|
71
71
|
**Documents.** `documents.core` (optional) owns file attachments of any record. A screen uploads through `POST /api/documents/upload` with the raw body and the headers `x-document-filename`, `x-document-owner-module`, `x-document-record-ref` and `x-document-description`, lists with `GET /api/documents?ownerModule=&recordRef=`, opens through `POST /api/documents/read-url` (a short-lived storage URL) and deletes through `POST /api/documents/delete`, all behind `documents.files.read` or `documents.files.manage` with CSRF first. A module reads its own records' attachments through the public capability `documents.attachments.v1` (`modules/documents/src/domain/attachments.ts`): `list(tenantId, ownerModule, recordRef)`, `open(tenantId, ownerModule, recordRef, id)` answering `{ contentType, bytes, filename, body }` or null for anything not readable (unknown, another pair, deleted, infected) and `delete(tenantId, ownerModule, recordRef, id)`; the reference pair is a scope, the caller's permission on its own record is the authorization, and the storage key never leaves documents.core. Checksums, scan verdicts and the object limits come from the storage port.
|
|
72
72
|
|
|
@@ -84,6 +84,12 @@ core:
|
|
|
84
84
|
confirmation: restore-database
|
|
85
85
|
supportsDryRun: true
|
|
86
86
|
effect: 'replaces the configured database with the backup in <dir> (pg_restore --clean --if-exists, or a staged replacement of the embedded data directory); refuses BACKUP_ADAPTER_MISMATCH across adapters and warns BACKUP_KEY_MISMATCH when the running environment holds keys the backup was not taken with'
|
|
87
|
+
database.restore.production:
|
|
88
|
+
command: pnpm flowdular database restore-production --input <dir> --target <database> --grant <token> --tenant <id> [--allow-key-mismatch] [--platform-url <origin>|--platform-stopped] --apply --confirm restore-database
|
|
89
|
+
risk: destructive
|
|
90
|
+
confirmation: restore-database
|
|
91
|
+
supportsDryRun: true
|
|
92
|
+
effect: 'the same restore without localOnly, so it runs in any environment but only under a verified approval grant (see approvals.destructive-not-local; the grant binds --input, --target and every other capability flag); --target must repeat the database of the migrator DSN (the embedded data directory for pglite) or RESTORE_TARGET_MISMATCH; MIGRATOR_ROLE_REQUIRED when FD_DATABASE_MIGRATOR_URL is absent or equals FD_DATABASE_URL; a key divergence is a BACKUP_KEY_MISMATCH refusal unless --allow-key-mismatch is part of the approved invocation; with --apply, PLATFORM_RUNNING while <origin>/api/health (from --platform-url, else http://127.0.0.1:$FD_PORT) answers, and PLATFORM_STATE_UNKNOWN when no endpoint is named or the probe times out unless --platform-stopped attests the platform is down'
|
|
87
93
|
workspace.state.migrate:
|
|
88
94
|
command: pnpm flowdular setup migrate-state [--apply --confirm migrate-legacy-state]
|
|
89
95
|
risk: destructive
|
|
@@ -146,6 +152,24 @@ modules:
|
|
|
146
152
|
risk: process
|
|
147
153
|
supportsDryRun: true
|
|
148
154
|
note: re-seals stored trigger secrets with the current encryption key; batches of 200 rows in one tenant-scoped transaction each, idempotent, and never prints a secret
|
|
155
|
+
documents.core:
|
|
156
|
+
documents.storage.rotate:
|
|
157
|
+
command: pnpm flowdular documents secrets-rotate [--apply]
|
|
158
|
+
risk: process
|
|
159
|
+
supportsDryRun: true
|
|
160
|
+
note: re-seals stored document objects in place with the current storage encryption key; batches of 200 rows in one tenant-scoped transaction each, idempotent, leaves an object under an unknown key or one that fails authentication as it is, and never prints object content
|
|
161
|
+
exports.core:
|
|
162
|
+
exports.storage.rotate:
|
|
163
|
+
command: pnpm flowdular exports secrets-rotate [--apply]
|
|
164
|
+
risk: process
|
|
165
|
+
supportsDryRun: true
|
|
166
|
+
note: re-seals stored export files in place with the current storage encryption key; batches of 200 rows in one tenant-scoped transaction each, idempotent, leaves a file under an unknown key or one that fails authentication as it is, and never prints file content
|
|
167
|
+
connectors.core:
|
|
168
|
+
connectors.secrets.rotate:
|
|
169
|
+
command: pnpm flowdular connectors secrets-rotate [--apply]
|
|
170
|
+
risk: process
|
|
171
|
+
supportsDryRun: true
|
|
172
|
+
note: re-seals stored connector credentials with the current encryption key and recomputes their fingerprints under it; batches of 200 rows in one tenant-scoped transaction each, idempotent, leaves a row under an unknown key as it is, and never prints a credential
|
|
149
173
|
notifications.core:
|
|
150
174
|
notifications.secrets.rotate:
|
|
151
175
|
command: pnpm flowdular notifications secrets-rotate [--apply]
|
|
@@ -240,14 +264,18 @@ modules:
|
|
|
240
264
|
|
|
241
265
|
# packages/cli/src/runner.ts, runExtensionCommand, in evaluation order.
|
|
242
266
|
approvals:
|
|
243
|
-
external:
|
|
244
|
-
destructive-not-local:
|
|
267
|
+
external: the same grant check as destructive-not-local; APPROVAL_VERIFIER_REQUIRED without a token or key
|
|
268
|
+
destructive-not-local: needs --grant <token> --tenant <id>, an approval grant verified under FD_APPROVAL_GRANT_KEY against the tenant, the capability id and the invocation digest (positional arguments plus every flag except --root, --json, --confirm, --grant and --tenant, so --apply is bound and an approval names the applied run, never a dry run); APPROVAL_VERIFIER_REQUIRED without a token or key, APPROVAL_GRANT_INVALID, APPROVAL_GRANT_EXPIRED or APPROVAL_GRANT_MISMATCH otherwise; the runner records no use, so a grant replays until it expires
|
|
245
269
|
localOnly: refused with LOCAL_ONLY_CAPABILITY unless FD_ENV or NODE_ENV is development or test (unset counts as development)
|
|
246
270
|
requiresApprovedSpec: needs --spec <path>; APPROVED_SPEC_REQUIRED, SPEC_VALIDATION_FAILED or SPEC_NOT_APPROVED otherwise
|
|
247
271
|
destructive-apply: --apply needs --confirm <descriptor.confirmation>; CONFIRMATION_REQUIRED otherwise
|
|
248
272
|
non-read-without-dry-run: EXPLICIT_APPLY_REQUIRED without --apply
|
|
249
273
|
dry-run: non-read with supportsDryRun and no --apply runs with apply=false and warns "Dry run only. No writes were authorized."
|
|
250
274
|
|
|
275
|
+
harnessGate:
|
|
276
|
+
localOnlyTools: CLI tools whose capability is localOnly are refused by the harness with TOOL_LOCAL_ONLY outside development and test, grant or not
|
|
277
|
+
grantedTools: external tools and non-local destructive CLI tools are offered only when the run carries a grant verified for the tool id and the input digest; one tool call per grant (APPROVAL_GRANT_CONSUMED afterwards); the consent gate still applies
|
|
278
|
+
|
|
251
279
|
moduleExtensions:
|
|
252
280
|
discovery: module.json cli.catalog (src/cli/commands.json), schema-validated without executing code
|
|
253
281
|
loadImplementation: module.json cli.entry imported only when the command runs (packages/cli/src/extensions.ts loadCliCommand)
|
|
@@ -259,6 +287,5 @@ moduleExtensions:
|
|
|
259
287
|
planned:
|
|
260
288
|
- migration.plan (served by "migration apply" without --apply, which is a read-only dry run)
|
|
261
289
|
- migration.apply.remote
|
|
262
|
-
- destructive.execute (signed approval receipts)
|
|
263
290
|
- redaction of *_TOKEN, *_SECRET, *_PASSWORD, DATABASE_URL in a shared logger (only packages/ai-provider/src/errors.ts redactSecrets exists)
|
|
264
291
|
- persisted CLI audit (CommandEnvelope carries a random auditId and evidence per invocation; nothing stores it)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowdular/module-catalog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"octane": "0.1.51",
|
|
18
18
|
"segment-state": "0.2.0",
|
|
19
|
-
"@flowdular/sdk": "0.
|
|
19
|
+
"@flowdular/sdk": "0.3.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@tsrx/typescript-plugin": "0.3.120",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "catalog.core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"repository": "Flowdular/official-modules",
|
|
5
|
-
"sourceCommit": "
|
|
6
|
-
"artifactSha256": "
|
|
5
|
+
"sourceCommit": "be391ac460e53e06a812fc9f0ea1260e2daa058f",
|
|
6
|
+
"artifactSha256": "7c645f660f44bb4c4540d32dd188d643aafb067f4c505a2722a7f2605a7b54b8",
|
|
7
7
|
"files": {
|
|
8
8
|
"LICENSE": "155d722071bad9d0d832482e06fd5a47f7034389cb63aabcb39f4282aa3499fc",
|
|
9
9
|
"migrations/0001_catalog_core.down.sql": "0962a1edf0bde959cd4facccf77bd5b634d6cc25f125a0c79a61770c8871093f",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"migrations/0004_catalog_idempotency_ledger.down.sql": "8ba72d8edc4d25ecf6d041888cf321690c25afdaf3723812ddd7d391d75370aa",
|
|
16
16
|
"migrations/0004_catalog_idempotency_ledger.up.sql": "41e86ba2051159c6cc9672bfae17c1d02051da975f1c43c566a2b66592673cb0",
|
|
17
17
|
"migrations/README.md": "2a15fd001713c2552a3c82b186246aa5fec136f43143cc2d9aa15bd491a09d41",
|
|
18
|
-
"module.json": "
|
|
19
|
-
"package.json": "
|
|
20
|
-
"spec/module.yaml": "
|
|
18
|
+
"module.json": "f2c66d18e3d54ef1ad8dcc3d26d1ee39a2982463dd3bc3fb26b159039933ed86",
|
|
19
|
+
"package.json": "3e05cf299b5cc5d948e4b3569bd93f2648a30b22f4f14d26bb31548a79529092",
|
|
20
|
+
"spec/module.yaml": "633a719d765d9779e2bf4ae0a608251767a62cbb4e15305dc13afcb1e52f14c9",
|
|
21
21
|
"src/acl/permissions.ts": "3375521a5a229736ffac5a948140ec347dc7a7e4fba80e778c0baaf9e0622590",
|
|
22
22
|
"src/agent/tools.ts": "3204d20886d2b864482adc1ed3303c653aebd207e16c4fce0c67a4979ea8db07",
|
|
23
23
|
"src/api/endpoints.ts": "34f1036e759353550be2acf67c6fa254afc790942d09489db2e76a082df5c3a3",
|
|
@@ -91,7 +91,7 @@ export default cliExtension;
|
|
|
91
91
|
|
|
92
92
|
## 4. What the runner does with the descriptor (`packages/cli/src/runner.ts`, `runExtensionCommand`)
|
|
93
93
|
|
|
94
|
-
- `risk: 'external'`: refused with `APPROVAL_VERIFIER_REQUIRED
|
|
94
|
+
- `risk: 'external'`, or `'destructive'` without `localOnly`: refused with `APPROVAL_VERIFIER_REQUIRED` unless `--grant <token> --tenant <id>` carries a verified approval grant for this capability and invocation digest (`APPROVAL_GRANT_INVALID`, `APPROVAL_GRANT_EXPIRED`, `APPROVAL_GRANT_MISMATCH` otherwise).
|
|
95
95
|
- `localOnly: true`: refused with `LOCAL_ONLY_CAPABILITY` unless `FD_ENV` or `NODE_ENV` is `development` or `test` (unset counts as development).
|
|
96
96
|
- `requiresApprovedSpec: true`: needs `--spec <path>` to a schema-valid spec with `status: approved`, otherwise `APPROVED_SPEC_REQUIRED`, `SPEC_VALIDATION_FAILED` or `SPEC_NOT_APPROVED`.
|
|
97
97
|
- `destructive` with `--apply`: needs `--confirm <confirmation>` (`CONFIRMATION_REQUIRED`).
|
|
@@ -81,9 +81,12 @@ Both are served by `platform/src/server/health.ts`. The shipped Docker and Kuber
|
|
|
81
81
|
pnpm flowdular database backup --output <dir> # dry run
|
|
82
82
|
pnpm flowdular database backup --output <dir> --apply
|
|
83
83
|
pnpm flowdular database restore --input <dir> --apply --confirm restore-database
|
|
84
|
+
pnpm flowdular database restore-production --input <dir> --target <db> --grant <token> --tenant <id> [--platform-url <origin>|--platform-stopped] --apply --confirm restore-database
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
Production restores run `database.restore.production`: an approval grant bound to the exact flags, `--target` equal to the migrator DSN database, a separate migrator DSN, a key mismatch refused unless the approval included `--allow-key-mismatch`, and a refusal while the health endpoint answers. PITR for the compose stack is `infra/docker/pitr.sh` (see `infra/README.md`); in Kubernetes it is the managed provider's job.
|
|
88
|
+
|
|
89
|
+
The trap: **the encryption keys live outside the database.** Agent provider credentials, agent run grants, workflow payloads and cursors, automation secrets, MFA secrets, stored objects and connector credentials are all stored as ciphertext, and the keys are environment variables (`FD_AGENT_CREDENTIAL_KEY`, `FD_AGENT_RUN_GRANT_KEY`, `FD_WORKFLOWS_PAYLOAD_KEY`, `FD_WORKFLOWS_CURSOR_KEY`, `FD_AUTOMATIONS_CREDENTIAL_KEY`, `FD_AUTH_MFA_KEY`, `FD_NOTIFICATIONS_SECRET_KEY`, `FD_STORAGE_ENCRYPTION_KEY`, `FD_CONNECTORS_SECRET_KEY`, `FD_AUDIT_ANCHOR_KEY`). A database backup without the matching keys restores rows nobody can read, and rotating a key without re-encrypting orphans everything encrypted under the old one; every sealing key has a `secrets-rotate` command in the runbook's rotation table, the storage key two (`documents` and `exports`). Back the keys up separately, restore them together with the dump, and record which key version a dump belongs to.
|
|
87
90
|
|
|
88
91
|
## 6. Rollback
|
|
89
92
|
|
|
@@ -92,6 +95,8 @@ The trap: **the encryption keys live outside the database.** Agent provider cred
|
|
|
92
95
|
3. To take one module out of service without a redeploy: `pnpm flowdular module disable <id> --apply` (it is a dry run without `--apply`), then rebuild the composition and redeploy. Its tables stay.
|
|
93
96
|
4. If the rollback is because of a key change, restore the previous key first; the image alone will not fix unreadable ciphertext.
|
|
94
97
|
|
|
98
|
+
- For data loss between two dumps use PITR (`pitr.sh restore --target-time`), never a `.down.sql`.
|
|
99
|
+
|
|
95
100
|
## 7. Production checklist
|
|
96
101
|
|
|
97
102
|
- `pnpm verify` and `pnpm build` pass on the commit being shipped (the image build runs both).
|
|
@@ -103,7 +108,7 @@ The trap: **the encryption keys live outside the database.** Agent provider cred
|
|
|
103
108
|
- Sign-up closed (`FD_AUTH_ALLOW_SIGN_UP`) unless the deployment is public; the first owner created with `flowdular auth workspace-create` (see `docs/cli.md`).
|
|
104
109
|
- Liveness on `/api/health`, readiness on `/api/ready`.
|
|
105
110
|
- `pnpm flowdular migration verify` clean after rollout, and `pnpm flowdular doctor --json` reports `status: healthy`.
|
|
106
|
-
- A restore has been rehearsed once, keys included.
|
|
111
|
+
- A restore has been rehearsed once, keys included, and a PITR restore once from the newest base backup.
|
|
107
112
|
|
|
108
113
|
## Pitfalls
|
|
109
114
|
|
|
@@ -85,7 +85,7 @@ export default cliExtension;
|
|
|
85
85
|
|
|
86
86
|
## 4. What the runner does with the descriptor (`packages/cli/src/runner.ts`, `runExtensionCommand`)
|
|
87
87
|
|
|
88
|
-
- `risk: 'external'`: refused with `APPROVAL_VERIFIER_REQUIRED
|
|
88
|
+
- `risk: 'external'`, or `'destructive'` without `localOnly`: refused with `APPROVAL_VERIFIER_REQUIRED` unless `--grant <token> --tenant <id>` carries a verified approval grant for this capability and invocation digest (`APPROVAL_GRANT_INVALID`, `APPROVAL_GRANT_EXPIRED`, `APPROVAL_GRANT_MISMATCH` otherwise).
|
|
89
89
|
- `localOnly: true`: refused with `LOCAL_ONLY_CAPABILITY` unless `FD_ENV` or `NODE_ENV` is `development` or `test` (unset counts as development).
|
|
90
90
|
- `requiresApprovedSpec: true`: needs `--spec <path>` to a schema-valid spec with `status: approved`, otherwise `APPROVED_SPEC_REQUIRED`, `SPEC_VALIDATION_FAILED` or `SPEC_NOT_APPROVED`.
|
|
91
91
|
- `destructive` with `--apply`: needs `--confirm <confirmation>` (`CONFIRMATION_REQUIRED`).
|
|
@@ -76,9 +76,12 @@ Both are served by `platform/src/server/health.ts`. The shipped Docker and Kuber
|
|
|
76
76
|
pnpm flowdular database backup --output <dir> # dry run
|
|
77
77
|
pnpm flowdular database backup --output <dir> --apply
|
|
78
78
|
pnpm flowdular database restore --input <dir> --apply --confirm restore-database
|
|
79
|
+
pnpm flowdular database restore-production --input <dir> --target <db> --grant <token> --tenant <id> [--platform-url <origin>|--platform-stopped] --apply --confirm restore-database
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
Production restores run `database.restore.production`: an approval grant bound to the exact flags, `--target` equal to the migrator DSN database, a separate migrator DSN, a key mismatch refused unless the approval included `--allow-key-mismatch`, and a refusal while the health endpoint answers. PITR for the compose stack is `infra/docker/pitr.sh` (see `infra/README.md`); in Kubernetes it is the managed provider's job.
|
|
83
|
+
|
|
84
|
+
The trap: **the encryption keys live outside the database.** Agent provider credentials, agent run grants, workflow payloads and cursors, automation secrets, MFA secrets, stored objects and connector credentials are all stored as ciphertext, and the keys are environment variables (`FD_AGENT_CREDENTIAL_KEY`, `FD_AGENT_RUN_GRANT_KEY`, `FD_WORKFLOWS_PAYLOAD_KEY`, `FD_WORKFLOWS_CURSOR_KEY`, `FD_AUTOMATIONS_CREDENTIAL_KEY`, `FD_AUTH_MFA_KEY`, `FD_NOTIFICATIONS_SECRET_KEY`, `FD_STORAGE_ENCRYPTION_KEY`, `FD_CONNECTORS_SECRET_KEY`, `FD_AUDIT_ANCHOR_KEY`). A database backup without the matching keys restores rows nobody can read, and rotating a key without re-encrypting orphans everything encrypted under the old one; every sealing key has a `secrets-rotate` command in the runbook's rotation table, the storage key two (`documents` and `exports`). Back the keys up separately, restore them together with the dump, and record which key version a dump belongs to.
|
|
82
85
|
|
|
83
86
|
## 6. Rollback
|
|
84
87
|
|
|
@@ -87,6 +90,8 @@ The trap: **the encryption keys live outside the database.** Agent provider cred
|
|
|
87
90
|
3. To take one module out of service without a redeploy: `pnpm flowdular module disable <id> --apply` (it is a dry run without `--apply`), then rebuild the composition and redeploy. Its tables stay.
|
|
88
91
|
4. If the rollback is because of a key change, restore the previous key first; the image alone will not fix unreadable ciphertext.
|
|
89
92
|
|
|
93
|
+
- For data loss between two dumps use PITR (`pitr.sh restore --target-time`), never a `.down.sql`.
|
|
94
|
+
|
|
90
95
|
## 7. Production checklist
|
|
91
96
|
|
|
92
97
|
- `pnpm verify` and `pnpm build` pass on the commit being shipped (the image build runs both).
|
|
@@ -98,7 +103,7 @@ The trap: **the encryption keys live outside the database.** Agent provider cred
|
|
|
98
103
|
- Sign-up closed (`FD_AUTH_ALLOW_SIGN_UP`) unless the deployment is public; the first owner created with `flowdular auth workspace-create` (see `docs/cli.md`).
|
|
99
104
|
- Liveness on `/api/health`, readiness on `/api/ready`.
|
|
100
105
|
- `pnpm flowdular migration verify` clean after rollout, and `pnpm flowdular doctor --json` reports `status: healthy`.
|
|
101
|
-
- A restore has been rehearsed once, keys included.
|
|
106
|
+
- A restore has been rehearsed once, keys included, and a PITR restore once from the newest base backup.
|
|
102
107
|
|
|
103
108
|
## Pitfalls
|
|
104
109
|
|
|
@@ -92,6 +92,8 @@ once the restored copy is staged. Being destructive, it needs
|
|
|
92
92
|
`--apply --confirm restore-database` and, like `database reset`, runs only when
|
|
93
93
|
`FD_ENV` or `NODE_ENV` is `development` or `test`.
|
|
94
94
|
|
|
95
|
+
`database restore-production` is the same restore without the local gate: it runs only under an approval grant (`--grant <token> --tenant <id>`) bound to the exact flags, needs `--target` equal to the database the migrator DSN names, a separate migrator DSN, and either a health probe that finds the platform down (`--platform-url` or `FD_PORT`) or `--platform-stopped`; a key mismatch is refused unless the approved invocation carried `--allow-key-mismatch`. See docs/operations.md, "Restore in production".
|
|
96
|
+
|
|
95
97
|
The full procedure, the key trap and the rotation status are in
|
|
96
98
|
[operations.md](operations.md).
|
|
97
99
|
|
|
@@ -117,6 +119,9 @@ flowdular agents secrets-rotate [--apply] # re-seal stored provider cre
|
|
|
117
119
|
flowdular automations secrets-rotate [--apply] # re-seal stored trigger secrets
|
|
118
120
|
flowdular workflows secrets-rotate [--apply] # re-seal stored run payloads
|
|
119
121
|
flowdular notifications secrets-rotate [--apply] # re-seal stored webhook signing secrets
|
|
122
|
+
flowdular connectors secrets-rotate [--apply] # re-seal stored connector credentials
|
|
123
|
+
flowdular documents secrets-rotate [--apply] # re-seal stored document objects with the storage key
|
|
124
|
+
flowdular exports secrets-rotate [--apply] # re-seal stored export files with the storage key
|
|
120
125
|
|
|
121
126
|
flowdular sandbox access --tenant <tenant> # grants and eligible members
|
|
122
127
|
flowdular sandbox grant --email <email> --tenant <tenant> [--apply]
|
|
@@ -8,8 +8,8 @@ release artifacts. The landing is in [Flowdular/landing](https://github.com/Flow
|
|
|
8
8
|
```sh
|
|
9
9
|
pnpm flowdular module search expenses
|
|
10
10
|
pnpm flowdular module info expenses.core
|
|
11
|
-
pnpm flowdular module install expenses.core@0.
|
|
12
|
-
pnpm flowdular module install expenses.core@0.
|
|
11
|
+
pnpm flowdular module install expenses.core@0.7.1
|
|
12
|
+
pnpm flowdular module install expenses.core@0.7.1 --apply
|
|
13
13
|
pnpm flowdular module enable expenses.core --apply
|
|
14
14
|
pnpm flowdular module validate --locked
|
|
15
15
|
```
|
|
@@ -135,11 +135,58 @@ is in place, so a failed copy leaves the running database untouched.
|
|
|
135
135
|
|
|
136
136
|
`database restore` is a destructive capability, gated exactly like
|
|
137
137
|
`database reset`: the runner refuses it with `LOCAL_ONLY_CAPABILITY` unless
|
|
138
|
-
`FD_ENV` or `NODE_ENV` is `development` or `test`.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
`FD_ENV` or `NODE_ENV` is `development` or `test`. The dry run on a staging
|
|
139
|
+
copy of the same backup is what proves the archive and the keys are good.
|
|
140
|
+
|
|
141
|
+
### Restore in production
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
flowdular database restore-production --input <dir> --target flowdular --grant <token> --tenant <id> # plan
|
|
145
|
+
flowdular database restore-production --input <dir> --target flowdular --grant <token> --tenant <id> \
|
|
146
|
+
--platform-url https://erp.example.com --apply --confirm restore-database
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`database.restore.production` is the same restore without the local gate. It
|
|
150
|
+
runs only under an approval grant from an approved `approvals.core` request,
|
|
151
|
+
verified against `FD_APPROVAL_GRANT_KEY`, the tenant, the capability and the
|
|
152
|
+
exact flags: the request has to name `--input`, `--target`, `--apply` and any
|
|
153
|
+
override (an approval names the applied run, and a dry run is a different
|
|
154
|
+
invocation), and a token issued for another invocation is refused with
|
|
155
|
+
`APPROVAL_GRANT_MISMATCH`. Open the request with `approvals.requests.v1` and a
|
|
156
|
+
`subjectRef` of `capability:<id>:<sha256 of the invocation input>`, read the
|
|
157
|
+
approved request back through `grant(tenantId, id, subjectModule)` for the
|
|
158
|
+
token, and pass it as `--grant <token> --tenant <id>`. `--target` must repeat
|
|
159
|
+
the database the migrator DSN names (`RESTORE_TARGET_MISMATCH` otherwise),
|
|
160
|
+
`FD_DATABASE_MIGRATOR_URL` must be set and distinct from `FD_DATABASE_URL`
|
|
161
|
+
(`MIGRATOR_ROLE_REQUIRED`), and a key divergence is a refusal
|
|
162
|
+
(`BACKUP_KEY_MISMATCH`) unless `--allow-key-mismatch` was part of the approved
|
|
163
|
+
invocation. With `--apply` the command probes `<--platform-url>/api/health`,
|
|
164
|
+
or `http://127.0.0.1:$FD_PORT/api/health`, and refuses with
|
|
165
|
+
`PLATFORM_RUNNING` while anything answers; when no endpoint is named or the
|
|
166
|
+
probe times out it refuses with `PLATFORM_STATE_UNKNOWN` unless
|
|
167
|
+
`--platform-stopped` attests the platform is down. Grants are HMAC-SHA256
|
|
168
|
+
under `FD_APPROVAL_GRANT_KEY` (rotate with `FD_APPROVAL_GRANT_KEY_PREVIOUS`)
|
|
169
|
+
and expire one hour after the approving decision; the runner records no use,
|
|
170
|
+
so a grant replays until it expires and the approval window should stay short.
|
|
171
|
+
`database reset` and `database restore` keep their local-only gate.
|
|
172
|
+
|
|
173
|
+
### Point-in-time recovery
|
|
174
|
+
|
|
175
|
+
A dump restores one moment; the compose stack also archives WAL so any moment
|
|
176
|
+
after a base backup can be recovered. See `infra/README.md`, "Backups and
|
|
177
|
+
PITR": `infra/docker/pitr.sh base-backup` after every rollout,
|
|
178
|
+
`pitr.sh restore --base <stamp> --target-time '<ts>' --confirm replace-cluster`
|
|
179
|
+
to recover, and the limits of the plain-copy archive. In Kubernetes the
|
|
180
|
+
managed provider owns PITR; the dump remains the portable copy.
|
|
181
|
+
|
|
182
|
+
### Rehearsal
|
|
183
|
+
|
|
184
|
+
Quarterly, on a scratch host: restore the newest dump into an empty database
|
|
185
|
+
with `database restore-production --platform-stopped`, read the key
|
|
186
|
+
comparison, start the app, check `migration verify` and `/api/ready`, then run
|
|
187
|
+
`pitr.sh restore` from the newest base backup to a time between two dumps and
|
|
188
|
+
confirm a row written after the dump is present. Record the date and the
|
|
189
|
+
elapsed time.
|
|
143
190
|
|
|
144
191
|
## Key rotation
|
|
145
192
|
|
|
@@ -151,15 +198,15 @@ this section.
|
|
|
151
198
|
Every module reads one current key plus an optional comma-separated list of
|
|
152
199
|
retired keys (up to eight):
|
|
153
200
|
|
|
154
|
-
| Key | Protects | Retired keys | Re-sealing the stored rows
|
|
155
|
-
| ------------------------------- | -------------------------- | ---------------------------------------- |
|
|
156
|
-
| `FD_AGENT_CREDENTIAL_KEY` | Agent provider credentials | `FD_AGENT_CREDENTIAL_KEY_PREVIOUS` | `pnpm flowdular agents secrets-rotate [--apply]`
|
|
157
|
-
| `FD_AUTOMATIONS_CREDENTIAL_KEY` | Automation trigger secrets | `FD_AUTOMATIONS_CREDENTIAL_KEY_PREVIOUS` | `pnpm flowdular automations secrets-rotate [--apply]`
|
|
158
|
-
| `FD_WORKFLOWS_PAYLOAD_KEY` | Workflow run payloads | `FD_WORKFLOWS_PAYLOAD_KEY_PREVIOUS` | `pnpm flowdular workflows secrets-rotate [--apply]`
|
|
159
|
-
| `FD_STORAGE_ENCRYPTION_KEY` | Stored objects | `FD_STORAGE_ENCRYPTION_KEY_PREVIOUS` |
|
|
160
|
-
| `FD_NOTIFICATIONS_SECRET_KEY` | Webhook signing secrets | `FD_NOTIFICATIONS_SECRET_KEY_PREVIOUS` | `pnpm flowdular notifications secrets-rotate [--apply]`
|
|
161
|
-
| `FD_CONNECTORS_SECRET_KEY` | Connector credentials | `FD_CONNECTORS_SECRET_KEY_PREVIOUS` |
|
|
162
|
-
| `FD_AUDIT_ANCHOR_KEY` | Audit chain anchors (HMAC) | `FD_AUDIT_ANCHOR_KEY_PREVIOUS` | `pnpm flowdular audit secrets-rotate [--apply]`
|
|
201
|
+
| Key | Protects | Retired keys | Re-sealing the stored rows |
|
|
202
|
+
| ------------------------------- | -------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
203
|
+
| `FD_AGENT_CREDENTIAL_KEY` | Agent provider credentials | `FD_AGENT_CREDENTIAL_KEY_PREVIOUS` | `pnpm flowdular agents secrets-rotate [--apply]` |
|
|
204
|
+
| `FD_AUTOMATIONS_CREDENTIAL_KEY` | Automation trigger secrets | `FD_AUTOMATIONS_CREDENTIAL_KEY_PREVIOUS` | `pnpm flowdular automations secrets-rotate [--apply]` |
|
|
205
|
+
| `FD_WORKFLOWS_PAYLOAD_KEY` | Workflow run payloads | `FD_WORKFLOWS_PAYLOAD_KEY_PREVIOUS` | `pnpm flowdular workflows secrets-rotate [--apply]` |
|
|
206
|
+
| `FD_STORAGE_ENCRYPTION_KEY` | Stored objects | `FD_STORAGE_ENCRYPTION_KEY_PREVIOUS` | `pnpm flowdular documents secrets-rotate [--apply]` and `pnpm flowdular exports secrets-rotate [--apply]`; see the storage note below |
|
|
207
|
+
| `FD_NOTIFICATIONS_SECRET_KEY` | Webhook signing secrets | `FD_NOTIFICATIONS_SECRET_KEY_PREVIOUS` | `pnpm flowdular notifications secrets-rotate [--apply]` |
|
|
208
|
+
| `FD_CONNECTORS_SECRET_KEY` | Connector credentials | `FD_CONNECTORS_SECRET_KEY_PREVIOUS` | `pnpm flowdular connectors secrets-rotate [--apply]` |
|
|
209
|
+
| `FD_AUDIT_ANCHOR_KEY` | Audit chain anchors (HMAC) | `FD_AUDIT_ANCHOR_KEY_PREVIOUS` | `pnpm flowdular audit secrets-rotate [--apply]` |
|
|
163
210
|
|
|
164
211
|
These commands run the same re-sealing pass over their own table, so the
|
|
165
212
|
procedure is the same for each. The credential key is the worked example;
|
|
@@ -227,6 +274,10 @@ Sources: `modules/audit/src/services/{anchor-key,anchor-rotation}.ts`,
|
|
|
227
274
|
`modules/automations/src/services/{secret-vault,secret-rotation}.ts`,
|
|
228
275
|
`modules/workflows/src/services/{payload-codec,payload-rotation,cursors}.ts`,
|
|
229
276
|
`modules/notifications/src/services/{secret-vault,secret-rotation}.ts`,
|
|
277
|
+
`modules/connectors/src/services/{credential-vault,credential-rotation}.ts`,
|
|
278
|
+
`packages/storage/src/reseal.ts`,
|
|
279
|
+
`modules/documents/src/services/storage-rotation.ts`,
|
|
280
|
+
`modules/exports/src/services/storage-rotation.ts`,
|
|
230
281
|
`modules/auth/src/services/totp.ts`.
|
|
231
282
|
|
|
232
283
|
## Storage
|
|
@@ -243,12 +294,42 @@ no longer holds leaves dangling references, and the reverse leaves orphans.
|
|
|
243
294
|
`.flowdular/data/storage`; stop the application before copying it.
|
|
244
295
|
|
|
245
296
|
Every object is sealed with AES-256-GCM under `FD_STORAGE_ENCRYPTION_KEY`, and
|
|
246
|
-
the key id is stored with the object, so
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
297
|
+
the key id is stored with the object, so the rotation follows the six steps
|
|
298
|
+
above with two commands instead of one, because the object store has no
|
|
299
|
+
listing and the rows that name the objects belong to two modules:
|
|
300
|
+
|
|
301
|
+
1. Generate the new key: `openssl rand -base64 32`.
|
|
302
|
+
2. Set `FD_STORAGE_ENCRYPTION_KEY=<new>` and
|
|
303
|
+
`FD_STORAGE_ENCRYPTION_KEY_PREVIOUS=<old>` in the secret store.
|
|
304
|
+
3. Deploy. Every new object is sealed with the new key and every stored one
|
|
305
|
+
still opens under the old one.
|
|
306
|
+
4. Dry run both passes and read the counts per key id:
|
|
307
|
+
`pnpm flowdular documents secrets-rotate --json` walks the stored document
|
|
308
|
+
rows, `pnpm flowdular exports secrets-rotate --json` the completed export
|
|
309
|
+
jobs. `stale` is the number of objects still sealed with a retired key;
|
|
310
|
+
`unknown` names objects under a key neither variable holds, and the pass
|
|
311
|
+
leaves those alone.
|
|
312
|
+
5. Run both again with `--apply`. Each object is re-sealed in place: the frame
|
|
313
|
+
is opened under the key it names, which authenticates its header, and the
|
|
314
|
+
same content type, size, checksum, scan verdict and creation time are
|
|
315
|
+
written back under the current key. Nothing else changes, not the object
|
|
316
|
+
key and not the row. A frame that fails authentication is counted under
|
|
317
|
+
`refused` and left as it is; restore it from the object store backup.
|
|
318
|
+
6. Repeat the dry runs until both report `stale: 0`, then remove
|
|
319
|
+
`FD_STORAGE_ENCRYPTION_KEY_PREVIOUS` and deploy again.
|
|
320
|
+
|
|
321
|
+
The application may stay up: the pass locks a row while it rewrites the
|
|
322
|
+
object, and both delete paths (a document removal and the export retention
|
|
323
|
+
sweep) lock the row before they remove the object and then the row, so a delete
|
|
324
|
+
that lands mid-pass waits for the rewrite and removes the re-sealed object
|
|
325
|
+
rather than racing it. A frame that fails to parse is counted under `refused`
|
|
326
|
+
like one that fails authentication. Do not snapshot the object store and the database apart while a
|
|
327
|
+
pass runs: a bucket snapshot taken mid-pass holds objects under both keys, and
|
|
328
|
+
restoring it beside a database from another moment leaves rows that name
|
|
329
|
+
objects a ring without the retired key cannot open. Take both after the pass,
|
|
330
|
+
or both before it. Dropping the retired key while `stale` is above zero makes
|
|
331
|
+
those objects unreadable, exactly as a lost database key makes a credential
|
|
332
|
+
unreadable.
|
|
252
333
|
|
|
253
334
|
## Logs
|
|
254
335
|
|
|
@@ -25,6 +25,7 @@ const environment = {
|
|
|
25
25
|
FD_DATABASE_PGLITE_DIRECTORY: join(stateDirectory, 'pglite'),
|
|
26
26
|
FD_AGENT_CREDENTIAL_KEY: buildSecret(),
|
|
27
27
|
FD_AGENT_RUN_GRANT_KEY: buildSecret(),
|
|
28
|
+
FD_APPROVAL_GRANT_KEY: buildSecret(),
|
|
28
29
|
FD_AUTOMATIONS_CREDENTIAL_KEY: buildSecret(),
|
|
29
30
|
FD_NOTIFICATIONS_SECRET_KEY: buildSecret(),
|
|
30
31
|
FD_WORKFLOWS_PAYLOAD_KEY: buildSecret(),
|
package/dist/bin.js
CHANGED
|
@@ -355,6 +355,7 @@ import { randomBytes } from "node:crypto";
|
|
|
355
355
|
var SECRET_KEYS = [
|
|
356
356
|
"FD_AGENT_CREDENTIAL_KEY",
|
|
357
357
|
"FD_AGENT_RUN_GRANT_KEY",
|
|
358
|
+
"FD_APPROVAL_GRANT_KEY",
|
|
358
359
|
"FD_AUTOMATIONS_CREDENTIAL_KEY",
|
|
359
360
|
"FD_NOTIFICATIONS_SECRET_KEY",
|
|
360
361
|
"FD_WORKFLOWS_PAYLOAD_KEY",
|
package/package.json
CHANGED
|
@@ -35,6 +35,8 @@ FD_TRUST_PROXY=false
|
|
|
35
35
|
# production without their key.
|
|
36
36
|
FD_AGENT_CREDENTIAL_KEY=
|
|
37
37
|
FD_AGENT_RUN_GRANT_KEY=
|
|
38
|
+
FD_APPROVAL_GRANT_KEY=
|
|
39
|
+
FD_APPROVAL_GRANT_KEY_PREVIOUS=
|
|
38
40
|
FD_AUTOMATIONS_CREDENTIAL_KEY=
|
|
39
41
|
FD_NOTIFICATIONS_SECRET_KEY=
|
|
40
42
|
FD_WORKFLOWS_PAYLOAD_KEY=
|