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
@@ -0,0 +1,159 @@
1
+ /**
2
+ * AWS Secrets Manager bag — snapshot reads, write-through, fail-loud, peer gap.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { isVaultError } from "../elements/vault/errors.ts";
7
+ import {
8
+ openAwsSecretsManagerBag,
9
+ type AwsSecretsManagerClient,
10
+ } from "./vault-aws-secrets-manager.ts";
11
+
12
+ /** One recorded call against the fake API. */
13
+ interface Call {
14
+ readonly op: "list" | "get" | "put" | "remove";
15
+ readonly name?: string;
16
+ readonly value?: string;
17
+ }
18
+
19
+ /** Fake API options — which operations should reject, and how. */
20
+ interface FakeOptions {
21
+ readonly listError?: Error;
22
+ readonly getError?: Error;
23
+ readonly putError?: Error;
24
+ }
25
+
26
+ /** In-memory stand-in for the SDK-backed client. */
27
+ function fakeClient(
28
+ seed: Record<string, string>,
29
+ calls: Call[],
30
+ options: FakeOptions = {},
31
+ ): AwsSecretsManagerClient {
32
+ const store = new Map(Object.entries(seed));
33
+ return {
34
+ async list() {
35
+ calls.push({ op: "list" });
36
+ if (options.listError) throw options.listError;
37
+ return [...store.keys()];
38
+ },
39
+ async get(name) {
40
+ calls.push({ op: "get", name });
41
+ if (options.getError) throw options.getError;
42
+ return store.get(name);
43
+ },
44
+ async put(name, value) {
45
+ calls.push({ op: "put", name, value });
46
+ if (options.putError) throw options.putError;
47
+ store.set(name, value);
48
+ },
49
+ async remove(name) {
50
+ calls.push({ op: "remove", name });
51
+ store.delete(name);
52
+ },
53
+ };
54
+ }
55
+
56
+ describe("aws secrets manager vault bag", () => {
57
+ test("list + get snapshots every secret", async () => {
58
+ const calls: Call[] = [];
59
+ const bag = await openAwsSecretsManagerBag({
60
+ client: fakeClient({ STRIPE_KEY: "sk_live", DATABASE_URL: "postgres://x" }, calls),
61
+ });
62
+ expect([...bag.names()].sort()).toEqual(["DATABASE_URL", "STRIPE_KEY"]);
63
+ expect(bag.get("STRIPE_KEY")).toBe("sk_live");
64
+ expect(calls.filter((c) => c.op === "get")).toHaveLength(2);
65
+ });
66
+
67
+ test("denied list falls back to the declared names", async () => {
68
+ const calls: Call[] = [];
69
+ const denied = new Error("denied");
70
+ denied.name = "AccessDeniedException";
71
+ const bag = await openAwsSecretsManagerBag({
72
+ secrets: { STRIPE_KEY: "seed" },
73
+ client: fakeClient({ STRIPE_KEY: "sk_live", OTHER: "nope" }, calls, { listError: denied }),
74
+ });
75
+ expect(bag.names()).toEqual(["STRIPE_KEY"]);
76
+ expect(bag.get("STRIPE_KEY")).toBe("sk_live");
77
+ });
78
+
79
+ test("denied list with no declared names fails loud", async () => {
80
+ const denied = new Error("denied");
81
+ denied.name = "AccessDeniedException";
82
+ let failed: unknown;
83
+ try {
84
+ await openAwsSecretsManagerBag({
85
+ client: fakeClient({}, [], { listError: denied }),
86
+ });
87
+ } catch (error) {
88
+ failed = error;
89
+ }
90
+ expect(isVaultError(failed, "PERMISSION_DENIED")).toBe(true);
91
+ });
92
+
93
+ test("a failing read is fatal, never a silently empty bag", async () => {
94
+ let failed: unknown;
95
+ try {
96
+ await openAwsSecretsManagerBag({
97
+ client: fakeClient({ A: "1" }, [], { getError: new Error("boom") }),
98
+ });
99
+ } catch (error) {
100
+ failed = error;
101
+ }
102
+ expect(isVaultError(failed, "BACKEND_ERROR")).toBe(true);
103
+ });
104
+
105
+ test("the authored message never carries the SDK error text", async () => {
106
+ let failed: unknown;
107
+ try {
108
+ await openAwsSecretsManagerBag({
109
+ client: fakeClient({ A: "1" }, [], { getError: new Error("SecretString=sk_live_leak") }),
110
+ });
111
+ } catch (error) {
112
+ failed = error;
113
+ }
114
+ expect((failed as Error).message).not.toContain("sk_live_leak");
115
+ });
116
+
117
+ test("set writes through and close settles it", async () => {
118
+ const calls: Call[] = [];
119
+ const bag = await openAwsSecretsManagerBag({ client: fakeClient({ A: "1" }, calls) });
120
+ bag.set?.("A", "2");
121
+ expect(bag.get("A")).toBe("2");
122
+ await bag.close?.();
123
+ expect(calls).toContainEqual({ op: "put", name: "A", value: "2" });
124
+ });
125
+
126
+ test("delete removes locally and remotely", async () => {
127
+ const calls: Call[] = [];
128
+ const bag = await openAwsSecretsManagerBag({ client: fakeClient({ A: "1" }, calls) });
129
+ expect(bag.delete?.("A")).toBe(true);
130
+ expect(bag.get("A")).toBeUndefined();
131
+ await bag.close?.();
132
+ expect(calls).toContainEqual({ op: "remove", name: "A" });
133
+ });
134
+
135
+ test("a lost write surfaces at close instead of vanishing", async () => {
136
+ const bag = await openAwsSecretsManagerBag({
137
+ client: fakeClient({ A: "1" }, [], { putError: new Error("throttled") }),
138
+ });
139
+ bag.set?.("A", "2");
140
+ let failed: unknown;
141
+ try {
142
+ await bag.close?.();
143
+ } catch (error) {
144
+ failed = error;
145
+ }
146
+ expect(isVaultError(failed, "BACKEND_ERROR")).toBe(true);
147
+ });
148
+
149
+ test("missing optional peer reports the install command", async () => {
150
+ let failed: unknown;
151
+ try {
152
+ await openAwsSecretsManagerBag({ region: "us-east-1" });
153
+ } catch (error) {
154
+ failed = error;
155
+ }
156
+ expect(isVaultError(failed, "MISSING_PEER")).toBe(true);
157
+ expect((failed as Error).message).toContain("@aws-sdk/client-secrets-manager");
158
+ });
159
+ });
@@ -0,0 +1,333 @@
1
+ /**
2
+ * AWS Secrets Manager backend for the `managed` vault driver.
3
+ *
4
+ * Reads are snapshotted at `open` (the {@link VaultBag} surface is
5
+ * synchronous); writes and deletes go straight to the API and are settled by
6
+ * `close`.
7
+ *
8
+ * `@aws-sdk/client-secrets-manager` is an optional peer, imported dynamically
9
+ * so a project on platform-injected secrets or another provider never pays for it.
10
+ * Tests inject an {@link AwsSecretsManagerClient} and skip the SDK entirely.
11
+ *
12
+ * SDK errors are never re-thrown: their messages can echo request payloads,
13
+ * which for this API means secret material. Every failure is translated into
14
+ * a {@link VaultError} with a message this module authored.
15
+ */
16
+
17
+ import { VaultError } from "../elements/vault/errors.ts";
18
+ import type { VaultBag } from "./vault-types.ts";
19
+
20
+ /** Package name of the optional peer, resolved at call time. */
21
+ const AWS_SM_PACKAGE = "@aws-sdk/client-secrets-manager";
22
+
23
+ /** AWS error name for a secret id that does not exist. */
24
+ const NOT_FOUND = "ResourceNotFoundException";
25
+
26
+ /**
27
+ * Secret-name-addressed client the bag talks to.
28
+ *
29
+ * Names are bag names (`STRIPE_KEY`), never fully-qualified AWS secret ids —
30
+ * the prefix belongs to the client, so the bag stays prefix-agnostic.
31
+ */
32
+ export interface AwsSecretsManagerClient {
33
+ /** Bag names visible to this credential under the configured prefix. */
34
+ list(): Promise<readonly string[]>;
35
+ /**
36
+ * Read one secret's string value.
37
+ *
38
+ * @param name - Bag name
39
+ */
40
+ get(name: string): Promise<string | undefined>;
41
+ /**
42
+ * Create or overwrite one secret.
43
+ *
44
+ * @param name - Bag name
45
+ * @param value - Cleartext
46
+ */
47
+ put(name: string, value: string): Promise<void>;
48
+ /**
49
+ * Delete one secret. A missing secret is not an error.
50
+ *
51
+ * @param name - Bag name
52
+ */
53
+ remove(name: string): Promise<void>;
54
+ /** Release the underlying SDK client. */
55
+ close?(): Promise<void>;
56
+ }
57
+
58
+ /** Options for {@link openAwsSecretsManagerBag}. */
59
+ export interface OpenAwsSecretsManagerOptions {
60
+ /** AWS region. Falls back to the SDK's own resolution chain. */
61
+ readonly region?: string;
62
+ /** Secret-name prefix scoping this app (`oke/prod/`). */
63
+ readonly prefix?: string;
64
+ /** Injected client for tests — skips the SDK import entirely. */
65
+ readonly client?: AwsSecretsManagerClient;
66
+ /**
67
+ * Declared names, used as the read set when `ListSecrets` is denied so a
68
+ * least-privilege credential still boots.
69
+ */
70
+ readonly secrets?: Readonly<Record<string, string>>;
71
+ }
72
+
73
+ /**
74
+ * Open a secret bag backed by AWS Secrets Manager.
75
+ *
76
+ * An unreachable or unauthorized API is fatal: the bag never silently
77
+ * degrades to a seed-only view, because that turns a credentials outage into
78
+ * a "missing secret contract" message pointing at the wrong thing.
79
+ *
80
+ * @param options - Region / prefix / injected client / declared names
81
+ * @throws VaultError `MISSING_PEER` when the SDK is absent and no client is injected
82
+ * @throws VaultError `BACKEND_ERROR` when the API rejects the read
83
+ */
84
+ export async function openAwsSecretsManagerBag(
85
+ options: OpenAwsSecretsManagerOptions = {},
86
+ ): Promise<VaultBag> {
87
+ const client = options.client ?? (await connectSecretsManager(options.region, options.prefix));
88
+ const map = new Map<string, string>(
89
+ Object.entries(options.secrets ?? {}).filter(
90
+ (e): e is [string, string] => typeof e[1] === "string",
91
+ ),
92
+ );
93
+
94
+ let listed: readonly string[] = [];
95
+ try {
96
+ listed = await client.list();
97
+ } catch (error) {
98
+ // ListSecrets is commonly denied on least-privilege roles; only fail when
99
+ // there is no declared read set to fall back to.
100
+ if (map.size === 0) throw asVaultError(error, "vault: aws secrets manager list failed");
101
+ }
102
+
103
+ const names = listed.length > 0 ? listed : [...map.keys()];
104
+ for (const name of names) {
105
+ let value: string | undefined;
106
+ try {
107
+ value = await client.get(name);
108
+ } catch (error) {
109
+ throw asVaultError(error, `vault: aws secrets manager read of "${name}" failed`);
110
+ }
111
+ if (value !== undefined) map.set(name, value);
112
+ }
113
+
114
+ const pending: Promise<void>[] = [];
115
+ let failure: VaultError | undefined;
116
+ /**
117
+ * Run a write in the background, holding its failure for `close`.
118
+ *
119
+ * @param work - In-flight API call
120
+ * @param message - Safe message when it rejects
121
+ */
122
+ function track(work: Promise<void>, message: string): void {
123
+ pending.push(
124
+ work.catch((error: unknown) => {
125
+ failure ??= asVaultError(error, message);
126
+ }),
127
+ );
128
+ }
129
+
130
+ return {
131
+ driverId: "managed",
132
+ get(name) {
133
+ return map.get(name);
134
+ },
135
+ names() {
136
+ return [...map.keys()];
137
+ },
138
+ set(name, value) {
139
+ map.set(name, value);
140
+ track(client.put(name, value), `vault: aws secrets manager write of "${name}" failed`);
141
+ },
142
+ delete(name) {
143
+ const had = map.delete(name);
144
+ track(client.remove(name), `vault: aws secrets manager delete of "${name}" failed`);
145
+ return had;
146
+ },
147
+ async close() {
148
+ await Promise.all(pending);
149
+ await client.close?.();
150
+ if (failure) throw failure;
151
+ },
152
+ };
153
+ }
154
+
155
+ /** Response fields this module reads off the SDK. */
156
+ interface SecretsManagerResult {
157
+ readonly SecretString?: string;
158
+ readonly SecretList?: readonly { readonly Name?: string }[];
159
+ readonly NextToken?: string;
160
+ }
161
+
162
+ /** Structural SDK client — the SDK's `SecretsManagerClient` satisfies it. */
163
+ interface SdkClientLike {
164
+ send(command: unknown): Promise<SecretsManagerResult>;
165
+ destroy?(): void;
166
+ }
167
+
168
+ /** Constructors pulled off the dynamically imported module. */
169
+ interface SecretsManagerBindings {
170
+ readonly SecretsManagerClient: new (config: { region?: string }) => SdkClientLike;
171
+ readonly ListSecretsCommand: new (input: {
172
+ Filters?: { Key: string; Values: string[] }[];
173
+ MaxResults?: number;
174
+ NextToken?: string;
175
+ }) => unknown;
176
+ readonly GetSecretValueCommand: new (input: { SecretId: string }) => unknown;
177
+ readonly PutSecretValueCommand: new (input: {
178
+ SecretId: string;
179
+ SecretString: string;
180
+ }) => unknown;
181
+ readonly CreateSecretCommand: new (input: { Name: string; SecretString: string }) => unknown;
182
+ readonly DeleteSecretCommand: new (input: {
183
+ SecretId: string;
184
+ ForceDeleteWithoutRecovery: boolean;
185
+ }) => unknown;
186
+ }
187
+
188
+ /**
189
+ * Build a real client over the optional AWS SDK peer.
190
+ *
191
+ * @param region - AWS region
192
+ * @param prefix - Secret-name prefix scoping this app
193
+ * @throws VaultError `MISSING_PEER` when the SDK is absent
194
+ */
195
+ async function connectSecretsManager(
196
+ region: string | undefined,
197
+ prefix: string | undefined,
198
+ ): Promise<AwsSecretsManagerClient> {
199
+ const sdk = await loadSecretsManager();
200
+ const at = prefix ?? "";
201
+ const client = new sdk.SecretsManagerClient(region === undefined ? {} : { region });
202
+
203
+ return {
204
+ async list() {
205
+ const names: string[] = [];
206
+ let token: string | undefined;
207
+ do {
208
+ const page: SecretsManagerResult = await client.send(
209
+ new sdk.ListSecretsCommand({
210
+ ...(at === "" ? {} : { Filters: [{ Key: "name", Values: [at] }] }),
211
+ MaxResults: 100,
212
+ ...(token === undefined ? {} : { NextToken: token }),
213
+ }),
214
+ );
215
+ for (const entry of page.SecretList ?? []) {
216
+ const id = entry.Name;
217
+ if (id === undefined || !id.startsWith(at)) continue;
218
+ names.push(id.slice(at.length));
219
+ }
220
+ token = page.NextToken;
221
+ } while (token !== undefined);
222
+ return names;
223
+ },
224
+ async get(name) {
225
+ try {
226
+ const result = await client.send(
227
+ new sdk.GetSecretValueCommand({ SecretId: `${at}${name}` }),
228
+ );
229
+ return result.SecretString;
230
+ } catch (error) {
231
+ if (errorName(error) === NOT_FOUND) return undefined;
232
+ throw error;
233
+ }
234
+ },
235
+ async put(name, value) {
236
+ try {
237
+ await client.send(
238
+ new sdk.PutSecretValueCommand({ SecretId: `${at}${name}`, SecretString: value }),
239
+ );
240
+ } catch (error) {
241
+ if (errorName(error) !== NOT_FOUND) throw error;
242
+ await client.send(
243
+ new sdk.CreateSecretCommand({ Name: `${at}${name}`, SecretString: value }),
244
+ );
245
+ }
246
+ },
247
+ async remove(name) {
248
+ try {
249
+ await client.send(
250
+ new sdk.DeleteSecretCommand({
251
+ SecretId: `${at}${name}`,
252
+ ForceDeleteWithoutRecovery: true,
253
+ }),
254
+ );
255
+ } catch (error) {
256
+ if (errorName(error) !== NOT_FOUND) throw error;
257
+ }
258
+ },
259
+ async close() {
260
+ client.destroy?.();
261
+ },
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Import the optional `@aws-sdk/client-secrets-manager` peer.
267
+ *
268
+ * @throws VaultError `MISSING_PEER` with the exact install command
269
+ */
270
+ async function loadSecretsManager(): Promise<SecretsManagerBindings> {
271
+ // Indirect specifier: the peer is optional, so it must not become a
272
+ // static dependency of the module graph.
273
+ const specifier: string = AWS_SM_PACKAGE;
274
+ let mod: unknown;
275
+ try {
276
+ mod = await import(specifier);
277
+ } catch {
278
+ throw new VaultError(
279
+ "MISSING_PEER",
280
+ `vault: managed provider "aws-secrets-manager" needs the optional peer \`${AWS_SM_PACKAGE}\` (bun add ${AWS_SM_PACKAGE})`,
281
+ );
282
+ }
283
+ const bindings = mod as Partial<SecretsManagerBindings>;
284
+ if (
285
+ !bindings.SecretsManagerClient ||
286
+ !bindings.ListSecretsCommand ||
287
+ !bindings.GetSecretValueCommand ||
288
+ !bindings.PutSecretValueCommand ||
289
+ !bindings.CreateSecretCommand ||
290
+ !bindings.DeleteSecretCommand
291
+ ) {
292
+ throw new VaultError(
293
+ "MISSING_PEER",
294
+ `vault: \`${AWS_SM_PACKAGE}\` is installed but does not export the SecretsManager client and commands`,
295
+ );
296
+ }
297
+ return {
298
+ SecretsManagerClient: bindings.SecretsManagerClient,
299
+ ListSecretsCommand: bindings.ListSecretsCommand,
300
+ GetSecretValueCommand: bindings.GetSecretValueCommand,
301
+ PutSecretValueCommand: bindings.PutSecretValueCommand,
302
+ CreateSecretCommand: bindings.CreateSecretCommand,
303
+ DeleteSecretCommand: bindings.DeleteSecretCommand,
304
+ };
305
+ }
306
+
307
+ /**
308
+ * AWS error discriminator (`ResourceNotFoundException`, …).
309
+ *
310
+ * Reads only `name`, never `message`: the latter can quote request payloads.
311
+ *
312
+ * @param error - Thrown value
313
+ */
314
+ function errorName(error: unknown): string | undefined {
315
+ if (typeof error !== "object" || error === null) return undefined;
316
+ const name = (error as { name?: unknown }).name;
317
+ return typeof name === "string" ? name : undefined;
318
+ }
319
+
320
+ /**
321
+ * Translate a foreign failure into a secret-free {@link VaultError}.
322
+ *
323
+ * @param error - Thrown value
324
+ * @param message - Safe message authored by this module
325
+ */
326
+ function asVaultError(error: unknown, message: string): VaultError {
327
+ if (error instanceof VaultError) return error;
328
+ const name = errorName(error);
329
+ if (name === "AccessDeniedException" || name === "UnrecognizedClientException") {
330
+ return new VaultError("PERMISSION_DENIED", `${message} — credentials denied`);
331
+ }
332
+ return new VaultError("BACKEND_ERROR", message);
333
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `vault` driver — adapter wiring and the never-fail-boot degradation path.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { mkdtemp, rm } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { connectPglite } from "./pglite.ts";
10
+ import { builtinVaultDriver, openBuiltinVaultAdapter, VAULT_BAG_ADAPTER } from "./vault-builtin.ts";
11
+ import type { BuiltinVaultAdapter } from "../elements/vault/builtin-adapter.ts";
12
+
13
+ describe("builtin vault driver", () => {
14
+ test("openBuiltinVaultAdapter unseals an injected connection", async () => {
15
+ const connection = await connectPglite({ url: "memory://vault-driver-test" });
16
+ try {
17
+ const first = await openBuiltinVaultAdapter({ connection, env: {} });
18
+ const init = await first.adapter.initialize();
19
+ await first.adapter.unseal(init.masterKey);
20
+ await first.adapter.set("prod/api/stripe", "sk_live_x");
21
+
22
+ // A second open over the same connection unseals from the master key.
23
+ const second = await openBuiltinVaultAdapter({
24
+ connection,
25
+ env: { OKE_VAULT_MASTER_KEY: init.masterKey },
26
+ });
27
+ expect(second.adapter.getUnsealer()).not.toBeNull();
28
+ expect((await second.adapter.get("prod/api/stripe"))?.value).toBe("sk_live_x");
29
+ } finally {
30
+ await connection.close();
31
+ }
32
+ });
33
+
34
+ test("open with no SQL configured degrades to the seed bag", async () => {
35
+ const bag = await builtinVaultDriver.open({
36
+ env: {},
37
+ secrets: { APP_NAME: "demo" },
38
+ });
39
+ try {
40
+ expect(bag.driverId).toBe("vault");
41
+ expect(bag.get("APP_NAME")).toBe("demo");
42
+ expect(bag.get("MISSING")).toBeUndefined();
43
+ expect(bag.names()).toEqual(["APP_NAME"]);
44
+ } finally {
45
+ await bag.close?.();
46
+ }
47
+ });
48
+
49
+ test("bag close auto-seals the adapter (SIGTERM → bootResult.close path)", async () => {
50
+ const connection = await connectPglite({ url: "memory://vault-autoseal" });
51
+ try {
52
+ const staging = await openBuiltinVaultAdapter({ connection, env: {} });
53
+ const init = await staging.adapter.initialize();
54
+ await staging.adapter.unseal(init.masterKey);
55
+ await staging.adapter.set("prod/api/stripe", "sk_live_z");
56
+ await staging.adapter.seal();
57
+
58
+ const bag = await builtinVaultDriver.open({
59
+ connection,
60
+ env: { OKE_VAULT_MASTER_KEY: init.masterKey },
61
+ });
62
+ const held = (bag as unknown as Record<symbol, BuiltinVaultAdapter | undefined>)[
63
+ VAULT_BAG_ADAPTER
64
+ ];
65
+ expect(held).toBeDefined();
66
+ expect(held!.getUnsealer()).not.toBeNull();
67
+ expect(bag.get("prod/api/stripe")).toBe("sk_live_z");
68
+
69
+ await bag.close?.();
70
+ expect(held!.getUnsealer()).toBeNull();
71
+ await expect(held!.get("prod/api/stripe")).rejects.toThrow(/sealed/i);
72
+ } finally {
73
+ await connection.close();
74
+ }
75
+ });
76
+
77
+ test("open snapshots live secrets from an initialized vault", async () => {
78
+ const dir = await mkdtemp(join(tmpdir(), "oke-vault-driver-"));
79
+ try {
80
+ const staging = await connectPglite({ url: dir });
81
+ const opened = await openBuiltinVaultAdapter({ connection: staging, env: {} });
82
+ const init = await opened.adapter.initialize();
83
+ await opened.adapter.unseal(init.masterKey);
84
+ await opened.adapter.set("STRIPE_KEY", "sk_live_y");
85
+ await staging.close();
86
+
87
+ const bag = await builtinVaultDriver.open({
88
+ url: dir,
89
+ env: { OKE_VAULT_MASTER_KEY: init.masterKey },
90
+ });
91
+ try {
92
+ expect(bag.names()).toEqual(["STRIPE_KEY"]);
93
+ expect(bag.get("STRIPE_KEY")).toBe("sk_live_y");
94
+ } finally {
95
+ await bag.close?.();
96
+ }
97
+
98
+ // Without the master key the vault stays sealed and the bag is empty —
99
+ // boot then reports ordinary gaps instead of a driver failure.
100
+ const sealed = await builtinVaultDriver.open({ url: dir, env: {} });
101
+ try {
102
+ expect(sealed.names()).toEqual([]);
103
+ } finally {
104
+ await sealed.close?.();
105
+ }
106
+ } finally {
107
+ await rm(dir, { recursive: true, force: true });
108
+ }
109
+ });
110
+ });