okengine 0.15.2 → 0.17.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 (256) hide show
  1. package/manifest.v1.schema.json +21 -2
  2. package/package.json +1 -1
  3. package/site/content/docs/elements/clock.mdx +7 -0
  4. package/site/content/docs/elements/flow.mdx +20 -12
  5. package/site/content/docs/elements/gate.mdx +108 -3
  6. package/site/content/docs/elements/signal.mdx +46 -17
  7. package/site/content/docs/elements/store.mdx +36 -32
  8. package/site/content/docs/elements/vault.mdx +26 -0
  9. package/site/content/docs/get-started/project-structure.mdx +4 -4
  10. package/site/content/docs/reference/cli.md +1 -1
  11. package/site/content/docs/reference/client.mdx +75 -17
  12. package/site/content/docs/reference/configuration.mdx +7 -4
  13. package/site/content/docs/reference/errors.mdx +24 -17
  14. package/site/content/docs/reference/fx.mdx +39 -22
  15. package/site/content/docs/reference/security.md +1 -1
  16. package/src/auth/api-key-sql.test.ts +47 -0
  17. package/src/auth/api-key-sql.ts +261 -0
  18. package/src/auth/api-keys.ts +276 -14
  19. package/src/auth/auth.test.ts +56 -0
  20. package/src/auth/config.ts +24 -5
  21. package/src/auth/index.ts +62 -1
  22. package/src/auth/plugin.ts +6 -1
  23. package/src/auth/schema.ts +8 -0
  24. package/src/auth/sessions.ts +18 -0
  25. package/src/auth/tables.ts +32 -0
  26. package/src/auth/tenant-config.ts +74 -0
  27. package/src/auth/tenant-tables.ts +11 -0
  28. package/src/auth/tenants.test.ts +63 -0
  29. package/src/auth/tenants.ts +360 -0
  30. package/src/cli/attach-host-console.test.ts +37 -0
  31. package/src/cli/attach-host-console.ts +6 -0
  32. package/src/cli/build.ts +2 -2
  33. package/src/client/budget.test.ts +1 -1
  34. package/src/client/create.ts +75 -5
  35. package/src/client/index.ts +13 -0
  36. package/src/client/live.test.ts +422 -0
  37. package/src/client/live.ts +389 -0
  38. package/src/client/notes-contract.test.ts +41 -0
  39. package/src/client/types.ts +85 -6
  40. package/src/client-react/index.ts +85 -1
  41. package/src/client-react/use-live.test.ts +129 -0
  42. package/src/compiler/effects-infer.ts +37 -2
  43. package/src/compiler/extract.test.ts +143 -11
  44. package/src/compiler/extract.ts +210 -30
  45. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
  46. package/src/compiler/fixtures/skyport.expected.json +2 -3
  47. package/src/compiler/response.ts +41 -11
  48. package/src/console/server/access.test.ts +116 -8
  49. package/src/console/server/access.ts +70 -23
  50. package/src/console/server/app.ts +45 -11
  51. package/src/console/server/console.test.ts +6 -8
  52. package/src/console/server/dev-identities.test.ts +127 -0
  53. package/src/console/server/dev-identities.ts +229 -0
  54. package/src/console/server/flows-invoke.test.ts +21 -4
  55. package/src/console/server/flows.ts +113 -58
  56. package/src/console/server/gates.test.ts +24 -0
  57. package/src/console/server/gates.ts +10 -30
  58. package/src/console/server/index.ts +1 -0
  59. package/src/console/server/live.test.ts +26 -1
  60. package/src/console/server/live.ts +4 -2
  61. package/src/console/server/operator-db.ts +2 -0
  62. package/src/console/server/state.ts +29 -55
  63. package/src/console/server/store.test.ts +17 -0
  64. package/src/console/server/store.ts +43 -1
  65. package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
  66. package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
  67. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  68. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  69. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  70. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  71. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  72. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  73. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  74. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  75. package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  80. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  81. package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
  82. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  83. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  84. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
  90. package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
  92. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  93. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  94. package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
  95. package/src/console/ui-next/dist/index.html +11 -9
  96. package/src/console/ui-next/index.html +3 -3
  97. package/src/console/ui-next/src/client.ts +167 -0
  98. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  99. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  100. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  101. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  102. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  103. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  104. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  105. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  106. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  107. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  108. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  109. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  110. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  111. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  112. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  113. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  114. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  115. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  116. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  117. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  118. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  119. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  120. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  121. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  122. package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
  123. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  124. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
  125. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
  126. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  127. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  128. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  129. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  130. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  131. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  132. package/src/console/ui-next/src/router.tsx +10 -1
  133. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  134. package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
  135. package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
  136. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  137. package/src/console/xss.gate.test.ts +1 -0
  138. package/src/drivers/index.ts +2 -0
  139. package/src/drivers/journal-postgres.ts +12 -3
  140. package/src/drivers/pg-rls.ts +26 -2
  141. package/src/drivers/pg-vault-rls.ts +68 -0
  142. package/src/drivers/signal-engine.ts +69 -15
  143. package/src/drivers/signal-live-iter.ts +65 -0
  144. package/src/drivers/signal-nats.ts +2 -1
  145. package/src/drivers/signal-postgres.ts +95 -12
  146. package/src/drivers/signal-redis.ts +2 -1
  147. package/src/drivers/signal-retention.ts +64 -0
  148. package/src/drivers/signal-types.ts +35 -5
  149. package/src/elements/clock/declare.ts +64 -2
  150. package/src/elements/clock/reconcile.ts +98 -25
  151. package/src/elements/clock/runtime.ts +13 -2
  152. package/src/elements/clock.test.ts +28 -0
  153. package/src/elements/clock.ts +9 -1
  154. package/src/elements/gate/declare.ts +2 -0
  155. package/src/elements/gate/permissions.ts +12 -0
  156. package/src/elements/gate.ts +6 -1
  157. package/src/elements/signal/declare.ts +44 -10
  158. package/src/elements/signal/delivery-modes.test.ts +90 -4
  159. package/src/elements/signal/order-lifecycle.test.ts +20 -4
  160. package/src/elements/signal/runtime.ts +42 -0
  161. package/src/elements/signal.test.ts +21 -8
  162. package/src/elements/signal.ts +1 -1
  163. package/src/elements/store/cache.ts +1 -1
  164. package/src/elements/store/declare.ts +7 -0
  165. package/src/elements/store/rls-identity.test.ts +29 -0
  166. package/src/elements/store/rls-identity.ts +3 -0
  167. package/src/elements/store/runtime.ts +14 -0
  168. package/src/elements/store/schema-decl.ts +162 -44
  169. package/src/elements/store/schema-tenant.ts +41 -0
  170. package/src/elements/store/sql-rls-isolation.test.ts +39 -0
  171. package/src/elements/store/sql-select.types.test.ts +71 -0
  172. package/src/elements/store/sql-session.ts +54 -32
  173. package/src/elements/store.ts +5 -0
  174. package/src/elements/vault/builtin-adapter.ts +15 -2
  175. package/src/elements/vault/declare.ts +8 -0
  176. package/src/elements/vault/runtime.ts +7 -0
  177. package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
  178. package/src/elements/vault/storage.ts +9 -0
  179. package/src/elements/vault/test-helpers.ts +2 -1
  180. package/src/i18n/catalogs/ar.ts +19 -0
  181. package/src/i18n/catalogs/en.ts +19 -0
  182. package/src/index.ts +2 -0
  183. package/src/kernel/adopt-routes.ts +56 -8
  184. package/src/kernel/api-key-host-persist.test.ts +143 -0
  185. package/src/kernel/app-tenant.ts +122 -0
  186. package/src/kernel/app.ts +196 -57
  187. package/src/kernel/auth-resolve.ts +13 -5
  188. package/src/kernel/boot-bind/clock.ts +9 -3
  189. package/src/kernel/boot.test.ts +4 -4
  190. package/src/kernel/boot.ts +2 -0
  191. package/src/kernel/budget.test.ts +1 -1
  192. package/src/kernel/clock-durable.ts +8 -0
  193. package/src/kernel/clock-per-tenant-name.ts +5 -0
  194. package/src/kernel/clock-reconcile.ts +8 -0
  195. package/src/kernel/errors-live-resume.ts +15 -0
  196. package/src/kernel/errors-tenant.ts +29 -0
  197. package/src/kernel/errors.registry.test.ts +31 -3
  198. package/src/kernel/errors.ts +43 -3
  199. package/src/kernel/flow.ts +26 -5
  200. package/src/kernel/fx-auth-keys.test.ts +88 -0
  201. package/src/kernel/fx-auth-keys.ts +203 -0
  202. package/src/kernel/fx-auth-tenants.test.ts +87 -0
  203. package/src/kernel/fx-auth-tenants.ts +286 -0
  204. package/src/kernel/fx-live-stream.ts +149 -0
  205. package/src/kernel/fx-live.test.ts +157 -0
  206. package/src/kernel/fx-runtime.ts +15 -0
  207. package/src/kernel/fx-tenant-store.ts +213 -0
  208. package/src/kernel/fx.test.ts +108 -1
  209. package/src/kernel/fx.ts +206 -28
  210. package/src/kernel/hooks.ts +2 -2
  211. package/src/kernel/http-resource.ts +9 -18
  212. package/src/kernel/index.ts +2 -0
  213. package/src/kernel/journal.ts +12 -0
  214. package/src/kernel/live-http.test.ts +78 -0
  215. package/src/kernel/live-http.ts +114 -0
  216. package/src/kernel/live-resume.test.ts +125 -0
  217. package/src/kernel/on.ts +51 -0
  218. package/src/kernel/pipeline-api-key.test.ts +192 -7
  219. package/src/kernel/pipeline-tenant.ts +49 -0
  220. package/src/kernel/pipeline.test.ts +1 -1
  221. package/src/kernel/pipeline.ts +41 -4
  222. package/src/kernel/resource-mount.test.ts +10 -27
  223. package/src/kernel/tenant-resolve.test.ts +101 -0
  224. package/src/kernel/tenant-resolve.ts +124 -0
  225. package/src/kernel/tenant-roles.test.ts +87 -0
  226. package/src/kernel/triggers.ts +59 -21
  227. package/src/manifest/diff.test.ts +11 -2
  228. package/src/manifest/diff.ts +53 -11
  229. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  230. package/src/manifest/fixtures/skyport.manifest.json +0 -1
  231. package/src/manifest/types.ts +58 -6
  232. package/src/release/build-lib.ts +14 -1
  233. package/src/release/limits.ts +2 -2
  234. package/src/release/measure.ts +55 -1
  235. package/src/client/live-gap.test.ts +0 -35
  236. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
  237. package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
  238. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
  239. package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
  240. package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
  241. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
  242. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
  243. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
  244. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
  245. package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
  246. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
  247. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
  248. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
  249. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  250. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
  251. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
  252. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
  253. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
  254. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
  255. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
  256. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Tenant KV prefix + vault request-time path — lazy chunk.
