okengine 0.3.6 → 0.5.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 (205) hide show
  1. package/AGENTS.md +2 -0
  2. package/package.json +14 -12
  3. package/site/content/docs/ai/index.mdx +24 -0
  4. package/site/content/docs/ai/llms-txt.mdx +3 -0
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +5 -3
  7. package/site/content/docs/console/gates.mdx +46 -8
  8. package/site/content/docs/console/index.mdx +54 -0
  9. package/site/content/docs/console/meta.json +1 -0
  10. package/site/content/docs/elements/ai.mdx +2 -0
  11. package/site/content/docs/elements/channel.mdx +2 -0
  12. package/site/content/docs/elements/clock.mdx +1 -4
  13. package/site/content/docs/elements/flow.mdx +4 -10
  14. package/site/content/docs/elements/gate.mdx +189 -48
  15. package/site/content/docs/elements/index.mdx +45 -0
  16. package/site/content/docs/elements/meta.json +1 -1
  17. package/site/content/docs/elements/signal.mdx +1 -5
  18. package/site/content/docs/elements/store.mdx +27 -6
  19. package/site/content/docs/elements/vault.mdx +10 -11
  20. package/site/content/docs/get-started/basic-usage.mdx +79 -43
  21. package/site/content/docs/get-started/index.mdx +33 -0
  22. package/site/content/docs/get-started/installation.mdx +95 -43
  23. package/site/content/docs/get-started/introduction.mdx +128 -75
  24. package/site/content/docs/get-started/meta.json +1 -1
  25. package/site/content/docs/get-started/why.mdx +141 -0
  26. package/site/content/docs/index.mdx +9 -31
  27. package/site/content/docs/plugins/anonymous.mdx +95 -0
  28. package/site/content/docs/plugins/compression.mdx +2 -2
  29. package/site/content/docs/plugins/cors.mdx +2 -2
  30. package/site/content/docs/plugins/csrf.mdx +2 -2
  31. package/site/content/docs/plugins/email-otp.mdx +111 -0
  32. package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +2 -2
  33. package/site/content/docs/plugins/index.mdx +69 -0
  34. package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
  35. package/site/content/docs/plugins/magic-link.mdx +112 -0
  36. package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
  37. package/site/content/docs/plugins/meta.json +10 -1
  38. package/site/content/docs/plugins/passkey.mdx +128 -0
  39. package/site/content/docs/plugins/phone-number.mdx +111 -0
  40. package/site/content/docs/plugins/two-factor.mdx +116 -0
  41. package/site/content/docs/plugins/username.mdx +117 -0
  42. package/site/content/docs/reference/client.mdx +331 -0
  43. package/site/content/docs/reference/configuration.mdx +1 -1
  44. package/site/content/docs/reference/environment-variables.mdx +5 -3
  45. package/site/content/docs/reference/fx.mdx +36 -8
  46. package/site/content/docs/reference/index.mdx +45 -0
  47. package/site/content/docs/reference/meta.json +11 -1
  48. package/site/content/docs/reference/plugins.mdx +25 -14
  49. package/src/auth/auth.test.ts +20 -2
  50. package/src/auth/bindings.ts +439 -0
  51. package/src/auth/breach-check.ts +112 -0
  52. package/src/auth/config.ts +288 -0
  53. package/src/auth/cookies.ts +123 -0
  54. package/src/auth/gate-auth.test.ts +379 -0
  55. package/src/auth/identity.ts +190 -0
  56. package/src/auth/index.ts +117 -1
  57. package/src/auth/method-context.ts +33 -0
  58. package/src/auth/operator.ts +27 -1
  59. package/src/auth/password-policy.test.ts +126 -0
  60. package/src/auth/password-policy.ts +77 -0
  61. package/src/auth/plugin.ts +62 -4
  62. package/src/auth/rate.ts +45 -0
  63. package/src/auth/schema.ts +260 -0
  64. package/src/auth/secondary-storage.ts +37 -0
  65. package/src/auth/sessions.ts +58 -1
  66. package/src/auth/tables.ts +4 -0
  67. package/src/auth/verification.ts +78 -0
  68. package/src/cli/competitor-mention-removal.test.ts +117 -0
  69. package/src/cli/dev.test.ts +3 -3
  70. package/src/cli/dev.ts +20 -0
  71. package/src/cli/meilisearch-local.test.ts +69 -0
  72. package/src/cli/meilisearch-local.ts +188 -0
  73. package/src/cli/schema.ts +95 -23
  74. package/src/client/auth.ts +120 -0
  75. package/src/client-react/index.ts +93 -0
  76. package/src/compiler/aot.test.ts +2 -1
  77. package/src/compiler/extract.ts +19 -0
  78. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
  79. package/src/compiler/response.ts +16 -2
  80. package/src/console/server/app.ts +10 -6
  81. package/src/console/server/auth-rate.test.ts +3 -3
  82. package/src/console/server/bind.ts +12 -1
  83. package/src/console/server/channels.test.ts +1 -1
  84. package/src/console/server/console-gates.ts +14 -0
  85. package/src/console/server/console.test.ts +6 -6
  86. package/src/console/server/flows-invoke.test.ts +2 -2
  87. package/src/console/server/flows.ts +2 -0
  88. package/src/console/server/gates.ts +8 -1
  89. package/src/console/server/operator-db.test.ts +4 -4
  90. package/src/console/server/operator-db.ts +22 -4
  91. package/src/console/server/security.gate.test.ts +3 -3
  92. package/src/console/server/store.test.ts +1 -1
  93. package/src/console/server/store.ts +11 -1
  94. package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
  95. package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
  96. package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
  97. package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
  98. package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
  99. package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
  100. package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
  101. package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
  102. package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
  103. package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
  104. package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
  105. package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
  106. package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
  107. package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
  108. package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
  109. package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
  110. package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
  111. package/src/console/ui/dist/index.html +2 -2
  112. package/src/console/ui/gates/fixture.ts +4 -0
  113. package/src/console/ui/gates/types.ts +2 -0
  114. package/src/console/ui/shell/client.ts +1 -0
  115. package/src/docker/compose.ts +5 -0
  116. package/src/docker/docker.test.ts +41 -0
  117. package/src/docker/recipes/index.ts +10 -2
  118. package/src/docker/recipes/meilisearch.ts +31 -0
  119. package/src/drivers/conformance.test.ts +16 -1
  120. package/src/drivers/conformance.ts +40 -3
  121. package/src/drivers/index.ts +14 -2
  122. package/src/drivers/libsql.ts +4 -4
  123. package/src/drivers/meilisearch.integration.test.ts +77 -0
  124. package/src/drivers/meilisearch.test.ts +181 -0
  125. package/src/drivers/meilisearch.ts +208 -0
  126. package/src/drivers/memory.ts +4 -4
  127. package/src/drivers/pgvector.ts +6 -6
  128. package/src/drivers/types.ts +93 -12
  129. package/src/drivers/vault-driver-removal.test.ts +6 -0
  130. package/src/drivers/vault-types.ts +4 -4
  131. package/src/elements/ai/runtime.ts +6 -0
  132. package/src/elements/ai.test.ts +22 -0
  133. package/src/elements/gate/boot.ts +136 -0
  134. package/src/elements/gate/config.ts +69 -0
  135. package/src/elements/gate/declare.ts +51 -1
  136. package/src/elements/gate/runtime.ts +3 -1
  137. package/src/elements/gate.test.ts +77 -0
  138. package/src/elements/gate.ts +20 -1
  139. package/src/elements/index.ts +8 -0
  140. package/src/elements/store/index-boot.test.ts +49 -7
  141. package/src/elements/store/runtime.ts +50 -15
  142. package/src/elements/store.ts +2 -0
  143. package/src/elements/vault.test.ts +27 -4
  144. package/src/elements/vault.ts +1 -1
  145. package/src/index.ts +15 -0
  146. package/src/kernel/app.ts +253 -32
  147. package/src/kernel/boot-bind/store.test.ts +9 -0
  148. package/src/kernel/boot-bind/store.ts +30 -2
  149. package/src/kernel/boot.test.ts +40 -3
  150. package/src/kernel/boot.ts +8 -0
  151. package/src/kernel/call.test.ts +46 -2
  152. package/src/kernel/concurrency.test.ts +58 -0
  153. package/src/kernel/concurrency.ts +48 -0
  154. package/src/kernel/edge.test.ts +3 -3
  155. package/src/kernel/flow.test.ts +2 -2
  156. package/src/kernel/fx.test.ts +12 -2
  157. package/src/kernel/fx.ts +97 -5
  158. package/src/kernel/hooks.test.ts +4 -4
  159. package/src/kernel/index.ts +22 -1
  160. package/src/kernel/pipeline.test.ts +12 -8
  161. package/src/kernel/pipeline.ts +23 -4
  162. package/src/kernel/plugin/decorate.test.ts +3 -3
  163. package/src/kernel/plugin/scoping.test.ts +3 -3
  164. package/src/kernel/plugin-elements.test.ts +51 -0
  165. package/src/kernel/plugin-needs.test.ts +83 -0
  166. package/src/kernel/plugin-needs.ts +129 -0
  167. package/src/kernel/plugin.ts +101 -0
  168. package/src/kernel/redacted.ts +74 -0
  169. package/src/kernel/registry-isolation.test.ts +5 -5
  170. package/src/kernel/registry.ts +102 -3
  171. package/src/kernel/router.ts +3 -3
  172. package/src/manifest/types.ts +2 -0
  173. package/src/plugins/anonymous.ts +58 -0
  174. package/src/plugins/auth/shared.ts +121 -0
  175. package/src/plugins/auth-methods.test.ts +176 -0
  176. package/src/plugins/compression.test.ts +5 -5
  177. package/src/plugins/config-source.test.ts +1 -1
  178. package/src/plugins/cors.test.ts +16 -10
  179. package/src/plugins/csrf.test.ts +1 -1
  180. package/src/plugins/email-otp.ts +161 -0
  181. package/src/plugins/index.ts +31 -0
  182. package/src/plugins/ip-allowlist.test.ts +19 -9
  183. package/src/plugins/magic-link.ts +163 -0
  184. package/src/plugins/maintenance-mode.test.ts +9 -5
  185. package/src/plugins/passkey.ts +216 -0
  186. package/src/plugins/phone-number.ts +149 -0
  187. package/src/plugins/security-headers.test.ts +14 -14
  188. package/src/plugins/two-factor.ts +249 -0
  189. package/src/plugins/username.ts +148 -0
  190. package/src/runs/runs.test.ts +6 -2
  191. package/src/runtime/primitives.ts +37 -4
  192. package/src/runtime/serve.test.ts +3 -2
  193. package/src/runtime/types.ts +25 -2
  194. package/src/test/create-test-app.test.ts +1 -1
  195. package/src/test/create-test-app.ts +4 -1
  196. package/src/test/provisions.integration.test.ts +2 -2
  197. package/site/content/docs/get-started/comparison.mdx +0 -65
  198. package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
  199. package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
  200. package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
  201. package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
  202. package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
  203. package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
  204. package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
  205. package/src/drivers/vault-infisical.ts +0 -57
