okengine 0.10.2 → 0.11.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.
Files changed (343) hide show
  1. package/package.json +76 -10
  2. package/site/content/docs/ai/mcp.mdx +0 -1
  3. package/site/content/docs/deployment/docker-swarm.mdx +48 -111
  4. package/site/content/docs/deployment/docker.mdx +43 -36
  5. package/site/content/docs/deployment/index.mdx +9 -9
  6. package/site/content/docs/deployment/kubernetes.mdx +4 -4
  7. package/site/content/docs/elements/ai.mdx +7 -13
  8. package/site/content/docs/elements/channel.mdx +9 -13
  9. package/site/content/docs/elements/clock.mdx +8 -14
  10. package/site/content/docs/elements/flow.mdx +46 -54
  11. package/site/content/docs/elements/gate.mdx +6 -11
  12. package/site/content/docs/elements/signal.mdx +2 -8
  13. package/site/content/docs/elements/store.mdx +79 -93
  14. package/site/content/docs/elements/vault.mdx +89 -76
  15. package/site/content/docs/get-started/basic-usage.mdx +8 -14
  16. package/site/content/docs/get-started/installation.mdx +36 -36
  17. package/site/content/docs/get-started/introduction.mdx +11 -0
  18. package/site/content/docs/get-started/why.mdx +3 -4
  19. package/site/content/docs/index.mdx +0 -1
  20. package/site/content/docs/meta.json +0 -1
  21. package/site/content/docs/plugins/anonymous.mdx +1 -1
  22. package/site/content/docs/plugins/magic-link.mdx +11 -11
  23. package/site/content/docs/plugins/otp.mdx +2 -2
  24. package/site/content/docs/plugins/passkey.mdx +1 -1
  25. package/site/content/docs/plugins/two-factor.mdx +1 -1
  26. package/site/content/docs/plugins/username.mdx +1 -1
  27. package/site/content/docs/recipes/caddy.mdx +1 -1
  28. package/site/content/docs/recipes/cockroachdb.mdx +2 -2
  29. package/site/content/docs/recipes/dragonfly.mdx +1 -1
  30. package/site/content/docs/recipes/index.mdx +1 -2
  31. package/site/content/docs/recipes/llama-cpp.mdx +21 -23
  32. package/site/content/docs/recipes/mailpit.mdx +11 -11
  33. package/site/content/docs/recipes/meilisearch.mdx +10 -10
  34. package/site/content/docs/recipes/meta.json +0 -1
  35. package/site/content/docs/recipes/ollama.mdx +1 -1
  36. package/site/content/docs/recipes/pgdog.mdx +12 -9
  37. package/site/content/docs/recipes/postgres.mdx +3 -3
  38. package/site/content/docs/recipes/redis.mdx +2 -2
  39. package/site/content/docs/recipes/rustfs.mdx +10 -7
  40. package/site/content/docs/recipes/sglang.mdx +1 -1
  41. package/site/content/docs/recipes/supabase-docker.mdx +1 -1
  42. package/site/content/docs/recipes/timescale.mdx +2 -2
  43. package/site/content/docs/recipes/valkey.mdx +1 -1
  44. package/site/content/docs/recipes/vllm.mdx +1 -1
  45. package/site/content/docs/recipes/yugabytedb.mdx +2 -2
  46. package/site/content/docs/reference/cli.md +61 -38
  47. package/site/content/docs/reference/configuration.mdx +145 -70
  48. package/site/content/docs/reference/environment-variables.mdx +36 -28
  49. package/site/content/docs/reference/errors.mdx +28 -29
  50. package/site/content/docs/reference/fx.mdx +14 -7
  51. package/site/content/docs/reference/i18n.mdx +17 -6
  52. package/site/content/docs/reference/index.mdx +5 -0
  53. package/site/content/docs/reference/meta.json +1 -0
  54. package/site/content/docs/reference/migrating-environments.mdx +158 -0
  55. package/site/content/docs/reference/plugins.mdx +2 -2
  56. package/src/auth/gate-auth.test.ts +12 -10
  57. package/src/cli/ai-setup/ai-setup.test.ts +30 -16
  58. package/src/cli/ai-setup/apply.ts +111 -15
  59. package/src/cli/ai-setup/catalog.ts +3 -3
  60. package/src/cli/ai-setup/index.ts +1 -1
  61. package/src/cli/ai-setup/prompts.ts +1 -1
  62. package/src/cli/ai-setup/recommend.test.ts +1 -1
  63. package/src/cli/ask-seed.test.ts +3 -3
  64. package/src/cli/build.ts +8 -9
  65. package/src/cli/db-auto-push.test.ts +6 -3
  66. package/src/cli/db-auto-push.ts +7 -5
  67. package/src/cli/db-seed.ts +7 -8
  68. package/src/cli/db.test.ts +70 -71
  69. package/src/cli/db.ts +86 -12
  70. package/src/cli/dev-controller.test.ts +35 -0
  71. package/src/cli/dev-controller.ts +298 -0
  72. package/src/cli/dev-controls.ts +11 -65
  73. package/src/cli/dev-db-push.test.ts +47 -15
  74. package/src/cli/dev-schema-sync.test.ts +11 -11
  75. package/src/cli/dev-schema-sync.ts +10 -10
  76. package/src/cli/dev-session-lock.test.ts +106 -0
  77. package/src/cli/dev-session-lock.ts +209 -0
  78. package/src/cli/dev.test.ts +246 -114
  79. package/src/cli/dev.ts +153 -205
  80. package/src/cli/doc-staleness.test.ts +3 -6
  81. package/src/cli/docker-clean.test.ts +33 -1
  82. package/src/cli/docker-clean.ts +28 -33
  83. package/src/cli/docker-cli.test.ts +12 -11
  84. package/src/cli/docker.ts +95 -20
  85. package/src/cli/doctor.test.ts +40 -0
  86. package/src/cli/doctor.ts +142 -9
  87. package/src/cli/drizzle-env.test.ts +11 -13
  88. package/src/cli/drizzle-env.ts +18 -11
  89. package/src/cli/ensure-drizzle-config.ts +11 -14
  90. package/src/cli/hero-meta.test.ts +15 -15
  91. package/src/cli/hero-meta.ts +10 -9
  92. package/src/cli/index.ts +36 -6
  93. package/src/cli/load-config.images.test.ts +44 -10
  94. package/src/cli/load-config.source.test.ts +50 -0
  95. package/src/cli/load-config.ts +35 -29
  96. package/src/cli/meilisearch-local.ts +1 -2
  97. package/src/cli/mode.ts +7 -58
  98. package/src/cli/registry.ts +175 -19
  99. package/src/cli/resolve-dev-sql-env.test.ts +8 -16
  100. package/src/cli/resolve-dev-sql-env.ts +13 -15
  101. package/src/cli/safe-defaults.test.ts +29 -11
  102. package/src/cli/schema.ts +65 -16
  103. package/src/cli/test.ts +58 -0
  104. package/src/cli/tui/App.tsx +252 -0
  105. package/src/cli/tui/DevLive.tsx +116 -0
  106. package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
  107. package/src/cli/tui/components/HeaderBar.tsx +67 -0
  108. package/src/cli/tui/components/LogStream.tsx +47 -0
  109. package/src/cli/tui/components/PanelCard.tsx +39 -0
  110. package/src/cli/tui/components/SelectRow.tsx +37 -0
  111. package/src/cli/tui/components/SlashPalette.tsx +172 -0
  112. package/src/cli/tui/components/Spinner.tsx +32 -0
  113. package/src/cli/tui/components/StatusBar.tsx +51 -0
  114. package/src/cli/tui/components/StatusDot.tsx +37 -0
  115. package/src/cli/tui/components/TabBar.tsx +49 -0
  116. package/src/cli/tui/components/Wordmark.tsx +43 -0
  117. package/src/cli/tui/format.ts +38 -0
  118. package/src/cli/tui/keys.test.ts +18 -0
  119. package/src/cli/tui/keys.ts +26 -0
  120. package/src/cli/tui/launch.test.ts +18 -0
  121. package/src/cli/tui/launch.ts +57 -0
  122. package/src/cli/tui/panels/Dashboard.tsx +201 -0
  123. package/src/cli/tui/panels/Database.tsx +198 -0
  124. package/src/cli/tui/panels/Dev.tsx +257 -0
  125. package/src/cli/tui/panels/Docker.tsx +357 -0
  126. package/src/cli/tui/panels/Navigator.test.ts +22 -0
  127. package/src/cli/tui/panels/Navigator.tsx +213 -0
  128. package/src/cli/tui/slash-catalog.test.ts +31 -0
  129. package/src/cli/tui/slash-catalog.ts +184 -0
  130. package/src/cli/tui/slash-run.ts +54 -0
  131. package/src/cli/tui/state/ports.ts +95 -0
  132. package/src/cli/tui/theme.ts +24 -0
  133. package/src/cli/vault-cmd.test.ts +402 -0
  134. package/src/cli/vault-cmd.ts +727 -69
  135. package/src/cli/vault-secure-input.test.ts +56 -0
  136. package/src/cli/vault-secure-input.ts +124 -0
  137. package/src/compiler/effects-infer.ts +7 -0
  138. package/src/compiler/extract.ts +12 -0
  139. package/src/compiler/fixtures/skyport/oke.config.ts +7 -5
  140. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
  141. package/src/compiler/fixtures/skyport.expected.json +1 -1
  142. package/src/compiler/generate-adopt.ts +27 -1
  143. package/src/compiler-entry.ts +7 -0
  144. package/src/config/define-config.test.ts +111 -47
  145. package/src/config/driver-defaults.test.ts +115 -0
  146. package/src/config/driver-defaults.ts +156 -0
  147. package/src/config/index.ts +246 -131
  148. package/src/config/resolve-driver.test.ts +14 -19
  149. package/src/console/server/app.ts +4 -2
  150. package/src/console/server/console.test.ts +58 -1
  151. package/src/console/server/flows.ts +22 -2
  152. package/src/console/server/index.ts +3 -0
  153. package/src/console/server/operator-db.test.ts +55 -13
  154. package/src/console/server/operator-db.ts +305 -263
  155. package/src/console/server/serve.ts +16 -7
  156. package/src/console/server/state.ts +31 -9
  157. package/src/console/server/vault.test.ts +120 -5
  158. package/src/console/server/vault.ts +208 -12
  159. package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
  160. package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
  161. package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
  162. package/src/console/ui/dist/index.html +2 -2
  163. package/src/console/ui/shell/client.ts +16 -0
  164. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
  165. package/src/console/ui/vault/VaultA11yView.tsx +33 -1
  166. package/src/console/ui/vault/backend.test.ts +75 -0
  167. package/src/console/ui/vault/backend.ts +117 -0
  168. package/src/console/ui/vault/fixture.ts +21 -2
  169. package/src/console/ui/vault/index.ts +11 -1
  170. package/src/console/ui/vault/types.ts +26 -0
  171. package/src/docker/compose.ts +164 -61
  172. package/src/docker/derive.ts +106 -12
  173. package/src/docker/docker.test.ts +156 -110
  174. package/src/docker/helpers.ts +98 -0
  175. package/src/docker/images-config.test.ts +163 -0
  176. package/src/docker/index.ts +30 -5
  177. package/src/docker/recipes/index.ts +0 -3
  178. package/src/docker/recipes/llama-cpp.ts +4 -4
  179. package/src/docker/recipes/pgdog.ts +9 -2
  180. package/src/docker/resources.test.ts +71 -0
  181. package/src/docker/resources.ts +176 -0
  182. package/src/docker/stack-id.test.ts +11 -4
  183. package/src/docker/stack-id.ts +42 -32
  184. package/src/docker/stack.integration.test.ts +4 -15
  185. package/src/docker/types.ts +35 -3
  186. package/src/drivers/ai-ollama.integration.test.ts +2 -2
  187. package/src/drivers/conformance.test.ts +11 -12
  188. package/src/drivers/drizzle-dialect.test.ts +0 -4
  189. package/src/drivers/drizzle-dialect.ts +7 -13
  190. package/src/drivers/index.ts +24 -7
  191. package/src/drivers/journal-postgres.test.ts +3 -1
  192. package/src/drivers/journal-postgres.ts +4 -2
  193. package/src/drivers/meilisearch.ts +1 -1
  194. package/src/drivers/pglite.ts +28 -5
  195. package/src/drivers/types.ts +9 -11
  196. package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
  197. package/src/drivers/vault-aws-secrets-manager.ts +333 -0
  198. package/src/drivers/vault-builtin.test.ts +110 -0
  199. package/src/drivers/vault-builtin.ts +184 -0
  200. package/src/drivers/vault-driver-removal.test.ts +13 -0
  201. package/src/drivers/vault-managed.test.ts +95 -0
  202. package/src/drivers/vault-managed.ts +101 -25
  203. package/src/drivers/vault-types.ts +25 -5
  204. package/src/elements/ai/runtime.ts +1 -1
  205. package/src/elements/clock/chaos.test.ts +4 -1
  206. package/src/elements/clock/durable.ts +57 -4
  207. package/src/elements/clock.test.ts +13 -14
  208. package/src/elements/gate/constants.ts +18 -0
  209. package/src/elements/gate.test.ts +1 -1
  210. package/src/elements/gate.ts +2 -2
  211. package/src/elements/store/domain-ddl.test.ts +5 -12
  212. package/src/elements/store/emit-drizzle.ts +44 -19
  213. package/src/elements/store/index-boot.test.ts +15 -116
  214. package/src/elements/store/load-plugin-tables.ts +1 -1
  215. package/src/elements/store/resource-list-docs.fixture.ts +12 -6
  216. package/src/elements/store/resource-list-docs.test.ts +64 -44
  217. package/src/elements/store/resource.test.ts +17 -10
  218. package/src/elements/store/runtime.ts +9 -12
  219. package/src/elements/store/schema-decl.test.ts +16 -32
  220. package/src/elements/store/seed.test.ts +1 -2
  221. package/src/elements/store/seed.ts +7 -8
  222. package/src/elements/store/sql-condition.test.ts +2 -2
  223. package/src/elements/store/sql-session.test.ts +8 -10
  224. package/src/elements/store/sql-session.ts +10 -2
  225. package/src/elements/store.test.ts +13 -13
  226. package/src/elements/vault/audit.ts +233 -0
  227. package/src/elements/vault/boot-chain.test.ts +78 -0
  228. package/src/elements/vault/boot-chain.ts +43 -39
  229. package/src/elements/vault/builtin-adapter.test.ts +479 -0
  230. package/src/elements/vault/builtin-adapter.ts +1184 -0
  231. package/src/elements/vault/crypto.test.ts +237 -0
  232. package/src/elements/vault/crypto.ts +431 -0
  233. package/src/elements/vault/declare.ts +164 -3
  234. package/src/elements/vault/errors.ts +56 -0
  235. package/src/elements/vault/kms-unseal.test.ts +76 -0
  236. package/src/elements/vault/kms-unseal.ts +181 -0
  237. package/src/elements/vault/path.ts +90 -0
  238. package/src/elements/vault/resilience.test.ts +102 -0
  239. package/src/elements/vault/resilience.ts +107 -0
  240. package/src/elements/vault/runtime.ts +36 -4
  241. package/src/elements/vault/security-checklist.test.ts +130 -0
  242. package/src/elements/vault/storage.ts +448 -0
  243. package/src/elements/vault/types.ts +288 -0
  244. package/src/elements/vault/unseal.ts +150 -0
  245. package/src/elements/vault.test.ts +72 -4
  246. package/src/elements/vault.ts +91 -2
  247. package/src/full.ts +248 -0
  248. package/src/http.ts +41 -0
  249. package/src/i18n/interpolate-simple.ts +37 -0
  250. package/src/i18n/messages.ts +51 -4
  251. package/src/i18n-entry.ts +7 -0
  252. package/src/index.ts +35 -145
  253. package/src/journal-entry.ts +20 -0
  254. package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
  255. package/src/kernel/app.ts +40 -42
  256. package/src/kernel/auto-registry.test.ts +2 -2
  257. package/src/kernel/boot-bind/ai.test.ts +5 -5
  258. package/src/kernel/boot-bind/ai.ts +2 -1
  259. package/src/kernel/boot-bind/channel.test.ts +6 -6
  260. package/src/kernel/boot-bind/channel.ts +5 -4
  261. package/src/kernel/boot-bind/clock.ts +5 -5
  262. package/src/kernel/boot-bind/gate.ts +6 -7
  263. package/src/kernel/boot-bind/honor-config.test.ts +42 -42
  264. package/src/kernel/boot-bind/journal.ts +5 -5
  265. package/src/kernel/boot-bind/signal.ts +3 -1
  266. package/src/kernel/boot-bind/store.test.ts +55 -37
  267. package/src/kernel/boot-bind/store.ts +24 -34
  268. package/src/kernel/boot-bind/vault.ts +19 -4
  269. package/src/kernel/boot.test.ts +12 -4
  270. package/src/kernel/boot.ts +31 -19
  271. package/src/kernel/budget-entry.ts +1 -1
  272. package/src/kernel/compensate.test.ts +288 -1
  273. package/src/kernel/compensate.ts +126 -0
  274. package/src/kernel/concurrency.test.ts +10 -9
  275. package/src/kernel/effects-stamping.test.ts +27 -2
  276. package/src/kernel/element-registries.ts +5 -3
  277. package/src/kernel/flow.ts +4 -2
  278. package/src/kernel/fx.test.ts +119 -3
  279. package/src/kernel/fx.ts +215 -40
  280. package/src/kernel/index.ts +14 -0
  281. package/src/kernel/journal.ts +130 -7
  282. package/src/kernel/pipeline.test.ts +20 -6
  283. package/src/kernel/redacted.test.ts +35 -0
  284. package/src/kernel/redacted.ts +24 -2
  285. package/src/kernel/router/create-default.ts +16 -0
  286. package/src/kernel/router/create-edge.ts +18 -0
  287. package/src/kernel/router/create.ts +23 -0
  288. package/src/kernel/router/index.ts +23 -0
  289. package/src/kernel/router/linear.ts +88 -0
  290. package/src/kernel/router/regexp.ts +188 -0
  291. package/src/kernel/router/smart.ts +120 -0
  292. package/src/kernel/router/trie.ts +108 -0
  293. package/src/kernel/router/types.ts +56 -0
  294. package/src/kernel/router.ts +18 -532
  295. package/src/kernel-entry.ts +43 -0
  296. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  297. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  298. package/src/plugins/config-source.test.ts +7 -7
  299. package/src/release/build-lib.ts +58 -0
  300. package/src/release/exports.test.ts +4 -2
  301. package/src/release/exports.ts +30 -4
  302. package/src/release/measure.ts +101 -22
  303. package/src/runs/duckdb.ts +7 -2
  304. package/src/shared/optional-peer.ts +41 -0
  305. package/src/term.test.ts +3 -44
  306. package/src/test/create-test-app.ts +12 -0
  307. package/src/test/provisions.integration.test.ts +1 -1
  308. package/src/test/reset-element-registries.ts +2 -0
  309. package/src/upgrade/codemods.test.ts +7 -20
  310. package/src/upgrade/codemods.ts +93 -60
  311. package/site/content/docs/console/access.mdx +0 -29
  312. package/site/content/docs/console/ai.mdx +0 -35
  313. package/site/content/docs/console/architecture.mdx +0 -35
  314. package/site/content/docs/console/channels.mdx +0 -37
  315. package/site/content/docs/console/clock.mdx +0 -51
  316. package/site/content/docs/console/flows.mdx +0 -31
  317. package/site/content/docs/console/gates.mdx +0 -73
  318. package/site/content/docs/console/index.mdx +0 -54
  319. package/site/content/docs/console/manifest-diff.mdx +0 -34
  320. package/site/content/docs/console/meta.json +0 -24
  321. package/site/content/docs/console/overview.mdx +0 -40
  322. package/site/content/docs/console/plugins.mdx +0 -41
  323. package/site/content/docs/console/privacy.mdx +0 -32
  324. package/site/content/docs/console/runs.mdx +0 -50
  325. package/site/content/docs/console/signals.mdx +0 -40
  326. package/site/content/docs/console/store.mdx +0 -32
  327. package/site/content/docs/console/tenancy.mdx +0 -32
  328. package/site/content/docs/console/traces.mdx +0 -38
  329. package/site/content/docs/console/vault.mdx +0 -41
  330. package/site/content/docs/recipes/openbao.mdx +0 -141
  331. package/src/cli/ask-dev-mode.ts +0 -40
  332. package/src/cli/dev-mode.test.ts +0 -45
  333. package/src/cli/dev-mode.ts +0 -78
  334. package/src/cli/openbao-bootstrap.test.ts +0 -150
  335. package/src/cli/openbao-bootstrap.ts +0 -321
  336. package/src/cli/openbao-restart.integration.test.ts +0 -145
  337. package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
  338. package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
  339. package/src/docker/recipes/openbao.ts +0 -47
  340. package/src/drivers/libsql.ts +0 -179
  341. package/src/drivers/sqlite.ts +0 -44
  342. package/src/drivers/vault-openbao.test.ts +0 -97
  343. package/src/drivers/vault-openbao.ts +0 -143
