okengine 0.8.0 → 0.9.1

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 (89) hide show
  1. package/package.json +3 -2
  2. package/site/content/docs/deployment/docker-swarm.mdx +228 -0
  3. package/site/content/docs/deployment/docker.mdx +212 -0
  4. package/site/content/docs/deployment/index.mdx +83 -0
  5. package/site/content/docs/deployment/kubernetes.mdx +176 -0
  6. package/site/content/docs/deployment/meta.json +5 -0
  7. package/site/content/docs/deployment/reverse-proxy.mdx +216 -0
  8. package/site/content/docs/elements/channel.mdx +10 -5
  9. package/site/content/docs/elements/signal.mdx +10 -8
  10. package/site/content/docs/elements/store.mdx +34 -0
  11. package/site/content/docs/get-started/index.mdx +5 -0
  12. package/site/content/docs/index.mdx +5 -0
  13. package/site/content/docs/meta.json +10 -1
  14. package/site/content/docs/plugins/index.mdx +1 -2
  15. package/site/content/docs/plugins/magic-link.mdx +2 -2
  16. package/site/content/docs/plugins/meta.json +1 -2
  17. package/site/content/docs/plugins/otp.mdx +214 -0
  18. package/site/content/docs/plugins/two-factor.mdx +2 -2
  19. package/site/content/docs/reference/cli.md +3 -2
  20. package/site/content/docs/reference/configuration.mdx +16 -0
  21. package/site/content/docs/reference/environment-variables.mdx +9 -8
  22. package/site/content/docs/reference/plugins.mdx +1 -1
  23. package/src/auth/auth.test.ts +36 -0
  24. package/src/auth/bindings.ts +3 -12
  25. package/src/auth/identity.ts +33 -0
  26. package/src/auth/index.ts +5 -0
  27. package/src/auth/otp-capability.ts +119 -0
  28. package/src/auth/otp-seal.test.ts +61 -0
  29. package/src/auth/otp-seal.ts +84 -0
  30. package/src/auth/schema.ts +3 -0
  31. package/src/auth/sessions.ts +26 -27
  32. package/src/auth/tables.ts +4 -0
  33. package/src/auth/verification.ts +61 -1
  34. package/src/cli/dev-app-runner.ts +4 -0
  35. package/src/cli/docker.ts +4 -1
  36. package/src/cli/load-config.images.test.ts +4 -0
  37. package/src/cli/load-config.ts +3 -0
  38. package/src/cli/registry.ts +1 -1
  39. package/src/console/server/operator-db.ts +34 -9
  40. package/src/docker/compose.ts +162 -6
  41. package/src/docker/derive.ts +60 -3
  42. package/src/docker/docker.test.ts +374 -1
  43. package/src/docker/helpers.ts +2 -0
  44. package/src/docker/index.ts +11 -0
  45. package/src/docker/recipes/caddy.ts +51 -0
  46. package/src/docker/recipes/dragonfly.ts +31 -0
  47. package/src/docker/recipes/index.ts +25 -2
  48. package/src/docker/recipes/pgdog.ts +84 -0
  49. package/src/docker/recipes/redis.ts +6 -3
  50. package/src/docker/recipes/traefik.ts +83 -0
  51. package/src/docker/recipes/valkey.ts +30 -0
  52. package/src/docker/stack-id.ts +5 -0
  53. package/src/docker/types.ts +18 -0
  54. package/src/drivers/channel-taqnyat-whatsapp.ts +94 -0
  55. package/src/drivers/channel-types.ts +1 -0
  56. package/src/elements/channel/otp-delivery.test.ts +76 -0
  57. package/src/elements/channel/otp-delivery.ts +291 -0
  58. package/src/elements/channel/runtime.ts +152 -114
  59. package/src/elements/channel.ts +12 -2
  60. package/src/index.ts +3 -0
  61. package/src/kernel/app.ts +60 -4
  62. package/src/kernel/boot-bind/channel.ts +51 -0
  63. package/src/kernel/boot-bind/gate.ts +14 -19
  64. package/src/kernel/boot-bind/honor-config.test.ts +18 -0
  65. package/src/kernel/boot-bind/signal.ts +20 -0
  66. package/src/kernel/boot-bind/store.test.ts +82 -0
  67. package/src/kernel/boot-bind/store.ts +22 -0
  68. package/src/kernel/boot.test.ts +5 -5
  69. package/src/kernel/fx.test.ts +3 -0
  70. package/src/kernel/fx.ts +49 -0
  71. package/src/kernel/graceful-shutdown.test.ts +76 -0
  72. package/src/kernel/graceful-shutdown.ts +106 -0
  73. package/src/kernel/horizontal-child.ts +257 -0
  74. package/src/kernel/horizontal.integration.test.ts +229 -0
  75. package/src/kernel/index.ts +8 -0
  76. package/src/kernel/ready.test.ts +76 -0
  77. package/src/plugins/auth-delivery.mailpit.integration.test.ts +5 -5
  78. package/src/plugins/auth-methods.security.test.ts +19 -29
  79. package/src/plugins/auth-methods.test.ts +7 -6
  80. package/src/plugins/index.ts +12 -8
  81. package/src/plugins/magic-link.ts +1 -23
  82. package/src/plugins/otp.test.ts +238 -0
  83. package/src/plugins/otp.ts +572 -0
  84. package/src/plugins/taqnyat.live.test.ts +5 -7
  85. package/src/release/official-plugins.ts +1 -2
  86. package/site/content/docs/plugins/email-otp.mdx +0 -117
  87. package/site/content/docs/plugins/phone-number.mdx +0 -172
  88. package/src/plugins/email-otp.ts +0 -214
  89. package/src/plugins/phone-number.ts +0 -206
