okengine 0.17.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +14 -10
  3. package/manifest.v1.schema.json +61 -2
  4. package/package.json +19 -5
  5. package/site/content/docs/ai/index.mdx +1 -1
  6. package/site/content/docs/ai/mcp.mdx +10 -1
  7. package/site/content/docs/ai/skills.mdx +9 -9
  8. package/site/content/docs/elements/ai.mdx +1 -1
  9. package/site/content/docs/elements/clock.mdx +1 -1
  10. package/site/content/docs/elements/flow.mdx +25 -1
  11. package/site/content/docs/elements/gate.mdx +3 -2
  12. package/site/content/docs/elements/store.mdx +289 -341
  13. package/site/content/docs/elements/vault.mdx +5 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +3 -10
  15. package/site/content/docs/get-started/index.mdx +1 -1
  16. package/site/content/docs/get-started/installation.mdx +2 -3
  17. package/site/content/docs/get-started/introduction.mdx +58 -121
  18. package/site/content/docs/get-started/meta.json +9 -1
  19. package/site/content/docs/get-started/project-structure.mdx +4 -11
  20. package/site/content/docs/get-started/testing.mdx +328 -0
  21. package/site/content/docs/get-started/why.mdx +93 -71
  22. package/site/content/docs/index.mdx +44 -11
  23. package/site/content/docs/meta.json +8 -5
  24. package/site/content/docs/plugins/apple.mdx +151 -0
  25. package/site/content/docs/plugins/discord.mdx +139 -0
  26. package/site/content/docs/plugins/facebook.mdx +134 -0
  27. package/site/content/docs/plugins/figma.mdx +138 -0
  28. package/site/content/docs/plugins/github.mdx +138 -0
  29. package/site/content/docs/plugins/google.mdx +153 -0
  30. package/site/content/docs/plugins/index.mdx +47 -1
  31. package/site/content/docs/plugins/meta.json +10 -0
  32. package/site/content/docs/plugins/microsoft.mdx +151 -0
  33. package/site/content/docs/plugins/oauth.mdx +188 -0
  34. package/site/content/docs/plugins/x.mdx +125 -0
  35. package/site/content/docs/providers/index.mdx +2 -0
  36. package/site/content/docs/recipes/index.mdx +2 -0
  37. package/site/content/docs/reference/cli.md +3 -2
  38. package/site/content/docs/reference/client.mdx +58 -1
  39. package/site/content/docs/reference/configuration.mdx +2 -4
  40. package/site/content/docs/reference/fx.mdx +3 -1
  41. package/site/content/docs/reference/index.mdx +0 -5
  42. package/site/content/docs/reference/meta.json +2 -2
  43. package/site/content/docs/reference/okid.mdx +137 -0
  44. package/src/auth/bindings.ts +1 -1
  45. package/src/auth/config.ts +9 -0
  46. package/src/auth/identity-sql.ts +314 -0
  47. package/src/auth/identity.ts +140 -2
  48. package/src/auth/index.ts +17 -1
  49. package/src/auth/method-context.ts +3 -0
  50. package/src/auth/oauth-as/cimd.ts +132 -0
  51. package/src/auth/oauth-as/crypto.test.ts +101 -0
  52. package/src/auth/oauth-as/crypto.ts +393 -0
  53. package/src/auth/oauth-as/errors.ts +68 -0
  54. package/src/auth/oauth-as/http.test.ts +419 -0
  55. package/src/auth/oauth-as/http.ts +842 -0
  56. package/src/auth/oauth-as/stores.ts +61 -0
  57. package/src/auth/oauth-as/tables.ts +142 -0
  58. package/src/auth/tables.ts +0 -11
  59. package/src/bench/README.md +83 -0
  60. package/src/bench/REPORT.md +176 -0
  61. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  62. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  63. package/src/bench/g03-signal-once.bench.ts +157 -0
  64. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  65. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  66. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  67. package/src/bench/g05-sustained-full.bench.ts +265 -0
  68. package/src/bench/g06-mixed-load.bench.ts +260 -0
  69. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  70. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  71. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  72. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  73. package/src/bench/g08-store-sql.bench.ts +178 -0
  74. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  75. package/src/bench/g10-observability-contention.bench.ts +246 -0
  76. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  77. package/src/bench/g13-elements.bench.ts +427 -0
  78. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  79. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  80. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  81. package/src/bench/lib/event-loop-lag.ts +26 -0
  82. package/src/bench/lib/infra.ts +60 -0
  83. package/src/bench/lib/report.ts +52 -0
  84. package/src/bench/lib/rss-sampler.ts +61 -0
  85. package/src/bench/lib/signal-pg.ts +88 -0
  86. package/src/bench/load-app.ts +337 -0
  87. package/src/bench/load-child.ts +108 -0
  88. package/src/bench/smoke.bench.ts +43 -0
  89. package/src/cli/competitor-mention-removal.test.ts +28 -0
  90. package/src/cli/doctor-fd.ts +117 -0
  91. package/src/cli/doctor.test.ts +192 -0
  92. package/src/cli/doctor.ts +129 -1
  93. package/src/client/create.ts +95 -1
  94. package/src/client/index.ts +9 -2
  95. package/src/client/transport.ts +11 -4
  96. package/src/client/use-live-query.ts +154 -0
  97. package/src/client-react/index.ts +15 -1
  98. package/src/client-react/live-resource.ts +246 -0
  99. package/src/client-react/use-live-query.test.ts +475 -0
  100. package/src/client-react/use-live-query.ts +530 -0
  101. package/src/compiler/extract.test.ts +518 -0
  102. package/src/compiler/extract.ts +386 -19
  103. package/src/console/server/invoke-user-flow.ts +2 -1
  104. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
  105. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
  107. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
  108. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  109. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
  110. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
  111. package/src/console/ui-next/dist/index.html +1 -1
  112. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  113. package/src/drivers/cdc-outbox.ts +389 -0
  114. package/src/drivers/memory.ts +20 -0
  115. package/src/drivers/oauth-apple.ts +156 -0
  116. package/src/drivers/oauth-discord.ts +79 -0
  117. package/src/drivers/oauth-facebook.ts +80 -0
  118. package/src/drivers/oauth-figma.ts +116 -0
  119. package/src/drivers/oauth-github.ts +92 -0
  120. package/src/drivers/oauth-google.ts +142 -0
  121. package/src/drivers/oauth-microsoft.ts +174 -0
  122. package/src/drivers/oauth-oidc.ts +293 -0
  123. package/src/drivers/oauth-shared.ts +326 -0
  124. package/src/drivers/oauth-types.ts +159 -0
  125. package/src/drivers/oauth-x.ts +77 -0
  126. package/src/drivers/oauth2-common.ts +95 -0
  127. package/src/drivers/oauth2-token.ts +61 -0
  128. package/src/drivers/pg-rls-row-passes.ts +251 -0
  129. package/src/drivers/pg-rls.ts +2 -0
  130. package/src/drivers/postgres.ts +45 -2
  131. package/src/drivers/signal-postgres.ts +2 -1
  132. package/src/elements/channel/runtime.ts +29 -2
  133. package/src/elements/channel.test.ts +52 -0
  134. package/src/elements/gate/boot.ts +29 -2
  135. package/src/elements/store/emit-drizzle.ts +147 -14
  136. package/src/elements/store/field-ddl.test.ts +118 -0
  137. package/src/elements/store/field-types.test.ts +455 -0
  138. package/src/elements/store/list-query.golden.json +777 -0
  139. package/src/elements/store/list-query.parity.test.ts +396 -0
  140. package/src/elements/store/list-query.ts +792 -0
  141. package/src/elements/store/live-default.test.ts +136 -0
  142. package/src/elements/store/live-http.test.ts +160 -0
  143. package/src/elements/store/live-isolation.test.ts +291 -0
  144. package/src/elements/store/live-query-runtime.test.ts +323 -0
  145. package/src/elements/store/live-query-runtime.ts +403 -0
  146. package/src/elements/store/live-query-server.test.ts +377 -0
  147. package/src/elements/store/live-query-server.ts +102 -0
  148. package/src/elements/store/live-query.ts +97 -0
  149. package/src/elements/store/resource.ts +189 -680
  150. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  151. package/src/elements/store/schema-decl.ts +539 -41
  152. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  153. package/src/elements/store/sql-session.ts +297 -35
  154. package/src/elements/store/table.ts +102 -21
  155. package/src/elements/store.test.ts +3 -1
  156. package/src/elements/store.ts +12 -1
  157. package/src/elements/vault/chaos-child.ts +74 -1
  158. package/src/elements/vault/chaos.test.ts +4 -2
  159. package/src/elements/vault/storage.ts +4 -2
  160. package/src/index.ts +5 -2
  161. package/src/kernel/app-auth.ts +1 -0
  162. package/src/kernel/app.ts +116 -2
  163. package/src/kernel/auth-sharing.test.ts +196 -0
  164. package/src/kernel/boot.test.ts +3 -3
  165. package/src/kernel/errors.ts +8 -0
  166. package/src/kernel/fx.test.ts +1 -0
  167. package/src/kernel/fx.ts +14 -2
  168. package/src/kernel/horizontal-child.ts +2 -1
  169. package/src/kernel/http-resource.ts +33 -7
  170. package/src/kernel/identity-host-persist.test.ts +119 -0
  171. package/src/kernel/instance-id.ts +4 -2
  172. package/src/kernel/journal.ts +2 -1
  173. package/src/kernel/mcp-tool.test.ts +95 -0
  174. package/src/kernel/on.ts +9 -0
  175. package/src/kernel/realtime-bind.ts +326 -0
  176. package/src/kernel/resource-live.ts +117 -0
  177. package/src/kernel/triggers.ts +86 -4
  178. package/src/manifest/diff.ts +37 -0
  179. package/src/manifest/types.ts +64 -2
  180. package/src/okid.bench.test.ts +64 -0
  181. package/src/okid.test.ts +338 -0
  182. package/src/okid.ts +245 -0
  183. package/src/plugins/anonymous.ts +19 -1
  184. package/src/plugins/auth/shared.ts +15 -0
  185. package/src/plugins/index.ts +2 -0
  186. package/src/plugins/magic-link.ts +10 -8
  187. package/src/plugins/mcp-oauth.ts +208 -0
  188. package/src/plugins/oauth/flow-store.ts +117 -0
  189. package/src/plugins/oauth/link.ts +69 -0
  190. package/src/plugins/oauth/shared.ts +108 -0
  191. package/src/plugins/oauth/token-vault.ts +100 -0
  192. package/src/plugins/oauth.security.test.ts +535 -0
  193. package/src/plugins/oauth.ts +532 -0
  194. package/src/plugins/otp.ts +48 -6
  195. package/src/plugins/passkey.ts +20 -1
  196. package/src/plugins/two-factor.ts +11 -0
  197. package/src/plugins/username.ts +40 -7
  198. package/src/release/build-lib.ts +7 -1
  199. package/src/release/measure.ts +1 -0
  200. package/src/release/official-plugins.ts +4 -1
  201. package/src/runs/collect.ts +2 -1
  202. package/src/runs/drivers/files.ts +2 -1
  203. package/src/test/create-test-app.ts +114 -5
  204. package/src/test/export-bundle.test.ts +33 -0
  205. package/src/test/live-signals.test.ts +83 -0
  206. package/src/test/tenant-isolation.test.ts +175 -0
  207. package/src/testing.ts +26 -0
  208. package/src/upgrade/codemods.ts +1 -1
  209. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -0,0 +1,665 @@
