okengine 0.8.0 → 0.9.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.
- package/package.json +3 -2
- package/site/content/docs/deployment/docker-swarm.mdx +228 -0
- package/site/content/docs/deployment/docker.mdx +212 -0
- package/site/content/docs/deployment/index.mdx +83 -0
- package/site/content/docs/deployment/kubernetes.mdx +176 -0
- package/site/content/docs/deployment/meta.json +5 -0
- package/site/content/docs/deployment/reverse-proxy.mdx +216 -0
- package/site/content/docs/elements/channel.mdx +3 -1
- package/site/content/docs/elements/signal.mdx +10 -8
- package/site/content/docs/elements/store.mdx +34 -0
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/index.mdx +5 -0
- package/site/content/docs/meta.json +10 -1
- package/site/content/docs/plugins/index.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +2 -2
- package/site/content/docs/plugins/meta.json +1 -2
- package/site/content/docs/plugins/otp.mdx +202 -0
- package/site/content/docs/plugins/two-factor.mdx +2 -2
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/configuration.mdx +16 -0
- package/site/content/docs/reference/environment-variables.mdx +9 -8
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/src/auth/auth.test.ts +36 -0
- package/src/auth/bindings.ts +3 -12
- package/src/auth/identity.ts +33 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/otp-capability.ts +119 -0
- package/src/auth/otp-seal.test.ts +61 -0
- package/src/auth/otp-seal.ts +84 -0
- package/src/auth/schema.ts +3 -0
- package/src/auth/sessions.ts +26 -27
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +61 -1
- package/src/cli/dev-app-runner.ts +4 -0
- package/src/cli/docker.ts +4 -1
- package/src/cli/load-config.images.test.ts +4 -0
- package/src/cli/load-config.ts +3 -0
- package/src/cli/registry.ts +1 -1
- package/src/console/server/operator-db.ts +34 -9
- package/src/docker/compose.ts +162 -6
- package/src/docker/derive.ts +60 -3
- package/src/docker/docker.test.ts +374 -1
- package/src/docker/helpers.ts +2 -0
- package/src/docker/index.ts +11 -0
- package/src/docker/recipes/caddy.ts +51 -0
- package/src/docker/recipes/dragonfly.ts +31 -0
- package/src/docker/recipes/index.ts +25 -2
- package/src/docker/recipes/pgdog.ts +84 -0
- package/src/docker/recipes/redis.ts +6 -3
- package/src/docker/recipes/traefik.ts +83 -0
- package/src/docker/recipes/valkey.ts +30 -0
- package/src/docker/stack-id.ts +5 -0
- package/src/docker/types.ts +18 -0
- package/src/drivers/channel-taqnyat-whatsapp.ts +94 -0
- package/src/drivers/channel-types.ts +1 -0
- package/src/elements/channel/otp-delivery.test.ts +76 -0
- package/src/elements/channel/otp-delivery.ts +291 -0
- package/src/elements/channel/runtime.ts +152 -114
- package/src/elements/channel.ts +12 -2
- package/src/index.ts +3 -0
- package/src/kernel/app.ts +60 -4
- package/src/kernel/boot-bind/channel.ts +51 -0
- package/src/kernel/boot-bind/gate.ts +14 -19
- package/src/kernel/boot-bind/honor-config.test.ts +18 -0
- package/src/kernel/boot-bind/signal.ts +20 -0
- package/src/kernel/boot-bind/store.test.ts +82 -0
- package/src/kernel/boot-bind/store.ts +22 -0
- package/src/kernel/boot.test.ts +5 -5
- package/src/kernel/fx.test.ts +3 -0
- package/src/kernel/fx.ts +49 -0
- package/src/kernel/graceful-shutdown.test.ts +76 -0
- package/src/kernel/graceful-shutdown.ts +106 -0
- package/src/kernel/horizontal-child.ts +257 -0
- package/src/kernel/horizontal.integration.test.ts +229 -0
- package/src/kernel/index.ts +8 -0
- package/src/kernel/ready.test.ts +76 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +5 -5
- package/src/plugins/auth-methods.security.test.ts +19 -29
- package/src/plugins/auth-methods.test.ts +7 -6
- package/src/plugins/index.ts +12 -8
- package/src/plugins/magic-link.ts +1 -23
- package/src/plugins/otp.test.ts +236 -0
- package/src/plugins/otp.ts +570 -0
- package/src/plugins/taqnyat.live.test.ts +4 -6
- package/src/release/official-plugins.ts +1 -2
- package/site/content/docs/plugins/email-otp.mdx +0 -117
- package/site/content/docs/plugins/phone-number.mdx +0 -172
- package/src/plugins/email-otp.ts +0 -214
- package/src/plugins/phone-number.ts +0 -206
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phone-number OTP Gate auth method plugin (E.164).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { issueSessionWithScopes } from "../auth/sessions.ts";
|
|
6
|
-
import {
|
|
7
|
-
createVerificationStore,
|
|
8
|
-
findActiveVerification,
|
|
9
|
-
generateOtp,
|
|
10
|
-
hashChallenge,
|
|
11
|
-
putVerification,
|
|
12
|
-
type VerificationStore,
|
|
13
|
-
} from "../auth/verification.ts";
|
|
14
|
-
import { plugin, type PluginDef } from "../kernel/plugin.ts";
|
|
15
|
-
import {
|
|
16
|
-
AuthFailed,
|
|
17
|
-
AuthRateLimited,
|
|
18
|
-
SessionTokensOut,
|
|
19
|
-
bindPublicAuth,
|
|
20
|
-
createMethodRuntime,
|
|
21
|
-
fail,
|
|
22
|
-
flow,
|
|
23
|
-
z,
|
|
24
|
-
type AuthMethodOptions,
|
|
25
|
-
} from "./auth/shared.ts";
|
|
26
|
-
|
|
27
|
-
const E164 = /^\+[1-9]\d{7,14}$/;
|
|
28
|
-
const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
29
|
-
const MAX_ATTEMPTS = 5;
|
|
30
|
-
|
|
31
|
-
/** Phone principal store (phone → userId). */
|
|
32
|
-
export interface PhoneStore {
|
|
33
|
-
readonly byPhone: Map<string, string>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Create an empty phone → user map.
|
|
38
|
-
*/
|
|
39
|
-
export function createPhoneStore(): PhoneStore {
|
|
40
|
-
return { byPhone: new Map() };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Options for {@link phoneNumber}. */
|
|
44
|
-
export interface PhoneNumberOptions extends AuthMethodOptions {
|
|
45
|
-
readonly ttlMs?: number;
|
|
46
|
-
readonly phones?: PhoneStore;
|
|
47
|
-
readonly verifications?: VerificationStore;
|
|
48
|
-
/** Return raw OTP in the request response (test / local). */
|
|
49
|
-
readonly exposeDevOtp?: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Phone OTP request + verify (E.164, 6-digit, hashed, 5 attempts).
|
|
54
|
-
*
|
|
55
|
-
* @param opts - TTL / stores / dev OTP
|
|
56
|
-
*/
|
|
57
|
-
export function phoneNumber(opts: PhoneNumberOptions = {}): PluginDef {
|
|
58
|
-
const runtime = createMethodRuntime(opts);
|
|
59
|
-
const phones = opts.phones ?? createPhoneStore();
|
|
60
|
-
const verifications = opts.verifications ?? createVerificationStore();
|
|
61
|
-
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
62
|
-
|
|
63
|
-
const request = flow({
|
|
64
|
-
name: "auth.requestPhoneOtp",
|
|
65
|
-
unit: "auth",
|
|
66
|
-
plane: "user",
|
|
67
|
-
in: z.object({
|
|
68
|
-
phone: z.string().min(8),
|
|
69
|
-
/** OTP message language for the Taqnyat Verify path. */
|
|
70
|
-
lang: z.enum(["en", "ar"]).optional(),
|
|
71
|
-
}),
|
|
72
|
-
out: z.object({
|
|
73
|
-
ok: z.literal(true),
|
|
74
|
-
devOtp: z.string().optional(),
|
|
75
|
-
}),
|
|
76
|
-
errors: { AuthFailed, AuthRateLimited },
|
|
77
|
-
effects: { sends: ["auth-phone-otp", "sms-otp"] },
|
|
78
|
-
do: async (input, fx) => {
|
|
79
|
-
const phone = input.phone.trim();
|
|
80
|
-
if (!E164.test(phone)) return fail("AuthFailed", { reason: "invalid_phone" });
|
|
81
|
-
const now = runtime.now();
|
|
82
|
-
for (const row of verifications.rows.values()) {
|
|
83
|
-
if (row.identifier === `phone-otp:${phone}` && row.consumedAt === null) {
|
|
84
|
-
row.consumedAt = now;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Provider-managed OTP (Taqnyat Verify) when a Taqnyat SMS driver is
|
|
89
|
-
// bound via Channel. Throws loudly for unsupported SMS drivers; returns
|
|
90
|
-
// undefined only when no SMS driver is bound (local/dev path).
|
|
91
|
-
const requestId = crypto.randomUUID();
|
|
92
|
-
const provider = await fx
|
|
93
|
-
.sendOtp({
|
|
94
|
-
to: phone,
|
|
95
|
-
requestId,
|
|
96
|
-
...(input.lang ? { lang: input.lang } : {}),
|
|
97
|
-
})
|
|
98
|
-
.catch((err: unknown) => {
|
|
99
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
-
if (msg.includes("no SMS driver bound")) return undefined;
|
|
101
|
-
throw err;
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (provider) {
|
|
105
|
-
putVerification(verifications, {
|
|
106
|
-
id: crypto.randomUUID(),
|
|
107
|
-
identifier: `phone-otp:${phone}`,
|
|
108
|
-
value: `taqnyat:${requestId}`,
|
|
109
|
-
expiresAt: now + ttlMs,
|
|
110
|
-
createdAt: now,
|
|
111
|
-
consumedAt: null,
|
|
112
|
-
attempts: 0,
|
|
113
|
-
});
|
|
114
|
-
return { ok: true as const };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const otp = generateOtp(6);
|
|
118
|
-
putVerification(verifications, {
|
|
119
|
-
id: crypto.randomUUID(),
|
|
120
|
-
identifier: `phone-otp:${phone}`,
|
|
121
|
-
value: await hashChallenge(otp),
|
|
122
|
-
expiresAt: now + ttlMs,
|
|
123
|
-
createdAt: now,
|
|
124
|
-
consumedAt: null,
|
|
125
|
-
attempts: 0,
|
|
126
|
-
});
|
|
127
|
-
return {
|
|
128
|
-
ok: true as const,
|
|
129
|
-
...(opts.exposeDevOtp ? { devOtp: otp } : {}),
|
|
130
|
-
};
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const verify = flow({
|
|
135
|
-
name: "auth.verifyPhoneOtp",
|
|
136
|
-
unit: "auth",
|
|
137
|
-
plane: "user",
|
|
138
|
-
in: z.object({
|
|
139
|
-
phone: z.string().min(8),
|
|
140
|
-
otp: z.string().min(4).max(8),
|
|
141
|
-
/** OTP message language for the Taqnyat Verify path. */
|
|
142
|
-
lang: z.enum(["en", "ar"]).optional(),
|
|
143
|
-
}),
|
|
144
|
-
out: SessionTokensOut,
|
|
145
|
-
errors: { AuthFailed, AuthRateLimited },
|
|
146
|
-
effects: { sends: ["sms-otp"] },
|
|
147
|
-
do: async (input, fx) => {
|
|
148
|
-
const phone = input.phone.trim();
|
|
149
|
-
if (!E164.test(phone)) return fail("AuthFailed", { reason: "invalid_phone" });
|
|
150
|
-
const now = runtime.now();
|
|
151
|
-
const row = findActiveVerification(verifications, `phone-otp:${phone}`, now);
|
|
152
|
-
if (!row) return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
153
|
-
if (row.attempts >= MAX_ATTEMPTS) {
|
|
154
|
-
row.consumedAt = now;
|
|
155
|
-
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (row.value.startsWith("taqnyat:")) {
|
|
159
|
-
const requestId = row.value.slice("taqnyat:".length);
|
|
160
|
-
try {
|
|
161
|
-
await fx.verifyOtp({
|
|
162
|
-
to: phone,
|
|
163
|
-
requestId,
|
|
164
|
-
code: input.otp.trim(),
|
|
165
|
-
...(input.lang ? { lang: input.lang } : {}),
|
|
166
|
-
});
|
|
167
|
-
} catch {
|
|
168
|
-
row.attempts += 1;
|
|
169
|
-
if (row.attempts >= MAX_ATTEMPTS) row.consumedAt = now;
|
|
170
|
-
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
171
|
-
}
|
|
172
|
-
row.consumedAt = now;
|
|
173
|
-
} else {
|
|
174
|
-
const hash = await hashChallenge(input.otp.trim());
|
|
175
|
-
if (hash !== row.value) {
|
|
176
|
-
row.attempts += 1;
|
|
177
|
-
if (row.attempts >= MAX_ATTEMPTS) row.consumedAt = now;
|
|
178
|
-
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
179
|
-
}
|
|
180
|
-
row.consumedAt = now;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let userId = phones.byPhone.get(phone);
|
|
184
|
-
if (!userId) {
|
|
185
|
-
userId = crypto.randomUUID();
|
|
186
|
-
phones.byPhone.set(phone, userId);
|
|
187
|
-
}
|
|
188
|
-
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
189
|
-
id: userId,
|
|
190
|
-
plane: "user",
|
|
191
|
-
scopes: [],
|
|
192
|
-
});
|
|
193
|
-
return {
|
|
194
|
-
accessToken: issued.accessToken,
|
|
195
|
-
refreshToken: issued.refreshToken,
|
|
196
|
-
accessExpiresAt: issued.accessExpiresAt,
|
|
197
|
-
userId,
|
|
198
|
-
};
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
return plugin("phoneNumber", { version: "0.0.1", config: { method: "phone-number" } })
|
|
203
|
-
.needs("auth")
|
|
204
|
-
.binding(bindPublicAuth("/phone/request", request, "otp"))
|
|
205
|
-
.binding(bindPublicAuth("/phone/verify", verify, "otp"));
|
|
206
|
-
}
|