3
+ *
4
+ * Inlined wrappers would sit on every `createFx` / Store-only `oke()` graph
5
+ * even when `gate.auth.tenant` is off.
6
+ */
7
+
8
+ import type { KvStoreDecl } from "../elements/store.ts";
9
+ import type { VaultRuntime } from "../elements/vault.ts";
10
+ import type { Manifest } from "../manifest/types.ts";
11
+ import type { SessionCrypto, SessionStore } from "../auth/sessions.ts";
12
+ import type { TenantStore } from "../auth/tenants.ts";
13
+ import { throwOke } from "./errors.ts";
14
+ import type { FxAuthIdentity } from "./fx-auth-keys.ts";
15
+ import { bind, type AttachAuthTenantMethodsOptions } from "./fx-auth-tenants.ts";
16
+
17
+ /**
18
+ * Whether this KV namespace is tenant-prefixed.
19
+ *
20
+ * @param decl - KV store decl
21
+ * @param tenantEnabled - `gate.auth.tenant` is on
22
+ */
23
+ export function kvTenantScoped(decl: KvStoreDecl, tenantEnabled: boolean): boolean {
24
+ if (decl.tenantScoped === false) return false;
25
+ if (decl.tenantScoped === true) return true;
26
+ return tenantEnabled;
27
+ }
28
+
29
+ /**
30
+ * Prefix KV args with `{tenantId}:`. Fails loud when no tenant is resolved.
31
+ *
32
+ * @param tenantId - Live `fx.tenant.id`
33
+ * @param decl - KV store decl
34
+ * @param tenantEnabled - Tenancy switch
35
+ * @param prop - Handle method
36
+ * @param args - Original args
37
+ */
38
+ export function rewriteKvArgs(
39
+ tenantId: string | null,
40
+ decl: KvStoreDecl,
41
+ tenantEnabled: boolean,
42
+ prop: string | symbol,
43
+ args: unknown[],
44
+ ): unknown[] {
45
+ if (!kvTenantScoped(decl, tenantEnabled)) return args;
46
+ if (!tenantId) throwOke("TENANT_REQUIRED");
47
+ const prefix = `${tenantId}:`;
48
+ if (prop === "list") {
49
+ const userPrefix = typeof args[0] === "string" ? args[0] : "";
50
+ return [`${prefix}${userPrefix}`];
51
+ }
52
+ if (typeof args[0] !== "string") return args;
53
+ return [`${prefix}${args[0]}`, ...args.slice(1)];
54
+ }
55
+
56
+ /**
57
+ * Strip the tenant prefix from `list()` results.
58
+ *
59
+ * @param tenantId - Live `fx.tenant.id`
60
+ * @param decl - KV store decl
61
+ * @param tenantEnabled - Tenancy switch
62
+ * @param keys - Physical keys
63
+ */
64
+ export function stripKvPrefix(
65
+ tenantId: string | null,
66
+ decl: KvStoreDecl,
67
+ tenantEnabled: boolean,
68
+ keys: string[],
69
+ ): string[] {
70
+ if (!kvTenantScoped(decl, tenantEnabled) || !tenantId) return keys;
71
+ const prefix = `${tenantId}:`;
72
+ return keys.map((k) => (k.startsWith(prefix) ? k.slice(prefix.length) : k));
73
+ }
74
+
75
+ /**
76
+ * Request-time vault path `{tenantId}/{contract}` when the contract is per-tenant.
77
+ *
78
+ * @param tenantId - Live `fx.tenant.id`
79
+ * @param tenantEnabled - Tenancy switch
80
+ * @param contracts - Vault runtime contracts
81
+ * @param contractName - Capability name
82
+ */
83
+ export function vaultStoragePath(
84
+ tenantId: string | null,
85
+ tenantEnabled: boolean,
86
+ contracts: VaultRuntime["contracts"] | undefined,
87
+ contractName: string,
88
+ ): string {
89
+ const decl = contracts?.get(contractName);
90
+ const perTenant =
91
+ decl?.perTenant === true || (tenantEnabled && decl !== undefined && decl.perTenant !== false);
92
+ if (!perTenant) return contractName;
93
+ if (!tenantId) throwOke("TENANT_REQUIRED");
94
+ return `${tenantId}/${contractName}`;
95
+ }
96
+
97
+ /**
98
+ * Compact KV rewrite used by `createFx` so long helper names stay off the
99
+ * Store-only `oke()` graph.
100
+ *
101
+ * @param mode - `in` prefixes args; `out` strips `list()` keys
102
+ * @param tenantId - Live `fx.tenant.id`
103
+ * @param decl - KV store decl
104
+ * @param tenantEnabled - Tenancy switch
105
+ * @param prop - Handle method
106
+ * @param payload - Args or list keys
107
+ */
108
+ export function kv(
109
+ mode: "in" | "out",
110
+ tenantId: string | null,
111
+ decl: KvStoreDecl,
112
+ tenantEnabled: boolean,
113
+ prop: string | symbol,
114
+ payload: unknown,
115
+ ): unknown {
116
+ if (mode === "out") return stripKvPrefix(tenantId, decl, tenantEnabled, payload as string[]);
117
+ return rewriteKvArgs(tenantId, decl, tenantEnabled, prop, payload as unknown[]);
118
+ }
119
+
120
+ /**
121
+ * Compact vault path used by `createFx`.
122
+ *
123
+ * @param tenantId - Live `fx.tenant.id`
124
+ * @param tenantEnabled - Tenancy switch
125
+ * @param contracts - Vault runtime contracts
126
+ * @param contractName - Capability name
127
+ */
128
+ export function path(
129
+ tenantId: string | null,
130
+ tenantEnabled: boolean,
131
+ contracts: VaultRuntime["contracts"] | undefined,
132
+ contractName: string,
133
+ ): string {
134
+ return vaultStoragePath(tenantId, tenantEnabled, contracts, contractName);
135
+ }
136
+
137
+ /**
138
+ * Fail loud when a per-tenant vault row is missing.
139
+ *
140
+ * @param storagePath - Physical `{tenantId}/{contract}` path
141
+ */
142
+ export function missingVault(storagePath: string): Error {
143
+ return new Error(`fx.vault.get: missing per-tenant secret "${storagePath}"`);
144
+ }
145
+
146
+ /**
147
+ * Attach tenant auth methods and KV prefixing onto a live `fx` bag.
148
+ * Called from `createFx` only when tenancy is on.
149
+ *
150
+ * @param fx - Mutable fx bag
151
+ * @param ctx - Tenant id + createFx options + gates
152
+ */
153
+ export function install(
154
+ fx: {
155
+ auth: FxAuthIdentity;
156
+ store: (ref: never) => object;
157
+ },
158
+ ctx: {
159
+ readonly tenantId: string | null;
160
+ readonly options: {
161
+ readonly tenantStore?: TenantStore;
162
+ readonly tenantEnabled?: boolean;
163
+ readonly sessions?: SessionStore;
164
+ readonly sessionCrypto?: SessionCrypto;
165
+ readonly manifest?: Manifest | null;
166
+ };
167
+ readonly now: () => number;
168
+ readonly gated: AttachAuthTenantMethodsOptions["gated"];
169
+ },
170
+ ): void {
171
+ const next = bind(
172
+ fx.auth,
173
+ {
174
+ tenantStore: ctx.options.tenantStore,
175
+ sessions: ctx.options.sessions,
176
+ sessionCrypto: ctx.options.sessionCrypto,
177
+ manifest: ctx.options.manifest ?? undefined,
178
+ },
179
+ ctx.now,
180
+ ctx.gated,
181
+ );
182
+ fx.auth = next;
183
+ if (ctx.options.tenantEnabled !== true) return;
184
+ const inner = fx.store;
185
+ (fx as { store: typeof inner }).store = ((ref: never) => {
186
+ const handle = inner(ref);
187
+ if (
188
+ typeof ref === "object" &&
189
+ ref !== null &&
190
+ "facet" in ref &&
191
+ (ref as KvStoreDecl).facet === "kv"
192
+ ) {
193
+ const decl = ref as KvStoreDecl;
194
+ return new Proxy(handle, {
195
+ get(target, prop, receiver) {
196
+ const val = Reflect.get(target, prop, receiver);
197
+ if (typeof val !== "function" || prop === "then") return val;
198
+ return (...args: unknown[]) => {
199
+ const callArgs = rewriteKvArgs(ctx.tenantId, decl, true, prop, args);
200
+ const result = (val as (...a: unknown[]) => unknown).apply(target, callArgs);
201
+ if (prop === "list") {
202
+ return Promise.resolve(result as Promise<string[]>).then((keys) =>
203
+ stripKvPrefix(ctx.tenantId, decl, true, keys),
204
+ );
205
+ }
206
+ return result;
207
+ };
208
+ },
209
+ });
210
+ }
211
+ return handle;
212
+ }) as typeof inner;
213
+ }
@@ -86,6 +86,52 @@ describe("fx — capability enforcement", () => {
86
86
  expect(ttl).toBeGreaterThan(0);
87
87
  });
