okengine 0.15.2 → 0.17.0
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/manifest.v1.schema.json +21 -2
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +7 -0
- package/site/content/docs/elements/flow.mdx +20 -12
- package/site/content/docs/elements/gate.mdx +108 -3
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +36 -32
- package/site/content/docs/elements/vault.mdx +26 -0
- package/site/content/docs/get-started/project-structure.mdx +4 -4
- package/site/content/docs/reference/cli.md +1 -1
- package/site/content/docs/reference/client.mdx +75 -17
- package/site/content/docs/reference/configuration.mdx +7 -4
- package/site/content/docs/reference/errors.mdx +24 -17
- package/site/content/docs/reference/fx.mdx +39 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +261 -0
- package/src/auth/api-keys.ts +276 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/config.ts +24 -5
- package/src/auth/index.ts +62 -1
- package/src/auth/plugin.ts +6 -1
- package/src/auth/schema.ts +8 -0
- package/src/auth/sessions.ts +18 -0
- package/src/auth/tables.ts +32 -0
- package/src/auth/tenant-config.ts +74 -0
- package/src/auth/tenant-tables.ts +11 -0
- package/src/auth/tenants.test.ts +63 -0
- package/src/auth/tenants.ts +360 -0
- package/src/cli/attach-host-console.test.ts +37 -0
- package/src/cli/attach-host-console.ts +6 -0
- package/src/cli/build.ts +2 -2
- package/src/client/budget.test.ts +1 -1
- package/src/client/create.ts +75 -5
- package/src/client/index.ts +13 -0
- package/src/client/live.test.ts +422 -0
- package/src/client/live.ts +389 -0
- package/src/client/notes-contract.test.ts +41 -0
- package/src/client/types.ts +85 -6
- package/src/client-react/index.ts +85 -1
- package/src/client-react/use-live.test.ts +129 -0
- package/src/compiler/effects-infer.ts +37 -2
- package/src/compiler/extract.test.ts +143 -11
- package/src/compiler/extract.ts +210 -30
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +2 -3
- package/src/compiler/response.ts +41 -11
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +45 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +113 -58
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +10 -30
- package/src/console/server/index.ts +1 -0
- package/src/console/server/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/state.ts +29 -55
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +43 -1
- package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
- package/src/console/ui-next/dist/index.html +11 -9
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +167 -0
- package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
- package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/journal-postgres.ts +12 -3
- package/src/drivers/pg-rls.ts +26 -2
- package/src/drivers/pg-vault-rls.ts +68 -0
- package/src/drivers/signal-engine.ts +69 -15
- package/src/drivers/signal-live-iter.ts +65 -0
- package/src/drivers/signal-nats.ts +2 -1
- package/src/drivers/signal-postgres.ts +95 -12
- package/src/drivers/signal-redis.ts +2 -1
- package/src/drivers/signal-retention.ts +64 -0
- package/src/drivers/signal-types.ts +35 -5
- package/src/elements/clock/declare.ts +64 -2
- package/src/elements/clock/reconcile.ts +98 -25
- package/src/elements/clock/runtime.ts +13 -2
- package/src/elements/clock.test.ts +28 -0
- package/src/elements/clock.ts +9 -1
- package/src/elements/gate/declare.ts +2 -0
- package/src/elements/gate/permissions.ts +12 -0
- package/src/elements/gate.ts +6 -1
- package/src/elements/signal/declare.ts +44 -10
- package/src/elements/signal/delivery-modes.test.ts +90 -4
- package/src/elements/signal/order-lifecycle.test.ts +20 -4
- package/src/elements/signal/runtime.ts +42 -0
- package/src/elements/signal.test.ts +21 -8
- package/src/elements/signal.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/rls-identity.test.ts +29 -0
- package/src/elements/store/rls-identity.ts +3 -0
- package/src/elements/store/runtime.ts +14 -0
- package/src/elements/store/schema-decl.ts +162 -44
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +54 -32
- package/src/elements/store.ts +5 -0
- package/src/elements/vault/builtin-adapter.ts +15 -2
- package/src/elements/vault/declare.ts +8 -0
- package/src/elements/vault/runtime.ts +7 -0
- package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
- package/src/elements/vault/storage.ts +9 -0
- package/src/elements/vault/test-helpers.ts +2 -1
- package/src/i18n/catalogs/ar.ts +19 -0
- package/src/i18n/catalogs/en.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +196 -57
- package/src/kernel/auth-resolve.ts +13 -5
- package/src/kernel/boot-bind/clock.ts +9 -3
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/clock-durable.ts +8 -0
- package/src/kernel/clock-per-tenant-name.ts +5 -0
- package/src/kernel/clock-reconcile.ts +8 -0
- package/src/kernel/errors-live-resume.ts +15 -0
- package/src/kernel/errors-tenant.ts +29 -0
- package/src/kernel/errors.registry.test.ts +31 -3
- package/src/kernel/errors.ts +43 -3
- package/src/kernel/flow.ts +26 -5
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +203 -0
- package/src/kernel/fx-auth-tenants.test.ts +87 -0
- package/src/kernel/fx-auth-tenants.ts +286 -0
- package/src/kernel/fx-live-stream.ts +149 -0
- package/src/kernel/fx-live.test.ts +157 -0
- package/src/kernel/fx-runtime.ts +15 -0
- package/src/kernel/fx-tenant-store.ts +213 -0
- package/src/kernel/fx.test.ts +108 -1
- package/src/kernel/fx.ts +206 -28
- package/src/kernel/hooks.ts +2 -2
- package/src/kernel/http-resource.ts +9 -18
- package/src/kernel/index.ts +2 -0
- package/src/kernel/journal.ts +12 -0
- package/src/kernel/live-http.test.ts +78 -0
- package/src/kernel/live-http.ts +114 -0
- package/src/kernel/live-resume.test.ts +125 -0
- package/src/kernel/on.ts +51 -0
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +41 -4
- package/src/kernel/resource-mount.test.ts +10 -27
- package/src/kernel/tenant-resolve.test.ts +101 -0
- package/src/kernel/tenant-resolve.ts +124 -0
- package/src/kernel/tenant-roles.test.ts +87 -0
- package/src/kernel/triggers.ts +59 -21
- package/src/manifest/diff.test.ts +11 -2
- package/src/manifest/diff.ts +53 -11
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +0 -1
- package/src/manifest/types.ts +58 -6
- package/src/release/build-lib.ts +14 -1
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +55 -1
- package/src/client/live-gap.test.ts +0 -35
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
- package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
|
@@ -41,6 +41,20 @@ const SEED_SPACES = [
|
|
|
41
41
|
] as const;
|
|
42
42
|
|
|
43
43
|
const SEED_MEMBERS = [
|
|
44
|
+
{
|
|
45
|
+
id: "mem_demo",
|
|
46
|
+
space_id: "space_eng",
|
|
47
|
+
name: "Demo User",
|
|
48
|
+
email: "demo@example.com",
|
|
49
|
+
role: "owner",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "mem_admin",
|
|
53
|
+
space_id: "space_eng",
|
|
54
|
+
name: "Avery Cole",
|
|
55
|
+
email: "admin@keel.dev",
|
|
56
|
+
role: "admin",
|
|
57
|
+
},
|
|
44
58
|
{
|
|
45
59
|
id: "mem_aria",
|
|
46
60
|
space_id: "space_eng",
|
|
@@ -48,6 +62,13 @@ const SEED_MEMBERS = [
|
|
|
48
62
|
email: "aria@keel.dev",
|
|
49
63
|
role: "project_manager",
|
|
50
64
|
},
|
|
65
|
+
{
|
|
66
|
+
id: "mem_lead",
|
|
67
|
+
space_id: "space_eng",
|
|
68
|
+
name: "Jules Hart",
|
|
69
|
+
email: "jules@keel.dev",
|
|
70
|
+
role: "lead",
|
|
71
|
+
},
|
|
51
72
|
{
|
|
52
73
|
id: "mem_ben",
|
|
53
74
|
space_id: "space_eng",
|
|
@@ -55,6 +76,13 @@ const SEED_MEMBERS = [
|
|
|
55
76
|
email: "ben@keel.dev",
|
|
56
77
|
role: "developer",
|
|
57
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: "mem_eli",
|
|
81
|
+
space_id: "space_eng",
|
|
82
|
+
name: "Eli Park",
|
|
83
|
+
email: "eli@keel.dev",
|
|
84
|
+
role: "contributor",
|
|
85
|
+
},
|
|
58
86
|
{
|
|
59
87
|
id: "mem_cai",
|
|
60
88
|
space_id: "space_des",
|
|
@@ -62,6 +90,20 @@ const SEED_MEMBERS = [
|
|
|
62
90
|
email: "cai@keel.dev",
|
|
63
91
|
role: "member",
|
|
64
92
|
},
|
|
93
|
+
{
|
|
94
|
+
id: "mem_member",
|
|
95
|
+
space_id: "space_des",
|
|
96
|
+
name: "Member",
|
|
97
|
+
email: "member@example.com",
|
|
98
|
+
role: "member",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "mem_commenter",
|
|
102
|
+
space_id: "space_gtm",
|
|
103
|
+
name: "Kit Rowe",
|
|
104
|
+
email: "kit@keel.dev",
|
|
105
|
+
role: "commenter",
|
|
106
|
+
},
|
|
65
107
|
{
|
|
66
108
|
id: "mem_dia",
|
|
67
109
|
space_id: "space_gtm",
|
|
@@ -69,13 +111,6 @@ const SEED_MEMBERS = [
|
|
|
69
111
|
email: "dia@keel.dev",
|
|
70
112
|
role: "guest",
|
|
71
113
|
},
|
|
72
|
-
{
|
|
73
|
-
id: "mem_eli",
|
|
74
|
-
space_id: "space_eng",
|
|
75
|
-
name: "Eli Park",
|
|
76
|
-
email: "eli@keel.dev",
|
|
77
|
-
role: "developer",
|
|
78
|
-
},
|
|
79
114
|
] as const;
|
|
80
115
|
|
|
81
116
|
const SEED_GOALS = [
|
|
@@ -640,14 +675,7 @@ function generateKeelVolume(): {
|
|
|
640
675
|
email: `${p.id}@keel.dev`,
|
|
641
676
|
role: "member",
|
|
642
677
|
}));
|
|
643
|
-
const emails = [
|
|
644
|
-
"aria@keel.dev",
|
|
645
|
-
"ben@keel.dev",
|
|
646
|
-
"cai@keel.dev",
|
|
647
|
-
"dia@keel.dev",
|
|
648
|
-
"eli@keel.dev",
|
|
649
|
-
...members.map((m) => m.email),
|
|
650
|
-
];
|
|
678
|
+
const emails = [...SEED_MEMBERS.map((m) => m.email), ...members.map((m) => m.email)];
|
|
651
679
|
|
|
652
680
|
const projects = Array.from({ length: GENERATED.projects }, (_, i) => ({
|
|
653
681
|
id: `proj_g_${String(i + 1).padStart(2, "0")}`,
|
package/src/drivers/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ interface JournalDbRow {
|
|
|
36
36
|
lease_expires_at: number | null;
|
|
37
37
|
created_at: number;
|
|
38
38
|
updated_at: number;
|
|
39
|
+
tenant: string | null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/** Minimal SQL + transaction surface for the postgres journal store. */
|
|
@@ -151,6 +152,9 @@ function rowToRun(row: Record<string, unknown>): JournalRun {
|
|
|
151
152
|
if (r.locked_by !== null && r.locked_by !== undefined) run.lockedBy = String(r.locked_by);
|
|
152
153
|
const leaseExpiresAt = numOrNull(r.lease_expires_at);
|
|
153
154
|
if (leaseExpiresAt !== null) run.leaseExpiresAt = leaseExpiresAt;
|
|
155
|
+
if (r.tenant !== null && r.tenant !== undefined && String(r.tenant).length > 0) {
|
|
156
|
+
run.tenant = String(r.tenant);
|
|
157
|
+
}
|
|
154
158
|
return run;
|
|
155
159
|
}
|
|
156
160
|
|
|
@@ -168,6 +172,7 @@ function runToParams(run: JournalRun): unknown[] {
|
|
|
168
172
|
run.leaseExpiresAt ?? null,
|
|
169
173
|
run.createdAt,
|
|
170
174
|
run.updatedAt,
|
|
175
|
+
run.tenant ?? null,
|
|
171
176
|
];
|
|
172
177
|
}
|
|
173
178
|
|
|
@@ -184,8 +189,10 @@ async function ensureSchema(sql: PostgresJournalSql): Promise<void> {
|
|
|
184
189
|
locked_by TEXT,
|
|
185
190
|
lease_expires_at BIGINT,
|
|
186
191
|
created_at BIGINT NOT NULL,
|
|
187
|
-
updated_at BIGINT NOT NULL
|
|
192
|
+
updated_at BIGINT NOT NULL,
|
|
193
|
+
tenant TEXT
|
|
188
194
|
)`);
|
|
195
|
+
await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS tenant TEXT`);
|
|
189
196
|
await sql.exec(
|
|
190
197
|
`CREATE INDEX IF NOT EXISTS oke_journal_runs_wake ON oke_journal_runs (status, wake_at)`,
|
|
191
198
|
);
|
|
@@ -308,7 +315,8 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
308
315
|
const text = sql.trim();
|
|
309
316
|
const state = view();
|
|
310
317
|
|
|
311
|
-
if (/^CREATE\s+(TABLE|INDEX)/i.test(text)
|
|
318
|
+
if (/^CREATE\s+(TABLE|INDEX)/i.test(text) || /^ALTER\s+TABLE/i.test(text))
|
|
319
|
+
return { changes: 0 };
|
|
312
320
|
|
|
313
321
|
const upsert =
|
|
314
322
|
/^INSERT\s+INTO\s+oke_journal_runs\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*ON\s+CONFLICT\s*\(\s*id\s*\)\s*DO\s+UPDATE\s+SET\s+.+$/i.exec(
|
|
@@ -337,6 +345,7 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
337
345
|
: Number(record.lease_expires_at),
|
|
338
346
|
created_at: Number(record.created_at ?? 0),
|
|
339
347
|
updated_at: Number(record.updated_at ?? 0),
|
|
348
|
+
tenant: (record.tenant as string | null) ?? null,
|
|
340
349
|
};
|
|
341
350
|
const idx = state.rows.findIndex((r) => r.id === next.id);
|
|
342
351
|
if (idx >= 0) state.rows[idx] = next;
|
|
@@ -423,7 +432,7 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
423
432
|
return api;
|
|
424
433
|
}
|
|
425
434
|
|
|
426
|
-
const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at`;
|
|
435
|
+
const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at, tenant = EXCLUDED.tenant`;
|
|
427
436
|
|
|
428
437
|
/** Postgres journal store with run-level lease coordination. */
|
|
429
438
|
export type PostgresJournalStore = JournalStore &
|
package/src/drivers/pg-rls.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Drizzle emit, and Gate identity helpers (`oke.gate` / `oke.user` / `oke.has_scope`).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { lazyRequire } from "../kernel/lazy-require.ts";
|
|
7
|
+
|
|
6
8
|
/** Policy command (`FOR`). */
|
|
7
9
|
export type SqlPolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
8
10
|
|
|
@@ -49,6 +51,11 @@ export type RlsIdentity = {
|
|
|
49
51
|
readonly gate: string;
|
|
50
52
|
readonly userId: string;
|
|
51
53
|
readonly scopes: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* When set, the prelude stamps `oke.tenant` (fourth GUC).
|
|
56
|
+
* Omit entirely when tenancy is off so the prelude stays three params.
|
|
57
|
+
*/
|
|
58
|
+
readonly tenantId?: string;
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
/**
|
|
@@ -98,6 +105,8 @@ LANGUAGE sql STABLE AS $$
|
|
|
98
105
|
ELSE jsonb_exists(current_setting('oke.scopes', true)::jsonb, p_scope)
|
|
99
106
|
END
|
|
100
107
|
$$`,
|
|
108
|
+
`CREATE OR REPLACE FUNCTION oke.tenant() RETURNS text
|
|
109
|
+
LANGUAGE sql STABLE AS $$ SELECT current_setting('oke.tenant', true) $$`,
|
|
101
110
|
`DO $oke_rls_role$
|
|
102
111
|
BEGIN
|
|
103
112
|
CREATE ROLE ${OKE_RLS_ROLE} NOSUPERUSER NOBYPASSRLS NOLOGIN;
|
|
@@ -122,6 +131,7 @@ $oke_app_search_path$`,
|
|
|
122
131
|
|
|
123
132
|
/**
|
|
124
133
|
* All {@link OKE_RLS_HELPER_STATEMENTS} as one script (PGlite `exec` / `oke db push`).
|
|
134
|
+
* Vault table RLS is attached by {@link installOkeRlsHelpers} after helpers exist.
|
|
125
135
|
*/
|
|
126
136
|
export const OKE_RLS_HELPER_SQL = `${OKE_RLS_HELPER_STATEMENTS.join(";\n")};`;
|
|
127
137
|
|
|
@@ -134,6 +144,10 @@ export async function installOkeRlsHelpers(exec: (sql: string) => Promise<unknow
|
|
|
134
144
|
for (const stmt of OKE_RLS_HELPER_STATEMENTS) {
|
|
135
145
|
await exec(stmt);
|
|
136
146
|
}
|
|
147
|
+
const { attach } = lazyRequire<{
|
|
148
|
+
attach: (exec: (sql: string) => Promise<unknown>) => Promise<void>;
|
|
149
|
+
}>(import.meta.dir, ["pg", "vault", "rls"].join("-"));
|
|
150
|
+
await attach(exec);
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
/**
|
|
@@ -449,12 +463,22 @@ export type RlsPreludeStatement = {
|
|
|
449
463
|
* @param identity - Gate principal
|
|
450
464
|
*/
|
|
451
465
|
export function buildRlsIdentityPreludeSql(identity: RlsIdentity): readonly RlsPreludeStatement[] {
|
|
466
|
+
const configs = [
|
|
467
|
+
"set_config('oke.gate', ?, true)",
|
|
468
|
+
"set_config('oke.user', ?, true)",
|
|
469
|
+
"set_config('oke.scopes', ?, true)",
|
|
470
|
+
];
|
|
471
|
+
const params: string[] = [identity.gate, identity.userId, rlsScopesJson(identity.scopes)];
|
|
472
|
+
if (identity.tenantId !== undefined) {
|
|
473
|
+
configs.push("set_config('oke.tenant', ?, true)");
|
|
474
|
+
params.push(identity.tenantId);
|
|
475
|
+
}
|
|
452
476
|
return [
|
|
453
477
|
{ sql: `SET LOCAL ROLE ${OKE_RLS_ROLE}` },
|
|
454
478
|
{ sql: "SET LOCAL row_security = on" },
|
|
455
479
|
{
|
|
456
|
-
sql: `SELECT
|
|
457
|
-
params
|
|
480
|
+
sql: `SELECT ${configs.join(", ")}`,
|
|
481
|
+
params,
|
|
458
482
|
},
|
|
459
483
|
];
|
|
460
484
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant RLS on `oke_vault_secrets` — kept off the Store-only `oke()` graph.
|
|
3
|
+
*
|
|
4
|
+
* Same `oke.tenant()` helper as domain tables. Loaded from helper install
|
|
5
|
+
* and Vault DDL via a computed require.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Same role as `OKE_RLS_ROLE` (`oke_app`) — duplicated so this module stays off `pg-rls`. */
|
|
9
|
+
const VAULT_RLS_ROLE = "oke_app";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Built-in Vault ciphertext table. Global rows (`tenant_id IS NULL`) stay
|
|
13
|
+
* visible; per-tenant rows are isolated. No-op until `oke_vault_secrets`
|
|
14
|
+
* and `oke.tenant()` exist.
|
|
15
|
+
*
|
|
16
|
+
* ENABLE without FORCE: the Vault adapter (table owner) still sees every
|
|
17
|
+
* row; Console / `oke_app` stamped SQL cannot list another tenant's
|
|
18
|
+
* ciphertext.
|
|
19
|
+
*/
|
|
20
|
+
export const OKE_VAULT_SECRETS_TENANT_RLS_SQL = `DO $oke_vault_rls$
|
|
21
|
+
BEGIN
|
|
22
|
+
IF to_regclass('public.oke_vault_secrets') IS NULL THEN
|
|
23
|
+
RETURN;
|
|
24
|
+
END IF;
|
|
25
|
+
IF NOT EXISTS (
|
|
26
|
+
SELECT 1 FROM pg_proc p
|
|
27
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
28
|
+
WHERE n.nspname = 'oke' AND p.proname = 'tenant' AND p.pronargs = 0
|
|
29
|
+
) THEN
|
|
30
|
+
RETURN;
|
|
31
|
+
END IF;
|
|
32
|
+
EXECUTE 'ALTER TABLE oke_vault_secrets ENABLE ROW LEVEL SECURITY';
|
|
33
|
+
BEGIN
|
|
34
|
+
EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON oke_vault_secrets TO ${VAULT_RLS_ROLE}';
|
|
35
|
+
EXCEPTION
|
|
36
|
+
WHEN undefined_object THEN NULL;
|
|
37
|
+
END;
|
|
38
|
+
IF NOT EXISTS (
|
|
39
|
+
SELECT 1 FROM pg_policies
|
|
40
|
+
WHERE schemaname = 'public'
|
|
41
|
+
AND tablename = 'oke_vault_secrets'
|
|
42
|
+
AND policyname = 'oke_vault_secrets_tenant'
|
|
43
|
+
) THEN
|
|
44
|
+
EXECUTE $p$
|
|
45
|
+
CREATE POLICY oke_vault_secrets_tenant ON oke_vault_secrets
|
|
46
|
+
AS PERMISSIVE FOR ALL TO public
|
|
47
|
+
USING (tenant_id = oke.tenant() OR tenant_id IS NULL)
|
|
48
|
+
WITH CHECK (tenant_id = oke.tenant() OR tenant_id IS NULL)
|
|
49
|
+
$p$;
|
|
50
|
+
END IF;
|
|
51
|
+
END
|
|
52
|
+
$oke_vault_rls$`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Attach tenant RLS to `oke_vault_secrets` when the table and `oke.tenant()`
|
|
56
|
+
* are both present. Safe to call from Vault DDL and from helper install
|
|
57
|
+
* (either boot order).
|
|
58
|
+
*
|
|
59
|
+
* @param exec - Statement runner
|
|
60
|
+
*/
|
|
61
|
+
export async function ensureVaultSecretsTenantRls(
|
|
62
|
+
exec: (sql: string) => Promise<unknown>,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
await exec(OKE_VAULT_SECRETS_TENANT_RLS_SQL);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Short name for the Store-only `oke()` helper-install path. */
|
|
68
|
+
export { ensureVaultSecretsTenantRls as attach };
|
|
@@ -12,10 +12,12 @@ import type { SignalDecl } from "../elements/signal/declare.ts";
|
|
|
12
12
|
import type { SignalDelivery } from "../manifest/types.ts";
|
|
13
13
|
import { DryRunWriteIsolationError, setDryRunMessageId, withDryRun } from "../kernel/dry-run.ts";
|
|
14
14
|
import { OkeError, OKE_ERRORS } from "../kernel/errors.ts";
|
|
15
|
+
import { LIVE_RESUME_GAP } from "../kernel/errors-live-resume.ts";
|
|
15
16
|
import {
|
|
16
17
|
SIGNAL_DEFAULT_LEASE_MS,
|
|
17
18
|
validateSignalEmitPayload,
|
|
18
19
|
type DeadLetter,
|
|
20
|
+
type LiveEvent,
|
|
19
21
|
type LiveHandler,
|
|
20
22
|
type SignalBus,
|
|
21
23
|
type SignalDiscardOptions,
|
|
@@ -30,7 +32,10 @@ import {
|
|
|
30
32
|
type SignalStats,
|
|
31
33
|
type SignalTransaction,
|
|
32
34
|
type SignalUnsubscribe,
|
|
35
|
+
type LiveSubscribeOptions,
|
|
33
36
|
} from "./signal-types.ts";
|
|
37
|
+
import { createLiveIterable } from "./signal-live-iter.ts";
|
|
38
|
+
import { liveIdsToPrune, skipAfterId } from "./signal-retention.ts";
|
|
34
39
|
|
|
35
40
|
/** Internal mutable message. */
|
|
36
41
|
interface MutMessage {
|
|
@@ -313,26 +318,68 @@ export async function createSignalEngine(
|
|
|
313
318
|
};
|
|
314
319
|
}
|
|
315
320
|
|
|
316
|
-
|
|
321
|
+
function live(signal: string, opts?: LiveSubscribeOptions): AsyncIterable<LiveEvent> {
|
|
317
322
|
const decl = requireDecl(signal);
|
|
318
323
|
if (decl.delivery !== "live") {
|
|
319
324
|
throw new Error(`signal "${signal}" is not delivery: "live"`);
|
|
320
325
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
const afterId = opts?.afterId;
|
|
327
|
+
return createLiveIterable((emit) => {
|
|
328
|
+
pruneLive(signal);
|
|
329
|
+
const history: LiveEvent[] = [];
|
|
330
|
+
for (const m of messages) {
|
|
331
|
+
if (m.signal === signal && m.delivery === "live") {
|
|
332
|
+
history.push({ id: m.id, payload: m.payload });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const skipped = skipAfterId(history, afterId);
|
|
336
|
+
if (afterId !== undefined && afterId.length > 0 && !skipped.found) {
|
|
337
|
+
throw new OkeError(LIVE_RESUME_GAP, { signal, afterId });
|
|
338
|
+
}
|
|
339
|
+
let set = liveHandlers.get(signal);
|
|
340
|
+
if (!set) {
|
|
341
|
+
set = new Set();
|
|
342
|
+
liveHandlers.set(signal, set);
|
|
331
343
|
}
|
|
344
|
+
const handler: LiveHandler = (event) => {
|
|
345
|
+
emit(event);
|
|
346
|
+
};
|
|
347
|
+
set.add(handler);
|
|
348
|
+
for (const event of skipped.rest) {
|
|
349
|
+
emit(event);
|
|
350
|
+
}
|
|
351
|
+
return () => {
|
|
352
|
+
set.delete(handler);
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function checkLiveResume(signal: string, afterId: string): Promise<void> {
|
|
358
|
+
requireDecl(signal);
|
|
359
|
+
pruneLive(signal);
|
|
360
|
+
const found = messages.some(
|
|
361
|
+
(m) => m.signal === signal && m.delivery === "live" && m.id === afterId,
|
|
362
|
+
);
|
|
363
|
+
if (!found) {
|
|
364
|
+
throw new OkeError(LIVE_RESUME_GAP, { signal, afterId });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function pruneLive(signal: string): void {
|
|
369
|
+
const decl = signals.get(signal);
|
|
370
|
+
const drop = liveIdsToPrune(
|
|
371
|
+
messages
|
|
372
|
+
.filter((m) => m.signal === signal && m.delivery === "live")
|
|
373
|
+
.map((m) => ({ id: m.id, createdAt: m.createdAt })),
|
|
374
|
+
decl?.retention,
|
|
375
|
+
now(),
|
|
376
|
+
);
|
|
377
|
+
if (drop.length === 0) return;
|
|
378
|
+
const dropSet = new Set(drop);
|
|
379
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
380
|
+
const m = messages[i]!;
|
|
381
|
+
if (m.delivery === "live" && dropSet.has(m.id)) messages.splice(i, 1);
|
|
332
382
|
}
|
|
333
|
-
return () => {
|
|
334
|
-
set!.delete(handler);
|
|
335
|
-
};
|
|
336
383
|
}
|
|
337
384
|
|
|
338
385
|
function toPublic(m: MutMessage): SignalMessage {
|
|
@@ -469,8 +516,9 @@ export async function createSignalEngine(
|
|
|
469
516
|
if (m.delivery !== "live" || m.status !== "pending") continue;
|
|
470
517
|
const handlers = liveHandlers.get(m.signal);
|
|
471
518
|
if (handlers && handlers.size > 0) {
|
|
519
|
+
const event: LiveEvent = { id: m.id, payload: m.payload };
|
|
472
520
|
for (const h of handlers) {
|
|
473
|
-
await h(
|
|
521
|
+
await h(event);
|
|
474
522
|
}
|
|
475
523
|
}
|
|
476
524
|
pushLive(m.signal, m.payload);
|
|
@@ -478,6 +526,11 @@ export async function createSignalEngine(
|
|
|
478
526
|
noteDelivered();
|
|
479
527
|
progress = true;
|
|
480
528
|
}
|
|
529
|
+
const touched = new Set<string>();
|
|
530
|
+
for (const m of messages) {
|
|
531
|
+
if (m.delivery === "live") touched.add(m.signal);
|
|
532
|
+
}
|
|
533
|
+
for (const name of touched) pruneLive(name);
|
|
481
534
|
return progress;
|
|
482
535
|
}
|
|
483
536
|
|
|
@@ -717,6 +770,7 @@ export async function createSignalEngine(
|
|
|
717
770
|
begin,
|
|
718
771
|
subscribe,
|
|
719
772
|
live,
|
|
773
|
+
checkLiveResume,
|
|
720
774
|
drain,
|
|
721
775
|
deadLetters,
|
|
722
776
|
inspect,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback-set → {@link AsyncIterable} adapter for {@link SignalBus.live}.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { LiveEvent } from "./signal-types.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Turn a live subscriber (replay-then-push) into an async iterable.
|
|
9
|
+
*
|
|
10
|
+
* `start` must register `emit` and replay retained events into it before
|
|
11
|
+
* resolving; `drain` later pushes new events through the same `emit`.
|
|
12
|
+
*
|
|
13
|
+
* @param start - Register + replay; return unsubscribe
|
|
14
|
+
*/
|
|
15
|
+
export function createLiveIterable(
|
|
16
|
+
start: (emit: (event: LiveEvent) => void) => (() => void) | Promise<() => void>,
|
|
17
|
+
): AsyncIterable<LiveEvent> {
|
|
18
|
+
return {
|
|
19
|
+
[Symbol.asyncIterator]() {
|
|
20
|
+
const queue: LiveEvent[] = [];
|
|
21
|
+
let notify: (() => void) | undefined;
|
|
22
|
+
let finished = false;
|
|
23
|
+
let unsub: (() => void) | undefined;
|
|
24
|
+
let started: Promise<void> | undefined;
|
|
25
|
+
|
|
26
|
+
const ensure = (): Promise<void> => {
|
|
27
|
+
if (started) return started;
|
|
28
|
+
const emit = (event: LiveEvent): void => {
|
|
29
|
+
queue.push(event);
|
|
30
|
+
notify?.();
|
|
31
|
+
};
|
|
32
|
+
const result = start(emit);
|
|
33
|
+
if (typeof result === "function") {
|
|
34
|
+
unsub = result;
|
|
35
|
+
started = Promise.resolve();
|
|
36
|
+
return started;
|
|
37
|
+
}
|
|
38
|
+
started = result.then((fn) => {
|
|
39
|
+
unsub = fn;
|
|
40
|
+
});
|
|
41
|
+
return started;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
async next() {
|
|
46
|
+
await ensure();
|
|
47
|
+
for (;;) {
|
|
48
|
+
const nextEvent = queue.shift();
|
|
49
|
+
if (nextEvent !== undefined) return { done: false, value: nextEvent };
|
|
50
|
+
if (finished) return { done: true, value: undefined };
|
|
51
|
+
await new Promise<void>((resolve) => {
|
|
52
|
+
notify = resolve;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async return() {
|
|
57
|
+
finished = true;
|
|
58
|
+
unsub?.();
|
|
59
|
+
notify?.();
|
|
60
|
+
return { done: true, value: undefined };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -108,7 +108,8 @@ export async function openNatsSignal(options: SignalOpenOptions): Promise<Signal
|
|
|
108
108
|
};
|
|
109
109
|
},
|
|
110
110
|
subscribe: (signal, subscriberId, handler) => outbox.subscribe(signal, subscriberId, handler),
|
|
111
|
-
live: (signal,
|
|
111
|
+
live: (signal, opts) => outbox.live(signal, opts),
|
|
112
|
+
checkLiveResume: (signal, afterId) => outbox.checkLiveResume(signal, afterId),
|
|
112
113
|
drain: () => outbox.drain(),
|
|
113
114
|
deadLetters: (s) => outbox.deadLetters(s),
|
|
114
115
|
inspect: (s) => outbox.inspect(s),
|