@@ -1,5 +1,12 @@
1
1
  /**
2
- * Durable operator plane for Console — `.oke/console.sqlite` + secret file.
2
+ * Durable operator plane for Console — Postgres schema `oke_console` + secret file.
3
+ *
4
+ * Same database as the app (`DATABASE_URL` / `OKE_STORE_SQL_URL`):
5
+ * - `public` — application + shared runtime tables
6
+ * - `oke_console` — Console operators / sessions (never mixed into `public`)
7
+ *
8
+ * When no Postgres URL is set (unit tests), falls back to PGlite under
9
+ * `.oke/console-pg` so reopen durability still works without Docker.
3
10
  *
4
11
  * Spec: wizard closes permanently once the first operator exists (console §2.5).
5
12
  * Claim codes stay ephemeral; operators, sessions, and the signing secret must
@@ -8,7 +15,6 @@
8
15
 
9
16
  import { mkdirSync } from "node:fs";
10
17
  import { join } from "node:path";
11
- import { Database } from "bun:sqlite";
12
18
  import { createOperatorStore, type OperatorStore } from "../../auth/operator.ts";
13
19
  import { createSessionStore, type SessionStore } from "../../auth/sessions.ts";
14
20
  import type {
@@ -19,11 +25,19 @@ import type {
19
25
  SessionRow,
20
26
  } from "../../auth/tables.ts";
21
27
  import { AUTH_TABLES } from "../../auth/tables.ts";
28
+ import type { SqlConnection } from "../../drivers/types.ts";
22
29
 
23
30
  /** Relative paths under project cwd. */
