okengine 0.14.1 → 0.16.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/package.json +3 -4
- package/site/content/docs/elements/ai.mdx +2 -1
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +3 -0
- package/site/content/docs/elements/flow.mdx +9 -0
- package/site/content/docs/elements/gate.mdx +80 -22
- package/site/content/docs/elements/signal.mdx +4 -4
- package/site/content/docs/elements/store.mdx +84 -27
- package/site/content/docs/elements/vault.mdx +4 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +28 -21
- package/site/content/docs/reference/fx.mdx +29 -18
- package/site/content/docs/reference/security.md +2 -2
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +254 -0
- package/src/auth/api-keys.ts +273 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/bindings.ts +14 -5
- package/src/auth/config.ts +5 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/auth/index.ts +25 -1
- package/src/auth/schema.ts +8 -0
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/attach-host-console.test.ts +40 -2
- package/src/cli/attach-host-console.ts +9 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +17 -2
- package/src/compiler/extract.test.ts +409 -33
- package/src/compiler/extract.ts +318 -67
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +1 -0
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- 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 +233 -98
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +8 -21
- package/src/console/server/index.ts +1 -0
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -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/security-headers.ts +6 -3
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +37 -55
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +1 -0
- 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/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- 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-B-OUtiAu.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.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-CGoZkILK.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-BDyXalNR.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -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-CL_X0GCj.js → skeleton-D-czQJT6.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.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-DkOXA12R.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.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-B1dB9Ft0.js +2 -0
- package/src/console/ui-next/dist/index.html +12 -10
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +202 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -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/kbd.tsx +2 -2
- 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/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- 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-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +3 -0
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/types.ts +10 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +38 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +18 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +260 -41
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +135 -35
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +17 -0
- package/src/http.ts +20 -3
- package/src/index.ts +11 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app.ts +145 -16
- package/src/kernel/auth-resolve.ts +10 -5
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +49 -9
- package/src/kernel/boot.test.ts +5 -5
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +12 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +198 -0
- package/src/kernel/fx.test.ts +17 -1
- package/src/kernel/fx.ts +59 -18
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +4 -4
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/pipeline.ts +4 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/types.ts +22 -3
- package/src/release/build-lib.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +8 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-bqkxMrJH.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DDCRmG62.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-HjTnVQoR.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/link-DF7SZ9Ek.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
|
@@ -78,6 +78,14 @@ async function liveRuntime(): Promise<{
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
describe("createDefaultGateAuthStores", () => {
|
|
82
|
+
test("user roles grant member only — no leftover app catalog", () => {
|
|
83
|
+
const auth = createDefaultGateAuthStores();
|
|
84
|
+
expect([...(auth.roles.grants.get("role_member") ?? [])]).toEqual(["member"]);
|
|
85
|
+
expect([...(auth.roles.grants.get("role_staff") ?? [])]).toEqual(["member"]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
81
89
|
describe("isApplicationScope", () => {
|
|
82
90
|
test("console:* is not application; everything else is", () => {
|
|
83
91
|
expect(isApplicationScope("console:store.sql:write")).toBe(false);
|
|
@@ -91,6 +99,21 @@ describe("projectGatesPanel", () => {
|
|
|
91
99
|
const auth = createDefaultGateAuthStores();
|
|
92
100
|
// Poison an operator role with an application scope.
|
|
93
101
|
setRoleGrants(auth.roles, "role_ops", ["console:store.sql:read", "booking:create"]);
|
|
102
|
+
auth.apiKeys.keys.set("key_fixture", {
|
|
103
|
+
id: "key_fixture",
|
|
104
|
+
plane: "user",
|
|
105
|
+
hash: "fixture",
|
|
106
|
+
name: "Fixture",
|
|
107
|
+
scopes: ["member"],
|
|
108
|
+
expiresAt: null,
|
|
109
|
+
rateLimit: null,
|
|
110
|
+
ipAllowlist: [],
|
|
111
|
+
creatorId: "user_demo",
|
|
112
|
+
creatorScopes: ["member"],
|
|
113
|
+
createdAt: 0,
|
|
114
|
+
lastUsedAt: null,
|
|
115
|
+
revokedAt: null,
|
|
116
|
+
});
|
|
94
117
|
|
|
95
118
|
const projection = projectGatesPanel({
|
|
96
119
|
manifest: MANIFEST,
|
|
@@ -318,6 +341,7 @@ describe("simulateGates — evaluate only", () => {
|
|
|
318
341
|
test("role principal resolves scopes from RoleStore", async () => {
|
|
319
342
|
const { runtime, close } = await liveRuntime();
|
|
320
343
|
const auth = createDefaultGateAuthStores();
|
|
344
|
+
setRoleGrants(auth.roles, "role_member", ["member", "booking:create"]);
|
|
321
345
|
try {
|
|
322
346
|
const ok = await simulateGates({
|
|
323
347
|
flowId: "bookings.create",
|
|
@@ -33,6 +33,7 @@ import { statusForFailure } from "../../compiler/response.ts";
|
|
|
33
33
|
import { gateDenialFailure } from "../../kernel/index.ts";
|
|
34
34
|
import { diffManifest } from "../../manifest/diff.ts";
|
|
35
35
|
import type { Gate as ManifestGate, Manifest, ManifestChange } from "../../manifest/types.ts";
|
|
36
|
+
import { defaultMemberIdentityIds } from "./dev-identities.ts";
|
|
36
37
|
|
|
37
38
|
/** Identity slice needed for Gates (avoids circular import with state). */
|
|
38
39
|
export interface GatesIdentity {
|
|
@@ -413,8 +414,8 @@ export function createDefaultGateAuthStores(): {
|
|
|
413
414
|
plane: "operator",
|
|
414
415
|
description: "Console operators",
|
|
415
416
|
});
|
|
416
|
-
setRoleGrants(roles, "role_member", ["member"
|
|
417
|
-
setRoleGrants(roles, "role_staff", ["member"
|
|
417
|
+
setRoleGrants(roles, "role_member", ["member"]);
|
|
418
|
+
setRoleGrants(roles, "role_staff", ["member"]);
|
|
418
419
|
setRoleGrants(roles, "role_ops", [
|
|
419
420
|
"console:store.sql:read",
|
|
420
421
|
"console:store.sql:write",
|
|
@@ -422,24 +423,9 @@ export function createDefaultGateAuthStores(): {
|
|
|
422
423
|
]);
|
|
423
424
|
|
|
424
425
|
const apiKeys = createApiKeyStore();
|
|
425
|
-
apiKeys.keys.set("key_demo", {
|
|
426
|
-
id: "key_demo",
|
|
427
|
-
plane: "user",
|
|
428
|
-
hash: "demo",
|
|
429
|
-
name: "Demo key",
|
|
430
|
-
scopes: ["member", "booking:create"],
|
|
431
|
-
expiresAt: null,
|
|
432
|
-
rateLimit: null,
|
|
433
|
-
ipAllowlist: [],
|
|
434
|
-
creatorId: "user_demo",
|
|
435
|
-
creatorScopes: ["member", "booking:create"],
|
|
436
|
-
createdAt: 0,
|
|
437
|
-
lastUsedAt: null,
|
|
438
|
-
revokedAt: null,
|
|
439
|
-
});
|
|
440
426
|
|
|
441
427
|
const roleMembers = new Map<string, string[]>([
|
|
442
|
-
["role_member",
|
|
428
|
+
["role_member", defaultMemberIdentityIds()],
|
|
443
429
|
["role_staff", []],
|
|
444
430
|
["role_ops", []],
|
|
445
431
|
]);
|
|
@@ -706,7 +692,7 @@ function resolvePrincipalContext(options: SimulateGatesOptions): {
|
|
|
706
692
|
return {
|
|
707
693
|
ctx: {
|
|
708
694
|
auth: { userId: null, scopes: new Set(), verified: false },
|
|
709
|
-
operator: { id:
|
|
695
|
+
operator: { id: key.creatorId },
|
|
710
696
|
meta: { ip: options.meta?.ip },
|
|
711
697
|
},
|
|
712
698
|
};
|
|
@@ -714,12 +700,13 @@ function resolvePrincipalContext(options: SimulateGatesOptions): {
|
|
|
714
700
|
return {
|
|
715
701
|
ctx: {
|
|
716
702
|
auth: {
|
|
717
|
-
userId:
|
|
703
|
+
userId: key.creatorId,
|
|
718
704
|
scopes: new Set(key.scopes),
|
|
719
705
|
verified: true,
|
|
706
|
+
apiKeyId: key.id,
|
|
720
707
|
},
|
|
721
708
|
operator: { id: null },
|
|
722
|
-
meta: { ip: options.meta?.ip, userId:
|
|
709
|
+
meta: { ip: options.meta?.ip, userId: key.creatorId },
|
|
723
710
|
},
|
|
724
711
|
};
|
|
725
712
|
}
|
|
@@ -181,7 +181,9 @@ describe("resolveInvokeAs", () => {
|
|
|
181
181
|
const resolved = resolveInvokeAs({ asGate: "member" }, IDENTITIES, MANIFEST);
|
|
182
182
|
expect(resolved.ok).toBe(true);
|
|
183
183
|
if (!resolved.ok) return;
|
|
184
|
-
expect(resolved.asUserId).toBe("
|
|
184
|
+
expect(resolved.asUserId).toBe("");
|
|
185
|
+
expect(resolved.principal.userId).toBe("");
|
|
186
|
+
expect(resolved.rls).toEqual({ gate: "member", userId: "", scopes: ["member"] });
|
|
185
187
|
expect([...resolved.principal.scopes]).toEqual(["member"]);
|
|
186
188
|
expect(resolved.principal.verified).toBe(true);
|
|
187
189
|
});
|
|
@@ -14,6 +14,8 @@ import type { AnyFlowDef } from "../../kernel/flow.ts";
|
|
|
14
14
|
import { isJsonResult } from "../../kernel/fx.ts";
|
|
15
15
|
import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
16
16
|
import type { Trigger } from "../../kernel/triggers.ts";
|
|
17
|
+
import type { RlsIdentity } from "../../drivers/pg-rls.ts";
|
|
18
|
+
import { resolveRlsIdentity } from "../../elements/store.ts";
|
|
17
19
|
import type { Manifest } from "../../manifest/types.ts";
|
|
18
20
|
import type { ConsoleIdentity } from "./state.ts";
|
|
19
21
|
|
|
@@ -32,6 +34,8 @@ export interface InvokeUserFlowInput {
|
|
|
32
34
|
* Envelope remask is skipped separately when this is true.
|
|
33
35
|
*/
|
|
34
36
|
readonly revealPii?: boolean;
|
|
37
|
+
/** Same bag as SQL console / browse — stamped onto host `fx.store`. */
|
|
38
|
+
readonly rls?: RlsIdentity;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/** Resolved Call API invoke-as principal. */
|
|
@@ -42,6 +46,7 @@ export type ResolvedInvokeAs =
|
|
|
42
46
|
readonly asUserId: string;
|
|
43
47
|
readonly asGate: string | null;
|
|
44
48
|
readonly bypassGates: boolean;
|
|
49
|
+
readonly rls: RlsIdentity | null;
|
|
45
50
|
}
|
|
46
51
|
| { readonly ok: false; readonly reason: string };
|
|
47
52
|
|
|
@@ -75,6 +80,12 @@ export function resolveInvokeAs(
|
|
|
75
80
|
asUserId: identity.id,
|
|
76
81
|
asGate: asGate ?? null,
|
|
77
82
|
bypassGates: false,
|
|
83
|
+
rls: resolveRlsIdentity({
|
|
84
|
+
asUserId: identity.id,
|
|
85
|
+
asGate: asGate ?? null,
|
|
86
|
+
identities,
|
|
87
|
+
manifest,
|
|
88
|
+
}),
|
|
78
89
|
};
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -89,6 +100,7 @@ export function resolveInvokeAs(
|
|
|
89
100
|
asUserId: "console:operator",
|
|
90
101
|
asGate: null,
|
|
91
102
|
bypassGates: true,
|
|
103
|
+
rls: null,
|
|
92
104
|
};
|
|
93
105
|
}
|
|
94
106
|
|
|
@@ -103,6 +115,7 @@ export function resolveInvokeAs(
|
|
|
103
115
|
asUserId: "public",
|
|
104
116
|
asGate: "public",
|
|
105
117
|
bypassGates: false,
|
|
118
|
+
rls: resolveRlsIdentity({ asGate: "public", manifest }),
|
|
106
119
|
};
|
|
107
120
|
}
|
|
108
121
|
|
|
@@ -114,13 +127,14 @@ export function resolveInvokeAs(
|
|
|
114
127
|
return {
|
|
115
128
|
ok: true,
|
|
116
129
|
principal: userPrincipal({
|
|
117
|
-
userId:
|
|
130
|
+
userId: "",
|
|
118
131
|
scopes,
|
|
119
132
|
verified: true,
|
|
120
133
|
}),
|
|
121
|
-
asUserId:
|
|
134
|
+
asUserId: "",
|
|
122
135
|
asGate,
|
|
123
136
|
bypassGates: false,
|
|
137
|
+
rls: resolveRlsIdentity({ asGate, manifest }),
|
|
124
138
|
};
|
|
125
139
|
}
|
|
126
140
|
|
|
@@ -353,6 +367,7 @@ export function bindHostInvokeUserFlow(app: InvokeHostApp): ConsoleInvokeUserFlo
|
|
|
353
367
|
runId,
|
|
354
368
|
...(input.bypassGates === true ? { bypassGates: true } : {}),
|
|
355
369
|
...(input.revealPii === true ? { revealPii: true } : {}),
|
|
370
|
+
...(input.rls ? { rls: input.rls } : {}),
|
|
356
371
|
principal: {
|
|
357
372
|
plane: "user",
|
|
358
373
|
userId: input.principal.userId.length > 0 ? input.principal.userId : null,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO-8601 UTC instant from a Console clock reading (epoch ms).
|
|
3
|
+
*
|
|
4
|
+
* Mutation receipts (`clock.run-now`, store edit, vault write, …) return
|
|
5
|
+
* this instead of a raw millisecond number so Call API JSON is readable.
|
|
6
|
+
*
|
|
7
|
+
* @param ms - Epoch milliseconds from `state.now()` / `Date.now()`
|
|
8
|
+
*/
|
|
9
|
+
export function isoAt(ms: number): string {
|
|
10
|
+
return new Date(ms).toISOString();
|
|
11
|
+
}
|
|
@@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
6
6
|
import type { Manifest } from "../../manifest/types.ts";
|
|
7
7
|
import type { WideEvent } from "../../runs/types.ts";
|
|
8
8
|
import { feedManifest, feedRun, subscribeLive } from "./live.ts";
|
|
9
|
-
import { createConsoleState, type ConsoleLiveMessage } from "./state.ts";
|
|
9
|
+
import { createConsoleState, setManifest, type ConsoleLiveMessage } from "./state.ts";
|
|
10
10
|
|
|
11
11
|
describe("console live Manifest feed", () => {
|
|
12
12
|
test("pushes manifest and diff to subscribers", () => {
|
|
@@ -28,6 +28,31 @@ describe("console live Manifest feed", () => {
|
|
|
28
28
|
expect(messages.some((m) => m.type === "manifest")).toBe(true);
|
|
29
29
|
expect(messages.some((m) => m.type === "manifest.diff")).toBe(true);
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test("seeded identities follow the Manifest — no invented booking:create", () => {
|
|
33
|
+
const state = createConsoleState({ silentClaim: true, secret: "x" });
|
|
34
|
+
expect(state.identities).toHaveLength(10);
|
|
35
|
+
expect(state.identities[0]?.id).toBe("user_demo");
|
|
36
|
+
expect(state.identities.find((row) => row.id === "user_member")?.scopes).toEqual(["member"]);
|
|
37
|
+
expect(state.identities.find((row) => row.id === "user_guest")?.scopes).toEqual([]);
|
|
38
|
+
expect(state.identities.find((row) => row.id === "user_demo")?.scopes).toEqual(["member"]);
|
|
39
|
+
|
|
40
|
+
setManifest(state, {
|
|
41
|
+
oke: "1.0",
|
|
42
|
+
app: "keel-like",
|
|
43
|
+
gates: {
|
|
44
|
+
member: { kind: "policy", scopes: ["member"] },
|
|
45
|
+
"task:write": { kind: "policy", scopes: ["task:write"] },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
expect(state.identities.find((row) => row.id === "user_demo")?.scopes).toEqual([
|
|
49
|
+
"member",
|
|
50
|
+
"task:write",
|
|
51
|
+
]);
|
|
52
|
+
expect(state.identities.find((row) => row.id === "user_demo")?.scopes).not.toContain(
|
|
53
|
+
"booking:create",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
31
56
|
});
|
|
32
57
|
|
|
33
58
|
describe("console live run feed", () => {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import type { WideEvent } from "../../runs/types.ts";
|
|
9
9
|
import { projectRun } from "./flows.ts";
|
|
10
10
|
import { piiFieldNamesFromManifest } from "./runs-pii.ts";
|
|
11
|
-
import
|
|
12
|
-
import { publishLive } from "./state.ts";
|
|
11
|
+
import { isKeelManifest, refreshSeededIdentities, seedKeelAccessRoles } from "./dev-identities.ts";
|
|
12
|
+
import { publishLive, type ConsoleLiveMessage, type ConsoleState } from "./state.ts";
|
|
13
13
|
|
|
14
14
|
/** WebSocket data bag for Console live clients. */
|
|
15
15
|
export interface ConsoleLiveData {
|
|
@@ -97,6 +97,8 @@ export function feedManifest(
|
|
|
97
97
|
): void {
|
|
98
98
|
const before = state.manifest;
|
|
99
99
|
state.manifest = manifest;
|
|
100
|
+
refreshSeededIdentities(state.identities, manifest);
|
|
101
|
+
if (isKeelManifest(manifest)) seedKeelAccessRoles(state.roles, state.roleMembers);
|
|
100
102
|
publishLive(state, { type: "manifest", manifest });
|
|
101
103
|
if (before) {
|
|
102
104
|
publishLive(state, { type: "manifest.diff", before, after: manifest });
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* Spec: wizard closes permanently once the first operator exists (console §2.5).
|
|
12
12
|
* Claim codes stay out of Postgres (optional local DX mirror: `.oke/claim-code`);
|
|
13
13
|
* operators, sessions, and the signing secret must survive restarts.
|
|
14
|
+
* API keys persist on the host `store.sql()` connection (`public.oke_api_keys`),
|
|
15
|
+
* not in this schema — Console attaches that store via `attachHostToConsole`.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import { mkdirSync } from "node:fs";
|
|
@@ -7,14 +7,16 @@ export const CONSOLE_CSP: string = [
|
|
|
7
7
|
"default-src 'self'",
|
|
8
8
|
"script-src 'self'",
|
|
9
9
|
"style-src 'self' 'unsafe-inline'",
|
|
10
|
-
"img-src 'self' data:",
|
|
10
|
+
"img-src 'self' data: blob:",
|
|
11
|
+
"media-src 'self' blob:",
|
|
11
12
|
"font-src 'self'",
|
|
12
13
|
"connect-src 'self' ws: wss:",
|
|
13
14
|
"object-src 'none'",
|
|
14
15
|
"base-uri 'self'",
|
|
15
16
|
"form-action 'self'",
|
|
16
17
|
"frame-ancestors 'none'",
|
|
17
|
-
|
|
18
|
+
// Files preview mints blob: URLs for PDF iframes, images, and A/V.
|
|
19
|
+
"frame-src 'self' blob:",
|
|
18
20
|
].join("; ");
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -27,13 +29,14 @@ export const CONSOLE_VITE_DEV_CSP: string = [
|
|
|
27
29
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
28
30
|
"style-src 'self' 'unsafe-inline'",
|
|
29
31
|
"img-src 'self' data: blob:",
|
|
32
|
+
"media-src 'self' blob:",
|
|
30
33
|
"font-src 'self'",
|
|
31
34
|
"connect-src 'self' ws: wss: http://127.0.0.1:* ws://127.0.0.1:*",
|
|
32
35
|
"object-src 'none'",
|
|
33
36
|
"base-uri 'self'",
|
|
34
37
|
"form-action 'self'",
|
|
35
38
|
"frame-ancestors 'none'",
|
|
36
|
-
"frame-src 'self'",
|
|
39
|
+
"frame-src 'self' blob:",
|
|
37
40
|
].join("; ");
|
|
38
41
|
|
|
39
42
|
/** CSP for sandboxed plugin panel iframes (no operator session reach). */
|
|
@@ -61,6 +61,7 @@ describe("serveConsole spaProxy", () => {
|
|
|
61
61
|
expect(html.status).toBe(200);
|
|
62
62
|
expect(html.headers.get("content-security-policy")).toBe(CONSOLE_VITE_DEV_CSP);
|
|
63
63
|
expect(html.headers.get("content-security-policy")).toContain("unsafe-inline");
|
|
64
|
+
expect(html.headers.get("content-security-policy")).toContain("frame-src 'self' blob:");
|
|
64
65
|
expect(await html.text()).toContain("vite-spa");
|
|
65
66
|
|
|
66
67
|
const client = await server.fetch(
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
parseSqlPolicySpec,
|
|
32
32
|
quotePgIdent,
|
|
33
33
|
sqlPolicyRowId,
|
|
34
|
+
emitStoreSchemaPolicySource,
|
|
35
|
+
installOkeRlsHelpers,
|
|
34
36
|
} from "../../drivers/pg-rls.ts";
|
|
35
37
|
import type { ConsoleStoreChild, ConsoleWillNotFire } from "./store.ts";
|
|
36
38
|
|
|
@@ -117,18 +119,52 @@ export async function listSqlTableRls(sql: SqlStoreHandle): Promise<ReadonlyMap<
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
/**
|
|
120
|
-
*
|
|
122
|
+
* Live RLS policy counts (`pg_policies`) keyed by table name.
|
|
123
|
+
*
|
|
124
|
+
* Counts in process — memory SQL has no `GROUP BY` / `COUNT(*)::int`.
|
|
125
|
+
*
|
|
126
|
+
* @param sql - Open SQL handle
|
|
127
|
+
*/
|
|
128
|
+
export async function listSqlTablePolicyCounts(
|
|
129
|
+
sql: SqlStoreHandle,
|
|
130
|
+
): Promise<ReadonlyMap<string, number>> {
|
|
131
|
+
try {
|
|
132
|
+
const rows = await sql.raw(`SELECT * FROM pg_policies`);
|
|
133
|
+
const out = new Map<string, number>();
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
if (!isAppCatalogSchema(row)) continue;
|
|
136
|
+
const name = stringOrNull(row.tablename ?? row.table);
|
|
137
|
+
if (name === null) continue;
|
|
138
|
+
out.set(name, (out.get(name) ?? 0) + 1);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
} catch {
|
|
142
|
+
return new Map();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Copy RLS flags and policy counts onto SQL table children. Catalog folders
|
|
148
|
+
* are unchanged.
|
|
121
149
|
*
|
|
122
150
|
* @param children - Store list children
|
|
123
151
|
* @param rlsByTable - Table name → RLS enabled
|
|
152
|
+
* @param declared - Manifest RLS when live catalog is empty
|
|
153
|
+
* @param policyCounts - Table name → live + declared policy count
|
|
124
154
|
*/
|
|
125
155
|
export function applySqlTableRls(
|
|
126
156
|
children: readonly ConsoleStoreChild[],
|
|
127
157
|
rlsByTable: ReadonlyMap<string, boolean>,
|
|
158
|
+
declared?: ReadonlyMap<string, boolean>,
|
|
159
|
+
policyCounts?: ReadonlyMap<string, number>,
|
|
128
160
|
): ConsoleStoreChild[] {
|
|
129
161
|
return children.map((child) => {
|
|
130
162
|
if (child.kind !== "table") return child;
|
|
131
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
...child,
|
|
165
|
+
rls: rlsByTable.get(child.name) ?? declared?.get(child.name) ?? child.rls === true,
|
|
166
|
+
rlsPolicyCount: policyCounts?.get(child.name) ?? child.rlsPolicyCount ?? 0,
|
|
167
|
+
};
|
|
132
168
|
});
|
|
133
169
|
}
|
|
134
170
|
|
|
@@ -156,6 +192,9 @@ export async function listSqlCatalog(
|
|
|
156
192
|
limit: number,
|
|
157
193
|
): Promise<readonly Record<string, unknown>[]> {
|
|
158
194
|
const live = await tryLiveCatalog(sql, kind, limit);
|
|
195
|
+
if (kind === "policy") {
|
|
196
|
+
return overlayDeclaredPolicies(live ?? [], manifest, storeName).slice(0, limit);
|
|
197
|
+
}
|
|
159
198
|
if (live) return live;
|
|
160
199
|
if (kind === "index") return manifestIndexes(manifest, storeName).slice(0, limit);
|
|
161
200
|
if (kind === "extension") return fallbackExtensions().slice(0, limit);
|
|
@@ -172,6 +211,11 @@ export async function createSqlPolicy(
|
|
|
172
211
|
sql: SqlStoreHandle,
|
|
173
212
|
patch: Readonly<Record<string, unknown>>,
|
|
174
213
|
): Promise<void> {
|
|
214
|
+
try {
|
|
215
|
+
await installOkeRlsHelpers((stmt) => sql.raw(stmt));
|
|
216
|
+
} catch {
|
|
217
|
+
// Memory / engines without CREATE FUNCTION — CREATE POLICY may still run.
|
|
218
|
+
}
|
|
175
219
|
const spec = parseSqlPolicySpec(patch);
|
|
176
220
|
if (patch.enableRls === true) {
|
|
177
221
|
await sql.raw(buildRowSecuritySql(spec.table, true));
|
|
@@ -393,11 +437,13 @@ function catalogColumnDescriptions(kind: SqlCatalogKind): Readonly<Record<string
|
|
|
393
437
|
return {
|
|
394
438
|
name: "Policy",
|
|
395
439
|
table: "Table",
|
|
440
|
+
origin: "Declared or live-only",
|
|
396
441
|
command: "Command",
|
|
397
442
|
roles: "Roles",
|
|
398
443
|
permissive: "Permissive or restrictive",
|
|
399
444
|
using: "USING expression",
|
|
400
445
|
with_check: "WITH CHECK expression",
|
|
446
|
+
code: "Copy as code",
|
|
401
447
|
};
|
|
402
448
|
}
|
|
403
449
|
return {
|
|
@@ -459,19 +505,113 @@ function isAppCatalogSchema(row: Record<string, unknown>): boolean {
|
|
|
459
505
|
function projectPolicyRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
460
506
|
const name = stringOrNull(row.name ?? row.policyname) ?? "";
|
|
461
507
|
const table = stringOrNull(row.table ?? row.tablename) ?? "";
|
|
508
|
+
const command = stringOrNull(row.command ?? row.cmd) ?? "ALL";
|
|
509
|
+
const roles = rolesText(row.roles);
|
|
510
|
+
const using = stringOrNull(row.using ?? row.qual);
|
|
511
|
+
const withCheck = stringOrNull(row.with_check);
|
|
512
|
+
const permissive = stringOrNull(row.permissive) ?? "PERMISSIVE";
|
|
462
513
|
return {
|
|
463
514
|
id: sqlPolicyRowId(table, name),
|
|
464
515
|
name,
|
|
465
516
|
table,
|
|
466
517
|
schema: stringOrNull(row.schema ?? row.schemaname) ?? "public",
|
|
467
|
-
command
|
|
468
|
-
roles
|
|
469
|
-
permissive
|
|
470
|
-
using
|
|
471
|
-
with_check:
|
|
518
|
+
command,
|
|
519
|
+
roles,
|
|
520
|
+
permissive,
|
|
521
|
+
using,
|
|
522
|
+
with_check: withCheck,
|
|
523
|
+
origin: stringOrNull(row.origin) ?? "live",
|
|
524
|
+
code: policyCopyAsCode({
|
|
525
|
+
name,
|
|
526
|
+
table,
|
|
527
|
+
command,
|
|
528
|
+
roles,
|
|
529
|
+
permissive,
|
|
530
|
+
using,
|
|
531
|
+
withCheck,
|
|
532
|
+
}),
|
|
472
533
|
};
|
|
473
534
|
}
|
|
474
535
|
|
|
536
|
+
function overlayDeclaredPolicies(
|
|
537
|
+
live: readonly Record<string, unknown>[],
|
|
538
|
+
manifest: Manifest | null,
|
|
539
|
+
storeName: string,
|
|
540
|
+
): Record<string, unknown>[] {
|
|
541
|
+
const byId = new Map<string, Record<string, unknown>>();
|
|
542
|
+
for (const row of live) {
|
|
543
|
+
byId.set(String(row.id), { ...row, origin: "live" });
|
|
544
|
+
}
|
|
545
|
+
for (const row of declaredPoliciesFromManifest(manifest, storeName)) {
|
|
546
|
+
const existing = byId.get(row.id);
|
|
547
|
+
byId.set(row.id, existing ? { ...existing, ...row, origin: "declared" } : row);
|
|
548
|
+
}
|
|
549
|
+
return [...byId.values()];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function declaredPoliciesFromManifest(
|
|
553
|
+
manifest: Manifest | null,
|
|
554
|
+
storeName: string,
|
|
555
|
+
): Array<Record<string, unknown> & { readonly id: string }> {
|
|
556
|
+
const tables = manifest?.stores?.[storeName]?.tables ?? {};
|
|
557
|
+
const out: Array<Record<string, unknown> & { readonly id: string }> = [];
|
|
558
|
+
for (const [table, spec] of Object.entries(tables)) {
|
|
559
|
+
for (const [name, policy] of Object.entries(spec.policies ?? {})) {
|
|
560
|
+
const command = (policy.for ?? "all").toUpperCase();
|
|
561
|
+
const roles = Array.isArray(policy.to) ? policy.to.join(", ") : (policy.to ?? "public");
|
|
562
|
+
const permissive = (policy.as ?? "permissive").toUpperCase();
|
|
563
|
+
out.push({
|
|
564
|
+
id: sqlPolicyRowId(table, name),
|
|
565
|
+
name,
|
|
566
|
+
table,
|
|
567
|
+
schema: "public",
|
|
568
|
+
command,
|
|
569
|
+
roles,
|
|
570
|
+
permissive,
|
|
571
|
+
using: policy.using ?? null,
|
|
572
|
+
with_check: policy.withCheck ?? null,
|
|
573
|
+
origin: "declared",
|
|
574
|
+
code: policyCopyAsCode({
|
|
575
|
+
name,
|
|
576
|
+
table,
|
|
577
|
+
command,
|
|
578
|
+
roles,
|
|
579
|
+
permissive,
|
|
580
|
+
using: policy.using,
|
|
581
|
+
withCheck: policy.withCheck,
|
|
582
|
+
}),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function policyCopyAsCode(input: {
|
|
590
|
+
readonly name: string;
|
|
591
|
+
readonly table: string;
|
|
592
|
+
readonly command: string;
|
|
593
|
+
readonly roles: string;
|
|
594
|
+
readonly permissive: string;
|
|
595
|
+
readonly using?: string | null;
|
|
596
|
+
readonly withCheck?: string | null;
|
|
597
|
+
}): string {
|
|
598
|
+
try {
|
|
599
|
+
return emitStoreSchemaPolicySource(
|
|
600
|
+
parseSqlPolicySpec({
|
|
601
|
+
name: input.name,
|
|
602
|
+
table: input.table,
|
|
603
|
+
command: input.command,
|
|
604
|
+
behavior: input.permissive,
|
|
605
|
+
roles: input.roles,
|
|
606
|
+
using: input.using ?? "",
|
|
607
|
+
withCheck: input.withCheck ?? "",
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
} catch {
|
|
611
|
+
return "";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
475
615
|
function projectTriggerRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
476
616
|
const enabled = row.enabled;
|
|
477
617
|
return {
|
|
@@ -41,4 +41,20 @@ describe("applySqlTableRls", () => {
|
|
|
41
41
|
const rows = [{ ...child("comments"), rls: false }];
|
|
42
42
|
expect(applySqlTableRls(rows, new Map())[0]?.rls).toBe(false);
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
test("Manifest declared RLS wins when live catalog is empty", () => {
|
|
46
|
+
const out = applySqlTableRls([child("bookings")], new Map(), new Map([["bookings", true]]));
|
|
47
|
+
expect(out[0]?.rls).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("stamps policy counts on tables and leaves catalog folders alone", () => {
|
|
51
|
+
const out = applySqlTableRls(
|
|
52
|
+
[child("comments"), child("policies", "policy")],
|
|
53
|
+
new Map([["comments", true]]),
|
|
54
|
+
undefined,
|
|
55
|
+
new Map([["comments", 2]]),
|
|
56
|
+
);
|
|
57
|
+
expect(out.find((row) => row.name === "comments")?.rlsPolicyCount).toBe(2);
|
|
58
|
+
expect(out.find((row) => row.name === "policies")?.rlsPolicyCount).toBeUndefined();
|
|
59
|
+
});
|
|
44
60
|
});
|