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
@@ -114,6 +114,62 @@ describe("auth plugin", () => {
114
114
  });
115
115
  });
116
116
 
117
+ describe("API key allowlist", () => {
118
+ test("hostname entry matches the resolved client IP", async () => {
119
+ const { authenticateApiKey } = await import("./api-keys.ts");
120
+ const store = createApiKeyStore();
121
+ const created = await createApiKey(store, {
122
+ plane: "user",
123
+ name: "ci",
124
+ scopes: ["member"],
125
+ creatorId: "u1",
126
+ creatorScopes: ["member"],
127
+ ipAllowlist: ["ci.example.com"],
128
+ });
129
+ const lookup = async (host: string) => (host === "ci.example.com" ? ["203.0.113.10"] : []);
130
+ expect(
131
+ await authenticateApiKey(store, created.secret, {
132
+ ip: "203.0.113.10",
133
+ lookup,
134
+ }),
135
+ ).not.toBeNull();
136
+ expect(
137
+ await authenticateApiKey(store, created.secret, {
138
+ ip: "198.51.100.7",
139
+ lookup,
140
+ }),
141
+ ).toBeNull();
142
+ });
143
+
144
+ test("hostname lookup failure is closed", async () => {
145
+ const { authenticateApiKey } = await import("./api-keys.ts");
146
+ const store = createApiKeyStore();
147
+ const created = await createApiKey(store, {
148
+ plane: "user",
149
+ name: "ci",
150
+ scopes: ["member"],
151
+ creatorId: "u1",
152
+ creatorScopes: ["member"],
153
+ ipAllowlist: ["ci.example.com", "203.0.113.10"],
154
+ });
155
+ const lookup = async () => {
156
+ throw new Error("nxdomain");
157
+ };
158
+ expect(
159
+ await authenticateApiKey(store, created.secret, {
160
+ ip: "203.0.113.10",
161
+ lookup,
162
+ }),
163
+ ).not.toBeNull();
164
+ expect(
165
+ await authenticateApiKey(store, created.secret, {
166
+ ip: "198.51.100.7",
167
+ lookup,
168
+ }),
169
+ ).toBeNull();
170
+ });
171
+ });
172
+
117
173
  describe("API key attenuation", () => {
118
174
  test("key cannot be created with a scope its creator lacks", async () => {
119
175
  const store = createApiKeyStore();
@@ -6,7 +6,7 @@
6
6
 
7
7
  import type { PasswordHashOptions } from "../runtime/types.ts";
8
8
  import type { SessionStore } from "./sessions.ts";
9
- import type { ApiKeyStore } from "./api-keys.ts";
9
+ import { createApiKeyStore, type ApiKeyStore } from "./api-keys.ts";
10
10
  import type { BreachCheckFn } from "./breach-check.ts";
11
11
  import type { PasswordPolicyOptions } from "./password-policy.ts";
12
12
  import {
@@ -16,6 +16,12 @@ import {
16
16
  type ResolvedAuthSchema,
17
17
  } from "./schema.ts";
18
18
  import { sessionCryptoFromAuthOptions, type AuthSessionOptions } from "./plugin.ts";
19
+ import { createTenantStore, type TenantStore } from "./tenants.ts";
20
+ import {
21
+ resolveTenantAuth,
22
+ type ResolvedTenantAuth,
23
+ type TenantAuthOptions,
24
+ } from "./tenant-config.ts";
19
25
 
20
26
  /** Email + password method knobs. */
21
27
  export interface EmailAndPasswordOptions {
@@ -111,10 +117,19 @@ export interface GateAuthOptions extends AuthSchemaOptions {
111
117
  */
112
118
  readonly sessions?: SessionStore;
113
119
  /**
114
- * In-memory API key store for Bearer key auth (tests / embedding).
115
- * Distinct from schema `apiKeys` table options.
120
+ * Shared API key store. Created automatically when omitted
121
+ * (`gate.auth.secret` is the HMAC pepper).
116
122
  */
117
123
  readonly apiKeyStore?: ApiKeyStore;
124
+ /**
125
+ * Multi-tenancy as an identity dimension. `true` enables defaults
126
+ * (`source: "claim"`, `required: false`). Off when omitted.
127
+ */
128
+ readonly tenant?: boolean | TenantAuthOptions;
129
+ /**
130
+ * Shared tenant registry. Created automatically when {@link tenant} is on.
131
+ */
132
+ readonly tenantStore?: TenantStore;
118
133
  /** Injectable clock. */
119
134
  readonly now?: () => number;
120
135
  }
@@ -170,7 +185,9 @@ export interface ResolvedGateAuth {
170
185
  };
171
186
  readonly hooks: AuthDatabaseHooks | undefined;
172
187
  readonly sessions: SessionStore | undefined;
173
- readonly apiKeyStore: ApiKeyStore | undefined;
188
+ readonly apiKeyStore: ApiKeyStore;
189
+ readonly tenant: ResolvedTenantAuth | undefined;
190
+ readonly tenantStore: TenantStore | undefined;
174
191
  readonly now: (() => number) | undefined;
175
192
  }
176
193
 
@@ -278,7 +295,9 @@ export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAu
278
295
  },
279
296
  hooks: auth.hooks,
280
297
  sessions: auth.sessions,
281
- apiKeyStore: auth.apiKeyStore,
298
+ apiKeyStore: auth.apiKeyStore ?? createApiKeyStore({ pepper: secret }),
299
+ tenant: auth.tenant ? resolveTenantAuth(auth.tenant) : undefined,
300
+ tenantStore: auth.tenant ? (auth.tenantStore ?? createTenantStore()) : auth.tenantStore,
282
301
  now: auth.now,
283
302
  };
284
303
  }
package/src/auth/index.ts CHANGED
@@ -161,17 +161,41 @@ export {
161
161
  } from "./attenuation.ts";
162
162
 
163
163
  export {
164
+ AUTH_API_KEYS_RESOURCE,
165
+ allowlistAllowsIp,
166
+ authenticateApiKey,
167
+ clientIpFromRequest,
164
168
  createApiKey,
165
169
  createApiKeyStore,
166
- authenticateApiKey,
170
+ getApiKey,
167
171
  hashApiKeySecret,
172
+ listApiKeys,
168
173
  revokeApiKey,
169
174
  rotateApiKey,
175
+ toApiKeyPublicRow,
176
+ updateApiKey,
177
+ type AllowlistLookup,
178
+ type ApiKeyPublicRow,
170
179
  type ApiKeyStore,
180
+ type AuthenticateApiKeyOptions,
171
181
  type CreateApiKeyOptions,
182
+ type CreateApiKeyStoreOptions,
172
183
  type CreatedApiKey,
184
+ type UpdateApiKeyOptions,
173
185
  } from "./api-keys.ts";
174
186
 
187
+ export {
188
+ apiKeySqlExec,
189
+ bindApiKeySqlPersist,
190
+ bindHostApiKeySql,
191
+ bindHostApiKeySqlFromStore,
192
+ ensureApiKeyTable,
193
+ hydrateApiKeyStore,
194
+ persistApiKeyRow,
195
+ type ApiKeySqlExec,
196
+ type HostApiKeySqlRuntime,
197
+ } from "./api-key-sql.ts";
198
+
175
199
  export {
176
200
  createOperatorInviteStore,
177
201
  createOperatorInvite,
@@ -236,4 +260,41 @@ export {
236
260
  type RoleRow,
237
261
  type SessionRow,
238
262
  type TablePlane,
263
+ type TenantMemberRow,
264
+ type TenantRoleRow,
265
+ type TenantRow,
239
266
  } from "./tables.ts";
267
+
268
+ export { AUTH_TENANT_TABLES } from "./tenant-tables.ts";
269
+
270
+ export {
271
+ AUTH_TENANTS_RESOURCE,
272
+ DEFAULT_TENANT_ROLE,
273
+ TenantError,
274
+ addMember,
275
+ createTenant,
276
+ createTenantStore,
277
+ deleteTenant,
278
+ getMember,
279
+ isMember,
280
+ listMembers,
281
+ listTenantIds,
282
+ listTenantsForUser,
283
+ removeMember,
284
+ tenantRoleScopeFailure,
285
+ tenantScopesForMember,
286
+ upsertTenantRole,
287
+ type CreateTenantOptions,
288
+ type TenantPublicRow,
289
+ type TenantStore,
290
+ type TenantStoreHooks,
291
+ type UpsertTenantRoleOptions,
292
+ } from "./tenants.ts";
293
+
294
+ export {
295
+ resolveTenantAuth,
296
+ type ResolvedTenantAuth,
297
+ type TenantAuthOptions,
298
+ type TenantResolveContext,
299
+ type TenantSource,
300
+ } from "./tenant-config.ts";
@@ -9,6 +9,7 @@ import { plugin, type PluginDef } from "../kernel/plugin.ts";
9
9
  import type { BreachCheckFn } from "./breach-check.ts";
10
10
  import type { PasswordPolicyOptions } from "./password-policy.ts";
11
11
  import { AUTH_TABLES } from "./tables.ts";
12
+ import { AUTH_TENANT_TABLES } from "./tenant-tables.ts";
12
13
 
13
14
  /** Session knobs beyond raw access/refresh TTLs. */
14
15
  export interface AuthSessionOptions {
@@ -83,7 +84,11 @@ export function auth(options: AuthPluginOptions = {}): PluginDef {
83
84
  /* principals resolved by session / API key middleware at runtime */
84
85
  });
85
86
 
86
- for (const name of Object.values(AUTH_TABLES)) {
87
+ const tables: readonly string[] = [
88
+ ...Object.values(AUTH_TABLES),
89
+ ...Object.values(AUTH_TENANT_TABLES),
90
+ ];
91
+ for (const name of tables) {
87
92
  builder = builder.table(name, undefined, {
88
93
  plane: name.includes("operator")
89
94
  ? "operator"
@@ -105,6 +105,10 @@ export const AUTH_MODEL_DEFAULT_FIELDS: Readonly<
105
105
  name: "name",
106
106
  scopes: "scopes",
107
107
  expiresAt: "expires_at",
108
+ rateLimit: "rate_limit",
109
+ ipAllowlist: "ip_allowlist",
110
+ creatorId: "creator_id",
111
+ creatorScopes: "creator_scopes",
108
112
  createdAt: "created_at",
109
113
  lastUsedAt: "last_used_at",
110
114
  revokedAt: "revoked_at",
@@ -190,6 +194,10 @@ const CORE_SQL_TYPES: Readonly<Record<string, AuthColumnSqlType>> = {
190
194
  scopes: "TEXT",
191
195
  audience: "TEXT",
192
196
  lastUsedAt: "INTEGER",
197
+ rateLimit: "TEXT",
198
+ ipAllowlist: "TEXT",
199
+ creatorId: "TEXT",
200
+ creatorScopes: "TEXT",
193
201
  };
194
202
 
195
203
  /**
@@ -43,6 +43,11 @@ export interface AccessClaims {
43
43
  * Validated when the verifier supplies {@link VerifyAccessOptions.audience}.
44
44
  */
45
45
  readonly aud?: string;
46
+ /**
47
+ * Active tenant for this access token (request-scoped).
48
+ * Never stored on the session row — switching issues a new token pair.
49
+ */
50
+ readonly tid?: string;
46
51
  }
47
52
 
48
53
  /** Options for {@link createSessionStore}. */
@@ -106,6 +111,8 @@ export async function issueSession(
106
111
  readonly id: string;
107
112
  readonly plane: AuthPlane;
108
113
  readonly scopes: Iterable<string>;
114
+ /** Optional tenant claim (not stored on the session row). */
115
+ readonly tenantId?: string | null;
109
116
  },
110
117
  ): Promise<IssuedSession> {
111
118
  const now = crypto.now ?? (() => Date.now());
@@ -136,6 +143,7 @@ export async function issueSession(
136
143
  store.sessions.set(sessionId, session);
137
144
 
138
145
  const refreshRaw = `rt_${cryptoRandomId()}`;
146
+ const tid = principal.tenantId ?? undefined;
139
147
  const refreshRow: RefreshTokenRow = {
140
148
  id: cryptoRandomId(),
141
149
  sessionId,
@@ -144,6 +152,7 @@ export async function issueSession(
144
152
  expiresAt: t + refreshTtl,
145
153
  usedAt: null,
146
154
  revokedAt: null,
155
+ ...(tid !== undefined && tid !== "" ? { tid } : {}),
147
156
  };
148
157
  store.refresh.set(refreshRow.id, refreshRow);
149
158
 
@@ -156,6 +165,7 @@ export async function issueSession(
156
165
  iat: t,
157
166
  exp: accessExpiresAt,
158
167
  ...(crypto.audience !== undefined ? { aud: crypto.audience } : {}),
168
+ ...(tid !== undefined && tid !== "" ? { tid } : {}),
159
169
  });
160
170
 
161
171
  return { session, accessToken, refreshToken: refreshRaw, accessExpiresAt };
@@ -211,6 +221,7 @@ export async function rotateRefresh(
211
221
  const refreshTtl = crypto.refreshTtlMs ?? REFRESH_TTL_MS;
212
222
 
213
223
  const newRefreshRaw = `rt_${cryptoRandomId()}`;
224
+ const rotatedTid = existing.tid;
214
225
  const newRefresh: RefreshTokenRow = {
215
226
  id: cryptoRandomId(),
216
227
  sessionId: session.id,
@@ -219,6 +230,9 @@ export async function rotateRefresh(
219
230
  expiresAt: t + refreshTtl,
220
231
  usedAt: null,
221
232
  revokedAt: null,
233
+ ...(rotatedTid !== undefined && rotatedTid !== null && rotatedTid !== ""
234
+ ? { tid: rotatedTid }
235
+ : {}),
222
236
  };
223
237
  store.refresh.set(newRefresh.id, newRefresh);
224
238
 
@@ -231,6 +245,9 @@ export async function rotateRefresh(
231
245
  iat: t,
232
246
  exp: accessExpiresAt,
233
247
  ...(session.audience !== undefined ? { aud: session.audience } : {}),
248
+ ...(rotatedTid !== undefined && rotatedTid !== null && rotatedTid !== ""
249
+ ? { tid: rotatedTid }
250
+ : {}),
234
251
  });
235
252
 
236
253
  return {
@@ -291,6 +308,7 @@ export async function issueSessionWithScopes(
291
308
  readonly id: string;
292
309
  readonly plane: AuthPlane;
293
310
  readonly scopes: Iterable<string>;
311
+ readonly tenantId?: string | null;
294
312
  },
295
313
  ): Promise<IssuedSession> {
296
314
  return issueSession(store, crypto, principal);
@@ -98,6 +98,8 @@ export interface ApiKeyRow {
98
98
  lastUsedAt: number | null;
99
99
  /** Epoch-ms when revoked; `null` while active. */
100
100
  revokedAt: number | null;
101
+ /** Optional tenant claim (tier-1 API-key tenancy). */
102
+ tenantId?: string | null;
101
103
  }
102
104
 
103
105
  /** Pending operator invitation (invite-only plane). */
@@ -136,4 +138,34 @@ export interface RefreshTokenRow {
136
138
  expiresAt: number;
137
139
  usedAt: number | null;
138
140
  revokedAt: number | null;
141
+ /** Tenant id stamped on this refresh row (request-scoped; not on the session). */
142
+ tid?: string | null;
143
+ }
144
+
145
+ /** Tenant registry row. */
146
+ export interface TenantRow {
147
+ id: string;
148
+ name: string;
149
+ slug: string | null;
150
+ createdAt: number;
151
+ createdBy: string;
152
+ }
153
+
154
+ /** N:N membership — one user, many tenants; one tenant, many users. */
155
+ export interface TenantMemberRow {
156
+ id: string;
157
+ tenantId: string;
158
+ userId: string;
159
+ /** Tenant role name (expanded via {@link TenantRoleRow}). */
160
+ role: string;
161
+ createdAt: number;
162
+ }
163
+
164
+ /** Tenant-scoped role → application scope names. */
165
+ export interface TenantRoleRow {
166
+ tenantId: string;
167
+ roleName: string;
168
+ scopes: string[];
169
+ createdAt: number;
170
+ updatedAt: number;
139
171
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `gate.auth.tenant` config — identity dimension opt-in.
3
+ */
4
+
5
+ /** How the request names a tenant. */
6
+ export type TenantSource = "claim" | "header" | "subdomain" | "resolve";
7
+
8
+ /** Public `gate.auth.tenant` bag (boolean shorthand or options). */
9
+ export interface TenantAuthOptions {
10
+ /**
11
+ * Pure B2B: every user-plane request needs a tenant.
12
+ * Default `false` (B2C+B2B coexistence).
13
+ */
14
+ readonly required?: boolean;
15
+ /** Resolution source. Default `"claim"`. */
16
+ readonly source?: TenantSource;
17
+ /** Header name for `source: "header"`. Default `x-oke-tenant`. */
18
+ readonly header?: string;
19
+ /**
20
+ * Tier-3 escape hatch. When it returns an id, membership is still
21
+ * checked unless {@link TenantAuthOptions.authoritative} is true.
22
+ */
23
+ readonly resolve?: (ctx: TenantResolveContext) => string | null | undefined;
24
+ /**
25
+ * When true, {@link TenantAuthOptions.resolve} is trusted without a
26
+ * membership query. Default false (fail-safe).
27
+ */
28
+ readonly authoritative?: boolean;
29
+ }
30
+
31
+ /** Context passed to {@link TenantAuthOptions.resolve}. */
32
+ export interface TenantResolveContext {
33
+ readonly auth: {
34
+ readonly userId: string | null;
35
+ readonly tenantId?: string | null;
36
+ };
37
+ readonly request?: Request;
38
+ readonly claimTenantId: string | null;
39
+ }
40
+
41
+ /** Fully resolved tenant auth config. */
42
+ export interface ResolvedTenantAuth {
43
+ readonly enabled: true;
44
+ readonly required: boolean;
45
+ readonly source: TenantSource;
46
+ readonly header: string;
47
+ readonly resolve?: TenantAuthOptions["resolve"];
48
+ readonly authoritative: boolean;
49
+ }
50
+
51
+ /**
52
+ * Normalize `gate.auth.tenant`.
53
+ *
54
+ * @param input - `true` or options bag
55
+ */
56
+ export function resolveTenantAuth(input: true | TenantAuthOptions): ResolvedTenantAuth {
57
+ if (input === true) {
58
+ return {
59
+ enabled: true,
60
+ required: false,
61
+ source: "claim",
62
+ header: "x-oke-tenant",
63
+ authoritative: false,
64
+ };
65
+ }
66
+ return {
67
+ enabled: true,
68
+ required: input.required === true,
69
+ source: input.source ?? "claim",
70
+ header: input.header ?? "x-oke-tenant",
71
+ ...(input.resolve !== undefined ? { resolve: input.resolve } : {}),
72
+ authoritative: input.authoritative === true,
73
+ };
74
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tenant registry table names — kept off AUTH_TABLES so Store-only graphs
3
+ * that already ship `oke_api_keys` do not also pin `oke_tenants`.
4
+ */
5
+
6
+ /** Opt-in via `gate.auth.tenant`. */
7
+ export const AUTH_TENANT_TABLES = {
8
+ tenants: "oke_tenants",
9
+ tenantMembers: "oke_tenant_members",
10
+ tenantRoles: "oke_tenant_roles",
11
+ } as const;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Tenant registry — user-plane catalog on write; membership helpers.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { isApplicationScope } from "../elements/gate/permissions.ts";
7
+ import {
8
+ createTenant,
9
+ createTenantStore,
10
+ tenantRoleScopeFailure,
11
+ upsertTenantRole,
12
+ } from "./tenants.ts";
13
+
14
+ describe("tenantRoleScopeFailure", () => {
15
+ const catalog = ["booking:create", "member", "console:store.sql:write", "console:*"];
16
+
17
+ test("unknown names and console:* fail the same way", () => {
18
+ const invented = tenantRoleScopeFailure(["this:was:never:declared"], catalog);
19
+ const consoleWrite = tenantRoleScopeFailure(["console:store.sql:write"], catalog);
20
+ const consoleStar = tenantRoleScopeFailure(["console:*"], catalog);
21
+ expect(invented?.reason).toBe("unknown_scope");
22
+ expect(consoleWrite?.reason).toBe("unknown_scope");
23
+ expect(consoleStar?.reason).toBe("unknown_scope");
24
+ expect(isApplicationScope("console:store.sql:write")).toBe(false);
25
+ expect(isApplicationScope("console:*")).toBe(false);
26
+ });
27
+
28
+ test("application catalog names are grantable", () => {
29
+ expect(tenantRoleScopeFailure(["booking:create", "member"], catalog)).toBeNull();
30
+ });
31
+ });
32
+
33
+ describe("upsertTenantRole", () => {
34
+ test("console-shaped upsert throws like an invented name", async () => {
35
+ const store = createTenantStore();
36
+ await createTenant(store, { name: "Acme", createdBy: "u1", id: "t1" });
37
+ const catalog = ["booking:create", "console:store.sql:write", "console:*"];
38
+ expect(() =>
39
+ upsertTenantRole(store, {
40
+ tenantId: "t1",
41
+ roleName: "admin",
42
+ scopes: ["console:store.sql:write"],
43
+ catalog,
44
+ }),
45
+ ).toThrow(/unknown or operator-plane scope/);
46
+ expect(() =>
47
+ upsertTenantRole(store, {
48
+ tenantId: "t1",
49
+ roleName: "admin",
50
+ scopes: ["console:*"],
51
+ catalog,
52
+ }),
53
+ ).toThrow(/unknown or operator-plane scope/);
54
+ expect(() =>
55
+ upsertTenantRole(store, {
56
+ tenantId: "t1",
57
+ roleName: "admin",
58
+ scopes: ["this:was:never:declared"],
59
+ catalog,
60
+ }),
61
+ ).toThrow(/unknown or operator-plane scope/);
62
+ });
63
+ });