create-flowdular 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/agent-template/.agents/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +7 -2
- package/agent-template/.ai/platform-capabilities.md +4 -2
- package/agent-template/.ai/policies/capabilities.yaml +30 -3
- package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
- package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
- package/agent-template/.ai/references/catalog/module.json +11 -1
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +25 -4
- package/agent-template/.ai/references/catalog/src/agent/tools.ts +19 -10
- package/agent-template/.ai/references/catalog/src/api/endpoints.ts +150 -10
- package/agent-template/.ai/references/catalog/src/api/list-cursor.ts +83 -0
- package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
- package/agent-template/.ai/references/catalog/src/client/api.ts +124 -36
- package/agent-template/.ai/references/catalog/src/client/contribution.tsrx +5 -0
- package/agent-template/.ai/references/catalog/src/client/state.ts +169 -3
- package/agent-template/.ai/references/catalog/src/domain/lists.ts +7 -0
- package/agent-template/.ai/references/catalog/src/domain/types.ts +20 -0
- package/agent-template/.ai/references/catalog/src/platform.ts +20 -0
- package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +143 -8
- package/agent-template/.ai/references/catalog/src/services/database-repository.ts +104 -17
- package/agent-template/.ai/references/catalog/src/services/item-export.ts +81 -0
- package/agent-template/.ai/references/catalog/src/services/migration.ts +27 -1
- package/agent-template/.ai/references/catalog/src/services/repository.ts +31 -2
- package/agent-template/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
- package/agent-template/.ai/references/catalog/tests/client-state.test.ts +124 -0
- package/agent-template/.ai/references/catalog/tests/endpoints.test.ts +269 -0
- package/agent-template/.ai/references/catalog/tests/export.test.ts +134 -0
- package/agent-template/.ai/references/catalog/tests/idempotency.test.ts +15 -14
- package/agent-template/.ai/references/catalog/tests/list.test.ts +217 -0
- package/agent-template/.ai/references/catalog/tests/migrations.test.ts +58 -2
- package/agent-template/.ai/references/catalog/tests/module.test.ts +2 -1
- package/agent-template/.ai/references/catalog/tests/support/database.ts +14 -0
- package/agent-template/.ai/references/catalog/translations/en.json +35 -4
- package/agent-template/.ai/references/catalog/translations/pl.json +35 -4
- package/agent-template/.ai/references/catalog.provenance.json +34 -26
- package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +7 -2
- package/agent-template/.claude/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +7 -2
- package/agent-template/docs/cli.md +5 -0
- package/agent-template/docs/module-distribution.md +2 -2
- package/agent-template/docs/modules.md +13 -0
- package/agent-template/docs/operations.md +101 -20
- package/agent-template/platform/scripts/build.mjs +1 -0
- package/assets/flowdular-banner.webp +0 -0
- package/dist/bin.js +1 -0
- package/package.json +1 -1
- package/template/default/.env.example +2 -0
- package/template/default/modules/example/package.json +1 -1
- package/template/default/package.json +1 -1
- package/template/default/platform/package.json +1 -1
- package/template/default/platform/src/generated/modules.server.ts +7 -4
- package/assets/flowdular-banner.png +0 -0
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"repository": "Flowdular/official-modules",
|
|
5
|
-
"sourceCommit": "
|
|
6
|
-
"artifactSha256": "
|
|
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": "
|
|
19
|
-
"package.json": "
|
|
20
|
-
"spec/module.yaml": "
|
|
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": "
|
|
23
|
-
"src/api/endpoints.ts": "
|
|
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": "
|
|
27
|
-
"src/client/api.ts": "
|
|
28
|
-
"src/client/contribution.tsrx": "
|
|
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": "
|
|
32
|
-
"src/domain/
|
|
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": "
|
|
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": "
|
|
42
|
+
"src/services/catalog-service.ts": "66e61dfbd9797d31e6814c18efcd21cfcb7b220f7df22677868f486f3fa0e9d1",
|
|
39
43
|
"src/services/data-classes.ts": "ea09b1a7def59fee43d57ca7dab6fdb4d646b51df2cacb60af96676c43602b33",
|
|
40
|
-
"src/services/database-repository.ts": "
|
|
44
|
+
"src/services/database-repository.ts": "1b72ce91a2f85f6572825766781a806d94fe0d2abb91872ee9acae3b1e70db2e",
|
|
41
45
|
"src/services/index.ts": "a08aefe5101814538666f861536287fc382e6c655d62ba9b9ef4bbfbc6ede4af",
|
|
42
|
-
"src/services/
|
|
43
|
-
"src/services/
|
|
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": "
|
|
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": "
|
|
48
|
-
"tests/
|
|
49
|
-
"tests/
|
|
50
|
-
"tests/
|
|
51
|
-
"tests/
|
|
52
|
-
"
|
|
53
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
12
|
-
pnpm flowdular module install expenses.core@0.
|
|
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`,
|