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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/agent-template/.agents/skills/cli-extension/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/deploy-operate/SKILL.md +7 -2
  4. package/agent-template/.ai/platform-capabilities.md +4 -2
  5. package/agent-template/.ai/policies/capabilities.yaml +30 -3
  6. package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
  7. package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
  8. package/agent-template/.ai/references/catalog/module.json +11 -1
  9. package/agent-template/.ai/references/catalog/package.json +2 -2
  10. package/agent-template/.ai/references/catalog/spec/module.yaml +25 -4
  11. package/agent-template/.ai/references/catalog/src/agent/tools.ts +19 -10
  12. package/agent-template/.ai/references/catalog/src/api/endpoints.ts +150 -10
  13. package/agent-template/.ai/references/catalog/src/api/list-cursor.ts +83 -0
  14. package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
  15. package/agent-template/.ai/references/catalog/src/client/api.ts +124 -36
  16. package/agent-template/.ai/references/catalog/src/client/contribution.tsrx +5 -0
  17. package/agent-template/.ai/references/catalog/src/client/state.ts +169 -3
  18. package/agent-template/.ai/references/catalog/src/domain/lists.ts +7 -0
  19. package/agent-template/.ai/references/catalog/src/domain/types.ts +20 -0
  20. package/agent-template/.ai/references/catalog/src/platform.ts +20 -0
  21. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +143 -8
  22. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +104 -17
  23. package/agent-template/.ai/references/catalog/src/services/item-export.ts +81 -0
  24. package/agent-template/.ai/references/catalog/src/services/migration.ts +27 -1
  25. package/agent-template/.ai/references/catalog/src/services/repository.ts +31 -2
  26. package/agent-template/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
  27. package/agent-template/.ai/references/catalog/tests/client-state.test.ts +124 -0
  28. package/agent-template/.ai/references/catalog/tests/endpoints.test.ts +269 -0
  29. package/agent-template/.ai/references/catalog/tests/export.test.ts +134 -0
  30. package/agent-template/.ai/references/catalog/tests/idempotency.test.ts +15 -14
  31. package/agent-template/.ai/references/catalog/tests/list.test.ts +217 -0
  32. package/agent-template/.ai/references/catalog/tests/migrations.test.ts +58 -2
  33. package/agent-template/.ai/references/catalog/tests/module.test.ts +2 -1
  34. package/agent-template/.ai/references/catalog/tests/support/database.ts +14 -0
  35. package/agent-template/.ai/references/catalog/translations/en.json +35 -4
  36. package/agent-template/.ai/references/catalog/translations/pl.json +35 -4
  37. package/agent-template/.ai/references/catalog.provenance.json +34 -26
  38. package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
  39. package/agent-template/.ai/skills/deploy-operate/SKILL.md +7 -2
  40. package/agent-template/.claude/skills/cli-extension/SKILL.md +1 -1
  41. package/agent-template/.claude/skills/deploy-operate/SKILL.md +7 -2
  42. package/agent-template/docs/cli.md +5 -0
  43. package/agent-template/docs/module-distribution.md +2 -2
  44. package/agent-template/docs/modules.md +13 -0
  45. package/agent-template/docs/operations.md +101 -20
  46. package/agent-template/platform/scripts/build.mjs +1 -0
  47. package/assets/flowdular-banner.webp +0 -0
  48. package/dist/bin.js +1 -0
  49. package/package.json +1 -1
  50. package/template/default/.env.example +2 -0
  51. package/template/default/modules/example/package.json +1 -1
  52. package/template/default/package.json +1 -1
  53. package/template/default/platform/package.json +1 -1
  54. package/template/default/platform/src/generated/modules.server.ts +7 -4
  55. 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`. 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(),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-flowdular",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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.2"
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.2",
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.2"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@octanejs/app-core": "0.0.47",
@@ -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 { createModuleMetrics } from '@flowdular/sdk/server';
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