okengine 0.13.0 → 0.15.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/manifest.v1.schema.json +7 -1
- package/package.json +4 -5
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +59 -11
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +30 -12
- package/site/content/docs/elements/flow.mdx +9 -3
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +87 -28
- package/site/content/docs/elements/vault.mdx +1 -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/installation.mdx +2 -2
- 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 +25 -21
- package/site/content/docs/reference/fx.mdx +27 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -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 +58 -3
- package/src/compiler/extract.test.ts +458 -29
- package/src/compiler/extract.ts +387 -69
- 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/response.ts +45 -1
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +121 -40
- 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/security-headers.ts +6 -3
- package/src/console/server/serve.ts +17 -2
- 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 +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/client.ts +35 -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/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- 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-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -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/detail/effects-summary.tsx +12 -4
- 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/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +4 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- 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/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +10 -0
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- 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 +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- 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-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/http.ts +20 -3
- package/src/index.ts +14 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +227 -77
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +54 -9
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +5 -3
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +15 -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-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +135 -6
- 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 +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/kernel/pipeline.test.ts +2 -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/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +41 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/bun.ts +41 -4
- 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 +10 -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-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.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-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.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-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
|
@@ -86,6 +86,18 @@ describe("compileWhere", () => {
|
|
|
86
86
|
expect(c.params).toEqual(["n1", "t"]);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
test("legacy array-valued StringChunk fragments still compile", () => {
|
|
90
|
+
const c = compileWhere({
|
|
91
|
+
queryChunks: [
|
|
92
|
+
notes.id,
|
|
93
|
+
{ constructor: { name: "StringChunk" }, value: [" = "] },
|
|
94
|
+
{ value: "n1" },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
expect(c.clause).toBe(`"id" = ?`);
|
|
98
|
+
expect(c.params).toEqual(["n1"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
89
101
|
test("inArray expands one placeholder per value", () => {
|
|
90
102
|
const c = compileWhere(inArray(notes.id, ["a", "b", "c"]));
|
|
91
103
|
expect(c.clause).toBe(`"id" in (?, ?, ?)`);
|
|
@@ -333,10 +333,33 @@ function chunksOf(node: unknown): readonly unknown[] {
|
|
|
333
333
|
return (node as { queryChunks: unknown[] }).queryChunks;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* True when `chunk` is a Drizzle `StringChunk` (or the synthetic array-valued
|
|
338
|
+
* shape `resource.ts` still emits). `Param` also has a `value` field — never
|
|
339
|
+
* treat those as SQL text.
|
|
340
|
+
*/
|
|
341
|
+
function isStringChunk(chunk: object): boolean {
|
|
342
|
+
const ctor = (chunk as { constructor?: { name?: unknown } }).constructor?.name;
|
|
343
|
+
if (ctor === "StringChunk") return true;
|
|
344
|
+
const value = (chunk as { value?: unknown }).value;
|
|
345
|
+
return (
|
|
346
|
+
Array.isArray(value) &&
|
|
347
|
+
value.every((part) => typeof part === "string") &&
|
|
348
|
+
!("encoder" in chunk) &&
|
|
349
|
+
!("name" in chunk)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Joined text of a Drizzle `StringChunk`.
|
|
355
|
+
*
|
|
356
|
+
* drizzle-orm `1.0.0-rc.5-169397b` stores `value` as a `string`. Earlier
|
|
357
|
+
* snapshots (and the synthetic chunks in `resource.ts`) store `string[]`.
|
|
358
|
+
*/
|
|
337
359
|
function chunkText(chunk: unknown): string | undefined {
|
|
338
|
-
if (!chunk || typeof chunk !== "object") return undefined;
|
|
360
|
+
if (!chunk || typeof chunk !== "object" || !isStringChunk(chunk)) return undefined;
|
|
339
361
|
const value = (chunk as { value?: unknown }).value;
|
|
362
|
+
if (typeof value === "string") return value;
|
|
340
363
|
return Array.isArray(value) ? value.join("") : undefined;
|
|
341
364
|
}
|
|
342
365
|
|
|
@@ -394,6 +417,7 @@ function asParamValue(chunk: unknown): { readonly found: boolean; readonly value
|
|
|
394
417
|
if (chunk === undefined) return { found: false };
|
|
395
418
|
if (chunk === null || typeof chunk !== "object") return { found: true, value: chunk };
|
|
396
419
|
if (Array.isArray(chunk)) return { found: false };
|
|
420
|
+
if (isStringChunk(chunk)) return { found: false };
|
|
397
421
|
if (!("value" in (chunk as object))) return { found: false };
|
|
398
422
|
if (Array.isArray((chunk as { value: unknown }).value)) return { found: false };
|
|
399
423
|
if ((chunk as { name?: unknown }).name) return { found: false };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real PGlite RLS isolation through {@link createSqlStoreHandle}.
|
|
3
|
+
*
|
|
4
|
+
* Covers the gap unit tests missed: a live policy, two concurrent identity
|
|
5
|
+
* bags on one shared connection, and row-level isolation. PGlite `query`
|
|
6
|
+
* rejects multi-command batches — the stamp is split statements pinned
|
|
7
|
+
* through `SqlConnection.transaction`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { connectPglite } from "../../drivers/pglite.ts";
|
|
12
|
+
import { connectPostgres } from "../../drivers/postgres.ts";
|
|
13
|
+
import { installOkeRlsHelpers } from "../../drivers/pg-rls.ts";
|
|
14
|
+
import type { SqlConnection } from "../../drivers/types.ts";
|
|
15
|
+
import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
|
|
16
|
+
|
|
17
|
+
const NOTES_DDL = `CREATE TABLE notes (
|
|
18
|
+
id text PRIMARY KEY,
|
|
19
|
+
owner text NOT NULL,
|
|
20
|
+
body text NOT NULL
|
|
21
|
+
)`;
|
|
22
|
+
|
|
23
|
+
const OWNER_POLICY = `CREATE POLICY owner_select ON notes
|
|
24
|
+
AS PERMISSIVE FOR SELECT TO public
|
|
25
|
+
USING (owner = oke.user())`;
|
|
26
|
+
|
|
27
|
+
/** File-scoped warmed PGlite. */
|
|
28
|
+
let conn: SqlConnection;
|
|
29
|
+
/** Stamped as alice. */
|
|
30
|
+
let asAlice: SqlStoreHandle;
|
|
31
|
+
/** Stamped as bob. */
|
|
32
|
+
let asBob: SqlStoreHandle;
|
|
33
|
+
/** No stamp — table-owner / superuser bypass. */
|
|
34
|
+
let asOwner: SqlStoreHandle;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
conn = await connectPglite({ url: "memory://sql-rls-isolation", role: "primary" });
|
|
38
|
+
await conn.exec(NOTES_DDL);
|
|
39
|
+
await conn.exec(`INSERT INTO notes VALUES ('a', 'alice', 'hi'), ('b', 'bob', 'yo')`);
|
|
40
|
+
await conn.exec(`ALTER TABLE notes ENABLE ROW LEVEL SECURITY`);
|
|
41
|
+
await installOkeRlsHelpers((sql) => conn.exec(sql));
|
|
42
|
+
await conn.exec(OWNER_POLICY);
|
|
43
|
+
asAlice = createSqlStoreHandle("sql:app", {
|
|
44
|
+
connection: conn,
|
|
45
|
+
classifications: new Map(),
|
|
46
|
+
routedRole: "primary",
|
|
47
|
+
domainDdl: "off",
|
|
48
|
+
rls: { gate: "member", userId: "alice", scopes: ["member"] },
|
|
49
|
+
});
|
|
50
|
+
asBob = createSqlStoreHandle("sql:app", {
|
|
51
|
+
connection: conn,
|
|
52
|
+
classifications: new Map(),
|
|
53
|
+
routedRole: "primary",
|
|
54
|
+
domainDdl: "off",
|
|
55
|
+
rls: { gate: "member", userId: "bob", scopes: ["member"] },
|
|
56
|
+
});
|
|
57
|
+
asOwner = createSqlStoreHandle("sql:app", {
|
|
58
|
+
connection: conn,
|
|
59
|
+
classifications: new Map(),
|
|
60
|
+
routedRole: "primary",
|
|
61
|
+
domainDdl: "off",
|
|
62
|
+
});
|
|
63
|
+
}, 15_000);
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
await conn.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("sql-session RLS isolation (pglite)", () => {
|
|
70
|
+
test("pglite query rejects a multi-command batch (the old prelude)", async () => {
|
|
71
|
+
await expect(
|
|
72
|
+
conn.query("BEGIN; SET LOCAL row_security = on; SELECT set_config('oke.user', $1, true)", [
|
|
73
|
+
"alice",
|
|
74
|
+
]),
|
|
75
|
+
).rejects.toThrow(/multiple commands/i);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("sequential identities see only their rows", async () => {
|
|
79
|
+
const alice = await asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`);
|
|
80
|
+
const bob = await asBob.raw(`SELECT id, owner FROM notes ORDER BY id`);
|
|
81
|
+
expect(alice).toEqual([{ id: "a", owner: "alice" }]);
|
|
82
|
+
expect(bob).toEqual([{ id: "b", owner: "bob" }]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("concurrent identities do not leak across the shared connection", async () => {
|
|
86
|
+
const [alice, bob, aliceAgain] = await Promise.all([
|
|
87
|
+
asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`),
|
|
88
|
+
asBob.raw(`SELECT id, owner FROM notes ORDER BY id`),
|
|
89
|
+
asAlice.raw(`SELECT id, owner FROM notes ORDER BY id`),
|
|
90
|
+
]);
|
|
91
|
+
expect(alice).toEqual([{ id: "a", owner: "alice" }]);
|
|
92
|
+
expect(bob).toEqual([{ id: "b", owner: "bob" }]);
|
|
93
|
+
expect(aliceAgain).toEqual([{ id: "a", owner: "alice" }]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("unstamped owner/superuser still sees every row", async () => {
|
|
97
|
+
const rows = await asOwner.raw(`SELECT id, owner FROM notes ORDER BY id`);
|
|
98
|
+
expect(rows).toEqual([
|
|
99
|
+
{ id: "a", owner: "alice" },
|
|
100
|
+
{ id: "b", owner: "bob" },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const LIVE_PG =
|
|
106
|
+
process.env.OKE_TEST_DOCKER === "1" &&
|
|
107
|
+
(process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL)?.startsWith("postgres");
|
|
108
|
+
if (!LIVE_PG) {
|
|
109
|
+
console.log("skip: live Postgres RLS isolation needs OKE_TEST_DOCKER=1 and DATABASE_URL");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("sql-session RLS isolation (live postgres)", () => {
|
|
113
|
+
const live = LIVE_PG ? test : test.skip;
|
|
114
|
+
const table = "oke_rls_isolation_notes";
|
|
115
|
+
|
|
116
|
+
live(
|
|
117
|
+
"split stamp isolates concurrent identities",
|
|
118
|
+
async () => {
|
|
119
|
+
const pg = await connectPostgres({
|
|
120
|
+
url: process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL,
|
|
121
|
+
role: "primary",
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
await pg.exec(`DROP TABLE IF EXISTS ${table}`);
|
|
125
|
+
await pg.exec(
|
|
126
|
+
`CREATE TABLE ${table} (id text PRIMARY KEY, owner text NOT NULL, body text NOT NULL)`,
|
|
127
|
+
);
|
|
128
|
+
await pg.exec(`INSERT INTO ${table} VALUES ('a', 'alice', 'hi'), ('b', 'bob', 'yo')`);
|
|
129
|
+
await pg.exec(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY`);
|
|
130
|
+
await installOkeRlsHelpers((sql) => pg.exec(sql));
|
|
131
|
+
await pg.exec(
|
|
132
|
+
`CREATE POLICY owner_select ON ${table} AS PERMISSIVE FOR SELECT TO public USING (owner = oke.user())`,
|
|
133
|
+
);
|
|
134
|
+
const alice = createSqlStoreHandle("sql:app", {
|
|
135
|
+
connection: pg,
|
|
136
|
+
classifications: new Map(),
|
|
137
|
+
routedRole: "primary",
|
|
138
|
+
domainDdl: "off",
|
|
139
|
+
rls: { gate: "member", userId: "alice", scopes: ["member"] },
|
|
140
|
+
});
|
|
141
|
+
const bob = createSqlStoreHandle("sql:app", {
|
|
142
|
+
connection: pg,
|
|
143
|
+
classifications: new Map(),
|
|
144
|
+
routedRole: "primary",
|
|
145
|
+
domainDdl: "off",
|
|
146
|
+
rls: { gate: "member", userId: "bob", scopes: ["member"] },
|
|
147
|
+
});
|
|
148
|
+
const [a, b] = await Promise.all([
|
|
149
|
+
alice.raw(`SELECT id, owner FROM ${table} ORDER BY id`),
|
|
150
|
+
bob.raw(`SELECT id, owner FROM ${table} ORDER BY id`),
|
|
151
|
+
]);
|
|
152
|
+
expect(a).toEqual([{ id: "a", owner: "alice" }]);
|
|
153
|
+
expect(b).toEqual([{ id: "b", owner: "bob" }]);
|
|
154
|
+
} finally {
|
|
155
|
+
try {
|
|
156
|
+
await pg.exec(`DROP TABLE IF EXISTS ${table}`);
|
|
157
|
+
} catch {
|
|
158
|
+
// Best-effort cleanup.
|
|
159
|
+
}
|
|
160
|
+
await pg.close();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
20_000,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RLS stamps on pooled postgres must `reserve()` one slot, then BEGIN/SET
|
|
3
|
+
* on that slot. `begin()` + parent `unsafe()` checkouts a second slot and
|
|
4
|
+
* deadlocks a tiny pool (`checkout timeout` from PgDog).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { connectPostgres, type PostgresClientLike } from "../../drivers/postgres.ts";
|
|
9
|
+
import { createSqlStoreHandle } from "./sql-session.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Two-slot pool. `reserve()` holds a slot until `release()`. Unreserved
|
|
13
|
+
* `BEGIN` occupies a slot until COMMIT on that same checkout.
|
|
14
|
+
*/
|
|
15
|
+
function createTinyPoolClient(size = 2): PostgresClientLike & {
|
|
16
|
+
readonly reserveCalls: { n: number };
|
|
17
|
+
readonly rootBegins: { n: number };
|
|
18
|
+
} {
|
|
19
|
+
const slots = Array.from({ length: size }, () => ({ held: false }));
|
|
20
|
+
const reserveCalls = { n: 0 };
|
|
21
|
+
const rootBegins = { n: 0 };
|
|
22
|
+
|
|
23
|
+
function checkout(): { held: boolean } {
|
|
24
|
+
const idle = slots.find((slot) => !slot.held);
|
|
25
|
+
if (!idle) {
|
|
26
|
+
throw new Error("checkout timeout");
|
|
27
|
+
}
|
|
28
|
+
return idle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rowsFor(sql: string): Record<string, unknown>[] {
|
|
32
|
+
return /select/i.test(sql) && !/set_config/i.test(sql) ? [{ id: "a", owner: "alice" }] : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const client: PostgresClientLike & {
|
|
36
|
+
readonly reserveCalls: { n: number };
|
|
37
|
+
readonly rootBegins: { n: number };
|
|
38
|
+
} = {
|
|
39
|
+
reserveCalls,
|
|
40
|
+
rootBegins,
|
|
41
|
+
async reserve() {
|
|
42
|
+
reserveCalls.n += 1;
|
|
43
|
+
const slot = checkout();
|
|
44
|
+
slot.held = true;
|
|
45
|
+
return {
|
|
46
|
+
async unsafe(sql) {
|
|
47
|
+
return rowsFor(sql);
|
|
48
|
+
},
|
|
49
|
+
release() {
|
|
50
|
+
slot.held = false;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
async unsafe(sql) {
|
|
55
|
+
const slot = checkout();
|
|
56
|
+
const head = sql.trim().split(/\s+/)[0]?.toUpperCase();
|
|
57
|
+
if (head === "BEGIN") {
|
|
58
|
+
rootBegins.n += 1;
|
|
59
|
+
slot.held = true;
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
if (head === "COMMIT" || head === "ROLLBACK") {
|
|
63
|
+
slot.held = false;
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return rowsFor(sql);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
return client;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("sql-session RLS stamp on pooled postgres", () => {
|
|
73
|
+
test("pins the stamp through reserve — no leaked root BEGIN", async () => {
|
|
74
|
+
const pool = createTinyPoolClient(2);
|
|
75
|
+
const connection = await connectPostgres({ client: pool, role: "primary" });
|
|
76
|
+
const handle = createSqlStoreHandle("sql:app", {
|
|
77
|
+
connection,
|
|
78
|
+
classifications: new Map(),
|
|
79
|
+
routedRole: "primary",
|
|
80
|
+
domainDdl: "off",
|
|
81
|
+
rls: { gate: "member", userId: "alice", scopes: ["member"] },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const [a, b] = await Promise.all([
|
|
85
|
+
handle.raw(`SELECT id, owner FROM notes`),
|
|
86
|
+
handle.raw(`SELECT id, owner FROM notes`),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
expect(a).toEqual([{ id: "a", owner: "alice" }]);
|
|
90
|
+
expect(b).toEqual([{ id: "a", owner: "alice" }]);
|
|
91
|
+
expect(pool.reserveCalls.n).toBe(2);
|
|
92
|
+
expect(pool.rootBegins.n).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("table browse SELECT pins through reserve like any other stamped query", async () => {
|
|
96
|
+
const pool = createTinyPoolClient(2);
|
|
97
|
+
const connection = await connectPostgres({ client: pool, role: "primary" });
|
|
98
|
+
const handle = createSqlStoreHandle("sql:app", {
|
|
99
|
+
connection,
|
|
100
|
+
classifications: new Map(),
|
|
101
|
+
routedRole: "primary",
|
|
102
|
+
domainDdl: "off",
|
|
103
|
+
rls: { gate: "member", userId: "alice", scopes: ["member"] },
|
|
104
|
+
});
|
|
105
|
+
const rows = await handle.raw(`SELECT * FROM "notes" LIMIT 50`);
|
|
106
|
+
expect(rows).toEqual([{ id: "a", owner: "alice" }]);
|
|
107
|
+
expect(pool.reserveCalls.n).toBe(1);
|
|
108
|
+
expect(pool.rootBegins.n).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("standalone BEGIN on a 2-slot pool reproduces checkout timeout", async () => {
|
|
112
|
+
const pool = createTinyPoolClient(2);
|
|
113
|
+
await expect(pool.unsafe("BEGIN")).resolves.toEqual([]);
|
|
114
|
+
await expect(pool.unsafe("BEGIN")).resolves.toEqual([]);
|
|
115
|
+
await expect(pool.unsafe("SET LOCAL ROLE oke_app")).rejects.toThrow("checkout timeout");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import type { DomainDdlMode } from "../../config/index.ts";
|
|
9
9
|
import type { ClassificationMap, SqlConnection, SqlRow } from "../../drivers/types.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildRlsIdentityPreludeSql,
|
|
12
|
+
installOkeRlsHelpers,
|
|
13
|
+
RLS_CONTEXT_DRIVERS,
|
|
14
|
+
type RlsIdentity,
|
|
15
|
+
} from "../../drivers/pg-rls.ts";
|
|
10
16
|
import { throwOke } from "../../kernel/errors.ts";
|
|
11
17
|
import { maskRows, tableFromSql } from "./classify.ts";
|
|
12
18
|
import { isMissingDomainRelationError } from "./missing-relation.ts";
|
|
@@ -28,6 +34,45 @@ import {
|
|
|
28
34
|
|
|
29
35
|
export type { WhereMap } from "./sql-condition.ts";
|
|
30
36
|
|
|
37
|
+
/** Serialize stamp frames on a shared connection so concurrent identities cannot interleave. */
|
|
38
|
+
const rlsStampTails = new WeakMap<SqlConnection, Promise<unknown>>();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when SQL must not enter an RLS identity frame (txn control or DDL).
|
|
42
|
+
*
|
|
43
|
+
* @param sql - Statement
|
|
44
|
+
*/
|
|
45
|
+
export function isRlsStampExemptSql(sql: string): boolean {
|
|
46
|
+
return /^\s*(begin|commit|rollback|create|alter|drop|truncate|grant|revoke|comment)\b/i.test(sql);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run `fn` after any in-flight stamp on `connection` finishes.
|
|
51
|
+
*
|
|
52
|
+
* @param connection - Shared SQL connection
|
|
53
|
+
* @param fn - Stamp frame
|
|
54
|
+
*/
|
|
55
|
+
async function withRlsStampLock<T>(connection: SqlConnection, fn: () => Promise<T>): Promise<T> {
|
|
56
|
+
const prev = rlsStampTails.get(connection) ?? Promise.resolve();
|
|
57
|
+
let release!: () => void;
|
|
58
|
+
const gate = new Promise<void>((resolve) => {
|
|
59
|
+
release = resolve;
|
|
60
|
+
});
|
|
61
|
+
rlsStampTails.set(
|
|
62
|
+
connection,
|
|
63
|
+
prev.then(
|
|
64
|
+
() => gate,
|
|
65
|
+
() => gate,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
await prev.catch(() => undefined);
|
|
69
|
+
try {
|
|
70
|
+
return await fn();
|
|
71
|
+
} finally {
|
|
72
|
+
release();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
31
76
|
/** Options for a SQL session. */
|
|
32
77
|
export interface SqlSessionOptions {
|
|
33
78
|
/** Open connection (already role-routed). */
|
|
@@ -44,6 +89,8 @@ export interface SqlSessionOptions {
|
|
|
44
89
|
* and local+autoPush). Default `ensure` for backward-compatible tests.
|
|
45
90
|
*/
|
|
46
91
|
readonly domainDdl?: DomainDdlMode;
|
|
92
|
+
/** Gate identity — stamps `oke.*` on postgres / pglite. */
|
|
93
|
+
readonly rls?: RlsIdentity;
|
|
47
94
|
}
|
|
48
95
|
|
|
49
96
|
/**
|
|
@@ -339,8 +386,9 @@ export function createSqlStoreHandle(
|
|
|
339
386
|
ref: `sql:${string}`,
|
|
340
387
|
options: SqlSessionOptions,
|
|
341
388
|
): SqlStoreHandle {
|
|
342
|
-
const { connection, classifications, revealPii } = options;
|
|
389
|
+
const { connection, classifications, revealPii, rls } = options;
|
|
343
390
|
const domainDdl: DomainDdlMode = options.domainDdl ?? "ensure";
|
|
391
|
+
let helpersReady = false;
|
|
344
392
|
|
|
345
393
|
function mask(rows: SqlRow[], table?: string): SqlRow[] {
|
|
346
394
|
return maskRows(rows, { classifications, table, revealPii });
|
|
@@ -362,11 +410,41 @@ export function createSqlStoreHandle(
|
|
|
362
410
|
}
|
|
363
411
|
|
|
364
412
|
function query(sql: string, params: readonly unknown[] = []): Promise<SqlRow[]> {
|
|
365
|
-
return withSchemaGuard(() =>
|
|
413
|
+
return withSchemaGuard(() => withRlsStamp((conn) => conn.query(sql, params), sql));
|
|
366
414
|
}
|
|
367
415
|
|
|
368
416
|
function exec(sql: string, params: readonly unknown[] = []): Promise<{ changes: number }> {
|
|
369
|
-
return withSchemaGuard(() =>
|
|
417
|
+
return withSchemaGuard(() => withRlsStamp((conn) => conn.exec(sql, params), sql));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function withRlsStamp<T>(fn: (conn: SqlConnection) => Promise<T>, sql: string): Promise<T> {
|
|
421
|
+
if (!rls || !RLS_CONTEXT_DRIVERS.has(connection.driverId)) return fn(connection);
|
|
422
|
+
if (isRlsStampExemptSql(sql)) return fn(connection);
|
|
423
|
+
const run = (): Promise<T> => applyRlsStamp(fn);
|
|
424
|
+
// PGlite is one backend session — concurrent identities must not interleave.
|
|
425
|
+
// Pooled postgres pins each stamp via `transaction()` instead.
|
|
426
|
+
if (connection.driverId === "pglite") {
|
|
427
|
+
return withRlsStampLock(connection, run);
|
|
428
|
+
}
|
|
429
|
+
return run();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function applyRlsStamp<T>(fn: (conn: SqlConnection) => Promise<T>): Promise<T> {
|
|
433
|
+
if (!rls) return fn(connection);
|
|
434
|
+
if (!helpersReady) {
|
|
435
|
+
await installOkeRlsHelpers((sql) => connection.exec(sql));
|
|
436
|
+
helpersReady = true;
|
|
437
|
+
}
|
|
438
|
+
const frame = async (tx: SqlConnection): Promise<T> => {
|
|
439
|
+
for (const stmt of buildRlsIdentityPreludeSql(rls)) {
|
|
440
|
+
await tx.exec(stmt.sql, stmt.params ?? []);
|
|
441
|
+
}
|
|
442
|
+
return fn(tx);
|
|
443
|
+
};
|
|
444
|
+
if (!connection.transaction) {
|
|
445
|
+
throw new Error("RLS stamp needs SqlConnection.transaction to pin SET LOCAL");
|
|
446
|
+
}
|
|
447
|
+
return connection.transaction(frame);
|
|
370
448
|
}
|
|
371
449
|
|
|
372
450
|
async function ensureFromMeta(table: TableHandle | unknown): Promise<void> {
|
|
@@ -45,7 +45,7 @@ describe("fx.store(db).upsert via app.fetch (real boot)", () => {
|
|
|
45
45
|
const db = store.sql("app", { schema: { notes } });
|
|
46
46
|
|
|
47
47
|
const seed = on(
|
|
48
|
-
http.post("/seed").
|
|
48
|
+
http.post("/seed").public(),
|
|
49
49
|
flow("notes.seed", {
|
|
50
50
|
in: UpsertIn,
|
|
51
51
|
out: UpsertOut,
|
package/src/elements/store.ts
CHANGED
|
@@ -26,6 +26,11 @@ export {
|
|
|
26
26
|
schema,
|
|
27
27
|
schemaTable,
|
|
28
28
|
schemaRelations,
|
|
29
|
+
schemaRls,
|
|
30
|
+
schemaPolicy,
|
|
31
|
+
schemaPolicyGate,
|
|
32
|
+
schemaPolicyOwner,
|
|
33
|
+
schemaPolicyScope,
|
|
29
34
|
isSchemaTableDecl,
|
|
30
35
|
isSchemaColumnDecl,
|
|
31
36
|
isSchemaRelationsDecl,
|
|
@@ -43,6 +48,8 @@ export type {
|
|
|
43
48
|
SchemaTableDecl,
|
|
44
49
|
SchemaTableWithColumns,
|
|
45
50
|
SchemaRelationsDecl,
|
|
51
|
+
SchemaPolicyDecl,
|
|
52
|
+
SchemaTableExtra,
|
|
46
53
|
SchemaRelationEntry,
|
|
47
54
|
SchemaRelationOne,
|
|
48
55
|
SchemaRelationMany,
|
|
@@ -123,6 +130,13 @@ export type {
|
|
|
123
130
|
WhereMap,
|
|
124
131
|
} from "./store/sql-session.ts";
|
|
125
132
|
|
|
133
|
+
export {
|
|
134
|
+
resolveRlsIdentity,
|
|
135
|
+
rlsIdentityFromAuth,
|
|
136
|
+
firstPolicyOrPublic,
|
|
137
|
+
policyGateForScopes,
|
|
138
|
+
} from "./store/rls-identity.ts";
|
|
139
|
+
export type { RlsIdentityRow, ResolveRlsIdentityInput } from "./store/rls-identity.ts";
|
|
126
140
|
export { createStoreRuntime, putTier2 } from "./store/runtime.ts";
|
|
127
141
|
export type {
|
|
128
142
|
StoreRuntime,
|
|
@@ -450,6 +450,23 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
|
|
|
450
450
|
* @param keyRow - That version's live wrapped DEK
|
|
451
451
|
*/
|
|
452
452
|
async function decryptRow(row: SecretRow, keyRow: KeyRow): Promise<string> {
|
|
453
|
+
try {
|
|
454
|
+
return await decryptRowOnce(row, keyRow);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// Stale key snapshot: rotation committed and dropped `pending` after
|
|
457
|
+
// this read. Reload the wrap and retry once — never return torn bytes.
|
|
458
|
+
if (!(err instanceof VaultError) || err.code !== "INVALID_KEY") throw err;
|
|
459
|
+
return await decryptRowOnce(row, await readKeyRow(row.id));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Unwrap the DEK and decrypt the value ciphertext.
|
|
465
|
+
*
|
|
466
|
+
* @param row - Stored secret version
|
|
467
|
+
* @param keyRow - That version's wrapped DEK
|
|
468
|
+
*/
|
|
469
|
+
async function decryptRowOnce(row: SecretRow, keyRow: KeyRow): Promise<string> {
|
|
453
470
|
const path = row.path;
|
|
454
471
|
const version = toInt(row.version);
|
|
455
472
|
const algorithm = row.algorithm as VaultAlgorithm;
|
package/src/full.ts
CHANGED
package/src/http.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ```ts
|
|
9
9
|
* import { on, flow, http, gate, oke, createBunRuntime } from "okengine/http";
|
|
10
10
|
*
|
|
11
|
-
* on(http.get("/").
|
|
11
|
+
* on(http.get("/").public(), flow("ping", { do: () => "Hi" }));
|
|
12
12
|
* const app = oke({ name: "ping" });
|
|
13
13
|
* createBunRuntime().serve(app);
|
|
14
14
|
* ```
|
|
@@ -24,15 +24,32 @@ export { fail, type FlowFailure } from "./kernel/errors.ts";
|
|
|
24
24
|
export { isFlowFailure } from "./kernel/hooks.ts";
|
|
25
25
|
export { plugin, isPlugin, type PluginDef } from "./kernel/plugin.ts";
|
|
26
26
|
export { createBunRuntime, APP_PORT, type Runtime, type ServeOptions } from "./runtime/index.ts";
|
|
27
|
-
export type {
|
|
27
|
+
export type {
|
|
28
|
+
OkeApp,
|
|
29
|
+
OkeOptions,
|
|
30
|
+
ReadyState,
|
|
31
|
+
RegisteredFlowUnits,
|
|
32
|
+
RoutesFromRegisteredUnits,
|
|
33
|
+
} from "./kernel/app.ts";
|
|
34
|
+
export { registerFlowUnits } from "./kernel/flow-units.ts";
|
|
35
|
+
export { stampFlowName, stampHttpPath } from "./kernel/stamp-http.ts";
|
|
28
36
|
|
|
29
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
oke as okeCore,
|
|
39
|
+
type OkeApp,
|
|
40
|
+
type OkeOptions,
|
|
41
|
+
type RoutesFromRegisteredUnits,
|
|
42
|
+
} from "./kernel/app.ts";
|
|
30
43
|
|
|
31
44
|
/**
|
|
32
45
|
* Create an HTTP-oriented app — defaults `router` to `"edge"` when omitted.
|
|
33
46
|
*
|
|
34
47
|
* @param options - Application options
|
|
35
48
|
*/
|
|
49
|
+
export function oke(
|
|
50
|
+
options: OkeOptions & { readonly registry: "ignore" },
|
|
51
|
+
): OkeApp<Record<string, never>, Record<string, never>>;
|
|
52
|
+
export function oke(options: OkeOptions): OkeApp<{}, RoutesFromRegisteredUnits>;
|
|
36
53
|
export function oke(options: OkeOptions): OkeApp {
|
|
37
54
|
return okeCore({
|
|
38
55
|
...options,
|
package/src/index.ts
CHANGED
|
@@ -14,10 +14,19 @@
|
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
oke,
|
|
19
|
+
type OkeApp,
|
|
20
|
+
type OkeOptions,
|
|
21
|
+
type ReadyState,
|
|
22
|
+
type RegisteredFlowUnits,
|
|
23
|
+
type RoutesFromRegisteredUnits,
|
|
24
|
+
} from "./kernel/app.ts";
|
|
18
25
|
export { on, type Binding } from "./kernel/on.ts";
|
|
19
26
|
export { flow, isFlow, type FlowDef } from "./kernel/flow.ts";
|
|
20
27
|
export { http, every, internal, table } from "./kernel/triggers.ts";
|
|
28
|
+
export { registerFlowUnits } from "./kernel/flow-units.ts";
|
|
29
|
+
export { stampFlowName, stampHttpPath } from "./kernel/stamp-http.ts";
|
|
21
30
|
export { fail, type FlowFailure, type FlowErrorValue } from "./kernel/errors.ts";
|
|
22
31
|
export { isFlowFailure } from "./kernel/hooks.ts";
|
|
23
32
|
export { plugin, isPlugin, type PluginDef, type PluginCapabilities } from "./kernel/plugin.ts";
|
|
@@ -71,6 +80,8 @@ export {
|
|
|
71
80
|
type SignalDecl,
|
|
72
81
|
type SignalOptions,
|
|
73
82
|
type SignalRuntime,
|
|
83
|
+
type DeadLetter,
|
|
84
|
+
type SignalFailureReason,
|
|
74
85
|
} from "./elements/signal.ts";
|
|
75
86
|
|
|
76
87
|
export {
|
|
@@ -143,6 +154,8 @@ export {
|
|
|
143
154
|
type AiModelDecl,
|
|
144
155
|
type AiPromptDecl,
|
|
145
156
|
type AiAgentDecl,
|
|
157
|
+
type AiMcpServerDecl,
|
|
158
|
+
type AiMcpServerOptions,
|
|
146
159
|
type AiRuntime,
|
|
147
160
|
} from "./elements/ai.ts";
|
|
148
161
|
|
|
@@ -89,7 +89,7 @@ describe("boot-level: stale barrel through a real oke() boot", () => {
|
|
|
89
89
|
effects: {},
|
|
90
90
|
do: () => ({ ok: true as const }),
|
|
91
91
|
});
|
|
92
|
-
on(http.get("/health").
|
|
92
|
+
on(http.get("/health").public(), mainFlow);
|
|
93
93
|
const app = oke({ name: "stale-barrel", gate: { unguardedHttp: "allow" } });
|
|
94
94
|
|
|
95
95
|
await expect(
|