24
31
  export const CONSOLE_OKE_DIR = ".oke";
25
- export const CONSOLE_SQLITE_NAME = "console.sqlite";
32
+ /** Signing secret file (still local — not a DB row). */
26
33
  export const CONSOLE_SECRET_NAME = "console.secret";
34
+ /** PGlite datadir when no Postgres URL is configured. */
35
+ export const CONSOLE_PGLITE_DIR = "console-pg";
36
+ /**
37
+ * Postgres schema for Console operator-plane tables.
38
+ * App domain tables stay in `public` on the same database.
39
+ */
40
+ export const CONSOLE_PG_SCHEMA = "oke_console";
27
41
 
28
42
  /** Opened Console persistence handle. */
29
43
  export interface ConsolePersistence {
@@ -34,11 +48,23 @@ export interface ConsolePersistence {
34
48
  /** Hydrated session + refresh Maps. */
35
49
  readonly sessions: SessionStore;
36
50
  /** Persist (or update) one operator + credential + roles/sso. */
37
- readonly persistOperator: (operatorId: string) => void;
51
+ readonly persistOperator: (operatorId: string) => Promise<void>;
38
52
  /** Persist the full session store (issue / revoke). */
39
- readonly persistSessions: () => void;
40
- /** Close the SQLite connection. */
41
- readonly close: () => void;
53
+ readonly persistSessions: () => Promise<void>;
54
+ /** Close the SQL connection. */
55
+ readonly close: () => Promise<void>;
56
+ }
57
+
58
+ /** Options for {@link openConsolePersistence}. */
59
+ export interface OpenConsolePersistenceOptions {
60
+ /** Optional `OKE_CONSOLE_SECRET` override. */
61
+ readonly envSecret?: string;
62
+ /** SQL URL override (`DATABASE_URL` / `OKE_STORE_SQL_URL` otherwise). */
63
+ readonly url?: string;
64
+ /** Injected connection (tests). */
65
+ readonly connection?: SqlConnection;
66
+ /** Injected env map (tests). */
67
+ readonly env?: Readonly<Record<string, string | undefined>>;
42
68
  }
43
69
 
44
70
  /**
@@ -48,18 +74,27 @@ export interface ConsolePersistence {
48
74
  */
49
75
  export function consoleOkePaths(cwd: string): {
50
76
  readonly dir: string;
51
- readonly sqlite: string;
52
77
  readonly secret: string;
78
+ readonly pglite: string;
53
79
  } {
54
80
  const dir = join(cwd, CONSOLE_OKE_DIR);
55
81
  mkdirSync(dir, { recursive: true });
56
82
  return {
57
83
  dir,
58
- sqlite: join(dir, CONSOLE_SQLITE_NAME),
59
84
  secret: join(dir, CONSOLE_SECRET_NAME),
85
+ pglite: join(dir, CONSOLE_PGLITE_DIR),
60
86
  };
61
87
  }
62
88
 
89
+ /**
90
+ * Qualify a table name inside {@link CONSOLE_PG_SCHEMA}.
91
+ *
92
+ * @param table - Bare table name (`oke_operators`, …)
93
+ */
94
+ export function consoleTable(table: string): string {
95
+ return `"${CONSOLE_PG_SCHEMA}"."${table}"`;
96
+ }
97
+
63
98
  /**
64
99
  * Load or create a stable Console signing secret under `.oke/console.secret`.
65
100
  *
@@ -85,192 +120,230 @@ export async function resolveConsoleSecret(cwd: string, envSecret?: string): Pro
85
120
  * Open Console operator DB, migrate schema, hydrate Maps.
86
121
  *
87
122
  * @param cwd - Project root
88
- * @param options - Optional env secret override
123
+ * @param options - Secret / URL / connection overrides
89
124
  */
90
125
  export async function openConsolePersistence(
91
126
  cwd: string,
92
- options: { readonly envSecret?: string } = {},
127
+ options: OpenConsolePersistenceOptions = {},
93
128
  ): Promise<ConsolePersistence> {
94
- const paths = consoleOkePaths(cwd);
95
129
  const secret = await resolveConsoleSecret(cwd, options.envSecret);
96
- const db = new Database(paths.sqlite, { create: true });
97
- db.exec("PRAGMA journal_mode = WAL;");
98
- migrateOperatorSchema(db);
99
- const operators = loadOperatorStore(db);
100
- const sessions = loadSessionStore(db);
130
+ const env = options.env ?? process.env;
131
+ const url = options.url ?? env.DATABASE_URL ?? env.OKE_STORE_SQL_URL;
132
+ const owned = options.connection === undefined;
133
+ const sql = options.connection ?? (await connectConsoleSql(cwd, url));
134
+
135
+ await migrateOperatorSchema(sql);
136
+ const operators = await loadOperatorStore(sql);
137
+ const sessions = await loadSessionStore(sql);
101
138
 
102
139
  return {
103
140
  secret,
104
141
  operators,
105
142
  sessions,
106
- persistOperator(operatorId: string) {
107
- persistOperator(db, operators, operatorId);
143
+ async persistOperator(operatorId: string) {
144
+ await persistOperator(sql, operators, operatorId);
108
145
  },
109
- persistSessions() {
110
- persistSessions(db, sessions);
146
+ async persistSessions() {
147
+ await persistSessions(sql, sessions);
111
148
  },
112
- close() {
113
- db.close();
149
+ async close() {
150
+ if (owned) await sql.close();
114
151
  },
115
152
  };
116
153
  }
117
154
 
118
155
  /**
119
- * Create operator tables if missing.
156
+ * Open Postgres (shared app URL) or PGlite under `.oke/console-pg`.
157
+ *
158
+ * @param cwd - Project root
159
+ * @param url - Resolved SQL URL, when configured
160
+ */
161
+ async function connectConsoleSql(cwd: string, url: string | undefined): Promise<SqlConnection> {
162
+ if (url !== undefined && /^postgres(ql)?:\/\//.test(url)) {
163
+ const { connectPostgres } = await import("../../drivers/postgres.ts");
164
+ return connectPostgres({ url });
165
+ }
166
+ const { connectPglite } = await import("../../drivers/pglite.ts");
167
+ const paths = consoleOkePaths(cwd);
168
+ // Explicit non-postgres URL (tests) wins; otherwise durable PGlite datadir.
169
+ if (url !== undefined && url.length > 0) {
170
+ return connectPglite({ url });
171
+ }
172
+ return connectPglite({ url: paths.pglite });
173
+ }
174
+
175
+ /**
176
+ * Create `oke_console` schema + operator tables if missing.
120
177
  *
121
- * @param db - bun:sqlite database
178
+ * @param sql - SQL connection
122
179
  */
123
- export function migrateOperatorSchema(db: Database): void {
124
- db.exec(`
125
- CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operators} (
180
+ export async function migrateOperatorSchema(sql: SqlConnection): Promise<void> {
181
+ await sql.exec(`CREATE SCHEMA IF NOT EXISTS "${CONSOLE_PG_SCHEMA}"`);
182
+ const ops = consoleTable(AUTH_TABLES.operators);
183
+ const creds = consoleTable(AUTH_TABLES.operatorCredentials);
184
+ const sso = consoleTable(AUTH_TABLES.operatorSsoLinks);
185
+ const roles = consoleTable(AUTH_TABLES.operatorRoles);
186
+ const sessions = consoleTable(AUTH_TABLES.sessions);
187
+ const refresh = consoleTable(AUTH_TABLES.refreshTokens);
188
+
189
+ await sql.exec(`
190
+ CREATE TABLE IF NOT EXISTS ${ops} (
126
191
  id TEXT PRIMARY KEY NOT NULL,
127
192
  email TEXT NOT NULL UNIQUE,
128
193
  name TEXT NOT NULL,
129
194
  status TEXT NOT NULL,
130
- mfa_enabled INTEGER NOT NULL DEFAULT 0,
195
+ mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
131
196
  invited_by TEXT,
132
- last_seen_at INTEGER
197
+ last_seen_at BIGINT
133
198
  );
134
- CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operatorCredentials} (
199
+ `);
200
+ await sql.exec(`
201
+ CREATE TABLE IF NOT EXISTS ${creds} (
135
202
  operator_id TEXT PRIMARY KEY NOT NULL,
136
203
  password_hash TEXT NOT NULL,
137
- login_enabled INTEGER NOT NULL DEFAULT 1,
138
- FOREIGN KEY (operator_id) REFERENCES ${AUTH_TABLES.operators}(id)
204
+ login_enabled BOOLEAN NOT NULL DEFAULT TRUE,
205
+ FOREIGN KEY (operator_id) REFERENCES ${ops}(id)
139
206
  );
140
- CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operatorSsoLinks} (
207
+ `);
208
+ await sql.exec(`
209
+ CREATE TABLE IF NOT EXISTS ${sso} (
141
210
  operator_id TEXT NOT NULL,
142
211
  provider TEXT NOT NULL,
143
212
  subject TEXT NOT NULL,
144
213
  PRIMARY KEY (operator_id, provider, subject),
145
- FOREIGN KEY (operator_id) REFERENCES ${AUTH_TABLES.operators}(id)
214
+ FOREIGN KEY (operator_id) REFERENCES ${ops}(id)
146
215
  );
147
- CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operatorRoles} (
216
+ `);
217
+ await sql.exec(`
218
+ CREATE TABLE IF NOT EXISTS ${roles} (
148
219
  operator_id TEXT NOT NULL,
149
220
  role TEXT NOT NULL,
150
221
  PRIMARY KEY (operator_id, role),
151
- FOREIGN KEY (operator_id) REFERENCES ${AUTH_TABLES.operators}(id)
222
+ FOREIGN KEY (operator_id) REFERENCES ${ops}(id)
152
223
  );
153
- CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.sessions} (
224
+ `);
225
+ await sql.exec(`
226
+ CREATE TABLE IF NOT EXISTS ${sessions} (
154
227
  id TEXT PRIMARY KEY NOT NULL,
155
228
  plane TEXT NOT NULL,
156
229
  principal_id TEXT NOT NULL,
157
230
  family_id TEXT NOT NULL,
158
- revoked_at INTEGER,
159
- created_at INTEGER NOT NULL,
160
- expires_at INTEGER NOT NULL,
161
- last_active_at INTEGER,
231
+ revoked_at BIGINT,
232
+ created_at BIGINT NOT NULL,
233
+ expires_at BIGINT NOT NULL,
234
+ last_active_at BIGINT,
162
235
  scopes TEXT NOT NULL DEFAULT '[]',
163
236
  audience TEXT
164
237
  );
165
- CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.refreshTokens} (
238
+ `);
239
+ await sql.exec(`
240
+ CREATE TABLE IF NOT EXISTS ${refresh} (
166
241
  id TEXT PRIMARY KEY NOT NULL,
167
242
  session_id TEXT NOT NULL,
168
243
  family_id TEXT NOT NULL,
169
244
  hash TEXT NOT NULL,
170
- expires_at INTEGER NOT NULL,
171
- used_at INTEGER,
172
- revoked_at INTEGER
245
+ expires_at BIGINT NOT NULL,
246
+ used_at BIGINT,
247
+ revoked_at BIGINT
173
248
  );
174
249
  `);
175
- ensureSessionColumns(db);
250
+ await ensureSessionColumns(sql);
176
251
  }
177
252
 
178
253
  /**
179
- * Add session columns when opening an older console.sqlite.
254
+ * Add session columns when opening an older Console schema.
180
255
  *
181
- * @param db - Open database
256
+ * @param sql - Open connection
182
257
  */
183
- function ensureSessionColumns(db: Database): void {
184
- const cols = db.query(`PRAGMA table_info(${AUTH_TABLES.sessions})`).all() as Array<{
185
- name: string;
186
- }>;
187
- const names = new Set(cols.map((c) => c.name));
188
- if (!names.has("last_active_at")) {
189
- db.exec(`ALTER TABLE ${AUTH_TABLES.sessions} ADD COLUMN last_active_at INTEGER`);
190
- }
191
- if (!names.has("scopes")) {
192
- db.exec(`ALTER TABLE ${AUTH_TABLES.sessions} ADD COLUMN scopes TEXT NOT NULL DEFAULT '[]'`);
193
- }
194
- if (!names.has("audience")) {
195
- db.exec(`ALTER TABLE ${AUTH_TABLES.sessions} ADD COLUMN audience TEXT`);
196
- }
258
+ async function ensureSessionColumns(sql: SqlConnection): Promise<void> {
259
+ const sessions = consoleTable(AUTH_TABLES.sessions);
260
+ await sql.exec(`ALTER TABLE ${sessions} ADD COLUMN IF NOT EXISTS last_active_at BIGINT`);
261
+ await sql.exec(
262
+ `ALTER TABLE ${sessions} ADD COLUMN IF NOT EXISTS scopes TEXT NOT NULL DEFAULT '[]'`,
263
+ );
264
+ await sql.exec(`ALTER TABLE ${sessions} ADD COLUMN IF NOT EXISTS audience TEXT`);
265
+ }
266
+
267
+ /**
268
+ * Coerce a SQL boolean / 0|1 cell to boolean.
269
+ *
270
+ * @param value - Driver cell
271
+ */
272
+ function asBool(value: unknown): boolean {
273
+ return value === true || value === 1 || value === "1" || value === "t" || value === "true";
197
274
  }
198
275
 
199
276
  /**
200
- * Hydrate a {@link SessionStore} from SQLite.
277
+ * Coerce a SQL numeric cell to number | null.
201
278
  *
202
- * @param db - Open database
279
+ * @param value - Driver cell
203
280
  */
204
- export function loadSessionStore(db: Database): SessionStore {
281
+ function asNum(value: unknown): number | null {
282
+ if (value === null || value === undefined) return null;
283
+ if (typeof value === "number") return value;
284
+ if (typeof value === "bigint") return Number(value);
285
+ const n = Number(value);
286
+ return Number.isFinite(n) ? n : null;
287
+ }
288
+
289
+ /**
290
+ * Hydrate a {@link SessionStore} from SQL.
291
+ *
292
+ * @param sql - Open connection
293
+ */
294
+ export async function loadSessionStore(sql: SqlConnection): Promise<SessionStore> {
205
295
  const store = createSessionStore();
296
+ const sessions = consoleTable(AUTH_TABLES.sessions);
297
+ const refresh = consoleTable(AUTH_TABLES.refreshTokens);
206
298
 
207
- const sessionRows = db
208
- .query(
209
- `SELECT id, plane, principal_id, family_id, revoked_at, created_at, expires_at, last_active_at, scopes, audience
210
- FROM ${AUTH_TABLES.sessions}`,
211
- )
212
- .all() as Array<{
213
- id: string;
214
- plane: SessionRow["plane"];
215
- principal_id: string;
216
- family_id: string;
217
- revoked_at: number | null;
218
- created_at: number;
219
- expires_at: number;
220
- last_active_at: number | null;
221
- scopes: string | null;
222
- audience: string | null;
223
- }>;
299
+ const sessionRows = await sql.query(
300
+ `SELECT id, plane, principal_id, family_id, revoked_at, created_at, expires_at, last_active_at, scopes, audience
301
+ FROM ${sessions}`,
302
+ );
224
303
 
225
304
  for (const row of sessionRows) {
226
305
  let scopes: string[] = [];
227
- if (row.scopes) {
306
+ const scopesRaw = row["scopes"];
307
+ if (typeof scopesRaw === "string" && scopesRaw.length > 0) {
228
308
  try {
229
- const parsed = JSON.parse(row.scopes) as unknown;
309
+ const parsed = JSON.parse(scopesRaw) as unknown;
230
310
  if (Array.isArray(parsed)) scopes = parsed.map(String);
231
311
  } catch {
232
312
  scopes = [];
233
313
  }
234
314
  }
315
+ const createdAt = asNum(row["created_at"]) ?? 0;
235
316
  const session: SessionRow = {
236
- id: row.id,
237
- plane: row.plane,
238
- principalId: row.principal_id,
239
- familyId: row.family_id,
240
- revokedAt: row.revoked_at,
241
- createdAt: row.created_at,
242
- expiresAt: row.expires_at,
243
- lastActiveAt: row.last_active_at ?? row.created_at,
317
+ id: String(row["id"]),
318
+ plane: row["plane"] as SessionRow["plane"],
319
+ principalId: String(row["principal_id"]),
320
+ familyId: String(row["family_id"]),
321
+ revokedAt: asNum(row["revoked_at"]),
322
+ createdAt,
323
+ expiresAt: asNum(row["expires_at"]) ?? 0,
324
+ lastActiveAt: asNum(row["last_active_at"]) ?? createdAt,
244
325
  scopes,
245
- ...(row.audience ? { audience: row.audience } : {}),
326
+ ...(typeof row["audience"] === "string" && row["audience"].length > 0
327
+ ? { audience: row["audience"] }
328
+ : {}),
246
329
  };
247
330
  store.sessions.set(session.id, session);
248
331
  }
249
332
 
250
- const refreshRows = db
251
- .query(
252
- `SELECT id, session_id, family_id, hash, expires_at, used_at, revoked_at
253
- FROM ${AUTH_TABLES.refreshTokens}`,
254
- )
255
- .all() as Array<{
256
- id: string;
257
- session_id: string;
258
- family_id: string;
259
- hash: string;
260
- expires_at: number;
261
- used_at: number | null;
262
- revoked_at: number | null;
263
- }>;
333
+ const refreshRows = await sql.query(
334
+ `SELECT id, session_id, family_id, hash, expires_at, used_at, revoked_at
335
+ FROM ${refresh}`,
336
+ );
264
337
 
265
338
  for (const row of refreshRows) {
266
339
  const token: RefreshTokenRow = {
267
- id: row.id,
268
- sessionId: row.session_id,
269
- familyId: row.family_id,
270
- hash: row.hash,
271
- expiresAt: row.expires_at,
272
- usedAt: row.used_at,
273
- revokedAt: row.revoked_at,
340
+ id: String(row["id"]),
341
+ sessionId: String(row["session_id"]),
342
+ familyId: String(row["family_id"]),
343
+ hash: String(row["hash"]),
344
+ expiresAt: asNum(row["expires_at"]) ?? 0,
345
+ usedAt: asNum(row["used_at"]),
346
+ revokedAt: asNum(row["revoked_at"]),
274
347
  };
275
348
  store.refresh.set(token.id, token);
276
349
  }
@@ -281,133 +354,117 @@ export function loadSessionStore(db: Database): SessionStore {
281
354
  /**
282
355
  * Replace session tables with the in-memory store snapshot.
283
356
  *
284
- * @param db - Open database
357
+ * @param sql - Open connection
285
358
  * @param store - In-memory session store
286
359
  */
287
- export function persistSessions(db: Database, store: SessionStore): void {
288
- db.exec(`DELETE FROM ${AUTH_TABLES.refreshTokens}`);
289
- db.exec(`DELETE FROM ${AUTH_TABLES.sessions}`);
290
-
291
- const insertSession = db.query(
292
- `INSERT INTO ${AUTH_TABLES.sessions}
293
- (id, plane, principal_id, family_id, revoked_at, created_at, expires_at, last_active_at, scopes, audience)
294
- VALUES ($id, $plane, $principal, $family, $revoked, $created, $expires, $lastActive, $scopes, $audience)`,
295
- );
360
+ export async function persistSessions(sql: SqlConnection, store: SessionStore): Promise<void> {
361
+ const sessions = consoleTable(AUTH_TABLES.sessions);
362
+ const refresh = consoleTable(AUTH_TABLES.refreshTokens);
363
+
364
+ await sql.exec(`DELETE FROM ${refresh}`);
365
+ await sql.exec(`DELETE FROM ${sessions}`);
366
+
296
367
  for (const session of store.sessions.values()) {
297
- insertSession.run({
298
- $id: session.id,
299
- $plane: session.plane,
300
- $principal: session.principalId,
301
- $family: session.familyId,
302
- $revoked: session.revokedAt,
303
- $created: session.createdAt,
304
- $expires: session.expiresAt,
305
- $lastActive: session.lastActiveAt,
306
- $scopes: JSON.stringify(session.scopes ?? []),
307
- $audience: session.audience ?? null,
308
- });
368
+ await sql.exec(
369
+ `INSERT INTO ${sessions}
370
+ (id, plane, principal_id, family_id, revoked_at, created_at, expires_at, last_active_at, scopes, audience)
371
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
372
+ [
373
+ session.id,
374
+ session.plane,
375
+ session.principalId,
376
+ session.familyId,
377
+ session.revokedAt,
378
+ session.createdAt,
379
+ session.expiresAt,
380
+ session.lastActiveAt,
381
+ JSON.stringify(session.scopes ?? []),
382
+ session.audience ?? null,
383
+ ],
384
+ );
309
385
  }
310
386
 
311
- const insertRefresh = db.query(
312
- `INSERT INTO ${AUTH_TABLES.refreshTokens}
313
- (id, session_id, family_id, hash, expires_at, used_at, revoked_at)
314
- VALUES ($id, $session, $family, $hash, $expires, $used, $revoked)`,
315
- );
316
387
  for (const token of store.refresh.values()) {
317
- insertRefresh.run({
318
- $id: token.id,
319
- $session: token.sessionId,
320
- $family: token.familyId,
321
- $hash: token.hash,
322
- $expires: token.expiresAt,
323
- $used: token.usedAt,
324
- $revoked: token.revokedAt,
325
- });
388
+ await sql.exec(
389
+ `INSERT INTO ${refresh}
390
+ (id, session_id, family_id, hash, expires_at, used_at, revoked_at)
391
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
392
+ [
393
+ token.id,
394
+ token.sessionId,
395
+ token.familyId,
396
+ token.hash,
397
+ token.expiresAt,
398
+ token.usedAt,
399
+ token.revokedAt,
400
+ ],
401
+ );
326
402
  }
327
403
  }
328
404
 
329
405
  /**
330
- * Hydrate an {@link OperatorStore} from SQLite.
406
+ * Hydrate an {@link OperatorStore} from SQL.
331
407
  *
332
- * @param db - Open database
408
+ * @param sql - Open connection
333
409
  */
334
- export function loadOperatorStore(db: Database): OperatorStore {
410
+ export async function loadOperatorStore(sql: SqlConnection): Promise<OperatorStore> {
335
411
  const store = createOperatorStore();
336
-
337
- const opRows = db
338
- .query(
339
- `SELECT id, email, name, status, mfa_enabled, invited_by, last_seen_at
340
- FROM ${AUTH_TABLES.operators}`,
341
- )
342
- .all() as Array<{
343
- id: string;
344
- email: string;
345
- name: string;
346
- status: OperatorRow["status"];
347
- mfa_enabled: number;
348
- invited_by: string | null;
349
- last_seen_at: number | null;
350
- }>;
412
+ const ops = consoleTable(AUTH_TABLES.operators);
413
+ const creds = consoleTable(AUTH_TABLES.operatorCredentials);
414
+ const sso = consoleTable(AUTH_TABLES.operatorSsoLinks);
415
+ const roles = consoleTable(AUTH_TABLES.operatorRoles);
416
+
417
+ const opRows = await sql.query(
418
+ `SELECT id, email, name, status, mfa_enabled, invited_by, last_seen_at
419
+ FROM ${ops}`,
420
+ );
351
421
 
352
422
  for (const row of opRows) {
353
- store.operators.set(row.id, {
354
- id: row.id,
355
- email: row.email,
356
- name: row.name,
357
- status: row.status,
358
- mfaEnabled: row.mfa_enabled === 1,
359
- invitedBy: row.invited_by,
360
- lastSeenAt: row.last_seen_at,
423
+ store.operators.set(String(row["id"]), {
424
+ id: String(row["id"]),
425
+ email: String(row["email"]),
426
+ name: String(row["name"]),
427
+ status: row["status"] as OperatorRow["status"],
428
+ mfaEnabled: asBool(row["mfa_enabled"]),
429
+ invitedBy: typeof row["invited_by"] === "string" ? row["invited_by"] : null,
430
+ lastSeenAt: asNum(row["last_seen_at"]),
361
431
  });
362
432
  }
363
433
 
364
- const credRows = db
365
- .query(
366
- `SELECT operator_id, password_hash, login_enabled
367
- FROM ${AUTH_TABLES.operatorCredentials}`,
368
- )
369
- .all() as Array<{
370
- operator_id: string;
371
- password_hash: string;
372
- login_enabled: number;
373
- }>;
434
+ const credRows = await sql.query(
435
+ `SELECT operator_id, password_hash, login_enabled
436
+ FROM ${creds}`,
437
+ );
374
438
 
375
439
  for (const row of credRows) {
376
440
  const cred: OperatorCredentialRow = {
377
- operatorId: row.operator_id,
378
- passwordHash: row.password_hash,
379
- loginEnabled: row.login_enabled === 1,
441
+ operatorId: String(row["operator_id"]),
442
+ passwordHash: String(row["password_hash"]),
443
+ loginEnabled: asBool(row["login_enabled"]),
380
444
  };
381
- store.credentials.set(row.operator_id, cred);
445
+ store.credentials.set(cred.operatorId, cred);
382
446
  }
383
447
 
384
- const ssoRows = db
385
- .query(`SELECT operator_id, provider, subject FROM ${AUTH_TABLES.operatorSsoLinks}`)
386
- .all() as Array<{
387
- operator_id: string;
388
- provider: string;
389
- subject: string;
390
- }>;
448
+ const ssoRows = await sql.query(`SELECT operator_id, provider, subject FROM ${sso}`);
391
449
 
392
450
  for (const row of ssoRows) {
393
451
  const link: OperatorSsoLinkRow = {
394
- operatorId: row.operator_id,
395
- provider: row.provider,
396
- subject: row.subject,
452
+ operatorId: String(row["operator_id"]),
453
+ provider: String(row["provider"]),
454
+ subject: String(row["subject"]),
397
455
  };
398
- const list = store.ssoLinks.get(row.operator_id) ?? [];
456
+ const list = store.ssoLinks.get(link.operatorId) ?? [];
399
457
  list.push(link);
400
- store.ssoLinks.set(row.operator_id, list);
458
+ store.ssoLinks.set(link.operatorId, list);
401
459
  }
402
460
 
403
- const roleRows = db
404
- .query(`SELECT operator_id, role FROM ${AUTH_TABLES.operatorRoles}`)
405
- .all() as Array<{ operator_id: string; role: string }>;
461
+ const roleRows = await sql.query(`SELECT operator_id, role FROM ${roles}`);
406
462
 
407
463
  for (const row of roleRows) {
408
- const list = store.roles.get(row.operator_id) ?? [];
409
- list.push(row.role);
410
- store.roles.set(row.operator_id, list);
464
+ const operatorId = String(row["operator_id"]);
465
+ const list = store.roles.get(operatorId) ?? [];
466
+ list.push(String(row["role"]));
467
+ store.roles.set(operatorId, list);
411
468
  }
412
469
 
413
470
  return store;
@@ -416,11 +473,15 @@ export function loadOperatorStore(db: Database): OperatorStore {
416
473
  /**
417
474
  * Upsert one operator (and related rows) from the in-memory store.
418
475
  *
419
- * @param db - Open database
476
+ * @param sql - Open connection
420
477
  * @param store - In-memory store
421
478
  * @param operatorId - Operator id
422
479
  */
423
- export function persistOperator(db: Database, store: OperatorStore, operatorId: string): void {
480
+ export async function persistOperator(
481
+ sql: SqlConnection,
482
+ store: OperatorStore,
483
+ operatorId: string,
484
+ ): Promise<void> {
424
485
  const op = store.operators.get(operatorId);
425
486
  if (!op) {
426
487
  throw new Error(`oke console: unknown operator ${operatorId}`);
@@ -430,65 +491,46 @@ export function persistOperator(db: Database, store: OperatorStore, operatorId:
430
491
  throw new Error(`oke console: missing credential for ${operatorId}`);
431
492
  }
432
493
 
433
- const upsertOp = db.query(
434
- `INSERT INTO ${AUTH_TABLES.operators}
494
+ const ops = consoleTable(AUTH_TABLES.operators);
495
+ const creds = consoleTable(AUTH_TABLES.operatorCredentials);
496
+ const sso = consoleTable(AUTH_TABLES.operatorSsoLinks);
497
+ const roles = consoleTable(AUTH_TABLES.operatorRoles);
498
+
499
+ await sql.exec(
500
+ `INSERT INTO ${ops}
435
501
  (id, email, name, status, mfa_enabled, invited_by, last_seen_at)
436
- VALUES ($id, $email, $name, $status, $mfa, $invited, $seen)
437
- ON CONFLICT(id) DO UPDATE SET
438
- email = excluded.email,
439
- name = excluded.name,
440
- status = excluded.status,
441
- mfa_enabled = excluded.mfa_enabled,
442
- invited_by = excluded.invited_by,
443
- last_seen_at = excluded.last_seen_at`,
502
+ VALUES (?, ?, ?, ?, ?, ?, ?)
503
+ ON CONFLICT (id) DO UPDATE SET
504
+ email = EXCLUDED.email,
505
+ name = EXCLUDED.name,
506
+ status = EXCLUDED.status,
507
+ mfa_enabled = EXCLUDED.mfa_enabled,
508
+ invited_by = EXCLUDED.invited_by,
509
+ last_seen_at = EXCLUDED.last_seen_at`,
510
+ [op.id, op.email, op.name, op.status, op.mfaEnabled, op.invitedBy, op.lastSeenAt],
444
511
  );
445
- upsertOp.run({
446
- $id: op.id,
447
- $email: op.email,
448
- $name: op.name,
449
- $status: op.status,
450
- $mfa: op.mfaEnabled ? 1 : 0,
451
- $invited: op.invitedBy,
452
- $seen: op.lastSeenAt,
453
- });
454
-
455
- const upsertCred = db.query(
456
- `INSERT INTO ${AUTH_TABLES.operatorCredentials}
512
+
513
+ await sql.exec(
514
+ `INSERT INTO ${creds}
457
515
  (operator_id, password_hash, login_enabled)
458
- VALUES ($id, $hash, $enabled)
459
- ON CONFLICT(operator_id) DO UPDATE SET
460
- password_hash = excluded.password_hash,
461
- login_enabled = excluded.login_enabled`,
462
- );
463
- upsertCred.run({
464
- $id: cred.operatorId,
465
- $hash: cred.passwordHash,
466
- $enabled: cred.loginEnabled ? 1 : 0,
467
- });
468
-
469
- db.query(`DELETE FROM ${AUTH_TABLES.operatorSsoLinks} WHERE operator_id = $id`).run({
470
- $id: operatorId,
471
- });
472
- const insertSso = db.query(
473
- `INSERT INTO ${AUTH_TABLES.operatorSsoLinks} (operator_id, provider, subject)
474
- VALUES ($id, $provider, $subject)`,
516
+ VALUES (?, ?, ?)
517
+ ON CONFLICT (operator_id) DO UPDATE SET
518
+ password_hash = EXCLUDED.password_hash,
519
+ login_enabled = EXCLUDED.login_enabled`,
520
+ [cred.operatorId, cred.passwordHash, cred.loginEnabled],
475
521
  );
522
+
523
+ await sql.exec(`DELETE FROM ${sso} WHERE operator_id = ?`, [operatorId]);
476
524
  for (const link of store.ssoLinks.get(operatorId) ?? []) {
477
- insertSso.run({
478
- $id: link.operatorId,
479
- $provider: link.provider,
480
- $subject: link.subject,
481
- });
525
+ await sql.exec(`INSERT INTO ${sso} (operator_id, provider, subject) VALUES (?, ?, ?)`, [
526
+ link.operatorId,
527
+ link.provider,
528
+ link.subject,
529
+ ]);
482
530
  }
483
531
 
484
- db.query(`DELETE FROM ${AUTH_TABLES.operatorRoles} WHERE operator_id = $id`).run({
485
- $id: operatorId,
486
- });
487
- const insertRole = db.query(
488
- `INSERT INTO ${AUTH_TABLES.operatorRoles} (operator_id, role)
489
- VALUES ($id, $role)`,
490
- );
532
+ await sql.exec(`DELETE FROM ${roles} WHERE operator_id = ?`, [operatorId]);
491
533
  for (const role of store.roles.get(operatorId) ?? []) {
492
- insertRole.run({ $id: operatorId, $role: role });
534
+ await sql.exec(`INSERT INTO ${roles} (operator_id, role) VALUES (?, ?)`, [operatorId, role]);
493
535
  }
494
536
  }