@@ -1,117 +0,0 @@
1
- ---
2
- title: "Email OTP"
3
- description: "Official plugin — six-digit email one-time codes to sign in under /auth."
4
- icon: "MailCheck"
5
- source: "docs/spec/unified-theory.md"
6
- ---
7
-
8
- `emailOtp()` stores a hashed 6-digit code (default 10 minutes, 5 attempts). Request sends the
9
- code via Channel (`auth-email-otp`); verify creates the user on first success and returns hybrid
10
- session tokens.
11
-
12
- <Callout title="The one rule">
13
- Enable `gate.auth`, then `.plug(emailOtp())`. Never log raw OTPs. Delivery goes through `fx.send`;
14
- use `exposeDevOtp` only for local DX without SMTP.
15
- </Callout>
16
-
17
- ## Quick start
18
-
19
- <Steps>
20
-
21
- <Step>
22
- ### Plug it
23
-
24
- ```typescript title="src/app.ts"
25
- import { oke } from "okengine";
26
- import { emailOtp } from "okengine/plugins";
27
-
28
- export const app = oke({
29
- name: "shop",
30
- env: "local",
31
- gate: { auth: {} },
32
- }).plug(emailOtp());
33
- ```
34
-
35
- </Step>
36
-
37
- <Step>
38
- ### Request a code
39
-
40
- ```typescript
41
- const { data } = await api.auth.requestEmailOtp({ email: "ali@example.com" });
42
- // Channel delivers auth-email-otp; data.devOtp only when exposeDevOtp
43
- ```
44
-
45
- `POST /auth/email-otp/request` — prior active codes for that email are invalidated.
46
-
47
- </Step>
48
-
49
- <Step>
50
- ### Verify
51
-
52
- ```typescript
53
- const { data } = await api.auth.verifyEmailOtp({
54
- email: "ali@example.com",
55
- otp,
56
- });
57
- ```
58
-
59
- `POST /auth/email-otp/verify`. Wrong code increments attempts; after 5 →
60
- `invalid_credentials`.
61
-
62
- </Step>
63
-
64
- </Steps>
65
-
66
- ## Options
67
-
68
- | Option | Type | Default | Meaning |
69
- | --------------- | ------------------- | -------------------------- | --------------------------------------- |
70
- | `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
71
- | `sessions` | `SessionStore` | active\* | Session store |
72
- | `ttlMs` | `number` | 10m | Challenge lifetime |
73
- | `from` | `string` | `OKE <no-reply@oke.local>` | Template From address |
74
- | `exposeDevOtp` | `boolean` | `false` | Include raw OTP in the request response |
75
- | `identities` | `IdentityStore` | new | Email → user map |
76
- | `verifications` | `VerificationStore` | new | Challenge store |
77
-
78
- ## Surfaces
79
-
80
- | Flow | Path | Gate |
81
- | ---------------------- | ------------------------------ | ------------------------ |
82
- | `auth.requestEmailOtp` | `POST /auth/email-otp/request` | `gate.public` + otp rate |
83
- | `auth.verifyEmailOtp` | `POST /auth/email-otp/verify` | `gate.public` + otp rate |
84
-
85
- **Consequence:** the plugin contributes the `auth-email-otp` Channel template and EN/AR
86
- catalog bodies (`{{otp}}`). Override copy by merging your own catalog at boot.
87
-
88
- ## Troubleshooting
89
-
90
- <Accordions>
91
- <Accordion title="No email with the code">
92
-
93
- In `local` / `test` the `console` driver captures mail — open the inbox, or run
94
- `oke dev --docker` and check Mailpit. For unit tests without SMTP, set `exposeDevOtp: true`.
95
-
96
- </Accordion>
97
- <Accordion title="AuthFailed after a few tries">
98
-
99
- Five failed attempts consume the challenge. Request a new OTP.
100
-
101
- </Accordion>
102
- </Accordions>
103
-
104
- ## Learn more
105
-
106
- - [Magic link](/docs/plugins/magic-link) — link instead of a code
107
- - [Phone number](/docs/plugins/phone-number) — SMS OTP (E.164; delivery deferred)
108
- - [Channel](/docs/elements/channel) — `fx.send`, Mailpit, consents
109
- - [Gate](/docs/elements/gate) — `gate.auth`
110
-
111
- ## Next
112
-
113
- <Cards>
114
- <Card title="Phone number" description="E.164 SMS OTP." href="/docs/plugins/phone-number" />
115
- <Card title="Magic link" description="Email link sign-in." href="/docs/plugins/magic-link" />
116
- <Card title="Channel" description="Email delivery and Mailpit." href="/docs/elements/channel" />
117
- </Cards>
@@ -1,172 +0,0 @@
1
- ---
2
- title: "Phone number"
3
- description: "Official plugin — E.164 phone OTP request and verify under /auth, with provider-managed delivery via Taqnyat Verify."
4
- icon: "Smartphone"
5
- source: "docs/spec/unified-theory.md"
6
- ---
7
-
8
- `phoneNumber()` signs people in with a phone number in E.164 form (`+15551234567`). Delivery
9
- has two shapes: provider-managed OTP through Taqnyat Verify when an SMS driver is bound, or a
10
- local hashed code (10 minutes, 5 attempts) you deliver yourself.
11
-
12
- <Callout title="The one rule">
13
- Enable `gate.auth`, then `.plug(phoneNumber())`. For real SMS set `drivers.channel.sms` to
14
- `taqnyat` — the plugin uses Taqnyat's Verify API, never a self-generated code. Use `exposeDevOtp`
15
- only when no SMS driver is bound.
16
- </Callout>
17
-
18
- ## Quick start
19
-
20
- <Steps>
21
-
22
- <Step>
23
- ### Plug it
24
-
25
- ```typescript title="src/app.ts"
26
- import { oke } from "okengine";
27
- import { phoneNumber } from "okengine/plugins";
28
-
29
- export const app = oke({
30
- name: "shop",
31
- env: "local",
32
- gate: { auth: {} },
33
- }).plug(phoneNumber({ exposeDevOtp: true }));
34
- ```
35
-
36
- </Step>
37
-
38
- <Step>
39
- ### Request a code
40
-
41
- ```typescript
42
- const { data, error } = await api.auth.requestPhoneOtp({ phone: "+15551234567" });
43
- ```
44
-
45
- `POST /auth/phone/request`. Invalid E.164 → `AuthFailed` / `invalid_phone`. Optional `lang:
46
- "en" | "ar"` picks the OTP message language on the Taqnyat path.
47
-
48
- </Step>
49
-
50
- <Step>
51
- ### Verify
52
-
53
- ```typescript
54
- const { data } = await api.auth.verifyPhoneOtp({
55
- phone: "+15551234567",
56
- otp: data!.devOtp!,
57
- });
58
- ```
59
-
60
- `POST /auth/phone/verify` — first success creates a phone → `userId` mapping.
61
-
62
- </Step>
63
-
64
- </Steps>
65
-
66
- ## Delivery
67
-
68
- | Bound SMS driver | Request behavior | Verify behavior |
69
- | -------------------- | ---------------------------------------------------------- | ------------------------------ |
70
- | `taqnyat` | Taqnyat Verify `sendOtp` (provider generates the code) | Taqnyat Verify `verifyOtp` |
71
- | none | Local 6-digit code, hashed at rest (`exposeDevOtp` for DX) | Local hash compare, 5 attempts |
72
- | any other SMS driver | Fails loudly — provider-managed OTP unsupported | — |
73
-
74
- **Consequence:** the request response never contains `devOtp` on the Taqnyat path — the code
75
- exists only inside the SMS Taqnyat sends.
76
-
77
- ### Taqnyat Verify
78
-
79
- ```typescript title="oke.config.ts"
80
- export default {
81
- drivers: {
82
- channel: {
83
- sms: { local: "console", docker: "taqnyat", test: "console", prod: "taqnyat" },
84
- },
85
- },
86
- };
87
- ```
88
-
89
- | Env | Meaning |
90
- | ---------------------- | -------------------------------------------- |
91
- | `TAQNYAT_BEARER_TOKEN` | Taqnyat bearer token (`TAQNYAT_TOKEN` alias) |
92
- | `TAQNYAT_SENDER` | Pre-approved sender id |
93
-
94
- Request stores a provider challenge (`taqnyat:<requestId>`, 10m TTL, 5 attempts) and calls
95
- `fx.sendOtp`; verify replays the same `requestId` with the code through `fx.verifyOtp`. Both
96
- are capability-gated `send` effects — dry-run records them without contacting Taqnyat.
97
-
98
- An SMS driver without Verify support (for example a generic SMS sender) makes the request fail
99
- with a clear error: use `exposeDevOtp` locally or switch `drivers.channel.sms` to `taqnyat`.
100
-
101
- ## Options
102
-
103
- | Option | Type | Default | Meaning |
104
- | --------------- | ------------------- | -------- | --------------------------------------------------------- |
105
- | `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
106
- | `sessions` | `SessionStore` | active\* | Session store |
107
- | `ttlMs` | `number` | 10m | Challenge lifetime |
108
- | `exposeDevOtp` | `boolean` | `false` | Include raw OTP in the request response (local path only) |
109
- | `phones` | `PhoneStore` | new | Phone → userId map |
110
- | `verifications` | `VerificationStore` | new | Challenge store |
111
-
112
- ## Surfaces
113
-
114
- | Flow | Path | Gate |
115
- | ---------------------- | -------------------------- | ------------------------ |
116
- | `auth.requestPhoneOtp` | `POST /auth/phone/request` | `gate.public` + otp rate |
117
- | `auth.verifyPhoneOtp` | `POST /auth/phone/verify` | `gate.public` + otp rate |
118
-
119
- E.164 pattern: `^\+[1-9]\d{7,14}$`.
120
-
121
- ## Live tests (opt-in)
122
-
123
- The Taqnyat live suite sends real SMS and burns real quota, so it is double-gated: it runs only when
124
- `OKE_SMS_LIVE=1` **and** the real credentials (`TAQNYAT_TOKEN`, `TAQNYAT_SENDER`, plus
125
- `OKE_TEST_TAQNYAT_PHONE` — `TAQNYAT_TO` also accepted) are all present. Credentials alone never send.
126
-
127
- The suite sends exactly one SMS and never asks for the code.
128
-
129
- ```bash
130
- OKE_SMS_LIVE=1 TAQNYAT_TOKEN=… TAQNYAT_SENDER=YourBrand \
131
- OKE_TEST_TAQNYAT_PHONE=+9665xxxxxxxx bun test src/plugins
132
- ```
133
-
134
- Without the flag the suite skips visibly — never a silent pass.
135
-
136
- ## Troubleshooting
137
-
138
- <Accordions>
139
- <Accordion title="invalid_phone">
140
-
141
- Number must start with `+` and a non-zero country digit, then 7–14 more digits. No spaces or
142
- dashes.
143
-
144
- </Accordion>
145
- <Accordion title="SMS driver does not support provider-managed OTP">
146
-
147
- The configured `drivers.channel.sms` driver has no Verify API. Either switch to `taqnyat` or
148
- remove the SMS driver so the local hashed path (`exposeDevOtp`) is used.
149
-
150
- </Accordion>
151
- <Accordion title="No SMS arrived">
152
-
153
- On the local path no SMS is sent by design — use `exposeDevOtp`, or bind `taqnyat` with valid
154
- `TAQNYAT_*` credentials. On the Taqnyat path check the sender id is pre-approved and the
155
- number is E.164.
156
-
157
- </Accordion>
158
- </Accordions>
159
-
160
- ## Learn more
161
-
162
- - [Email OTP](/docs/plugins/email-otp) — email variant of the same OTP rules
163
- - [Magic link](/docs/plugins/magic-link) — one-time email link sign-in
164
- - [Channel](/docs/elements/channel) — SMS / email drivers and delivery
165
-
166
- ## Next
167
-
168
- <Cards>
169
- <Card title="Email OTP" description="Six-digit email codes." href="/docs/plugins/email-otp" />
170
- <Card title="Two-factor" description="TOTP step-up." href="/docs/plugins/two-factor" />
171
- <Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
172
- </Cards>
@@ -1,214 +0,0 @@
1
- /**
2
- * Email OTP Gate auth method plugin.
3
- *
4
- * Delivers the one-time code via Channel (`fx.send` + `auth-email-otp`
5
- * template). {@link EmailOtpOptions.exposeDevOtp} remains available for
6
- * local DX without Mailpit / SMTP.
7
- */
8
-
9
- import {
10
- createIdentityStore,
11
- normalizeEmail,
12
- type IdentityStore,
13
- type UserIdentityRow,
14
- } from "../auth/identity.ts";
15
- import { issueSessionWithScopes } from "../auth/sessions.ts";
16
- import {
17
- createVerificationStore,
18
- findActiveVerification,
19
- generateOtp,
20
- hashChallenge,
21
- putVerification,
22
- type VerificationStore,
23
- } from "../auth/verification.ts";
24
- import { channel } from "../elements/channel.ts";
25
- import { plugin, type PluginDef } from "../kernel/plugin.ts";
26
- import {
27
- AuthFailed,
28
- AuthRateLimited,
29
- SessionTokensOut,
30
- bindPublicAuth,
31
- createMethodRuntime,
32
- fail,
33
- flow,
34
- z,
35
- type AuthMethodOptions,
36
- } from "./auth/shared.ts";
37
-
38
- const DEFAULT_TTL_MS = 10 * 60 * 1000;
39
- const MAX_ATTEMPTS = 5;
40
- const DEFAULT_FROM = "OKE <no-reply@oke.local>";
41
-
42
- /** Channel template for email OTP delivery. */
43
- export const emailOtpTemplate = channel.email({ from: DEFAULT_FROM }).template("auth-email-otp", {
44
- description: "Email OTP sign-in code",
45
- schema: z.object({
46
- email: z.string(),
47
- otp: z.string(),
48
- }),
49
- locales: ["en", "ar"],
50
- });
51
-
52
- /** Default EN/AR bodies for {@link emailOtpTemplate}. */
53
- export const emailOtpCatalog = {
54
- "auth-email-otp": {
55
- en: {
56
- subject: "Your sign-in code",
57
- text: "Your one-time sign-in code is: {{otp}}\n",
58
- html: "<p>Your one-time sign-in code is:</p><p><strong>{{otp}}</strong></p>",
59
- },
60
- ar: {
61
- subject: "رمز تسجيل الدخول",
62
- text: "رمز تسجيل الدخول لمرة واحدة هو: {{otp}}\n",
63
- html: '<p dir="rtl">رمز تسجيل الدخول لمرة واحدة هو:</p><p dir="rtl"><strong>{{otp}}</strong></p>',
64
- },
65
- },
66
- } as const;
67
-
68
- /** Options for {@link emailOtp}. */
69
- export interface EmailOtpOptions extends AuthMethodOptions {
70
- /** Challenge TTL (default 10m). */
71
- readonly ttlMs?: number;
72
- readonly identities?: IdentityStore;
73
- readonly verifications?: VerificationStore;
74
- /** Return raw OTP in the request response (test / local). */
75
- readonly exposeDevOtp?: boolean;
76
- /** Override the template `from` address. */
77
- readonly from?: string;
78
- }
79
-
80
- /**
81
- * Email OTP request + verify (6-digit, hashed, 5 attempts).
82
- *
83
- * @param opts - TTL / stores / dev OTP
84
- */
85
- export function emailOtp(opts: EmailOtpOptions = {}): PluginDef {
86
- const runtime = createMethodRuntime(opts);
87
- const identities = opts.identities ?? createIdentityStore();
88
- const verifications = opts.verifications ?? createVerificationStore();
89
- const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
90
- const tmpl =
91
- opts.from !== undefined
92
- ? channel.email({ from: opts.from }).template("auth-email-otp", {
93
- description: "Email OTP sign-in code",
94
- schema: z.object({
95
- email: z.string(),
96
- otp: z.string(),
97
- }),
98
- locales: ["en", "ar"],
99
- })
100
- : emailOtpTemplate;
101
-
102
- const request = flow({
103
- name: "auth.requestEmailOtp",
104
- unit: "auth",
105
- plane: "user",
106
- in: z.object({ email: z.string().min(3) }),
107
- out: z.object({
108
- ok: z.literal(true),
109
- devOtp: z.string().optional(),
110
- }),
111
- errors: { AuthFailed, AuthRateLimited },
112
- effects: { sends: ["auth-email-otp"] },
113
- do: async (input, fx) => {
114
- const email = normalizeEmail(input.email);
115
- if (!email.includes("@")) return fail("AuthFailed", { reason: "invalid_email" });
116
- const otp = generateOtp(6);
117
- const now = runtime.now();
118
- // Invalidate prior active challenges for this email.
119
- for (const row of verifications.rows.values()) {
120
- if (row.identifier === `email-otp:${email}` && row.consumedAt === null) {
121
- row.consumedAt = now;
122
- }
123
- }
124
- putVerification(verifications, {
125
- id: crypto.randomUUID(),
126
- identifier: `email-otp:${email}`,
127
- value: await hashChallenge(otp),
128
- expiresAt: now + ttlMs,
129
- createdAt: now,
130
- consumedAt: null,
131
- attempts: 0,
132
- });
133
- await fx.send(tmpl, {
134
- to: email,
135
- data: { email, otp },
136
- });
137
- return {
138
- ok: true as const,
139
- ...(opts.exposeDevOtp ? { devOtp: otp } : {}),
140
- };
141
- },
142
- });
143
-
144
- const verify = flow({
145
- name: "auth.verifyEmailOtp",
146
- unit: "auth",
147
- plane: "user",
148
- in: z.object({
149
- email: z.string().min(3),
150
- otp: z.string().min(4).max(8),
151
- }),
152
- out: SessionTokensOut,
153
- errors: { AuthFailed, AuthRateLimited },
154
- do: async (input) => {
155
- const email = normalizeEmail(input.email);
156
- const now = runtime.now();
157
- const row = findActiveVerification(verifications, `email-otp:${email}`, now);
158
- if (!row) return fail("AuthFailed", { reason: "invalid_credentials" });
159
- if (row.attempts >= MAX_ATTEMPTS) {
160
- row.consumedAt = now;
161
- return fail("AuthFailed", { reason: "invalid_credentials" });
162
- }
163
- const hash = await hashChallenge(input.otp.trim());
164
- if (hash !== row.value) {
165
- row.attempts += 1;
166
- if (row.attempts >= MAX_ATTEMPTS) row.consumedAt = now;
167
- return fail("AuthFailed", { reason: "invalid_credentials" });
168
- }
169
- row.consumedAt = now;
170
- const user = ensureUserByEmail(identities, email, now);
171
- const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
172
- id: user.id,
173
- plane: "user",
174
- scopes: [],
175
- });
176
- return {
177
- accessToken: issued.accessToken,
178
- refreshToken: issued.refreshToken,
179
- accessExpiresAt: issued.accessExpiresAt,
180
- userId: user.id,
181
- };
182
- },
183
- });
184
-
185
- return plugin("emailOtp", { version: "0.0.1", config: { method: "email-otp" } })
186
- .needs("auth")
187
- .needs("channel")
188
- .channelTemplate(tmpl)
189
- .channelCatalog(emailOtpCatalog)
190
- .binding(bindPublicAuth("/email-otp/request", request, "otp"))
191
- .binding(bindPublicAuth("/email-otp/verify", verify, "otp"));
192
- }
193
-
194
- function ensureUserByEmail(store: IdentityStore, email: string, now: number): UserIdentityRow {
195
- const existingId = store.byEmail.get(email);
196
- if (existingId) {
197
- const existing = store.users.get(existingId);
198
- if (existing) return existing;
199
- }
200
- const id = crypto.randomUUID();
201
- const user: UserIdentityRow = {
202
- id,
203
- email,
204
- name: email.split("@")[0] || "user",
205
- emailVerified: true,
206
- status: "active",
207
- createdAt: now,
208
- updatedAt: now,
209
- extra: {},
210
- };
211
- store.users.set(id, user);
212
- store.byEmail.set(email, id);
213
- return user;
214
- }