1
+ /**
2
+ * RLS parity gate — `oke.row_passes_policies` replay vs native Postgres RLS.
3
+ *
4
+ * BLOCKING GATE for the Realtime round: every case constructs a real table,
5
+ * installs real `CREATE POLICY` DDL, opens a REAL stamped store handle per
6
+ * identity (same `rls` prelude production uses — `SET LOCAL ROLE oke_app`,
7
+ * `set_config('oke.*')`, pinned transaction), reads ground truth natively,
8
+ * then asserts the synthetic-JSONB replay returns the IDENTICAL boolean.
9
+ *
10
+ * Ground truth per command:
11
+ * - SELECT/UPDATE/DELETE visibility → stamped point lookup `WHERE pk = $1`.
12
+ * - INSERT / WITH CHECK → stamped INSERT attempt (accepted = policy passed).
13
+ *
14
+ * Policy families covered (adversarial, not happy-path):
15
+ * owner · tenant · gate · scope(string)
16
+ * plus two mandatory multi-policy compositions:
17
+ * A. gate + owner — two PERMISSIVE policies, Postgres ORs them
18
+ * ("member + admin firehose" family)
19
+ * B. gate∧tenant compound USING + scope WITH CHECK ("tenant + scope" family)
20
+ *
21
+ * Run: bun test src/elements/store/rls-row-passes-policies.parity.test.ts
22
+ *
23
+ * Live-Postgres leg (parity must hold on real PG, not just PGlite):
24
+ * OKE_TEST_DOCKER=1 DATABASE_URL=postgres://… bun test src/elements/store/rls-row-passes-policies.parity.test.ts
25
+ */
26
+
27
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
28
+ import { connectPglite } from "../../drivers/pglite.ts";
29
+ import { installOkeRlsHelpers, type RlsIdentity } from "../../drivers/pg-rls.ts";
30
+ import {
31
+ ROW_PASSES_POLICIES_STATEMENTS as ROW_PASSES_STATEMENTS,
32
+ buildInlineRowPassesSql,
33
+ } from "../../drivers/pg-rls-row-passes.ts";
34
+ import type { SqlConnection } from "../../drivers/types.ts";
35
+ import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
36
+
37
+ /** Adversarial row image (JSONB payload shape mirrors table columns). */
38
+ type RowImage = Readonly<Record<string, unknown>>;
39
+
40
+ interface VisibilityCase {
41
+ readonly name: string;
42
+ readonly id: string;
43
+ readonly row: RowImage;
44
+ readonly visible: boolean;
45
+ }
46
+
47
+ const aliceMember: RlsIdentity = { gate: "member", userId: "alice", scopes: ["member"] };
48
+ const bobMember: RlsIdentity = { gate: "member", userId: "bob", scopes: ["member"] };
49
+ const acmeCreator: RlsIdentity = {
50
+ gate: "member",
51
+ userId: "u1",
52
+ scopes: ["member", "invoices:create"],
53
+ tenantId: "acme",
54
+ };
55
+ const acmeMemberNoScope: RlsIdentity = {
56
+ gate: "member",
57
+ userId: "u1",
58
+ scopes: ["member"],
59
+ tenantId: "acme",
60
+ };
61
+ const globexMember: RlsIdentity = {
62
+ gate: "member",
63
+ userId: "u1",
64
+ scopes: ["member"],
65
+ tenantId: "globex",
66
+ };
67
+
68
+ function handleFor(conn: SqlConnection, rls?: RlsIdentity): SqlStoreHandle {
69
+ return createSqlStoreHandle("sql:parity", {
70
+ connection: conn,
71
+ classifications: new Map(),
72
+ routedRole: "primary",
73
+ domainDdl: "off",
74
+ ...(rls ? { rls } : {}),
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Compare native ground truth and replay verdicts for one visibility case.
80
+ * Both probes run through the SAME stamped handle → identical GUC state.
81
+ */
82
+ async function assertVisibilityParity(
83
+ conn: SqlConnection,
84
+ identity: RlsIdentity,
85
+ table: string,
86
+ testCase: VisibilityCase,
87
+ ): Promise<void> {
88
+ const h = handleFor(conn, identity);
89
+ const errs: unknown[] = [];
90
+ let native = false;
91
+ let replay = false;
92
+
93
+ try {
94
+ const rows = await h.raw(`SELECT 1 AS ok FROM "${table}" WHERE id = ? LIMIT 1`, [testCase.id]);
95
+ native = rows.length > 0;
96
+ } catch (err) {
97
+ errs.push(err);
98
+ }
99
+ try {
100
+ const sql = buildInlineRowPassesSql(table, testCase.row, "SELECT");
101
+ const out = await h.raw(sql, []);
102
+ replay = out[0]?.ok === true;
103
+ } catch (err) {
104
+ errs.push(err);
105
+ }
106
+
107
+ if (errs.length > 0) throw errs[0];
108
+ expect(replay).toBe(testCase.visible);
109
+ expect(native).toBe(testCase.visible);
110
+ }
111
+
112
+ /** WITH CHECK parity via a real INSERT attempt under the stamp. */
113
+ async function assertInsertParity(
114
+ conn: SqlConnection,
115
+ identity: RlsIdentity,
116
+ table: string,
117
+ columns: string,
118
+ placeholders: string,
119
+ row: RowImage,
120
+ values: readonly unknown[],
121
+ ): Promise<void> {
122
+ const h = handleFor(conn, identity);
123
+ let accepted = false;
124
+ try {
125
+ await h.raw(`INSERT INTO "${table}" ${columns} VALUES ${placeholders}`, [...values]);
126
+ accepted = true;
127
+ } catch {
128
+ accepted = false;
129
+ }
130
+ const inline = buildInlineRowPassesSql(table, row, "INSERT");
131
+ const out = await h.raw(inline, []);
132
+ const replay = out[0]?.ok === true;
133
+ expect(replay).toBe(accepted);
134
+ }
135
+
136
+ describe("row_passes_policies install statements", () => {
137
+ test("statements are single-command (no statement stacking)", () => {
138
+ for (const stmt of ROW_PASSES_STATEMENTS) {
139
+ expect(
140
+ stmt.includes("COMMIT") ||
141
+ stmt.includes(";") === false ||
142
+ countTopLevelSemicolons(stmt) <= 0,
143
+ ).toBe(true);
144
+ }
145
+ });
146
+ });
147
+
148
+ function countTopLevelSemicolons(sql: string): number {
149
+ let inDollar = false;
150
+ let count = 0;
151
+ for (let i = 0; i < sql.length; i += 1) {
152
+ if (sql.startsWith("$fn$", i)) {
153
+ inDollar = !inDollar;
154
+ i += 3;
155
+ continue;
156
+ }
157
+ if (sql[i] === ";" && !inDollar) count += 1;
158
+ }
159
+ return count;
160
+ }
161
+
162
+ async function freshPglite(name: string): Promise<SqlConnection> {
163
+ const c = await connectPglite({ url: `memory://${name}`, role: "primary" });
164
+ await installOkeRlsHelpers((sql) => c.exec(sql));
165
+ for (const stmt of ROW_PASSES_STATEMENTS) await c.exec(stmt);
166
+ return c;
167
+ }
168
+
169
+ describe("RLS parity — owner family (pglite)", () => {
170
+ let conn: SqlConnection;
171
+ let seed: SqlStoreHandle;
172
+
173
+ beforeAll(async () => {
174
+ conn = await freshPglite("parity-owner");
175
+ await conn.exec(`CREATE TABLE notes (
176
+ id text PRIMARY KEY, owner text NOT NULL, body text NOT NULL)`);
177
+ await conn.exec(`GRANT SELECT, INSERT, UPDATE, DELETE ON notes TO oke_app`);
178
+ await conn.exec(`ALTER TABLE notes ENABLE ROW LEVEL SECURITY`);
179
+ await conn.exec(`CREATE POLICY owner_all ON notes
180
+ AS PERMISSIVE FOR ALL TO public USING (owner = oke.user()) WITH CHECK (owner = oke.user())`);
181
+ // Seed as the table owner (connection user bypasses RLS on own tables).
182
+ seed = handleFor(conn);
183
+ await seed.raw(`INSERT INTO notes VALUES ('a','alice','hi'), ('b','bob','yo')`);
184
+ }, 30_000);
185
+
186
+ afterAll(async () => {
187
+ await conn.close();
188
+ });
189
+
190
+ test("own row passes; other's row denied — SELECT and UPDATE contexts", async () => {
191
+ await assertVisibilityParity(conn, aliceMember, "notes", {
192
+ name: "alice sees own row",
193
+ id: "a",
194
+ row: { id: "a", owner: "alice", body: "hi" },
195
+ visible: true,
196
+ });
197
+ await assertVisibilityParity(conn, aliceMember, "notes", {
198
+ name: "alice denied bob row",
199
+ id: "b",
200
+ row: { id: "b", owner: "bob", body: "yo" },
201
+ visible: false,
202
+ });
203
+ await assertVisibilityParity(conn, bobMember, "notes", {
204
+ name: "bob before-image of alice-owned row invisible (revoked path)",
205
+ id: "a",
206
+ row: { id: "a", owner: "alice", body: "moved" },
207
+ visible: false,
208
+ });
209
+ });
210
+
211
+ test("empty userId denies everything (adversarial)", async () => {
212
+ await assertVisibilityParity(conn, { gate: "public", userId: "", scopes: [] }, "notes", {
213
+ name: "anonymous denied",
214
+ id: "a",
215
+ row: { id: "a", owner: "alice", body: "hi" },
216
+ visible: false,
217
+ });
218
+ });
219
+ });
220
+
221
+ describe("RLS parity — tenant family (pglite)", () => {
222
+ let conn: SqlConnection;
223
+
224
+ beforeAll(async () => {
225
+ conn = await freshPglite("parity-tenant");
226
+ await conn.exec(`CREATE TABLE bookings (
227
+ id text PRIMARY KEY, tenant_id text NOT NULL, body text NOT NULL)`);
228
+ await conn.exec(`GRANT SELECT, INSERT, UPDATE, DELETE ON bookings TO oke_app`);
229
+ await conn.exec(`ALTER TABLE bookings ENABLE ROW LEVEL SECURITY`);
230
+ await conn.exec(`CREATE POLICY tenant_all ON bookings
231
+ AS PERMISSIVE FOR ALL TO public
232
+ USING (tenant_id = oke.tenant()) WITH CHECK (tenant_id = oke.tenant())`);
233
+ await handleFor(conn).raw(`INSERT INTO bookings VALUES ('1','acme','a'), ('2','globex','b')`);
234
+ }, 30_000);
235
+
236
+ afterAll(async () => {
237
+ await conn.close();
238
+ });
239
+
240
+ test("matching tenant passes; cross-tenant denied", async () => {
241
+ await assertVisibilityParity(conn, acmeMemberNoScope, "bookings", {
242
+ name: "acme sees acme row",
243
+ id: "1",
244
+ row: { id: "1", tenant_id: "acme", body: "a" },
245
+ visible: true,
246
+ });
247
+ await assertVisibilityParity(conn, globexMember, "bookings", {
248
+ name: "globex denied acme row",
249
+ id: "1",
250
+ row: { id: "1", tenant_id: "acme", body: "a" },
251
+ visible: false,
252
+ });
253
+ });
254
+
255
+ test("reassigned-away row: after-image invisible, before-image replayable (CDC semantics)", async () => {
256
+ // Real CDC scenario: id=2 belonged to acme ("was-mine"), then an
257
+ // unstamped/admin UPDATE reassigned it to globex. Classifying that event
258
+ // for an acme subscriber means:
259
+ // 1. after-image on heap → native stamped lookup is EMPTY (revoked)
260
+ // 2. BEFORE-image {id:'2',tenant_id:'acme'} was visible under the
261
+ // acme stamp — `row_passes_policies` must agree (delete path).
262
+ const acme = handleFor(conn, acmeMemberNoScope);
263
+
264
+ // Simulate the reassignment as the table owner (a stamped acme session
265
+ // cannot push its own row out of tenant scope — mirrors production,
266
+ // where cross-tenant moves happen through admin/unstamped writes).
267
+ await handleFor(conn).raw(`UPDATE bookings SET tenant_id = 'globex' WHERE id = '2'`);
268
+
269
+ // Native ground truth: after-image invisible.
270
+ const rows = await acme.raw(`SELECT 1 AS ok FROM bookings WHERE id = '2' LIMIT 1`);
271
+ expect(rows.length).toBe(0);
272
+
273
+ // Replay parity: before-image visible.
274
+ const probe = buildInlineRowPassesSql(
275
+ "bookings",
276
+ { id: "2", tenant_id: "acme", body: "was-mine" },
277
+ "SELECT",
278
+ );
279
+ const out = await acme.raw(probe, []);
280
+ expect(out[0]?.ok).toBe(true);
281
+ });
282
+
283
+ test("missing tenant GUC denies everything", async () => {
284
+ await assertVisibilityParity(
285
+ conn,
286
+ { gate: "member", userId: "u1", scopes: ["member"] },
287
+ "bookings",
288
+ {
289
+ name: "no tenant stamp denied",
290
+ id: "1",
291
+ row: { id: "1", tenant_id: "acme", body: "a" },
292
+ visible: false,
293
+ },
294
+ );
295
+ });
296
+ });
297
+
298
+ describe("RLS parity — gate + scope families (pglite)", () => {
299
+ let conn: SqlConnection;
300
+
301
+ beforeAll(async () => {
302
+ conn = await freshPglite("parity-gate-scope");
303
+ await conn.exec(`CREATE TABLE secrets (
304
+ id text PRIMARY KEY, visibility text NOT NULL, payload text NOT NULL)`);
305
+ await conn.exec(`GRANT SELECT, INSERT ON secrets TO oke_app`);
306
+ await conn.exec(`ALTER TABLE secrets ENABLE ROW LEVEL SECURITY`);
307
+ await conn.exec(`CREATE POLICY member_select ON secrets
308
+ AS PERMISSIVE FOR SELECT TO public USING (oke.gate() = 'member')`);
309
+ await conn.exec(`CREATE POLICY scope_insert ON secrets
310
+ AS PERMISSIVE FOR INSERT TO public WITH CHECK (oke.has_scope('secrets:write'))`);
311
+ await handleFor(conn).raw(`INSERT INTO secrets VALUES ('s1','public','x')`);
312
+ }, 30_000);
313
+
314
+ afterAll(async () => {
315
+ await conn.close();
316
+ });
317
+
318
+ test("gate match passes; wrong gate denied", async () => {
319
+ await assertVisibilityParity(conn, aliceMember, "secrets", {
320
+ name: "member select allowed",
321
+ id: "s1",
322
+ row: { id: "s1", visibility: "public", payload: "x" },
323
+ visible: true,
324
+ });
325
+ await assertVisibilityParity(
326
+ conn,
327
+ { gate: "partner", userId: "u9", scopes: ["partner"] },
328
+ "secrets",
329
+ {
330
+ name: "partner gate denied",
331
+ id: "s1",
332
+ row: { id: "s1", visibility: "public", payload: "x" },
333
+ visible: false,
334
+ },
335
+ );
336
+ });
337
+
338
+ test("scope literal survives rewrite ('secrets:write' is not a column)", async () => {
339
+ await assertInsertParity(
340
+ conn,
341
+ { ...aliceMember, scopes: ["secrets:write"] },
342
+ "secrets",
343
+ "(id, visibility, payload)",
344
+ "(?, ?, ?)",
345
+ { id: "s2", visibility: "internal", payload: "y" },
346
+ ["s2", "internal", "y"],
347
+ );
348
+ await assertInsertParity(
349
+ conn,
350
+ { ...aliceMember, scopes: [] },
351
+ "secrets",
352
+ "(id, visibility, payload)",
353
+ "(?, ?, ?)",
354
+ { id: "s3", visibility: "internal", payload: "z" },
355
+ ["s3", "internal", "z"],
356
+ );
357
+ });
358
+ });
359
+
360
+ describe("RLS parity — composition A: gate + owner, two PERMISSIVE (pglite)", () => {
361
+ let conn: SqlConnection;
362
+
363
+ beforeAll(async () => {
364
+ conn = await freshPglite("parity-comp-a");
365
+ await conn.exec(`CREATE TABLE docs (
366
+ id text PRIMARY KEY, owner text NOT NULL, title text NOT NULL)`);
367
+ await conn.exec(`GRANT SELECT ON docs TO oke_app`);
368
+ await conn.exec(`ALTER TABLE docs ENABLE ROW LEVEL SECURITY`);
369
+ await conn.exec(`CREATE POLICY member_select ON docs
370
+ AS PERMISSIVE FOR SELECT TO public USING (oke.gate() = 'member')`);
371
+ await conn.exec(`CREATE POLICY owner_select ON docs
372
+ AS PERMISSIVE FOR SELECT TO public USING (owner = oke.user())`);
373
+ await handleFor(conn).raw(`INSERT INTO docs VALUES ('d1','zara','hers'), ('d2','ali','his')`);
374
+ }, 30_000);
375
+
376
+ afterAll(async () => {
377
+ await conn.close();
378
+ });
379
+
380
+ test("permissive OR — member non-owner via gate; guest owner via owner; guest stranger denied", async () => {
381
+ await assertVisibilityParity(conn, bobMember, "docs", {
382
+ name: "member non-owner sees via gate policy",
383
+ id: "d1",
384
+ row: { id: "d1", owner: "zara", title: "hers" },
385
+ visible: true,
386
+ });
387
+ await assertVisibilityParity(
388
+ conn,
389
+ { gate: "guest", userId: "ali", scopes: ["guest"] },
390
+ "docs",
391
+ {
392
+ name: "guest owner sees via owner policy",
393
+ id: "d2",
394
+ row: { id: "d2", owner: "ali", title: "his" },
395
+ visible: true,
396
+ },
397
+ );
398
+ await assertVisibilityParity(
399
+ conn,
400
+ { gate: "guest", userId: "mallory", scopes: ["guest"] },
401
+ "docs",
402
+ {
403
+ name: "guest stranger denied by both",
404
+ id: "d1",
405
+ row: { id: "d1", owner: "zara", title: "hers" },
406
+ visible: false,
407
+ },
408
+ );
409
+ });
410
+ });
411
+
412
+ describe("RLS parity — composition B: gate∧tenant compound + scope INSERT (pglite)", () => {
413
+ let conn: SqlConnection;
414
+
415
+ beforeAll(async () => {
416
+ conn = await freshPglite("parity-comp-b");
417
+ await conn.exec(`CREATE TABLE invoices (
418
+ id text PRIMARY KEY, tenant_id text NOT NULL, total integer NOT NULL)`);
419
+ await conn.exec(`GRANT SELECT, INSERT ON invoices TO oke_app`);
420
+ await conn.exec(`ALTER TABLE invoices ENABLE ROW LEVEL SECURITY`);
421
+ await conn.exec(`CREATE POLICY member_select ON invoices
422
+ AS PERMISSIVE FOR SELECT TO public
423
+ USING (oke.gate() = 'member' AND tenant_id = oke.tenant())`);
424
+ await conn.exec(`CREATE POLICY scope_insert ON invoices
425
+ AS PERMISSIVE FOR INSERT TO public WITH CHECK (oke.has_scope('invoices:create'))`);
426
+ await handleFor(conn).raw(`INSERT INTO invoices VALUES ('i1','acme',100)`);
427
+ }, 30_000);
428
+
429
+ afterAll(async () => {
430
+ await conn.close();
431
+ });
432
+
433
+ test("compound expression respects tenant conjunct", async () => {
434
+ await assertVisibilityParity(conn, acmeCreator, "invoices", {
435
+ name: "acme member sees own invoice",
436
+ id: "i1",
437
+ row: { id: "i1", tenant_id: "acme", total: 100 },
438
+ visible: true,
439
+ });
440
+ await assertVisibilityParity(conn, globexMember, "invoices", {
441
+ name: "globex member denied acme invoice",
442
+ id: "i1",
443
+ row: { id: "i1", tenant_id: "acme", total: 100 },
444
+ visible: false,
445
+ });
446
+ });
447
+
448
+ test("INSERT requires invoices:create scope even when tenant matches", async () => {
449
+ await assertInsertParity(
450
+ conn,
451
+ acmeMemberNoScope,
452
+ "invoices",
453
+ "(id, tenant_id, total)",
454
+ "(?, ?, ?)",
455
+ { id: "i2", tenant_id: "acme", total: 5 },
456
+ ["i2", "acme", 5],
457
+ );
458
+ await assertInsertParity(
459
+ conn,
460
+ acmeCreator,
461
+ "invoices",
462
+ "(id, tenant_id, total)",
463
+ "(?, ?, ?)",
464
+ { id: "i3", tenant_id: "acme", total: 7 },
465
+ ["i3", "acme", 7],
466
+ );
467
+ });
468
+ });
469
+
470
+ describe("RLS parity — regression gate: TO-role + command-clause selection (pglite)", () => {
471
+ // Added after the adversarial audit (release-blocking finding round):
472
+ //
473
+ // 1. TO-ROLE LEAK — a PERMISSIVE policy granted `TO <role>` where the stamp
474
+ // role is NOT a member must be invisible to native evaluation. The old
475
+ // replay ignored pg_policies.roles entirely, so an internal wide policy
476
+ // (TO internal_role USING (true)) falsely widened every subscriber's
477
+ // visibility through the replay path.
478
+ //
479
+ // 2. INSERT CLAUSE SELECTION — native INSERT evaluates WITH CHECK only;
480
+ // the old replay evaluated qual-first and skipped INSERT-only policies,
481
+ // diverging whenever a table mixes ALL/UPDATE policies with distinct
482
+ // USING/WITH CHECK clauses.
483
+
484
+ let conn: SqlConnection;
485
+
486
+ beforeAll(async () => {
487
+ conn = await freshPglite("parity-regressions");
488
+ await conn.exec(`CREATE TABLE tenant_wide (
489
+ id text PRIMARY KEY, tenant_id text NOT NULL, note text NOT NULL)`);
490
+ await conn.exec(`GRANT SELECT, INSERT ON tenant_wide TO oke_app`);
491
+ await conn.exec(`ALTER TABLE tenant_wide ENABLE ROW LEVEL SECURITY`);
492
+ await conn.exec(`CREATE POLICY tenant_sel ON tenant_wide
493
+ AS PERMISSIVE FOR SELECT TO public
494
+ USING (tenant_id = oke.tenant())`);
495
+ // Non-applicable role: oke_app has no membership in oke_internal —
496
+ // natively this policy never applies to stamped sessions.
497
+ await conn.exec(`DO $do$ BEGIN
498
+ CREATE ROLE oke_internal NOLOGIN;
499
+ EXCEPTION WHEN duplicate_object THEN NULL; END $do$`);
500
+ await conn.exec(`CREATE POLICY wide_internal ON tenant_wide
501
+ AS PERMISSIVE FOR SELECT TO oke_internal
502
+ USING (true)`);
503
+ await handleFor(conn).raw(`INSERT INTO tenant_wide VALUES ('1','acme','secret')`);
504
+
505
+ await conn.exec(`CREATE TABLE mixed_cmds (
506
+ id text PRIMARY KEY, tenant_id text NOT NULL)`);
507
+ await conn.exec(`GRANT SELECT, INSERT, UPDATE, DELETE ON mixed_cmds TO oke_app`);
508
+ await conn.exec(`ALTER TABLE mixed_cmds ENABLE ROW LEVEL SECURITY`);
509
+ // ALL policy with deliberately DISTINCT USING vs WITH CHECK:
510
+ await conn.exec(`CREATE POLICY upsert_all ON mixed_cmds
511
+ AS PERMISSIVE FOR ALL TO public
512
+ USING (tenant_id = oke.tenant())
513
+ WITH CHECK (tenant_id <> 'locked')`);
514
+ await handleFor(conn).raw(`INSERT INTO mixed_cmds VALUES ('0','base')`);
515
+ }, 30_000);
516
+
517
+ afterAll(async () => {
518
+ await conn.close();
519
+ });
520
+
521
+ test("regression: TO-restricted permissive policy must NOT widen replay visibility", async () => {
522
+ const globexMemberNoTenantMatch = { ...globexMember };
523
+ const h = handleFor(conn, globexMemberNoTenantMatch);
524
+ // Native ground truth: only tenant_sel applies (wide_internal is TO
525
+ // oke_internal); globex is not 'acme' → row invisible.
526
+ const rows = await h.raw(`SELECT 1 AS ok FROM "tenant_wide" WHERE id = ? LIMIT 1`, ["1"]);
527
+ expect(rows.length).toBe(0);
528
+ // Replay must agree — old fn returned true here (LEAK).
529
+ const inline = buildInlineRowPassesSql(
530
+ "tenant_wide",
531
+ { id: "1", tenant_id: "acme", note: "secret" },
532
+ "SELECT",
533
+ );
534
+ const out = await h.raw(inline, []);
535
+ expect(out[0]?.ok === true).toBe(false);
536
+ });
537
+
538
+ test("regression: INSERT evaluates WITH CHECK even when ALL-policy USING denies", async () => {
539
+ // Native: INSERT on mixed_cmds evaluates ONLY WC (tenant<>'locked') →
540
+ // a globex-stamped insert of an 'acme' row is ACCEPTED despite the
541
+ // USING clause disagreeing with the stamp's tenant.
542
+ let accepted = false;
543
+ try {
544
+ await handleFor(conn, globexMember).raw(
545
+ `INSERT INTO "mixed_cmds" ("id", "tenant_id") VALUES (?, ?)`,
546
+ ["9", "acme"],
547
+ );
548
+ accepted = true;
549
+ } catch {
550
+ accepted = false;
551
+ }
552
+ expect(accepted).toBe(true);
553
+ // Replay must agree — old fn evaluated USING (qual) first and denied.
554
+ const out = await handleFor(conn, globexMember).raw(
555
+ buildInlineRowPassesSql("mixed_cmds", { id: "9", tenant_id: "acme" }, "INSERT"),
556
+ [],
557
+ );
558
+ expect(out[0]?.ok === true).toBe(true);
559
+ });
560
+ });
561
+
562
+ /**
563
+ * Live-Postgres leg — same parity matrix executed against real Postgres.
564
+ * The rewrite algorithm is pure SQL text manipulation; the risky deltas
565
+ * between PGlite and real PG are catalog contents (pg_policies.qual
566
+ * formatting) and regex behavior. Both must hold identically or the gate
567
+ * fails loudly here.
568
+ */
569
+ const LIVE_PG =
570
+ process.env.OKE_TEST_DOCKER === "1" &&
571
+ (process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL)?.startsWith("postgres");
572
+ if (!LIVE_PG) {
573
+ console.log("skip: live Postgres parity needs OKE_TEST_DOCKER=1 and DATABASE_URL");
574
+ }
575
+
576
+ describe.skipIf(!LIVE_PG)("RLS parity — owner family (live postgres)", () => {
577
+ // describe.skipIf also skips beforeAll/afterAll — plain `test.skip` would
578
+ // still open a live PG connection and fail loud on ECONNREFUSED.
579
+ let conn: SqlConnection;
580
+
581
+ beforeAll(async () => {
582
+ const { connectPostgres } = await import("../../drivers/postgres.ts");
583
+ conn = await connectPostgres({
584
+ url: process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL!,
585
+ role: "primary",
586
+ });
587
+ await installOkeRlsHelpers((sql) => conn.exec(sql));
588
+ for (const stmt of ROW_PASSES_STATEMENTS) await conn.exec(stmt);
589
+ await conn.exec(`DROP TABLE IF EXISTS oke_parity_notes`);
590
+ await conn.exec(`CREATE TABLE oke_parity_notes (
591
+ id text PRIMARY KEY, owner text NOT NULL, body text NOT NULL)`);
592
+ await conn.exec(`GRANT SELECT, INSERT, UPDATE, DELETE ON oke_parity_notes TO oke_app`);
593
+ await conn.exec(`ALTER TABLE oke_parity_notes ENABLE ROW LEVEL SECURITY`);
594
+ await conn.exec(`CREATE POLICY owner_all ON oke_parity_notes
595
+ AS PERMISSIVE FOR ALL TO public USING (owner = oke.user()) WITH CHECK (owner = oke.user())`);
596
+ const seed = handleFor(conn);
597
+ await seed.raw(`INSERT INTO oke_parity_notes VALUES ('a','alice','hi'), ('b','bob','yo')`);
598
+ }, 30_000);
599
+
600
+ afterAll(async () => {
601
+ await conn.exec(`DROP TABLE IF EXISTS oke_parity_notes`);
602
+ await conn.close();
603
+ });
604
+
605
+ test("replay matches native stamped verdicts", async () => {
606
+ await assertVisibilityParity(conn, aliceMember, "oke_parity_notes", {
607
+ name: "alice sees own row",
608
+ id: "a",
609
+ row: { id: "a", owner: "alice", body: "hi" },
610
+ visible: true,
611
+ });
612
+ await assertVisibilityParity(conn, aliceMember, "oke_parity_notes", {
613
+ name: "alice denied bob row",
614
+ id: "b",
615
+ row: { id: "b", owner: "bob", body: "yo" },
616
+ visible: false,
617
+ });
618
+ });
619
+
620
+ test("compound + scope expressions survive PG catalog round-trip", async () => {
621
+ await conn.exec(`DROP TABLE IF EXISTS oke_parity_invoices`);
622
+ await conn.exec(`CREATE TABLE oke_parity_invoices (
623
+ id text PRIMARY KEY, tenant_id text NOT NULL, total integer NOT NULL)`);
624
+ await conn.exec(`GRANT SELECT, INSERT ON oke_parity_invoices TO oke_app`);
625
+ await conn.exec(`ALTER TABLE oke_parity_invoices ENABLE ROW LEVEL SECURITY`);
626
+ await conn.exec(`CREATE POLICY member_select ON oke_parity_invoices
627
+ AS PERMISSIVE FOR SELECT TO public
628
+ USING (oke.gate() = 'member' AND tenant_id = oke.tenant())`);
629
+ await conn.exec(`CREATE POLICY scope_insert ON oke_parity_invoices
630
+ AS PERMISSIVE FOR INSERT TO public WITH CHECK (oke.has_scope('invoices:create'))`);
631
+ await handleFor(conn).raw(`INSERT INTO oke_parity_invoices VALUES ('i1','acme',100)`);
632
+
633
+ await assertVisibilityParity(conn, acmeCreator, "oke_parity_invoices", {
634
+ name: "acme member sees own invoice",
635
+ id: "i1",
636
+ row: { id: "i1", tenant_id: "acme", total: 100 },
637
+ visible: true,
638
+ });
639
+ await assertVisibilityParity(conn, globexMember, "oke_parity_invoices", {
640
+ name: "globex member denied acme invoice",
641
+ id: "i1",
642
+ row: { id: "i1", tenant_id: "acme", total: 100 },
643
+ visible: false,
644
+ });
645
+ await assertInsertParity(
646
+ conn,
647
+ acmeMemberNoScope,
648
+ "oke_parity_invoices",
649
+ "(id, tenant_id, total)",
650
+ "(?, ?, ?)",
651
+ { id: "i2", tenant_id: "acme", total: 5 },
652
+ ["i2", "acme", 5],
653
+ );
654
+ await assertInsertParity(
655
+ conn,
656
+ acmeCreator,
657
+ "oke_parity_invoices",
658
+ "(id, tenant_id, total)",
659
+ "(?, ?, ?)",
660
+ { id: "i4", tenant_id: "acme", total: 9 },
661
+ ["i4", "acme", 9],
662
+ );
663
+ await conn.exec(`DROP TABLE IF EXISTS oke_parity_invoices`);
664
+ }, 30_000);
665
+ });