@flowdular/sdk 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 (174) hide show
  1. package/.ai/platform-capabilities.md +4 -2
  2. package/.ai/policies/capabilities.yaml +30 -3
  3. package/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
  4. package/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
  5. package/.ai/references/catalog/module.json +11 -1
  6. package/.ai/references/catalog/package.json +2 -2
  7. package/.ai/references/catalog/spec/module.yaml +25 -4
  8. package/.ai/references/catalog/src/agent/tools.ts +19 -10
  9. package/.ai/references/catalog/src/api/endpoints.ts +150 -10
  10. package/.ai/references/catalog/src/api/list-cursor.ts +83 -0
  11. package/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
  12. package/.ai/references/catalog/src/client/api.ts +124 -36
  13. package/.ai/references/catalog/src/client/contribution.tsrx +5 -0
  14. package/.ai/references/catalog/src/client/state.ts +169 -3
  15. package/.ai/references/catalog/src/domain/lists.ts +7 -0
  16. package/.ai/references/catalog/src/domain/types.ts +20 -0
  17. package/.ai/references/catalog/src/platform.ts +20 -0
  18. package/.ai/references/catalog/src/services/catalog-service.ts +143 -8
  19. package/.ai/references/catalog/src/services/database-repository.ts +104 -17
  20. package/.ai/references/catalog/src/services/item-export.ts +81 -0
  21. package/.ai/references/catalog/src/services/migration.ts +27 -1
  22. package/.ai/references/catalog/src/services/repository.ts +31 -2
  23. package/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
  24. package/.ai/references/catalog/tests/client-state.test.ts +124 -0
  25. package/.ai/references/catalog/tests/endpoints.test.ts +269 -0
  26. package/.ai/references/catalog/tests/export.test.ts +134 -0
  27. package/.ai/references/catalog/tests/idempotency.test.ts +15 -14
  28. package/.ai/references/catalog/tests/list.test.ts +217 -0
  29. package/.ai/references/catalog/tests/migrations.test.ts +58 -2
  30. package/.ai/references/catalog/tests/module.test.ts +2 -1
  31. package/.ai/references/catalog/tests/support/database.ts +14 -0
  32. package/.ai/references/catalog/translations/en.json +35 -4
  33. package/.ai/references/catalog/translations/pl.json +35 -4
  34. package/.ai/references/catalog.provenance.json +34 -26
  35. package/.ai/skills/cli-extension/SKILL.md +1 -1
  36. package/.ai/skills/deploy-operate/SKILL.md +7 -2
  37. package/README.md +1 -1
  38. package/assets/flowdular-banner.webp +0 -0
  39. package/modules/access/module.json +1 -1
  40. package/modules/access/spec/module.yaml +1 -1
  41. package/modules/agents/module.json +1 -1
  42. package/modules/agents/spec/module.yaml +1 -1
  43. package/modules/approvals/migrations/0005_approvals_grant_audit.down.sql +4 -0
  44. package/modules/approvals/migrations/0005_approvals_grant_audit.up.sql +20 -0
  45. package/modules/approvals/module.json +2 -2
  46. package/modules/approvals/package.json +1 -1
  47. package/modules/approvals/spec/module.yaml +12 -3
  48. package/modules/approvals/src/domain/capability.ts +12 -0
  49. package/modules/approvals/src/domain/grant.ts +69 -0
  50. package/modules/approvals/src/domain/types.ts +14 -0
  51. package/modules/approvals/src/index.ts +9 -0
  52. package/modules/approvals/src/platform.ts +8 -0
  53. package/modules/approvals/src/server/runtime.ts +4 -0
  54. package/modules/approvals/src/services/approvals-service.ts +80 -0
  55. package/modules/approvals/src/services/database-repository.ts +65 -6
  56. package/modules/approvals/src/services/migration.ts +34 -0
  57. package/modules/approvals/src/services/repository.ts +8 -0
  58. package/modules/audit/module.json +1 -1
  59. package/modules/audit/spec/module.yaml +1 -1
  60. package/modules/auth/module.json +2 -2
  61. package/modules/auth/package.json +1 -1
  62. package/modules/auth/spec/module.yaml +2 -2
  63. package/modules/auth/src/middleware/authentication.ts +5 -1
  64. package/modules/auth/src/services/auth-service.ts +26 -0
  65. package/modules/auth/src/services/database-repository.ts +9 -0
  66. package/modules/auth/src/services/repository.ts +2 -0
  67. package/modules/automations/module.json +1 -1
  68. package/modules/automations/spec/module.yaml +1 -1
  69. package/modules/connectors/migrations/0003_connectors_rotation_inventory.down.sql +2 -0
  70. package/modules/connectors/migrations/0003_connectors_rotation_inventory.up.sql +19 -0
  71. package/modules/connectors/module.json +8 -4
  72. package/modules/connectors/package.json +2 -1
  73. package/modules/connectors/spec/module.yaml +3 -2
  74. package/modules/connectors/src/cli/commands.json +17 -0
  75. package/modules/connectors/src/cli/index.ts +126 -0
  76. package/modules/connectors/src/services/credential-rotation.ts +221 -0
  77. package/modules/connectors/src/services/credential-vault.ts +6 -0
  78. package/modules/connectors/src/services/migration.ts +36 -0
  79. package/modules/directory/module.json +1 -1
  80. package/modules/directory/spec/module.yaml +1 -1
  81. package/modules/documents/migrations/0003_documents_rotation_inventory.down.sql +2 -0
  82. package/modules/documents/migrations/0003_documents_rotation_inventory.up.sql +18 -0
  83. package/modules/documents/module.json +8 -4
  84. package/modules/documents/package.json +2 -1
  85. package/modules/documents/spec/module.yaml +3 -2
  86. package/modules/documents/src/cli/commands.json +17 -0
  87. package/modules/documents/src/cli/index.ts +145 -0
  88. package/modules/documents/src/services/database-repository.ts +15 -4
  89. package/modules/documents/src/services/documents-service.ts +13 -9
  90. package/modules/documents/src/services/migration.ts +35 -0
  91. package/modules/documents/src/services/repository.ts +12 -2
  92. package/modules/documents/src/services/storage-rotation.ts +157 -0
  93. package/modules/exports/migrations/0003_exports_rotation_inventory.down.sql +1 -0
  94. package/modules/exports/migrations/0003_exports_rotation_inventory.up.sql +9 -0
  95. package/modules/exports/module.json +8 -4
  96. package/modules/exports/package.json +2 -1
  97. package/modules/exports/spec/module.yaml +3 -2
  98. package/modules/exports/src/cli/commands.json +17 -0
  99. package/modules/exports/src/cli/index.ts +145 -0
  100. package/modules/exports/src/server/index.ts +0 -1
  101. package/modules/exports/src/services/data-classes.ts +16 -13
  102. package/modules/exports/src/services/database-repository.ts +30 -32
  103. package/modules/exports/src/services/migration.ts +27 -0
  104. package/modules/exports/src/services/repository.ts +9 -10
  105. package/modules/exports/src/services/storage-rotation.ts +138 -0
  106. package/modules/import/module.json +1 -1
  107. package/modules/import/spec/module.yaml +1 -1
  108. package/modules/metering/module.json +1 -1
  109. package/modules/metering/spec/module.yaml +1 -1
  110. package/modules/notifications/module.json +1 -1
  111. package/modules/notifications/spec/module.yaml +1 -1
  112. package/modules/reports/module.json +1 -1
  113. package/modules/reports/spec/module.yaml +1 -1
  114. package/modules/sandbox/module.json +1 -1
  115. package/modules/sandbox/spec/module.yaml +1 -1
  116. package/modules/search/module.json +1 -1
  117. package/modules/search/spec/module.yaml +1 -1
  118. package/modules/system/migrations/0001_system_module_activations.down.sql +2 -0
  119. package/modules/system/migrations/0001_system_module_activations.up.sql +17 -0
  120. package/modules/system/module.json +3 -2
  121. package/modules/system/package.json +2 -2
  122. package/modules/system/spec/module.yaml +98 -1
  123. package/modules/system/src/client/ModulesView.tsrx +101 -8
  124. package/modules/system/src/client/api.ts +52 -11
  125. package/modules/system/src/client/module-columns.tsrx +32 -6
  126. package/modules/system/src/client/state.ts +26 -0
  127. package/modules/system/src/domain/modules.ts +93 -0
  128. package/modules/system/src/index.ts +13 -0
  129. package/modules/system/src/platform.ts +41 -1
  130. package/modules/system/src/server/capability.ts +6 -0
  131. package/modules/system/src/server/endpoints.ts +125 -5
  132. package/modules/system/src/server/index.ts +26 -0
  133. package/modules/system/src/server/module-catalog.ts +19 -0
  134. package/modules/system/src/server/runtime.ts +102 -0
  135. package/modules/system/src/services/database-repository.ts +91 -0
  136. package/modules/system/src/services/migration.ts +39 -0
  137. package/modules/system/src/services/module-activation-service.ts +245 -0
  138. package/modules/system/src/services/repository.ts +17 -0
  139. package/modules/system/translations/en.json +17 -1
  140. package/modules/system/translations/pl.json +17 -1
  141. package/modules/users/module.json +2 -2
  142. package/modules/users/package.json +1 -1
  143. package/modules/users/spec/module.yaml +6 -9
  144. package/modules/users/src/client/UsersView.tsrx +27 -13
  145. package/modules/users/src/client/member-columns.tsrx +13 -4
  146. package/modules/users/src/client/state.ts +16 -0
  147. package/modules/users/src/services/users-service.ts +3 -0
  148. package/modules/users/translations/en.json +2 -1
  149. package/modules/users/translations/pl.json +2 -1
  150. package/modules/workflows/module.json +2 -2
  151. package/modules/workflows/package.json +1 -1
  152. package/modules/workflows/spec/module.yaml +2 -2
  153. package/modules/workflows/src/services/database-repository.ts +93 -53
  154. package/package.json +1 -1
  155. package/packages/client/package.json +1 -0
  156. package/packages/client/src/ApplicationShell.tsrx +46 -5
  157. package/packages/client/src/index.ts +5 -0
  158. package/packages/client/src/shell/modules.ts +49 -0
  159. package/packages/client/src/shell/types.ts +3 -0
  160. package/packages/contracts/src/index.ts +11 -1
  161. package/packages/database/src/backup.ts +1 -0
  162. package/packages/database/src/migrations.ts +7 -0
  163. package/packages/harness/src/runtime.ts +169 -10
  164. package/packages/harness/src/tool-adapters.ts +6 -13
  165. package/packages/kernel/src/approval-grant.ts +310 -0
  166. package/packages/kernel/src/index.ts +20 -0
  167. package/packages/server/src/endpoint.ts +17 -1
  168. package/packages/server/src/index.ts +10 -0
  169. package/packages/server/src/module-activation.ts +69 -0
  170. package/packages/storage/src/envelope.ts +70 -21
  171. package/packages/storage/src/index.ts +7 -1
  172. package/packages/storage/src/port.ts +12 -1
  173. package/packages/storage/src/reseal.ts +128 -0
  174. 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
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Flowdular: Build your business platform](https://cdn.jsdelivr.net/npm/@flowdular/sdk@latest/assets/flowdular-banner.png)](https://flowdular.com)
1
+ [![Flowdular: Build your business platform](https://cdn.jsdelivr.net/npm/@flowdular/sdk@latest/assets/flowdular-banner.webp)](https://flowdular.com)
2
2
 
3
3
  # Flowdular SDK
4
4
 
Binary file
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -12,7 +12,7 @@ capabilities:
12
12
  - translations
13
13
  dependencies:
14
14
  - id: system.core
15
- range: ^0.7.0
15
+ range: ^0.8.0
16
16
  - id: auth.core
17
17
  range: ^0.13.0
18
18
  requires:
@@ -20,7 +20,7 @@
20
20
  "dependencies": [
21
21
  {
22
22
  "id": "system.core",
23
- "range": "^0.7.0"
23
+ "range": "^0.8.0"
24
24
  },
25
25
  {
26
26
  "id": "auth.core",
@@ -14,7 +14,7 @@ capabilities:
14
14
  - cli
15
15
  dependencies:
16
16
  - id: system.core
17
- range: ^0.7.0
17
+ range: ^0.8.0
18
18
  - id: auth.core
19
19
  range: ^0.13.0
20
20
  - id: metering.core
@@ -0,0 +1,4 @@
1
+ -- The issuance ledger carries no data any other table depends on.
2
+ DROP POLICY IF EXISTS approvals_audit_tenant_policy ON approvals_audit;
3
+ DROP INDEX IF EXISTS approvals_audit_request_idx;
4
+ DROP TABLE IF EXISTS approvals_audit;
@@ -0,0 +1,20 @@
1
+ -- The grant an approved request yields is derived from the row and the key on
2
+ -- every read, so nothing about the token itself is stored. What is stored is
3
+ -- the fact of its issuance: one row per resolved request whose subject names
4
+ -- a capability, with the capability, the input digest, the key that signs and
5
+ -- the moment the grant stops verifying, written in the deciding transaction.
6
+ CREATE TABLE IF NOT EXISTS approvals_audit (
7
+ id TEXT PRIMARY KEY,
8
+ tenant_id TEXT NOT NULL,
9
+ request_id TEXT NOT NULL,
10
+ action TEXT NOT NULL,
11
+ metadata_json TEXT NOT NULL,
12
+ occurred_at BIGINT NOT NULL
13
+ );
14
+ CREATE INDEX IF NOT EXISTS approvals_audit_request_idx
15
+ ON approvals_audit (tenant_id, request_id, occurred_at, id);
16
+ ALTER TABLE approvals_audit ENABLE ROW LEVEL SECURITY;
17
+ ALTER TABLE approvals_audit FORCE ROW LEVEL SECURITY;
18
+ CREATE POLICY approvals_audit_tenant_policy ON approvals_audit
19
+ USING (tenant_id = current_setting('coreloom.tenant_id', true))
20
+ WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
@@ -3,7 +3,7 @@
3
3
  "schemaVersion": 1,
4
4
  "id": "approvals.core",
5
5
  "package": "@flowdular/module-approvals",
6
- "version": "0.1.13",
6
+ "version": "0.1.14",
7
7
  "platformApi": "^0.1.0",
8
8
  "profile": "full",
9
9
  "capabilities": ["api", "database", "client", "translations"],
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowdular/module-approvals",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  schemaVersion: 2
2
2
  id: approvals.core
3
- specVersion: 0.1.13
3
+ specVersion: 0.1.14
4
4
  status: approved
5
5
  name: Approvals Core
6
6
  description: Turns a policy decision that needs a person into an approval request with named requirements, a decision record that is append-only, notifications to the people who can decide, and a workflow node that waits for the decision, so every module delegates who may decide instead of writing its own approval state machine.
@@ -12,7 +12,7 @@ capabilities:
12
12
  - translations
13
13
  dependencies:
14
14
  - id: system.core
15
- range: ^0.7.0
15
+ range: ^0.8.0
16
16
  - id: auth.core
17
17
  range: ^0.13.0
18
18
  tenancy: required
@@ -33,6 +33,7 @@ invariants:
33
33
  - A requirement naming both a role key and a scope is satisfied only by members holding both; a requirement that resolves to no eligible decider, to fewer deciders than the decisions it needs, or to more than 200 deciders is refused at open with a stable code and no request is created.
34
34
  - A member who gains eligibility after a request opened may decide it (eligibility is re-read at decision time) but is not listed as a decider, because the decider list is the snapshot taken at open.
35
35
  - approvals.core declares one data class to the platform registry while it composes, and performs its operations itself on its own lease under its own tenant transaction. approvals.core.requests holds the request row, the eligibility snapshot taken when it opened and every decision recorded on it, keeps them 400 days by default, sweeps only resolved requests in bounded batches so a request still asking is never taken from the people who have to answer it however old it is, and exports one workspace by keyset in bounded pages carrying each request with its decisions and their comments. An erasure removes the resolved requests the subject opened with their eligibility and decision rows, and keeps but redacts every row naming the subject in somebody else's request, a decision as well as an eligibility row of a pending request, writing a fixed marker over a comment and a tombstone unique to the row over the account, because the ledger is append-only and one fixed account value would collide with the unique decider index, or with the eligibility key, as soon as two erased subjects had answered or been eligible for the same request; the remaining deciders keep their own rows and no account of the subject survives; the answer counts the removals and the redactions apart, removed for the rows that went and redacted for the rows that stayed without the subject in them, and both fill the batch the run gave it so a pass that only redacted still moves the run on; the count answers every request the subject opened in any state.
36
+ - A request whose subject reference encodes a capability id and an input digest (the prefix capability, the id and the sha256 hex digest, separated by colons, within the subjectRef bound) yields, once approved, a grant signed with HMAC-SHA256 under FD_APPROVAL_GRANT_KEY that names the workspace, the capability, the digest, the request and a one hour window from the approving decision; the grant is derived from the row and the current key on every read of the grant operation of approvals.requests.v1, handed only to the subject module that opened the request and never stored, a previous key kept in FD_APPROVAL_GRANT_KEY_PREVIOUS still verifies grants it signed, no grant is yielded for a rejected, cancelled or expired request or once the window closed, the approving transaction writes one grant.issued row to the issuance ledger with the capability, the digest, the signing key id and the expiry, the agent harness admits one tool call per grant within a run and refuses the repeat, and the CLI runner, which runs outside the platform and records no use, accepts the same grant for the same invocation until the window closes.
36
37
  - Whether a member may read a request is settled before its state is reported. A member who is neither the requester, nor named by the eligibility snapshot, nor eligible now, nor holding approvals.requests.manage is answered as if the request did not exist, on the read, on a decision and on a cancellation alike, so none of them reveals that the request exists or what became of it.
37
38
  permissions:
38
39
  - id: approvals.requests.read
@@ -42,7 +43,7 @@ permissions:
42
43
  - id: approvals.requests.manage
43
44
  description: Read every request of the active workspace and cancel a pending one.
44
45
  dataOwnership:
45
- - approvals.core owns approval requests, resolved eligibility snapshots and decision rows.
46
+ - approvals.core owns approval requests, resolved eligibility snapshots, decision rows and the grant issuance ledger.
46
47
  - The subject module owns the record the request gates and its own domain state; it changes that state only after reading the request's terminal state.
47
48
  - auth.core owns roles and memberships; eligibility is resolved through its public surface at creation and at decision time.
48
49
  entities:
@@ -267,6 +268,10 @@ decisions:
267
268
  question: Which data classes does approvals.core declare, how long are they kept, and what does an erasure leave behind?
268
269
  answer: One class, approvals.core.requests, covering requests, their eligibility rows and their decisions, kept 400 days by default. The sweep takes resolved requests only, with their eligibility and decision rows; a pending request is never swept however old it is. The export pages requests with their decisions and comments, keyset by id. An erasure removes the resolved requests the subject opened with their rows, and redacts rather than removes the rows naming the subject in somebody else's request, writing a fixed marker over a decision's comment and a per-row tombstone over the account on that decision and on the eligibility rows, in pending requests as well as resolved ones. The append-only ledger keeps its row count, a snapshot goes on reporting how many people could answer, the people who can still answer keep their own rows, and no account of the subject survives anywhere. The count answers how many requests the subject opened, in any state.
269
270
  decidedBy: default
271
+ - id: D-APPROVALS-GRANT
272
+ question: How does an approved request let the CLI runner or the harness run an external or destructive capability?
273
+ answer: The subject reference encodes the capability id and the digest of the exact invocation input; once approved the request yields a signed grant bound to the workspace, the capability, the digest and a one hour window, which the runner takes as the --grant flag and the harness as the run's grants. The token is read through the grant operation by the subject module alone, derived on read from the row and FD_APPROVAL_GRANT_KEY rather than stored, and the issuance is written to the ledger in the approving transaction; the harness spends a grant on one call, the runner cannot record use and is bounded by the window.
274
+ decidedBy: default
270
275
  acceptanceScenarios:
271
276
  - id: APPROVALS-OPEN
272
277
  given: A subject module needs a person to agree to an action on one of its records, with a role requirement and a count of 1.
@@ -312,6 +317,10 @@ acceptanceScenarios:
312
317
  given: A workspace holds more requests than one page of the inbox carries.
313
318
  when: A member pages through the inbox under a scope and a status filter.
314
319
  then: A page carries at most 200 requests (50 unless the request asks for more), ordered by createdAt in the requested direction with the id as the tie-break, and a full page answers an opaque cursor bound to the workspace, the member, the sort and the filters, so a cursor used under another workspace, sort or filter is refused rather than reinterpreted.
320
+ - id: APPROVALS-GRANT
321
+ given: A workspace with FD_APPROVAL_GRANT_KEY configured and a request opened with a subject reference that encodes a capability id and an input digest.
322
+ when: An eligible member approves it, another such request is rejected, a third cancelled and a fourth left to expire, and the key is later rotated with the old key kept as previous.
323
+ then: Reading the grant of the approved request through the capability as the subject module answers a grant that verifies for that workspace, capability and digest until one hour after the approving decision, the same token on every read under the same key and a token under the new key after the rotation that the rotated ring still verifies, the ledger carries one grant.issued row written with the approval naming the capability, the digest, the signing key id and the expiry, the rejected, cancelled and expired requests and an approved request whose subject names a record answer no grant and no ledger row, another module naming the request is answered no grant, and the retention sweep removes the ledger rows with the request.
315
324
  - id: APPROVALS-TENANT-BOUNDARY
316
325
  given: Two workspaces with requests and decisions.
317
326
  when: A runtime transaction bound to one workspace reads or writes any of them, or the expiry routing read runs.
@@ -50,6 +50,18 @@ export interface ApprovalsRequests {
50
50
  */
51
51
  open(input: OpenApprovalInput): Promise<ApprovalRequest>;
52
52
  get(tenantId: string, id: string): Promise<ApprovalRequest | null>;
53
+ /**
54
+ * The signed token an approved request yields while its window is open:
55
+ * what the CLI runner takes as `--grant` and the harness as `grants`. Only
56
+ * a request whose `subjectRef` encodes a capability and an input digest
57
+ * (`encodeCapabilitySubjectRef`) yields one, and only to the module that
58
+ * opened it: `subjectModule` has to match the request's. Null otherwise.
59
+ */
60
+ grant(
61
+ tenantId: string,
62
+ id: string,
63
+ subjectModule: string,
64
+ ): Promise<string | null>;
53
65
  /**
54
66
  * The first `limit` matches, newest first; two requests opened at the same
55
67
  * millisecond come in descending id order.