create-flowdular 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- 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 +4 -2
- package/agent-template/.ai/policies/capabilities.yaml +30 -3
- package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
- package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
- package/agent-template/.ai/references/catalog/module.json +11 -1
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +25 -4
- package/agent-template/.ai/references/catalog/src/agent/tools.ts +19 -10
- package/agent-template/.ai/references/catalog/src/api/endpoints.ts +150 -10
- package/agent-template/.ai/references/catalog/src/api/list-cursor.ts +83 -0
- package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
- package/agent-template/.ai/references/catalog/src/client/api.ts +124 -36
- package/agent-template/.ai/references/catalog/src/client/contribution.tsrx +5 -0
- package/agent-template/.ai/references/catalog/src/client/state.ts +169 -3
- package/agent-template/.ai/references/catalog/src/domain/lists.ts +7 -0
- package/agent-template/.ai/references/catalog/src/domain/types.ts +20 -0
- package/agent-template/.ai/references/catalog/src/platform.ts +20 -0
- package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +143 -8
- package/agent-template/.ai/references/catalog/src/services/database-repository.ts +104 -17
- package/agent-template/.ai/references/catalog/src/services/item-export.ts +81 -0
- package/agent-template/.ai/references/catalog/src/services/migration.ts +27 -1
- package/agent-template/.ai/references/catalog/src/services/repository.ts +31 -2
- package/agent-template/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
- package/agent-template/.ai/references/catalog/tests/client-state.test.ts +124 -0
- package/agent-template/.ai/references/catalog/tests/endpoints.test.ts +269 -0
- package/agent-template/.ai/references/catalog/tests/export.test.ts +134 -0
- package/agent-template/.ai/references/catalog/tests/idempotency.test.ts +15 -14
- package/agent-template/.ai/references/catalog/tests/list.test.ts +217 -0
- package/agent-template/.ai/references/catalog/tests/migrations.test.ts +58 -2
- package/agent-template/.ai/references/catalog/tests/module.test.ts +2 -1
- package/agent-template/.ai/references/catalog/tests/support/database.ts +14 -0
- package/agent-template/.ai/references/catalog/translations/en.json +35 -4
- package/agent-template/.ai/references/catalog/translations/pl.json +35 -4
- package/agent-template/.ai/references/catalog.provenance.json +34 -26
- 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/modules.md +13 -0
- package/agent-template/docs/operations.md +101 -20
- package/agent-template/platform/scripts/build.mjs +1 -0
- package/assets/flowdular-banner.webp +0 -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
- package/template/default/platform/src/generated/modules.server.ts +7 -4
- package/assets/flowdular-banner.png +0 -0
|
@@ -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(),
|
|
Binary file
|
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=
|
|
@@ -5,7 +5,10 @@ import type {
|
|
|
5
5
|
PlatformServerContext,
|
|
6
6
|
} from '@flowdular/sdk/modules/auth/server';
|
|
7
7
|
import type { WebMount } from '@flowdular/sdk/server';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
bindModuleCompositions,
|
|
10
|
+
createModuleMetrics,
|
|
11
|
+
} from '@flowdular/sdk/server';
|
|
9
12
|
import { createServerComposition as system_core } from '@flowdular/sdk/modules/system/platform';
|
|
10
13
|
import { createServerComposition as access_core } from '@flowdular/sdk/modules/access/platform';
|
|
11
14
|
import { createServerComposition as reports_core } from '@flowdular/sdk/modules/reports/platform';
|
|
@@ -30,14 +33,14 @@ import { createServerComposition as users_core } from '@flowdular/sdk/modules/us
|
|
|
30
33
|
export function composeModuleServer(
|
|
31
34
|
context: PlatformServerContext,
|
|
32
35
|
): readonly PlatformServerComposition[] {
|
|
33
|
-
return [
|
|
36
|
+
return bindModuleCompositions([
|
|
34
37
|
{
|
|
35
38
|
...system_core({
|
|
36
39
|
...context,
|
|
37
40
|
agentDefinitions: context.agentDefinitions.forModule('system.core'),
|
|
38
41
|
dataClasses: context.dataClasses.forModule('system.core'),
|
|
39
42
|
capabilities: context.capabilities.forModule('system.core', {
|
|
40
|
-
provides: [],
|
|
43
|
+
provides: ['system.modules.v1'],
|
|
41
44
|
requires: [],
|
|
42
45
|
}),
|
|
43
46
|
metrics: createModuleMetrics('system.core'),
|
|
@@ -316,7 +319,7 @@ export function composeModuleServer(
|
|
|
316
319
|
}),
|
|
317
320
|
moduleId: 'users.core',
|
|
318
321
|
},
|
|
319
|
-
];
|
|
322
|
+
]);
|
|
320
323
|
}
|
|
321
324
|
|
|
322
325
|
export const moduleWebMounts: readonly WebMount[] = [] as const;
|
|
Binary file
|