88
88
 
89
+ test("kv prefixes keys with tenant id and fails loud without one", async () => {
90
+ const { createStoreRuntime, store } = await import("../elements/store.ts");
91
+ const { memoryKvDriver } = await import("../drivers/index.ts");
92
+ const decl = store.kv("drafts");
93
+ const rt = createStoreRuntime({
94
+ drivers: { kv: memoryKvDriver },
95
+ kv: { drafts: {} },
96
+ });
97
+ rt.register(decl);
98
+ const acme = createFx({
99
+ flow: "drafts.acme",
100
+ effects: { reads: ["kv:drafts"], writes: ["kv:drafts"] },
101
+ storeRuntime: rt,
102
+ tenantEnabled: true,
103
+ tenant: { id: "acme" },
104
+ });
105
+ const globex = createFx({
106
+ flow: "drafts.globex",
107
+ effects: { reads: ["kv:drafts"], writes: ["kv:drafts"] },
108
+ storeRuntime: rt,
109
+ tenantEnabled: true,
110
+ tenant: { id: "globex" },
111
+ });
112
+ await acme.store(decl).set("note", "alice");
113
+ await globex.store(decl).set("note", "bob");
114
+ expect(await acme.store(decl).get("note")).toBe("alice");
115
+ expect(await globex.store(decl).get("note")).toBe("bob");
116
+ expect(await acme.store(decl).list()).toEqual(["note"]);
117
+
118
+ const none = createFx({
119
+ flow: "drafts.none",
120
+ effects: { writes: ["kv:drafts"] },
121
+ storeRuntime: rt,
122
+ tenantEnabled: true,
123
+ tenant: { id: null },
124
+ });
125
+ let err: unknown;
126
+ try {
127
+ await none.store(decl).set("note", "x");
128
+ } catch (e) {
129
+ err = e;
130
+ }
131
+ expect(err).toBeInstanceOf(OkeError);
132
+ expect((err as OkeError).code).toBe(1015);
133
+ });
134
+
89
135
  test("index driverId is meilisearch before first I/O (not a function, not memory)", async () => {
90
136
  const { createStoreRuntime, store } = await import("../elements/store.ts");
91
137
  const { meilisearchDriver } = await import("../drivers/index.ts");
@@ -334,6 +380,28 @@ describe("fx.vault — object surface", () => {
334
380
  /fx\.vault\.status needs a bound Vault backend/,
335
381
  );
336
382
  });
383
+
384
+ test("per-tenant contracts store under {tenantId}/{contract}", async () => {
385
+ const { createVaultRuntime, vault } = await import("../elements/vault.ts");
386
+ const { adapter, rows } = memoryAdapter();
387
+ const stripe = vault.secret("STRIPE_KEY", { perTenant: true, dev: "sk_unused" });
388
+ const runtime = createVaultRuntime({
389
+ secrets: [stripe],
390
+ tenantEnabled: true,
391
+ });
392
+ const fx = createFx({
393
+ flow: "billing.key",
394
+ effects: { secrets: ["STRIPE_KEY"] },
395
+ vaultRuntime: runtime,
396
+ vaultAdapter: adapter as never,
397
+ tenantEnabled: true,
398
+ tenant: { id: "acme" },
399
+ });
400
+ await fx.vault.set("STRIPE_KEY", "sk_acme");
401
+ expect([...rows.keys()]).toEqual(["acme/STRIPE_KEY"]);
402
+ const redacted = await fx.vault.get("STRIPE_KEY");
403
+ expect(redacted.reveal()).toBe("sk_acme");
404
+ });
337
405
  });
