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
@@ -87,6 +87,7 @@ describe('catalog migrations', () => {
87
87
  it('declares forced PostgreSQL row security for every tenant table', () => {
88
88
  for (const migration of databaseMigrations) {
89
89
  const sql = migration.sql.postgresql ?? '';
90
+ if (!sql.includes('CREATE TABLE')) continue;
90
91
  expect(sql).toContain('ENABLE ROW LEVEL SECURITY');
91
92
  expect(sql).toContain('FORCE ROW LEVEL SECURITY');
92
93
  expect(sql).toContain("current_setting('coreloom.tenant_id', true)");
@@ -110,9 +111,9 @@ describe('catalog migrations', () => {
110
111
  transaction.execute({
111
112
  text: `INSERT INTO catalog_items
112
113
  (id, tenant_id, sku, sku_normalized, name, kind, unit,
113
- base_price_minor, currency, status, created_at)
114
+ base_price_minor, currency, status, created_at, updated_at)
114
115
  VALUES ('item-1', 'tenant-a', 'SKU-1', 'sku-1', 'Bolt', 'product',
115
- 'pcs', 500, 'EUR', 'active', 1)`,
116
+ 'pcs', 500, 'EUR', 'active', 1, 1)`,
116
117
  }),
117
118
  { tenantId: 'tenant-a', access: 'write' },
118
119
  );
@@ -139,6 +140,61 @@ describe('catalog migrations', () => {
139
140
  ).toEqual([{ sku: 'SKU-1' }]);
140
141
  });
141
142
 
143
+ it('adds the update time and the list indexes, and refuses a half-applied schema', async () => {
144
+ await apply();
145
+ const schema = lease.database.schema;
146
+ expect(await schema.hasColumn('catalog_items', 'updated_at')).toBe(true);
147
+ expect(await schema.hasIndex('catalog_items_tenant_name_idx')).toBe(true);
148
+ expect(await schema.hasIndex('catalog_items_tenant_updated_idx')).toBe(
149
+ true,
150
+ );
151
+
152
+ await lease.database.execute({
153
+ text: 'DROP INDEX catalog_items_tenant_name_idx',
154
+ });
155
+ await lease.database.execute({
156
+ text: `DELETE FROM ${DATABASE_MIGRATION_LEDGER} WHERE namespace = 'catalog.core'`,
157
+ });
158
+ expect((await status()).at(-1)).toMatchObject({
159
+ id: '0005_catalog_list_indexes',
160
+ state: 'partial',
161
+ });
162
+ });
163
+
164
+ it('backfills the update time of rows that predate it', async () => {
165
+ await runDatabaseMigrations(
166
+ lease.database,
167
+ 'catalog.core',
168
+ databaseMigrations.slice(0, 4),
169
+ );
170
+ await lease.database.transaction(
171
+ (transaction) =>
172
+ transaction.execute({
173
+ text: `INSERT INTO catalog_items
174
+ (id, tenant_id, sku, sku_normalized, name, kind, unit,
175
+ base_price_minor, currency, status, created_at)
176
+ VALUES ('item-1', 'tenant-a', 'SKU-1', 'sku-1', 'Bolt', 'product',
177
+ 'pcs', 500, 'EUR', 'active', 1234)`,
178
+ }),
179
+ { tenantId: 'tenant-a', access: 'write' },
180
+ );
181
+ expect((await apply()).at(-1)).toMatchObject({
182
+ id: '0005_catalog_list_indexes',
183
+ action: 'applied',
184
+ });
185
+ expect(
186
+ (
187
+ await lease.database.transaction(
188
+ (transaction) =>
189
+ transaction.query<{ updated_at: string | number }>({
190
+ text: 'SELECT updated_at FROM catalog_items',
191
+ }),
192
+ { tenantId: 'tenant-a', access: 'read' },
193
+ )
194
+ ).rows.map((row) => Number(row.updated_at)),
195
+ ).toEqual([1234]);
196
+ });
197
+
142
198
  it('runs clean on a second migration pass', async () => {
143
199
  await apply();
144
200
 
@@ -13,6 +13,7 @@ import {
13
13
  CatalogServiceError,
14
14
  } from '../src/services/catalog-service.ts';
15
15
  import {
16
+ allItems,
16
17
  closeCatalogTestDatabases,
17
18
  createCatalogTestDatabase,
18
19
  type CatalogTestDatabase,
@@ -161,7 +162,7 @@ describe('catalog.core', () => {
161
162
  },
162
163
  TEST_ACTOR,
163
164
  );
164
- expect(await service.list('tenant-b')).toEqual([]);
165
+ expect(await allItems(service, 'tenant-b')).toEqual([]);
165
166
  });
166
167
 
167
168
  it('keeps lifecycle revisions append-only and tenant-scoped', async () => {
@@ -4,6 +4,20 @@ import {
4
4
  DatabaseCatalogRepository,
5
5
  migrateCatalogDatabase,
6
6
  } from '../../src/services/database-repository.ts';
7
+ import {
8
+ FIRST_LIST_PAGE,
9
+ type CatalogService,
10
+ } from '../../src/services/catalog-service.ts';
11
+ import type { CatalogItem } from '../../src/domain/types.ts';
12
+
13
+ /** The first page at the ceiling in SKU order: every item of a small test tenant. */
14
+ export async function allItems(
15
+ service: CatalogService,
16
+ tenantId: string,
17
+ ): Promise<readonly CatalogItem[]> {
18
+ return (await service.listPage(tenantId, { ...FIRST_LIST_PAGE, sort: 'sku' }))
19
+ .items;
20
+ }
7
21
 
8
22
  const TENANT_TABLES = [
9
23
  'catalog_items',
@@ -7,6 +7,16 @@
7
7
  "drawer.editTitle": "Edit catalog item",
8
8
  "action.edit": "Edit",
9
9
  "action.history": "History",
10
+ "action.archiveSelected": "Archive selected",
11
+ "action.restoreSelected": "Restore selected",
12
+ "action.archiveRefused": "Select at least one active item.",
13
+ "action.restoreRefused": "Select at least one archived item.",
14
+ "action.close": "Close",
15
+ "archiveMany.title": "Archive selected items",
16
+ "archiveMany.description": "Archive {count} selected items? Archived items can be restored later.",
17
+ "common.messages": "Messages",
18
+ "denied.title": "Catalog access denied",
19
+ "denied.description": "Your account may not read catalog items in this workspace.",
10
20
  "delete.title": "Delete catalog item",
11
21
  "delete.description": "Delete {name} permanently? This cannot be undone.",
12
22
  "delete.drawerDescription": "This permanently removes the archived catalog item.",
@@ -17,7 +27,13 @@
17
27
  "error.delete": "Could not delete the catalog item.",
18
28
  "error.history": "Could not load catalog item history.",
19
29
  "error.load": "Could not load catalog items.",
30
+ "error.archiveMany": "Could not archive the selected items.",
31
+ "error.restoreMany": "Could not restore the selected items.",
20
32
  "error.request": "The catalog operation failed.",
33
+ "export.action": "Export CSV",
34
+ "export.started": "The catalog export started. It appears on the Exports screen once the file is ready.",
35
+ "export.link": "Open Exports",
36
+ "export.error": "Could not start the export.",
21
37
  "form.cancel": "Cancel",
22
38
  "form.currency": "Currency",
23
39
  "form.kind": "Kind",
@@ -37,27 +53,42 @@
37
53
  "status.active": "Active",
38
54
  "status.archived": "Archived",
39
55
  "filters.label": "Filters",
40
- "filters.includeArchived": "Include archived items",
56
+ "filters.kind": "Kind",
57
+ "filters.allKinds": "All kinds",
58
+ "filters.status": "Status",
59
+ "filters.allStatuses": "All statuses",
41
60
  "lifecycle.title": "Lifecycle",
42
61
  "lifecycle.description": "Archive an item before deleting it permanently.",
43
62
  "lifecycle.history": "View history",
44
63
  "lifecycle.archive": "Archive item",
45
64
  "lifecycle.restore": "Restore item",
46
65
  "lifecycle.delete": "Delete item",
66
+ "notice.archivedMany": "Archived {updated}, missing {missing}, refused {refused}.",
67
+ "notice.restoredMany": "Restored {updated}, missing {missing}, refused {refused}.",
47
68
  "page.action.new": "New item",
48
69
  "page.action.refresh": "Refresh",
49
70
  "page.description": "Products and services with stable tenant-scoped SKUs.",
50
71
  "page.eyebrow": "Commercial master data",
51
72
  "page.title": "Catalog",
73
+ "pagination.label": "Catalog pages",
74
+ "pagination.previous": "Previous",
75
+ "pagination.next": "Next",
76
+ "pagination.size": "Items per page",
77
+ "pagination.summary": "Page {page} · from item {from}",
52
78
  "price.fallback": "{amount} {currency} minor",
79
+ "selection.label": "Select all items on screen",
80
+ "selection.rowLabel": "Select {name}",
81
+ "selection.clear": "Clear selection",
82
+ "selection.summary": "{count} selected",
53
83
  "table.caption": "Catalog items",
54
- "table.column.item": "Item",
84
+ "table.column.name": "Name",
85
+ "table.column.sku": "SKU",
55
86
  "table.column.kind": "Kind",
56
87
  "table.column.price": "Price",
57
88
  "table.column.status": "Status",
89
+ "table.column.updated": "Updated",
58
90
  "table.column.actions": "Actions",
59
- "table.count.one": "1 item",
60
- "table.count.other": "items: {count}",
91
+ "table.count.page": "{count} on this page",
61
92
  "table.empty.hint": "Create the first product or service.",
62
93
  "table.empty.title": "No catalog items yet",
63
94
  "table.emptyFiltered.hint": "No item in this workspace matches the current filter.",
@@ -7,6 +7,16 @@
7
7
  "drawer.editTitle": "Edytuj pozycję katalogu",
8
8
  "action.edit": "Edytuj",
9
9
  "action.history": "Historia",
10
+ "action.archiveSelected": "Archiwizuj zaznaczone",
11
+ "action.restoreSelected": "Przywróć zaznaczone",
12
+ "action.archiveRefused": "Zaznacz co najmniej jedną aktywną pozycję.",
13
+ "action.restoreRefused": "Zaznacz co najmniej jedną zarchiwizowaną pozycję.",
14
+ "action.close": "Zamknij",
15
+ "archiveMany.title": "Archiwizuj zaznaczone pozycje",
16
+ "archiveMany.description": "Zarchiwizować zaznaczone pozycje ({count})? Zarchiwizowane pozycje można później przywrócić.",
17
+ "common.messages": "Komunikaty",
18
+ "denied.title": "Brak dostępu do katalogu",
19
+ "denied.description": "Twoje konto nie może odczytywać pozycji katalogu w tej przestrzeni roboczej.",
10
20
  "delete.title": "Usuń pozycję katalogu",
11
21
  "delete.description": "Usunąć trwale pozycję {name}? Tej operacji nie można cofnąć.",
12
22
  "delete.drawerDescription": "Ta operacja trwale usuwa zarchiwizowaną pozycję katalogu.",
@@ -17,7 +27,13 @@
17
27
  "error.delete": "Nie udało się usunąć pozycji katalogu.",
18
28
  "error.history": "Nie udało się wczytać historii pozycji katalogu.",
19
29
  "error.load": "Nie udało się wczytać pozycji katalogu.",
30
+ "error.archiveMany": "Nie udało się zarchiwizować zaznaczonych pozycji.",
31
+ "error.restoreMany": "Nie udało się przywrócić zaznaczonych pozycji.",
20
32
  "error.request": "Operacja na katalogu nie powiodła się.",
33
+ "export.action": "Eksportuj CSV",
34
+ "export.started": "Eksport katalogu wystartował. Pojawi się na ekranie Eksporty, gdy plik będzie gotowy.",
35
+ "export.link": "Otwórz Eksporty",
36
+ "export.error": "Nie udało się rozpocząć eksportu.",
21
37
  "form.cancel": "Anuluj",
22
38
  "form.currency": "Waluta",
23
39
  "form.kind": "Rodzaj",
@@ -37,27 +53,42 @@
37
53
  "status.active": "Aktywna",
38
54
  "status.archived": "Zarchiwizowana",
39
55
  "filters.label": "Filtry",
40
- "filters.includeArchived": "Pokaż zarchiwizowane pozycje",
56
+ "filters.kind": "Rodzaj",
57
+ "filters.allKinds": "Wszystkie rodzaje",
58
+ "filters.status": "Status",
59
+ "filters.allStatuses": "Wszystkie statusy",
41
60
  "lifecycle.title": "Cykl życia",
42
61
  "lifecycle.description": "Zarchiwizuj pozycję przed jej trwałym usunięciem.",
43
62
  "lifecycle.history": "Zobacz historię",
44
63
  "lifecycle.archive": "Archiwizuj pozycję",
45
64
  "lifecycle.restore": "Przywróć pozycję",
46
65
  "lifecycle.delete": "Usuń pozycję",
66
+ "notice.archivedMany": "Zarchiwizowano {updated}, brak {missing}, odrzucono {refused}.",
67
+ "notice.restoredMany": "Przywrócono {updated}, brak {missing}, odrzucono {refused}.",
47
68
  "page.action.new": "Nowa pozycja",
48
69
  "page.action.refresh": "Odśwież",
49
70
  "page.description": "Produkty i usługi ze stałym SKU w obrębie przestrzeni roboczej.",
50
71
  "page.eyebrow": "Dane podstawowe sprzedaży",
51
72
  "page.title": "Katalog",
73
+ "pagination.label": "Strony katalogu",
74
+ "pagination.previous": "Poprzednia",
75
+ "pagination.next": "Następna",
76
+ "pagination.size": "Pozycji na stronie",
77
+ "pagination.summary": "Strona {page} · od pozycji {from}",
52
78
  "price.fallback": "{amount} {currency} w groszach",
79
+ "selection.label": "Zaznacz wszystkie pozycje na ekranie",
80
+ "selection.rowLabel": "Zaznacz {name}",
81
+ "selection.clear": "Wyczyść zaznaczenie",
82
+ "selection.summary": "Zaznaczono: {count}",
53
83
  "table.caption": "Pozycje katalogu",
54
- "table.column.item": "Pozycja",
84
+ "table.column.name": "Nazwa",
85
+ "table.column.sku": "SKU",
55
86
  "table.column.kind": "Rodzaj",
56
87
  "table.column.price": "Cena",
57
88
  "table.column.status": "Status",
89
+ "table.column.updated": "Zmieniono",
58
90
  "table.column.actions": "Akcje",
59
- "table.count.one": "1 pozycja",
60
- "table.count.other": "pozycje: {count}",
91
+ "table.count.page": "Na tej stronie: {count}",
61
92
  "table.empty.hint": "Dodaj pierwszy produkt lub usługę.",
62
93
  "table.empty.title": "Nie ma jeszcze pozycji katalogu",
63
94
  "table.emptyFiltered.hint": "Żadna pozycja w tej przestrzeni roboczej nie pasuje do filtra.",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "id": "catalog.core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "repository": "Flowdular/official-modules",
5
- "sourceCommit": "b8281b38498b1e3164e6619ec9e8fe922ad7dc10",
6
- "artifactSha256": "6c98785c10fac14cb1a913c6462cb42e18fe824a7d0dd7620746605fd351441d",
5
+ "sourceCommit": "edad0ec198877c0359f11512b7c0055f87d5f8b4",
6
+ "artifactSha256": "3e1c8530eed822c35e12f24545492bf7614bbd47e3a9255a869423d7e1662ef4",
7
7
  "files": {
8
8
  "LICENSE": "155d722071bad9d0d832482e06fd5a47f7034389cb63aabcb39f4282aa3499fc",
9
9
  "migrations/0001_catalog_core.down.sql": "0962a1edf0bde959cd4facccf77bd5b634d6cc25f125a0c79a61770c8871093f",
@@ -14,43 +14,51 @@
14
14
  "migrations/0003_catalog_history_service_actors.up.sql": "7caae787ca2fc1e331ccbfda4df5982beec76136e8dc486aab5532ccd338a722",
15
15
  "migrations/0004_catalog_idempotency_ledger.down.sql": "8ba72d8edc4d25ecf6d041888cf321690c25afdaf3723812ddd7d391d75370aa",
16
16
  "migrations/0004_catalog_idempotency_ledger.up.sql": "41e86ba2051159c6cc9672bfae17c1d02051da975f1c43c566a2b66592673cb0",
17
+ "migrations/0005_catalog_list_indexes.down.sql": "3dd547d738e75513abef502afc4bf9b51851ec9405b6b719c6134b897c5b1776",
18
+ "migrations/0005_catalog_list_indexes.up.sql": "dc3ece30b93fba0c91be58f772d1ae421cfbba49439e8c72a06aead6720f5db5",
17
19
  "migrations/README.md": "2a15fd001713c2552a3c82b186246aa5fec136f43143cc2d9aa15bd491a09d41",
18
- "module.json": "1b512c9b97bdec2155103f521a6545e558e50491f90c74a93d33cc74511d2125",
19
- "package.json": "67d50dc95723d21bb44bbab54ae14fe89641c52b9f62ba065908f67ebcf0ed7a",
20
- "spec/module.yaml": "ec931c78f351c7fc53f43fdd5ec9cd1a46db485f642648329bf1612883a3c958",
20
+ "module.json": "4a4e5b5414f97a6b023c0ed64d0df313aa3ab4e8771f8e33137e589818ad20fd",
21
+ "package.json": "6dd4972624c2a141b038c024137a29b0e5223a28f9021653f2aa6517fb464b56",
22
+ "spec/module.yaml": "c30518bd28e5d184ef122f194f5c1308ff5c522df6638085ccc8121a78d40971",
21
23
  "src/acl/permissions.ts": "3375521a5a229736ffac5a948140ec347dc7a7e4fba80e778c0baaf9e0622590",
22
- "src/agent/tools.ts": "3204d20886d2b864482adc1ed3303c653aebd207e16c4fce0c67a4979ea8db07",
23
- "src/api/endpoints.ts": "34f1036e759353550be2acf67c6fa254afc790942d09489db2e76a082df5c3a3",
24
+ "src/agent/tools.ts": "3403db7954906dc2c79227ba0420b5cb4bfb57063b7cfc285e6e556f16be9266",
25
+ "src/api/endpoints.ts": "205ef36d058f246f99b77336e7246a23aaca611c696a1f6f09758f66e8a93a29",
26
+ "src/api/list-cursor.ts": "dcb763edc0128a3aac95e9ca3c5158611a7f2c6a4c71484438c89b7f4d79bb0d",
24
27
  "src/client/CatalogHistoryDrawer.tsrx": "6cdba5249be81e70da703cb34c1a2210943ce5e5fb596ee77cf7952e2acf3ee3",
25
28
  "src/client/CatalogItemForm.tsrx": "7dafa4149975d9e7d5784c8a5391b5b64c2405991aaead713d94b61c41168c2c",
26
- "src/client/CatalogView.tsrx": "7c507931f33afea8414b002e3ad815740c8db75016c53d8a81effc6427bd958d",
27
- "src/client/api.ts": "5f404f6b25777bc8fe2eab5a35424593abc8e879fb83fa2ae4d637fc23fdce4b",
28
- "src/client/contribution.tsrx": "e95f755882005ee090c99a119ce2dda2ec70420daed4ce06021193905858c62f",
29
+ "src/client/CatalogView.tsrx": "bc1e543690e07d52b51626832f36afe3fde87bf2dc45642324507df8384d7303",
30
+ "src/client/api.ts": "eaa55af81d26aa0190d19b3102c363c1ee25b2bdc05436446ac95dcde62aea75",
31
+ "src/client/contribution.tsrx": "6cbf79e6188bdf24b9cec4a06ae621e03efd5d0c76ad72b28828eee9155ab893",
29
32
  "src/client/index.ts": "03eb0664cdafccb66cb25f2e1b07e6959c2dfa9bdbfffc3e7edf9b535ba4949a",
30
33
  "src/client/navigation-copy.ts": "9a8757e64f4001a93e2bc2861c686a19f791f9d67a396d9e658023b1ec4cea8f",
31
- "src/client/state.ts": "bc054bc269026b6abf60d99a2fc5dd94eb8a488fc1ce4c81a1ae1065d9db576b",
32
- "src/domain/types.ts": "4b26be80954d792b09a3a34d29d845b9f6119640be505daba325fec820e06ef6",
34
+ "src/client/state.ts": "61601f6037c4bb8a709b69b10c1a37e59b45b0c941240c436f7861c2b034dcc1",
35
+ "src/domain/lists.ts": "83749967f99fa0c4f44abc0c614108b844321e50ce6408553215e6233ad6e350",
36
+ "src/domain/types.ts": "374efeb24c4f7d41e3c660336e721fa362418579837d44a46724076d03287a0b",
33
37
  "src/domain/variables.ts": "131130e57838235b1f3fcb799da44baeae16522bb346d522827e022305c110b5",
34
38
  "src/index.ts": "5b27943018db9a18651d62add7d6f8397d7c492a204381fae74547b99b91990d",
35
- "src/platform.ts": "b5d809b071af812877f3fb09cce3ced4cdaed8f62611c3eb6d5fcaf6cd7333cb",
39
+ "src/platform.ts": "f048082a33c1c688e4952750c762c8b65a4626ff52cc9c4eaca2156014bac631",
36
40
  "src/server/index.ts": "90eae107d863418124e5d10c4d1fe8a119bd14da605b36de708c18d8500c4b5e",
37
41
  "src/server/runtime.ts": "5f4389a6adf8265f6737c489baf686f63cc3550662103d28b2da9800dd9f5b63",
38
- "src/services/catalog-service.ts": "51f1dcd7c0335f7b417fb45ba700c0098e7193246ee91659c27cab8608282693",
42
+ "src/services/catalog-service.ts": "66e61dfbd9797d31e6814c18efcd21cfcb7b220f7df22677868f486f3fa0e9d1",
39
43
  "src/services/data-classes.ts": "ea09b1a7def59fee43d57ca7dab6fdb4d646b51df2cacb60af96676c43602b33",
40
- "src/services/database-repository.ts": "e6ff61ba197899d5618a02ce743c90d4bf09ae64dcfd1bd3a9985b18a2d23e80",
44
+ "src/services/database-repository.ts": "1b72ce91a2f85f6572825766781a806d94fe0d2abb91872ee9acae3b1e70db2e",
41
45
  "src/services/index.ts": "a08aefe5101814538666f861536287fc382e6c655d62ba9b9ef4bbfbc6ede4af",
42
- "src/services/migration.ts": "89c8e333482c80bc819deba0cf4f4e37e94137933af0f6503f4f9ebcb4f5f371",
43
- "src/services/repository.ts": "98d27a40ff02137ec0c6d8799d9a853d40a76eab2564dd500806c4e84f6dc3a3",
46
+ "src/services/item-export.ts": "b2a4613b43a98d0d41699481bb5e85a770f9a60cb2241579b065effb6936c80b",
47
+ "src/services/migration.ts": "57c9a857d4074973e14b5e3a1c4548a92ad0faca371419e7daf2334f3ef701ab",
48
+ "src/services/repository.ts": "9f560033875d05654148b4a11897465ee4eefa0d7fa701439e8f562da9fba1db",
44
49
  "src/services/target-idempotency.ts": "7111b56a30b691eaaaa64f5691895ac94b6e0735b0079794835bbbeba5102cb0",
45
- "tests/agent-tools.test.ts": "3cdda30b5454d58c6e8791a98e3daf6fe58b9852cd5c437b3614c3f6123baa91",
50
+ "tests/agent-tools.test.ts": "7762c14bc353ee3085df46a91cf370eb5c25b2edb7096f0a4b0e033f220ea726",
51
+ "tests/client-state.test.ts": "ea146c793b14d0f9bc5e36d92e92894ced6531b8fcbd11c8be1486b62313b8c3",
46
52
  "tests/data-classes.test.ts": "bea08e4d1d7380ccdebc211374b74a6034714b192fd98bfb90a2218067b35658",
47
- "tests/endpoints.test.ts": "24d05c7dd52820f90c777a1b814529f5188a9a5a9daeaacda07314f17b7483cc",
48
- "tests/idempotency.test.ts": "f2e0896635bcd95271b66181daa2c084411913cdf864ae8b4814f2beeb8c2943",
49
- "tests/migrations.test.ts": "47d3d741eddde8677be287730d2f96af556b4b7bf0d8378f9a12a7309af17d78",
50
- "tests/module.test.ts": "01bedfa1540470cc87c808c75120f6ede7c3b8e3d81e340d2076af0a66f1c7ab",
51
- "tests/support/database.ts": "f108cf6390da7669f8935301fac125a7e5653f06ba75327f4bdc28fdd409ba73",
52
- "translations/en.json": "406a40b77e3778e83cd4d30e7b3aee8f89c75baf7bc52f1befc6d16f3b877ab3",
53
- "translations/pl.json": "db6325b4980d978e1a3098758b5259a68e32469baa09d194b9d6746462776d27",
53
+ "tests/endpoints.test.ts": "a2d700f4c3eb6fbbef5c57ed5591017a27c98e17fe84fba8426fcd3a2820b210",
54
+ "tests/export.test.ts": "de83066272cee5f7f5736d636d7b9b97dab09778ba661816e401bf926b826402",
55
+ "tests/idempotency.test.ts": "faea4a2cfa04e81b7155399cba807af4c1ba4448cdfeab733d1afe801d27734f",
56
+ "tests/list.test.ts": "97605b50020841cbdac62f9d44b65f0f7dce9c4c4972af6409afeb6b9b66e59a",
57
+ "tests/migrations.test.ts": "fb796aecedb997f6395c2c15e45bf4abfae4677953fa8cb420c94e9cfba8222f",
58
+ "tests/module.test.ts": "af961a5bcddc41d304d14841f8810976f43f46b46ab7814142fdb921a773e92a",
59
+ "tests/support/database.ts": "4ff6d093ca2e80baab3dea476018661473d6474db63d67655f1edc667b8472ac",
60
+ "translations/en.json": "a86d70e02ccf46a2a132d5bee0de7c528c0eb56a85c269a78e0c2e7e0de24e37",
61
+ "translations/pl.json": "8157d83467e9dd8d9700e2fb188f1a730073f805d2a8b3f0ba98f02f2ca9a264",
54
62
  "tsconfig.json": "140bb4775df421146563d13d2f78ff74cb3c2decab6737db32938339248a92ba",
55
63
  "vitest.config.ts": "d13725b13c3712ec91c1646e9226fb172eec2599f72f4e6608bece9912e2bd81"
56
64
  }
@@ -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.8.0
12
+ pnpm flowdular module install expenses.core@0.8.0 --apply
13
13
  pnpm flowdular module enable expenses.core --apply
14
14
  pnpm flowdular module validate --locked
15
15
  ```
@@ -147,6 +147,19 @@ A running `pnpm dev` picks the change up live: the octane plugin reloads server
147
147
  routes when the generated composition changes and the client hot-reloads, so no
148
148
  rebuild is needed. `pnpm dev` and `pnpm build` run the sync automatically.
149
149
 
150
+ ### Per-workspace activation
151
+
152
+ Enabling composes a module into the application; whether a given workspace
153
+ uses it is an owner's decision made in Administration, Modules. Every composed
154
+ module is active until an owner deactivates it there, and the change applies to
155
+ that workspace alone: its endpoints answer 403 `MODULE_INACTIVE`, and the shell
156
+ hides the module's navigation, views, widgets and command search. `system.core`,
157
+ `auth.core`, `users.core` and `profile.core` cannot be deactivated, and neither
158
+ can a module another active module depends on; the refusal names the
159
+ dependents. Each change is audited. The state lives in `system.core` and is
160
+ published to other modules as the `system.modules.v1` capability; see
161
+ `.ai/platform-capabilities.md`.
162
+
150
163
  ## Versions, ranges and capabilities
151
164
 
152
165
  A module carries one version in three places: `module.json` `version`,