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.
@@ -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`. `risk: 'destructive'` without `localOnly`: the same.
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
- The trap: **the encryption keys live outside the database.** Agent provider credentials, agent run grants, workflow payloads and cursors, automation secrets and MFA secrets 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. Back the keys up separately, restore them together with the dump, and record which key version a dump belongs to.
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` always and non-local `destructive` always, gates `localOnly` to `development` and `test`, and treats every write command as a dry run without `--apply`.
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: refused with APPROVAL_VERIFIER_REQUIRED (no signed approval verifier exists)
244
- destructive-not-local: refused with APPROVAL_VERIFIER_REQUIRED
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)
@@ -3,7 +3,7 @@
3
3
  "schemaVersion": 1,
4
4
  "id": "catalog.core",
5
5
  "package": "@flowdular/module-catalog",
6
- "version": "0.7.0",
6
+ "version": "0.7.1",
7
7
  "profile": "full",
8
8
  "capabilities": ["api", "database", "client", "translations"],
9
9
  "platform": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowdular/module-catalog",
3
- "version": "0.7.0",
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.2.4"
19
+ "@flowdular/sdk": "0.3.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@tsrx/typescript-plugin": "0.3.120",
@@ -1,6 +1,6 @@
1
1
  schemaVersion: 1
2
2
  id: catalog.core
3
- specVersion: 0.7.0
3
+ specVersion: 0.7.1
4
4
  status: approved
5
5
  name: Product Catalog Core
6
6
  description: Provides tenant-scoped products and services with stable SKUs, units, prices, currencies, and lifecycle state.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "id": "catalog.core",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "repository": "Flowdular/official-modules",
5
- "sourceCommit": "b8281b38498b1e3164e6619ec9e8fe922ad7dc10",
6
- "artifactSha256": "6c98785c10fac14cb1a913c6462cb42e18fe824a7d0dd7620746605fd351441d",
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": "1b512c9b97bdec2155103f521a6545e558e50491f90c74a93d33cc74511d2125",
19
- "package.json": "67d50dc95723d21bb44bbab54ae14fe89641c52b9f62ba065908f67ebcf0ed7a",
20
- "spec/module.yaml": "ec931c78f351c7fc53f43fdd5ec9cd1a46db485f642648329bf1612883a3c958",
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`. `risk: 'destructive'` without `localOnly`: the same.
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
- The trap: **the encryption keys live outside the database.** Agent provider credentials, agent run grants, workflow payloads and cursors, automation secrets and MFA secrets 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. Back the keys up separately, restore them together with the dump, and record which key version a dump belongs to.
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`. `risk: 'destructive'` without `localOnly`: the same.
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
- The trap: **the encryption keys live outside the database.** Agent provider credentials, agent run grants, workflow payloads and cursors, automation secrets and MFA secrets 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. Back the keys up separately, restore them together with the dump, and record which key version a dump belongs to.
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.6.1
12
- pnpm flowdular module install expenses.core@0.6.1 --apply
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`. Production restores therefore
139
- run `pg_restore --clean --if-exists` by hand with the migrator credentials, and
140
- the dry run on a staging copy of the same backup is what proves the archive and
141
- the keys are good. Lifting that restriction waits on the signed approval
142
- verifier listed under `planned` in `.ai/policies/capabilities.yaml`.
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` | No re-sealing pass yet; see the storage note below |
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` | No re-sealing pass yet; the ring reads the previous key |
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 a rotation runs like the four above:
247
- put the retired key in `FD_STORAGE_ENCRYPTION_KEY_PREVIOUS`, deploy, and every
248
- object written earlier still opens while new ones use the current key. There is
249
- no re-sealing pass yet, so keep the retired key in the ring until every object
250
- written under it has been rewritten or deleted. Dropping it makes those objects
251
- unreadable, exactly as a lost database key makes a credential unreadable.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-flowdular",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Scaffold a Flowdular application: the platform, one example module and the secrets a fresh install needs.",
6
6
  "license": "MIT",
@@ -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=
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "octane": "0.1.51",
18
18
  "segment-state": "0.2.1",
19
- "@flowdular/sdk": "0.3.0"
19
+ "@flowdular/sdk": "0.3.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@tsrx/typescript-plugin": "0.3.120",
@@ -25,7 +25,7 @@
25
25
  "devDependencies": {
26
26
  "@tsrx/prettier-plugin": "0.3.120",
27
27
  "prettier": "3.6.2",
28
- "flowdular": "0.3.0",
28
+ "flowdular": "0.3.1",
29
29
  "rulesync": "16.21.0"
30
30
  }
31
31
  }
@@ -15,7 +15,7 @@
15
15
  "@octanejs/vite-plugin": "0.1.51",
16
16
  "octane": "0.1.51",
17
17
  "pg": "8.23.0",
18
- "@flowdular/sdk": "0.3.0"
18
+ "@flowdular/sdk": "0.3.1"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@octanejs/app-core": "0.0.47",