338
406
 
339
407
  describe("fx — wholesale swap", () => {
@@ -364,6 +432,9 @@ describe("fx — wholesale swap", () => {
364
432
  async deadLetters() {
365
433
  return [];
366
434
  },
435
+ live() {
436
+ throw new Error("live must not be used");
437
+ },
367
438
  async call() {
368
439
  return null;
369
440
  },
@@ -432,7 +503,43 @@ describe("fx — wholesale swap", () => {
432
503
  t: (key) => key,
433
504
  locale: "en",
434
505
  id: () => "fixed-id",
435
- auth: { userId: "u1", scopes: new Set(["a"]) },
506
+ auth: {
507
+ userId: "u1",
508
+ scopes: new Set(["a"]),
509
+ createApiKey: async () => {
510
+ throw new Error("auth.createApiKey must not be used");
511
+ },
512
+ listApiKeys: async () => [],
513
+ revokeApiKey: async () => {
514
+ throw new Error("auth.revokeApiKey must not be used");
515
+ },
516
+ rotateApiKey: async () => {
517
+ throw new Error("auth.rotateApiKey must not be used");
518
+ },
519
+ updateApiKey: async () => {
520
+ throw new Error("auth.updateApiKey must not be used");
521
+ },
522
+ listTenants: async () => [],
523
+ switchTenant: async () => {
524
+ throw new Error("auth.switchTenant must not be used");
525
+ },
526
+ createTenant: async () => {
527
+ throw new Error("auth.createTenant must not be used");
528
+ },
529
+ deleteTenant: async () => {
530
+ throw new Error("auth.deleteTenant must not be used");
531
+ },
532
+ addMember: async () => {
533
+ throw new Error("auth.addMember must not be used");
534
+ },
535
+ removeMember: async () => {
536
+ throw new Error("auth.removeMember must not be used");
537
+ },
538
+ listMembers: async () => [],
539
+ upsertTenantRole: async () => {
540
+ throw new Error("auth.upsertTenantRole must not be used");
541
+ },
542
+ },
436
543
  operator: { id: null },
437
544
  principal: { userId: "u1", operatorId: null, scopes: new Set(["a"]) },
438
545
  tenant: { id: "t1" },