@@ -0,0 +1,249 @@
1
+ /**
2
+ * TOTP two-factor Gate auth method plugin (RFC 6238, HMAC-SHA1).
3
+ *
4
+ * v1 verify accepts `{ userId, code }` after password sign-in when 2FA is enabled.
5
+ */
6
+
7
+ import { issueSessionWithScopes } from "../auth/sessions.ts";
8
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
9
+ import {
10
+ AuthFailed,
11
+ AuthRateLimited,
12
+ SessionTokensOut,
13
+ bindPublicAuth,
14
+ bindSessionAuth,
15
+ createMethodRuntime,
16
+ fail,
17
+ flow,
18
+ z,
19
+ type AuthMethodOptions,
20
+ } from "./auth/shared.ts";
21
+
22
+ /** Per-user TOTP + recovery state. */
23
+ export interface TwoFactorRow {
24
+ userId: string;
25
+ secret: string;
26
+ enabled: boolean;
27
+ /** SHA-256 hex of recovery codes (unused codes only). */
28
+ recoveryHashes: Set<string>;
29
+ createdAt: number;
30
+ }
31
+
32
+ /** In-memory two-factor store. */
33
+ export interface TwoFactorStore {
34
+ readonly byUserId: Map<string, TwoFactorRow>;
35
+ }
36
+
37
+ /**
38
+ * Create an empty two-factor store.
39
+ */
40
+ export function createTwoFactorStore(): TwoFactorStore {
41
+ return { byUserId: new Map() };
42
+ }
43
+
44
+ /** Options for {@link twoFactor}. */
45
+ export interface TwoFactorOptions extends AuthMethodOptions {
46
+ readonly factors?: TwoFactorStore;
47
+ /** Issuer label for otpauth URLs (default `oke`). */
48
+ readonly issuer?: string;
49
+ }
50
+
51
+ /**
52
+ * Enable / verify / disable TOTP two-factor (`oke_two_factor`).
53
+ *
54
+ * @param opts - Secret / session / factor store
55
+ */
56
+ export function twoFactor(opts: TwoFactorOptions = {}): PluginDef {
57
+ const runtime = createMethodRuntime(opts);
58
+ const factors = opts.factors ?? createTwoFactorStore();
59
+ const issuer = opts.issuer ?? "oke";
60
+
61
+ const enable = flow({
62
+ name: "auth.twoFactorEnable",
63
+ unit: "auth",
64
+ plane: "user",
65
+ out: z.object({
66
+ secret: z.string(),
67
+ otpauthUrl: z.string(),
68
+ recoveryCodes: z.array(z.string()),
69
+ }),
70
+ errors: { AuthFailed },
71
+ do: async (_input, fx) => {
72
+ const userId = fx.auth.userId;
73
+ if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
74
+ const secret = generateBase32Secret(20);
75
+ const recoveryCodes = Array.from({ length: 8 }, () => randomRecoveryCode());
76
+ const recoveryHashes = new Set<string>();
77
+ for (const code of recoveryCodes) {
78
+ recoveryHashes.add(await sha256Hex(code));
79
+ }
80
+ factors.byUserId.set(userId, {
81
+ userId,
82
+ secret,
83
+ enabled: true,
84
+ recoveryHashes,
85
+ createdAt: runtime.now(),
86
+ });
87
+ const label = encodeURIComponent(userId);
88
+ const otpauthUrl = `otpauth://totp/${encodeURIComponent(issuer)}:${label}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
89
+ return { secret, otpauthUrl, recoveryCodes };
90
+ },
91
+ });
92
+
93
+ const verify = flow({
94
+ name: "auth.twoFactorVerify",
95
+ unit: "auth",
96
+ plane: "user",
97
+ in: z.object({
98
+ userId: z.string().min(1),
99
+ code: z.string().min(6).max(16),
100
+ }),
101
+ out: SessionTokensOut,
102
+ errors: { AuthFailed, AuthRateLimited },
103
+ do: async (input) => {
104
+ const row = factors.byUserId.get(input.userId);
105
+ if (!row?.enabled) return fail("AuthFailed", { reason: "invalid_credentials" });
106
+ const code = input.code.trim().replace(/\s+/g, "");
107
+ const totpOk = await verifyTotp(row.secret, code);
108
+ if (!totpOk) {
109
+ const hash = await sha256Hex(code);
110
+ if (!row.recoveryHashes.has(hash)) {
111
+ return fail("AuthFailed", { reason: "invalid_credentials" });
112
+ }
113
+ row.recoveryHashes.delete(hash);
114
+ }
115
+ const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
116
+ id: input.userId,
117
+ plane: "user",
118
+ scopes: [],
119
+ });
120
+ return {
121
+ accessToken: issued.accessToken,
122
+ refreshToken: issued.refreshToken,
123
+ accessExpiresAt: issued.accessExpiresAt,
124
+ userId: input.userId,
125
+ };
126
+ },
127
+ });
128
+
129
+ const disable = flow({
130
+ name: "auth.twoFactorDisable",
131
+ unit: "auth",
132
+ plane: "user",
133
+ out: z.object({ ok: z.literal(true) }),
134
+ errors: { AuthFailed },
135
+ do: (_input, fx) => {
136
+ const userId = fx.auth.userId;
137
+ if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
138
+ factors.byUserId.delete(userId);
139
+ return { ok: true as const };
140
+ },
141
+ });
142
+
143
+ return plugin("twoFactor", { version: "0.0.1", config: { method: "two-factor" } })
144
+ .needs("auth")
145
+ .table("oke_two_factor", undefined, { plane: "user", description: "TOTP secrets + recovery" })
146
+ .binding(bindSessionAuth("/two-factor/enable", enable))
147
+ .binding(bindPublicAuth("/two-factor/verify", verify, "otp"))
148
+ .binding(bindSessionAuth("/two-factor/disable", disable));
149
+ }
150
+
151
+ /**
152
+ * Verify a 6-digit TOTP against a base32 secret (RFC 6238, 30s step, ±1 window).
153
+ *
154
+ * @param secretBase32 - Shared secret
155
+ * @param code - Submitted code
156
+ * @param nowSec - Epoch seconds (test injection)
157
+ */
158
+ export async function verifyTotp(
159
+ secretBase32: string,
160
+ code: string,
161
+ nowSec: number = Math.floor(Date.now() / 1000),
162
+ ): Promise<boolean> {
163
+ if (!/^\d{6}$/.test(code)) return false;
164
+ const key = base32Decode(secretBase32);
165
+ const counter = Math.floor(nowSec / 30);
166
+ for (let w = -1; w <= 1; w++) {
167
+ const otp = await hotp(key, counter + w);
168
+ if (otp === code) return true;
169
+ }
170
+ return false;
171
+ }
172
+
173
+ async function hotp(key: Uint8Array, counter: number): Promise<string> {
174
+ const msg = new Uint8Array(8);
175
+ let c = counter;
176
+ for (let i = 7; i >= 0; i--) {
177
+ msg[i] = c & 0xff;
178
+ c = Math.floor(c / 256);
179
+ }
180
+ const cryptoKey = await crypto.subtle.importKey(
181
+ "raw",
182
+ key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength) as ArrayBuffer,
183
+ { name: "HMAC", hash: "SHA-1" },
184
+ false,
185
+ ["sign"],
186
+ );
187
+ const sig = new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, msg));
188
+ const offset = sig[sig.length - 1]! & 0x0f;
189
+ const bin =
190
+ ((sig[offset]! & 0x7f) << 24) |
191
+ ((sig[offset + 1]! & 0xff) << 16) |
192
+ ((sig[offset + 2]! & 0xff) << 8) |
193
+ (sig[offset + 3]! & 0xff);
194
+ return (bin % 1_000_000).toString().padStart(6, "0");
195
+ }
196
+
197
+ function generateBase32Secret(byteLen: number): string {
198
+ const bytes = crypto.getRandomValues(new Uint8Array(byteLen));
199
+ return base32Encode(bytes);
200
+ }
201
+
202
+ function randomRecoveryCode(): string {
203
+ const bytes = crypto.getRandomValues(new Uint8Array(5));
204
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
205
+ }
206
+
207
+ async function sha256Hex(raw: string): Promise<string> {
208
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
209
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
210
+ }
211
+
212
+ const B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
213
+
214
+ function base32Encode(bytes: Uint8Array): string {
215
+ let bits = 0;
216
+ let value = 0;
217
+ let out = "";
218
+ for (const b of bytes) {
219
+ value = (value << 8) | b;
220
+ bits += 8;
221
+ while (bits >= 5) {
222
+ out += B32[(value >>> (bits - 5)) & 31];
223
+ bits -= 5;
224
+ }
225
+ }
226
+ if (bits > 0) out += B32[(value << (5 - bits)) & 31];
227
+ return out;
228
+ }
229
+
230
+ function base32Decode(input: string): Uint8Array {
231
+ const cleaned = input
232
+ .replace(/=+$/, "")
233
+ .toUpperCase()
234
+ .replace(/[^A-Z2-7]/g, "");
235
+ let bits = 0;
236
+ let value = 0;
237
+ const out: number[] = [];
238
+ for (const ch of cleaned) {
239
+ const idx = B32.indexOf(ch);
240
+ if (idx < 0) continue;
241
+ value = (value << 5) | idx;
242
+ bits += 5;
243
+ if (bits >= 8) {
244
+ out.push((value >>> (bits - 8)) & 0xff);
245
+ bits -= 8;
246
+ }
247
+ }
248
+ return new Uint8Array(out);
249
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Username + password Gate auth method plugin.
3
+ */
4
+
5
+ import { createBunCrypto } from "../runtime/primitives.ts";
6
+ import { issueSessionWithScopes } from "../auth/sessions.ts";
7
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
8
+ import {
9
+ AuthFailed,
10
+ AuthRateLimited,
11
+ SessionTokensOut,
12
+ bindPublicAuth,
13
+ createMethodRuntime,
14
+ fail,
15
+ flow,
16
+ z,
17
+ type AuthMethodOptions,
18
+ } from "./auth/shared.ts";
19
+
20
+ /** In-memory username credential row. */
21
+ export interface UsernameRow {
22
+ readonly userId: string;
23
+ readonly username: string;
24
+ passwordHash: string;
25
+ createdAt: number;
26
+ }
27
+
28
+ /** Username → credential map. */
29
+ export interface UsernameStore {
30
+ readonly byUsername: Map<string, UsernameRow>;
31
+ readonly byUserId: Map<string, UsernameRow>;
32
+ }
33
+
34
+ /**
35
+ * Create an empty username store.
36
+ */
37
+ export function createUsernameStore(): UsernameStore {
38
+ return { byUsername: new Map(), byUserId: new Map() };
39
+ }
40
+
41
+ /** Options for {@link username}. */
42
+ export interface UsernamePluginOptions extends AuthMethodOptions {
43
+ /** Shared username credential store. */
44
+ readonly usernames?: UsernameStore;
45
+ }
46
+
47
+ const UsernameIn = z.object({
48
+ username: z.string().min(3).max(64),
49
+ password: z.string().min(1),
50
+ });
51
+
52
+ function normalizeUsername(raw: string): string {
53
+ return raw.trim().toLowerCase();
54
+ }
55
+
56
+ /**
57
+ * Username + password sign-up / sign-in (`oke_usernames`).
58
+ *
59
+ * @param opts - Secret / session / store overrides
60
+ */
61
+ export function username(opts: UsernamePluginOptions = {}): PluginDef {
62
+ const runtime = createMethodRuntime(opts);
63
+ const usernames = opts.usernames ?? createUsernameStore();
64
+ const crypto = createBunCrypto();
65
+
66
+ const signUp = flow({
67
+ name: "auth.signUpUsername",
68
+ unit: "auth",
69
+ plane: "user",
70
+ in: UsernameIn,
71
+ out: SessionTokensOut,
72
+ errors: { AuthFailed, AuthRateLimited },
73
+ do: async (input) => {
74
+ const key = normalizeUsername(input.username);
75
+ if (!/^[a-z0-9._-]{3,64}$/.test(key)) {
76
+ return fail("AuthFailed", { reason: "invalid_credentials" });
77
+ }
78
+ if (usernames.byUsername.has(key)) {
79
+ return fail("AuthFailed", { reason: "invalid_credentials" });
80
+ }
81
+ const userId = crypto.randomUUID();
82
+ const passwordHash = await crypto.hashPassword(input.password, {
83
+ algorithm: "argon2id",
84
+ });
85
+ const row: UsernameRow = {
86
+ userId,
87
+ username: key,
88
+ passwordHash,
89
+ createdAt: runtime.now(),
90
+ };
91
+ usernames.byUsername.set(key, row);
92
+ usernames.byUserId.set(userId, row);
93
+ const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
94
+ id: userId,
95
+ plane: "user",
96
+ scopes: [],
97
+ });
98
+ return {
99
+ accessToken: issued.accessToken,
100
+ refreshToken: issued.refreshToken,
101
+ accessExpiresAt: issued.accessExpiresAt,
102
+ userId,
103
+ };
104
+ },
105
+ });
106
+
107
+ const signIn = flow({
108
+ name: "auth.signInUsername",
109
+ unit: "auth",
110
+ plane: "user",
111
+ in: UsernameIn,
112
+ out: SessionTokensOut,
113
+ errors: { AuthFailed, AuthRateLimited },
114
+ do: async (input) => {
115
+ const key = normalizeUsername(input.username);
116
+ const row = usernames.byUsername.get(key);
117
+ const hash = row?.passwordHash ?? (await dummyHash(crypto));
118
+ const ok = await crypto.verifyPassword(input.password, hash);
119
+ if (!ok || !row) return fail("AuthFailed", { reason: "invalid_credentials" });
120
+ const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
121
+ id: row.userId,
122
+ plane: "user",
123
+ scopes: [],
124
+ });
125
+ return {
126
+ accessToken: issued.accessToken,
127
+ refreshToken: issued.refreshToken,
128
+ accessExpiresAt: issued.accessExpiresAt,
129
+ userId: row.userId,
130
+ };
131
+ },
132
+ });
133
+
134
+ return plugin("username", { version: "0.0.1", config: { method: "username" } })
135
+ .needs("auth")
136
+ .table("oke_usernames", undefined, { plane: "user", description: "Username credentials" })
137
+ .binding(bindPublicAuth("/sign-up/username", signUp, "signUp"))
138
+ .binding(bindPublicAuth("/sign-in/username", signIn, "signIn"));
139
+ }
140
+
141
+ let dummyPasswordHash: string | null = null;
142
+
143
+ async function dummyHash(crypto: ReturnType<typeof createBunCrypto>): Promise<string> {
144
+ if (dummyPasswordHash === null) {
145
+ dummyPasswordHash = await crypto.hashPassword("oke-username-timing-dummy", "argon2id");
146
+ }
147
+ return dummyPasswordHash;
148
+ }
@@ -14,6 +14,7 @@ import { tmpdir } from "node:os";
14
14
  import { join } from "node:path";
15
15
 
16
16
  import { fsDriver, memoryFilesDriver } from "../drivers/index.ts";
17
+ import { gate } from "../elements/gate.ts";
17
18
  import { oke } from "../kernel/app.ts";
18
19
  import { flow } from "../kernel/flow.ts";
19
20
  import { on, resetBindings } from "../kernel/on.ts";
@@ -54,8 +55,10 @@ describe("zero-instrumentation dimensions", () => {
54
55
  });
55
56
  await runs.open();
56
57
 
58
+ const member = gate.policy("member", ({ auth }) => !!auth.verified);
59
+
57
60
  on(
58
- http.get("/book").gate("member"),
61
+ http.get("/book").gate(member),
59
62
  flow({
60
63
  name: "book.create",
61
64
  unit: "bookings",
@@ -74,8 +77,9 @@ describe("zero-instrumentation dimensions", () => {
74
77
  const app = oke({
75
78
  name: "runs-dims",
76
79
  runs,
80
+ gate: { policies: [member] },
77
81
  fx: {
78
- auth: { userId: "user_42", scopes: new Set(["book:write"]) },
82
+ auth: { userId: "user_42", scopes: new Set(["book:write"]), verified: true },
79
83
  tenant: { id: "org_a41" },
80
84
  },
81
85
  });
@@ -6,11 +6,13 @@
6
6
 
7
7
  import type {
8
8
  PasswordAlgorithm,
9
+ PasswordHashOptions,
9
10
  RuntimeCrypto,
10
11
  RuntimeEnv,
11
12
  RuntimeFiles,
12
13
  RuntimeTimers,
13
14
  } from "./types.ts";
15
+ import { ARGON2ID_MEMORY_COST_FLOOR, ARGON2ID_TIME_COST_FLOOR } from "./types.ts";
14
16
 
15
17
  /** HTML / WinterTC timers on `globalThis`. */
16
18
  export function createTimers(): RuntimeTimers {
@@ -124,7 +126,11 @@ export function createWebCrypto(): RuntimeCrypto {
124
126
  getRandomValues(array) {
125
127
  return web.getRandomValues(array);
126
128
  },
127
- async hashPassword(password, algorithm = "pbkdf2") {
129
+ async hashPassword(password, algorithmOrOptions = "pbkdf2") {
130
+ const algorithm =
131
+ typeof algorithmOrOptions === "string"
132
+ ? algorithmOrOptions
133
+ : (algorithmOrOptions.algorithm ?? "pbkdf2");
128
134
  if (algorithm !== "pbkdf2") {
129
135
  throw new Error(
130
136
  `web-standard runtime supports only pbkdf2 passwords (got ${algorithm}); use the Bun adapter for argon2id/bcrypt`,
@@ -156,10 +162,37 @@ export function createBunCrypto(): RuntimeCrypto {
156
162
  getRandomValues(array) {
157
163
  return web.getRandomValues(array);
158
164
  },
159
- async hashPassword(password, algorithm = "argon2id") {
165
+ async hashPassword(password, algorithmOrOptions = "argon2id") {
166
+ const opts: PasswordHashOptions =
167
+ typeof algorithmOrOptions === "string"
168
+ ? { algorithm: algorithmOrOptions }
169
+ : algorithmOrOptions;
170
+ const algorithm: PasswordAlgorithm = opts.algorithm ?? "argon2id";
160
171
  if (algorithm === "pbkdf2") return hashPbkdf2(password);
161
- const algo = algorithm === "bcrypt" ? "bcrypt" : "argon2id";
162
- return Bun.password.hash(password, algo);
172
+ if (algorithm === "bcrypt") {
173
+ if (opts.cost !== undefined) {
174
+ return Bun.password.hash(password, { algorithm: "bcrypt", cost: opts.cost });
175
+ }
176
+ return Bun.password.hash(password, "bcrypt");
177
+ }
178
+ // argon2id — never weaker than Bun's defaults (m=65536, t=2).
179
+ const memoryCost = opts.memoryCost ?? ARGON2ID_MEMORY_COST_FLOOR;
180
+ const timeCost = opts.timeCost ?? ARGON2ID_TIME_COST_FLOOR;
181
+ if (memoryCost < ARGON2ID_MEMORY_COST_FLOOR) {
182
+ throw new TypeError(
183
+ `argon2id memoryCost must be >= ${ARGON2ID_MEMORY_COST_FLOOR} (Bun default floor)`,
184
+ );
185
+ }
186
+ if (timeCost < ARGON2ID_TIME_COST_FLOOR) {
187
+ throw new TypeError(
188
+ `argon2id timeCost must be >= ${ARGON2ID_TIME_COST_FLOOR} (Bun default floor)`,
189
+ );
190
+ }
191
+ return Bun.password.hash(password, {
192
+ algorithm: "argon2id",
193
+ memoryCost,
194
+ timeCost,
195
+ });
163
196
  },
164
197
  async verifyPassword(password, hash) {
165
198
  if (hash.startsWith("pbkdf2$")) return verifyPbkdf2(password, hash);
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { gate } from "../elements/gate.ts";
2
3
  import { oke } from "../kernel/app.ts";
3
4
  import { flow, resetFlowSeq } from "../kernel/flow.ts";
4
5
  import { on, resetBindings } from "../kernel/on.ts";
@@ -55,14 +56,14 @@ function rawHttp(port: number, payload: string): Promise<string> {
55
56
 
56
57
  function buildApp() {
57
58
  on(
58
- http.get("/ping"),
59
+ http.get("/ping").gate(gate.public),
59
60
  flow({
60
61
  name: "ping",
61
62
  do: () => ({ ok: true as const, n: 1 }),
62
63
  }),
63
64
  );
64
65
  on(
65
- http.get("/notes/:id"),
66
+ http.get("/notes/:id").gate(gate.public),
66
67
  flow({
67
68
  name: "notes.get",
68
69
  do: ({ id }: { id: string }) => ({ id }),
@@ -130,6 +130,26 @@ export interface RuntimeTimers {
130
130
  /** Password algorithm accepted by {@link RuntimeCrypto.hashPassword}. */
131
131
  export type PasswordAlgorithm = "argon2id" | "bcrypt" | "pbkdf2";
132
132
 
133
+ /**
134
+ * Bun.password cost knobs. Defaults match Bun's argon2id floor
135
+ * (`memoryCost: 65536`, `timeCost: 2`) — never weaker.
136
+ */
137
+ export interface PasswordHashOptions {
138
+ /** Algorithm (Bun default `argon2id`). */
139
+ readonly algorithm?: PasswordAlgorithm;
140
+ /** Argon2 memory cost in KiB (floor 65536). */
141
+ readonly memoryCost?: number;
142
+ /** Argon2 time cost / iterations (floor 2). */
143
+ readonly timeCost?: number;
144
+ /** bcrypt cost (only when `algorithm: "bcrypt"`). */
145
+ readonly cost?: number;
146
+ }
147
+
148
+ /** Bun argon2id defaults — also the configuration floor. */
149
+ export const ARGON2ID_MEMORY_COST_FLOOR = 65_536;
150
+ /** Bun argon2id timeCost floor. */
151
+ export const ARGON2ID_TIME_COST_FLOOR = 2;
152
+
133
153
  /**
134
154
  * Crypto surface — Web Crypto plus password hash/verify.
135
155
  * Bun binds `Bun.password`; web-standard uses PBKDF2 via SubtleCrypto.
@@ -149,9 +169,12 @@ export interface RuntimeCrypto {
149
169
  * Hash a password.
150
170
  *
151
171
  * @param password - Plaintext
152
- * @param algorithm - Hash algorithm (Bun default `argon2id`)
172
+ * @param algorithmOrOptions - Algorithm string or Bun cost options
153
173
  */
154
- hashPassword(password: string, algorithm?: PasswordAlgorithm): Promise<string>;
174
+ hashPassword(
175
+ password: string,
176
+ algorithmOrOptions?: PasswordAlgorithm | PasswordHashOptions,
177
+ ): Promise<string>;
155
178
  /**
156
179
  * Verify a password against a hash from {@link RuntimeCrypto.hashPassword}.
157
180
  *
@@ -77,7 +77,7 @@ describe("createTestApp — four-applications surface", () => {
77
77
 
78
78
  const app = oke({
79
79
  name: "harness",
80
- gates: [member],
80
+ gate: { policies: [member] },
81
81
  signals: [orderPlaced],
82
82
  clocks: [clock("expire-stale", { every: "1h" })],
83
83
  channel: { templates: [orderConfirmed] },
@@ -215,8 +215,11 @@ export async function createTestApp<App extends OkeApp>(
215
215
  await app.boot({
216
216
  ...(options.boot ?? {}),
217
217
  env: "test",
218
+ // Test-only opt-out (honoured because env is "test" below). Production
219
+ // boots never skip posture via this flag.
220
+ unguardedHttp: options.boot?.unguardedHttp ?? appOpts.gate?.unguardedHttp ?? "allow",
218
221
  startScheduler: options.startScheduler ?? options.boot?.startScheduler ?? false,
219
- gates: options.gates ?? options.boot?.gates ?? appOpts.gates,
222
+ gates: options.gates ?? options.boot?.gates ?? appOpts.gate?.policies,
220
223
  secrets: options.secrets ?? options.boot?.secrets ?? appOpts.secrets,
221
224
  signals: options.signals ?? options.boot?.signals ?? appOpts.signals,
222
225
  stores: options.boot?.stores ?? appOpts.stores,
@@ -56,7 +56,7 @@ describe("Provisions integration", () => {
56
56
  effects: { secrets: ["STRIPE_KEY"], emits: ["order-news"] },
57
57
  do: async ({ orderId }, fx) => {
58
58
  const key = fx.vault(stripeKey);
59
- expect(key.startsWith("sk_")).toBe(true);
59
+ expect(key.reveal().startsWith("sk_")).toBe(true);
60
60
 
61
61
  await fx.step("create-intent", async () => ({ id: `pi_${orderId}` }));
62
62
  await fx.clock.sleep("verify-window", "2m");
@@ -111,7 +111,7 @@ describe("Provisions integration", () => {
111
111
 
112
112
  const app = oke({
113
113
  name: "provisions",
114
- gates: [member, canOrder],
114
+ gate: { policies: [member, canOrder] },
115
115
  secrets: [stripeKey],
116
116
  signals: [orderPlaced, orderNews],
117
117
  channel: { templates: [orderConfirmed] },
@@ -1,65 +0,0 @@
1
- ---
2
- title: Comparison
3
- description: How OKE compares to Hono, Elysia, Encore.ts, and iii — and when to pick it.
4
- source: docs/spec/unified-theory.md
5
- icon: Scale
6
- ---
7
-
8
- > **OKE is the batteries-included TypeScript backend for the Bun era:** contract-first APIs with end-to-end type safety, declarative infrastructure primitives, an OpenTelemetry-native Console, secure-by-default auth and ABAC — pure TypeScript, Web-Standards portable, MIT-licensed, self-hostable with zero cloud lock-in.
9
-
10
- _"Encore's batteries and dashboard, Elysia's speed and DX, Hono's portability — without the Rust lock-in, the cloud gravity, or the source-available license."_
11
-
12
- ## When to pick OKE
13
-
14
- Choose OKE if you want:
15
-
16
- - **One mental model** for HTTP, jobs, consumers, and durable work (`on(Trigger) → Effects`)
17
- - **Batteries included** — store, signals, clock, gates, vault, channels, and AI as first-class elements
18
- - **A local Console** derived from your code (not a separate SaaS product you must adopt)
19
- - **MIT + self-host** with drivers named after protocols (`postgres`, `redis`, `s3`), not vendors
20
- - Effects that power cache invalidation, live queries, and least privilege **without hand annotations**
21
-
22
- Prefer a thinner router (Hono / Elysia alone) if you only need HTTP and will assemble queues, cron, and auth yourself.
23
-
24
- ## Matrix
25
-
26
- | | Hono | Elysia | Encore.ts | iii | **OKE** |
27
- | ----------------------------------------------- | --------------- | ----------- | ------------------- | --------------------- | --------------------------------- |
28
- | Primary runtime | Multi (Web Std) | Bun-first | Node + Rust core | Rust engine, polyglot | **Bun-first, Web-Std portable** |
29
- | License | MIT | MIT | MPL-2.0 | ELv2 engine | **MIT** |
30
- | Typed client | hc RPC | Eden Treaty | generated | SDK | **contract-first + live queries** |
31
- | DB · queue · cron · storage | ❌ | ❌ | ✅ | ✅ | **✅ (7 elements)** |
32
- | Durable workflows | ❌ | ❌ | ❌ | partial | **✅ (a flag)** |
33
- | Local Console | ❌ | ❌ | ✅ | ✅ | **✅ (dev + prod)** |
34
- | Auto cache invalidation | ❌ | ❌ | ❌ | ❌ | **✅ (from effects)** |
35
- | Least-privilege by compiler | ❌ | ❌ | ❌ | ❌ | **✅** |
36
- | Human channels (email/SMS/WA) | ❌ | ❌ | ❌ | ❌ | **✅ (7th element)** |
37
- | AI in the application (models, prompts, agents) | ❌ | ❌ | ❌ | ❌ | **✅ (8th element)** |
38
- | Self-host, no lock-in | ✅ | ✅ | ✅ (Cloud optional) | ⚠️ | **✅ first-class** |
39
-
40
- ## vs Collections of libraries
41
-
42
- Frameworks that ship a router plus a queue library plus a cron library plus websockets share one flaw: the concepts do not derive from one another. Documentation sprawls; each new fashion adds a concept that never gets removed.
43
-
44
- OKE starts from one law so docs, traces, hooks, and agent surfaces stay **one shape**.
45
-
46
- ## vs Cloud-gravity platforms
47
-
48
- OKE is MIT, self-hostable, and names drivers after **protocols** — not vendors. Vendor choice lives in images keyed by role. There is no separate “deploy to our cloud” product you must adopt to get the Console.
49
-
50
- ## vs Rolling your own effect system
51
-
52
- Effects are inferred from what a Flow touches through `fx`. Cache invalidation, live queries, least privilege, deterministic tests, and Manifest Diff fall out of that one decision — without hand-written annotations for each capability.
53
-
54
- <Cards>
55
- <Card
56
- title="Installation"
57
- description="Install and scaffold in minutes."
58
- href="/docs/get-started/installation"
59
- />
60
- <Card
61
- title="Introduction"
62
- description="The one law, eight elements, ten exports."
63
- href="/docs/get-started/introduction"
64
- />
65
- </Cards>