@labyrinthe/otp-widget 0.0.0-stage → 1.2.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.
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Protocole JSON entre le widget (navigateur) et le serveur de l'intégrateur
3
+ * (`@labyrinthe/otp-widget/server`). Le navigateur ne voit jamais la clé API,
4
+ * ni `otp_id`, ni le code envoyé : seulement ce qui suit.
5
+ *
6
+ * POST {sendUrl} { "phone": "+243812345678", "locale": "fr", "send_id": "…" }
7
+ * 200 → SendSuccessBody erreur → ErrorBody
8
+ * POST {verifyUrl} { "code": "482913" }
9
+ * 200 → VerifySuccessBody | VerifyFailureBody erreur → ErrorBody
10
+ */
11
+ /** Why a code was refused (same values as the Labyrinthe API `reason`). */
12
+ type OtpFailureReason = 'invalid_code' | 'expired' | 'already_used' | 'max_attempts_reached';
13
+ /** Error codes sent by the server helper (stable, lower case). */
14
+ type ServerErrorCode =
15
+ /** The phone number cannot be used (invalid, blacklisted or opted out). */
16
+ 'invalid_phone'
17
+ /** A code was sent to this number too recently: wait `retry_after_seconds`. */
18
+ | 'resend_too_soon'
19
+ /** Too many requests (per number, per account or per API key). */
20
+ | 'rate_limited'
21
+ /** No code is pending for this browser (cookie missing, expired or tampered with): send a new one. */
22
+ | 'no_pending_verification'
23
+ /** Refused by the integrator (`beforeSend`) or by the origin / content-type checks. */
24
+ | 'forbidden'
25
+ /** Malformed request (missing field, wrong content type…). */
26
+ | 'invalid_request'
27
+ /**
28
+ * OTP service unavailable (Labyrinthe unreachable, misconfiguration, no credits…), or temporarily shut down by the
29
+ * Labyrinthe platform (FEATURE_DISABLED / OTP_SERVICE_DISABLED, maintenance): then `admin_message`, `until` and
30
+ * `retry_after_seconds` may be present.
31
+ */
32
+ | 'service_unavailable'
33
+ /** The integrator's Labyrinthe account is suspended (ACCOUNT_SUSPENDED): nothing the user can do. */
34
+ | 'account_suspended'
35
+ /**
36
+ * The send failed unexpectedly on the Labyrinthe side and its outcome is unknown (IDEMPOTENCY_KEY_UNRESOLVED):
37
+ * a code may have been sent. Never retried automatically; the user may ask for a new code (sent with a new key).
38
+ */
39
+ | 'send_failed'
40
+ /** Unexpected server error. */
41
+ | 'server_error';
42
+ type CodeType = 'numeric' | 'alphanumeric';
43
+ interface SendSuccessBody {
44
+ sent: true;
45
+ /** Masked number, for display ("+243 81•••••78"). */
46
+ phone_masked: string;
47
+ /** ISO 8601. */
48
+ expires_at: string;
49
+ max_attempts: number;
50
+ /** Seconds before "Resend" is enabled (account's anti-resend delay, returned by the API). */
51
+ resend_after_seconds: number;
52
+ /** Code length / alphabet (account's OTP settings, returned by the API 1.2.0, or the server's override). */
53
+ code_length?: number;
54
+ code_type?: CodeType;
55
+ /** ONLY when the integrator enabled `exposeTestCode` in development, with a `lsk_test_` key. */
56
+ test_code?: string;
57
+ }
58
+ interface VerifySuccessBody {
59
+ verified: true;
60
+ /** Whatever the integrator's `onVerified` returned (e.g. `{ redirect: "/dashboard" }`). */
61
+ data?: unknown;
62
+ }
63
+ interface VerifyFailureBody {
64
+ verified: false;
65
+ reason: OtpFailureReason;
66
+ /** Attempts left for this code (`attempts_remaining` of the API; estimated with an older SDK); null when unknown. */
67
+ attempts_left: number | null;
68
+ }
69
+ interface ErrorBody {
70
+ error: {
71
+ code: ServerErrorCode;
72
+ message: string;
73
+ retry_after_seconds?: number;
74
+ /** Maintenance only: message written by the Labyrinthe platform administrator, meant for end users. */
75
+ admin_message?: string;
76
+ /** Maintenance only: scheduled end (ISO 8601). */
77
+ until?: string;
78
+ };
79
+ }
80
+
81
+ /**
82
+ * État OTP côté serveur et son transport dans un cookie.
83
+ *
84
+ * Le cookie est **chiffré et authentifié** (AES-256-GCM, clé dérivée du secret
85
+ * par HKDF-SHA-256) : le navigateur ne peut ni lire `otp_id` ni modifier l'état.
86
+ * Avec un `store`, le cookie ne contient qu'un identifiant de session aléatoire
87
+ * (lui aussi chiffré) et l'état reste dans le store.
88
+ *
89
+ * Uniquement Web Crypto : Node ≥ 18, edge, Deno, Bun. Aucun module Node importé
90
+ * statiquement.
91
+ */
92
+ /** What the server keeps between `send` and `verify`. Never sent to the browser in clear. */
93
+ interface OtpSessionState {
94
+ /** Labyrinthe `otp_id`. */
95
+ otpId: string;
96
+ /** Normalised phone number the code was sent to. */
97
+ phone: string;
98
+ /** Expiry of the code, in milliseconds since the epoch. */
99
+ expiresAt: number;
100
+ /** Attempts left, tracked from `max_attempts` and the `invalid_code` answers; null when unknown. */
101
+ attemptsLeft: number | null;
102
+ }
103
+ /**
104
+ * Optional server-side store (Redis, database, your framework's session…).
105
+ * Without it, the state travels in the encrypted cookie.
106
+ */
107
+ interface OtpSessionStore {
108
+ get(id: string): OtpSessionState | null | undefined | Promise<OtpSessionState | null | undefined>;
109
+ set(id: string, state: OtpSessionState, ttlSeconds: number): void | Promise<void>;
110
+ delete(id: string): void | Promise<void>;
111
+ }
112
+ /** In-memory store, for development and tests (lost on restart, not shared between instances). */
113
+ declare function memoryStore(): OtpSessionStore & {
114
+ readonly size: number;
115
+ };
116
+ interface CookieOptions {
117
+ /** Default "labyrinthe_otp". */
118
+ name?: string;
119
+ /** Default "/". Restrict it (e.g. "/api/otp") when both endpoints share a prefix. */
120
+ path?: string;
121
+ domain?: string;
122
+ /** Default "Strict". Use "None" (with `secure`) only for endpoints on another site. */
123
+ sameSite?: 'Strict' | 'Lax' | 'None';
124
+ /** Default: true on HTTPS requests and when NODE_ENV is "production", false on plain-HTTP development. */
125
+ secure?: boolean;
126
+ }
127
+
128
+ /**
129
+ * The part of the `@labyrinthe/connect` client used here: pass a `Labyrinthe`
130
+ * instance. (Structural type: no runtime import of the SDK.)
131
+ */
132
+ interface OtpClient {
133
+ otp: {
134
+ send(params: {
135
+ phone: string;
136
+ template?: string;
137
+ locale?: 'fr' | 'en';
138
+ webOtpDomain?: string;
139
+ stripAccents?: boolean;
140
+ idempotencyKey?: string;
141
+ }): Promise<{
142
+ otpId: string;
143
+ expiresAt: Date;
144
+ maxAttempts: number;
145
+ /** `@labyrinthe/connect` ≥ 1.1.0 (API 1.2.0); absent or null with an older SDK. */
146
+ codeLength?: number | null;
147
+ codeType?: string | null;
148
+ resendAfterSeconds?: number | null;
149
+ readonly testCode: string | null;
150
+ }>;
151
+ verify(params: {
152
+ otpId: string;
153
+ code: string;
154
+ }): Promise<{
155
+ verified: boolean;
156
+ reason: string | null;
157
+ /** `@labyrinthe/connect` ≥ 1.1.0; absent or null with an older SDK (then estimated). */
158
+ attemptsRemaining?: number | null;
159
+ }>;
160
+ };
161
+ }
162
+ interface BeforeSendContext {
163
+ /** Normalised phone number (international, no "+"). */
164
+ phone: string;
165
+ /** Your framework's request: the Fetch `Request` (Next.js, edge…) or the Express `req` (e.g. `req.session`). */
166
+ request: Request | ExpressLikeRequest;
167
+ }
168
+ interface VerifiedContext extends BeforeSendContext {
169
+ /** Headers added to the JSON response. */
170
+ responseHeaders: Headers;
171
+ /** Adds a `Set-Cookie` header to the response (e.g. your own session cookie). */
172
+ setCookie: (cookie: string) => void;
173
+ }
174
+ interface OtpHandlersOptions {
175
+ /** A `Labyrinthe` client from `@labyrinthe/connect` (or a function returning it, called on first use). */
176
+ client: OtpClient | (() => OtpClient);
177
+ /**
178
+ * Secret used to encrypt the session cookie, at least 32 characters.
179
+ * Defaults to `process.env.LABYRINTHE_OTP_SECRET`. Not your API key.
180
+ */
181
+ secret?: string;
182
+ /** Keep the state in your own store; the cookie then only holds a random session id. */
183
+ store?: OtpSessionStore;
184
+ cookie?: CookieOptions;
185
+ /** Added to local numbers. Default "243" (DR Congo). */
186
+ defaultCountryCode?: string;
187
+ /** SMS template (must contain "{code}"). Default: the account's OTP settings. */
188
+ template?: string;
189
+ /** SMS language. Default: the widget's locale, then the account's OTP settings. */
190
+ locale?: 'fr' | 'en';
191
+ /**
192
+ * Web OTP API: host name of YOUR site, as seen in the browser address bar
193
+ * (e.g. "app.example.com", no scheme, port or path). The API ends the SMS with
194
+ * the line `@app.example.com #123456`, and Chrome on Android then offers to
195
+ * fill the code in `<labyrinthe-otp>` (top-level page on that exact host, HTTPS).
196
+ */
197
+ webOtpDomain?: string;
198
+ /** Replace accents in the SMS with ASCII (GSM-7, often half the cost). Default false. */
199
+ stripAccents?: boolean;
200
+ /**
201
+ * Override of the anti-resend delay sent to the widget. Default: the
202
+ * `resend_after_seconds` returned by the API (account's OTP settings), then 60.
203
+ * The API still enforces the real delay.
204
+ */
205
+ resendAfterSeconds?: number;
206
+ /**
207
+ * Override of the code length / alphabet sent to the widget. Default: the
208
+ * `code_length` / `code_type` returned by the API (account's OTP settings).
209
+ * Only needed with an API older than 1.2.0.
210
+ */
211
+ codeLength?: number;
212
+ codeType?: CodeType;
213
+ /**
214
+ * DEVELOPMENT ONLY: return the `test_code` of a `lsk_test_` key to the
215
+ * browser. Refused when NODE_ENV is "production". Never available with a live key.
216
+ */
217
+ exposeTestCode?: boolean;
218
+ /** Origins allowed to call the endpoints. Default: the request's own host only. */
219
+ allowedOrigins?: string[];
220
+ /**
221
+ * Your anti-abuse checks before a billed SMS is sent (rate limit per IP or
222
+ * account, captcha, allow-list…). Return false to refuse (403 `forbidden`).
223
+ */
224
+ beforeSend?: (context: BeforeSendContext) => boolean | undefined | Promise<boolean | undefined>;
225
+ /**
226
+ * Called once the code is verified: this is where you trust the phone number
227
+ * (log the user in, mark the number as verified…). Its return value is sent
228
+ * to the browser as `data` (keep it JSON and non-secret). The browser event
229
+ * alone must never be trusted.
230
+ */
231
+ onVerified: (context: VerifiedContext) => unknown;
232
+ /** Called with unexpected and upstream errors, for your logs (no key, no code, no otp_id inside). */
233
+ onError?: (error: unknown, context: {
234
+ action: 'send' | 'verify';
235
+ }) => void;
236
+ }
237
+ type FetchHandler = (request: Request) => Promise<Response>;
238
+ interface ExpressLikeRequest extends AsyncIterable<unknown> {
239
+ method?: string;
240
+ headers: Record<string, string | string[] | undefined>;
241
+ body?: unknown;
242
+ originalUrl?: string;
243
+ url?: string;
244
+ protocol?: string;
245
+ }
246
+ interface ExpressLikeResponse {
247
+ statusCode: number;
248
+ setHeader(name: string, value: string | string[]): unknown;
249
+ end(body?: string): unknown;
250
+ }
251
+ type ExpressHandler = (req: ExpressLikeRequest, res: ExpressLikeResponse, next: (error?: unknown) => void) => Promise<void>;
252
+ interface OtpHandlers {
253
+ /** Fetch API handler (Next.js App Router, Hono, Bun, Deno, edge). */
254
+ send: FetchHandler;
255
+ verify: FetchHandler;
256
+ /** Express / Connect / `node:http` handlers (with or without `express.json()`). */
257
+ express: {
258
+ send: ExpressHandler;
259
+ verify: ExpressHandler;
260
+ };
261
+ }
262
+ declare function createOtpHandlers(options: OtpHandlersOptions): OtpHandlers;
263
+
264
+ export { type BeforeSendContext, type CookieOptions, type ErrorBody, type ExpressHandler, type ExpressLikeRequest, type ExpressLikeResponse, type FetchHandler, type OtpClient, type OtpHandlers, type OtpHandlersOptions, type OtpSessionState, type OtpSessionStore, type SendSuccessBody, type ServerErrorCode, type VerifiedContext, type VerifyFailureBody, type VerifySuccessBody, createOtpHandlers, memoryStore };
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Protocole JSON entre le widget (navigateur) et le serveur de l'intégrateur
3
+ * (`@labyrinthe/otp-widget/server`). Le navigateur ne voit jamais la clé API,
4
+ * ni `otp_id`, ni le code envoyé : seulement ce qui suit.
5
+ *
6
+ * POST {sendUrl} { "phone": "+243812345678", "locale": "fr", "send_id": "…" }
7
+ * 200 → SendSuccessBody erreur → ErrorBody
8
+ * POST {verifyUrl} { "code": "482913" }
9
+ * 200 → VerifySuccessBody | VerifyFailureBody erreur → ErrorBody
10
+ */
11
+ /** Why a code was refused (same values as the Labyrinthe API `reason`). */
12
+ type OtpFailureReason = 'invalid_code' | 'expired' | 'already_used' | 'max_attempts_reached';
13
+ /** Error codes sent by the server helper (stable, lower case). */
14
+ type ServerErrorCode =
15
+ /** The phone number cannot be used (invalid, blacklisted or opted out). */
16
+ 'invalid_phone'
17
+ /** A code was sent to this number too recently: wait `retry_after_seconds`. */
18
+ | 'resend_too_soon'
19
+ /** Too many requests (per number, per account or per API key). */
20
+ | 'rate_limited'
21
+ /** No code is pending for this browser (cookie missing, expired or tampered with): send a new one. */
22
+ | 'no_pending_verification'
23
+ /** Refused by the integrator (`beforeSend`) or by the origin / content-type checks. */
24
+ | 'forbidden'
25
+ /** Malformed request (missing field, wrong content type…). */
26
+ | 'invalid_request'
27
+ /**
28
+ * OTP service unavailable (Labyrinthe unreachable, misconfiguration, no credits…), or temporarily shut down by the
29
+ * Labyrinthe platform (FEATURE_DISABLED / OTP_SERVICE_DISABLED, maintenance): then `admin_message`, `until` and
30
+ * `retry_after_seconds` may be present.
31
+ */
32
+ | 'service_unavailable'
33
+ /** The integrator's Labyrinthe account is suspended (ACCOUNT_SUSPENDED): nothing the user can do. */
34
+ | 'account_suspended'
35
+ /**
36
+ * The send failed unexpectedly on the Labyrinthe side and its outcome is unknown (IDEMPOTENCY_KEY_UNRESOLVED):
37
+ * a code may have been sent. Never retried automatically; the user may ask for a new code (sent with a new key).
38
+ */
39
+ | 'send_failed'
40
+ /** Unexpected server error. */
41
+ | 'server_error';
42
+ type CodeType = 'numeric' | 'alphanumeric';
43
+ interface SendSuccessBody {
44
+ sent: true;
45
+ /** Masked number, for display ("+243 81•••••78"). */
46
+ phone_masked: string;
47
+ /** ISO 8601. */
48
+ expires_at: string;
49
+ max_attempts: number;
50
+ /** Seconds before "Resend" is enabled (account's anti-resend delay, returned by the API). */
51
+ resend_after_seconds: number;
52
+ /** Code length / alphabet (account's OTP settings, returned by the API 1.2.0, or the server's override). */
53
+ code_length?: number;
54
+ code_type?: CodeType;
55
+ /** ONLY when the integrator enabled `exposeTestCode` in development, with a `lsk_test_` key. */
56
+ test_code?: string;
57
+ }
58
+ interface VerifySuccessBody {
59
+ verified: true;
60
+ /** Whatever the integrator's `onVerified` returned (e.g. `{ redirect: "/dashboard" }`). */
61
+ data?: unknown;
62
+ }
63
+ interface VerifyFailureBody {
64
+ verified: false;
65
+ reason: OtpFailureReason;
66
+ /** Attempts left for this code (`attempts_remaining` of the API; estimated with an older SDK); null when unknown. */
67
+ attempts_left: number | null;
68
+ }
69
+ interface ErrorBody {
70
+ error: {
71
+ code: ServerErrorCode;
72
+ message: string;
73
+ retry_after_seconds?: number;
74
+ /** Maintenance only: message written by the Labyrinthe platform administrator, meant for end users. */
75
+ admin_message?: string;
76
+ /** Maintenance only: scheduled end (ISO 8601). */
77
+ until?: string;
78
+ };
79
+ }
80
+
81
+ /**
82
+ * État OTP côté serveur et son transport dans un cookie.
83
+ *
84
+ * Le cookie est **chiffré et authentifié** (AES-256-GCM, clé dérivée du secret
85
+ * par HKDF-SHA-256) : le navigateur ne peut ni lire `otp_id` ni modifier l'état.
86
+ * Avec un `store`, le cookie ne contient qu'un identifiant de session aléatoire
87
+ * (lui aussi chiffré) et l'état reste dans le store.
88
+ *
89
+ * Uniquement Web Crypto : Node ≥ 18, edge, Deno, Bun. Aucun module Node importé
90
+ * statiquement.
91
+ */
92
+ /** What the server keeps between `send` and `verify`. Never sent to the browser in clear. */
93
+ interface OtpSessionState {
94
+ /** Labyrinthe `otp_id`. */
95
+ otpId: string;
96
+ /** Normalised phone number the code was sent to. */
97
+ phone: string;
98
+ /** Expiry of the code, in milliseconds since the epoch. */
99
+ expiresAt: number;
100
+ /** Attempts left, tracked from `max_attempts` and the `invalid_code` answers; null when unknown. */
101
+ attemptsLeft: number | null;
102
+ }
103
+ /**
104
+ * Optional server-side store (Redis, database, your framework's session…).
105
+ * Without it, the state travels in the encrypted cookie.
106
+ */
107
+ interface OtpSessionStore {
108
+ get(id: string): OtpSessionState | null | undefined | Promise<OtpSessionState | null | undefined>;
109
+ set(id: string, state: OtpSessionState, ttlSeconds: number): void | Promise<void>;
110
+ delete(id: string): void | Promise<void>;
111
+ }
112
+ /** In-memory store, for development and tests (lost on restart, not shared between instances). */
113
+ declare function memoryStore(): OtpSessionStore & {
114
+ readonly size: number;
115
+ };
116
+ interface CookieOptions {
117
+ /** Default "labyrinthe_otp". */
118
+ name?: string;
119
+ /** Default "/". Restrict it (e.g. "/api/otp") when both endpoints share a prefix. */
120
+ path?: string;
121
+ domain?: string;
122
+ /** Default "Strict". Use "None" (with `secure`) only for endpoints on another site. */
123
+ sameSite?: 'Strict' | 'Lax' | 'None';
124
+ /** Default: true on HTTPS requests and when NODE_ENV is "production", false on plain-HTTP development. */
125
+ secure?: boolean;
126
+ }
127
+
128
+ /**
129
+ * The part of the `@labyrinthe/connect` client used here: pass a `Labyrinthe`
130
+ * instance. (Structural type: no runtime import of the SDK.)
131
+ */
132
+ interface OtpClient {
133
+ otp: {
134
+ send(params: {
135
+ phone: string;
136
+ template?: string;
137
+ locale?: 'fr' | 'en';
138
+ webOtpDomain?: string;
139
+ stripAccents?: boolean;
140
+ idempotencyKey?: string;
141
+ }): Promise<{
142
+ otpId: string;
143
+ expiresAt: Date;
144
+ maxAttempts: number;
145
+ /** `@labyrinthe/connect` ≥ 1.1.0 (API 1.2.0); absent or null with an older SDK. */
146
+ codeLength?: number | null;
147
+ codeType?: string | null;
148
+ resendAfterSeconds?: number | null;
149
+ readonly testCode: string | null;
150
+ }>;
151
+ verify(params: {
152
+ otpId: string;
153
+ code: string;
154
+ }): Promise<{
155
+ verified: boolean;
156
+ reason: string | null;
157
+ /** `@labyrinthe/connect` ≥ 1.1.0; absent or null with an older SDK (then estimated). */
158
+ attemptsRemaining?: number | null;
159
+ }>;
160
+ };
161
+ }
162
+ interface BeforeSendContext {
163
+ /** Normalised phone number (international, no "+"). */
164
+ phone: string;
165
+ /** Your framework's request: the Fetch `Request` (Next.js, edge…) or the Express `req` (e.g. `req.session`). */
166
+ request: Request | ExpressLikeRequest;
167
+ }
168
+ interface VerifiedContext extends BeforeSendContext {
169
+ /** Headers added to the JSON response. */
170
+ responseHeaders: Headers;
171
+ /** Adds a `Set-Cookie` header to the response (e.g. your own session cookie). */
172
+ setCookie: (cookie: string) => void;
173
+ }
174
+ interface OtpHandlersOptions {
175
+ /** A `Labyrinthe` client from `@labyrinthe/connect` (or a function returning it, called on first use). */
176
+ client: OtpClient | (() => OtpClient);
177
+ /**
178
+ * Secret used to encrypt the session cookie, at least 32 characters.
179
+ * Defaults to `process.env.LABYRINTHE_OTP_SECRET`. Not your API key.
180
+ */
181
+ secret?: string;
182
+ /** Keep the state in your own store; the cookie then only holds a random session id. */
183
+ store?: OtpSessionStore;
184
+ cookie?: CookieOptions;
185
+ /** Added to local numbers. Default "243" (DR Congo). */
186
+ defaultCountryCode?: string;
187
+ /** SMS template (must contain "{code}"). Default: the account's OTP settings. */
188
+ template?: string;
189
+ /** SMS language. Default: the widget's locale, then the account's OTP settings. */
190
+ locale?: 'fr' | 'en';
191
+ /**
192
+ * Web OTP API: host name of YOUR site, as seen in the browser address bar
193
+ * (e.g. "app.example.com", no scheme, port or path). The API ends the SMS with
194
+ * the line `@app.example.com #123456`, and Chrome on Android then offers to
195
+ * fill the code in `<labyrinthe-otp>` (top-level page on that exact host, HTTPS).
196
+ */
197
+ webOtpDomain?: string;
198
+ /** Replace accents in the SMS with ASCII (GSM-7, often half the cost). Default false. */
199
+ stripAccents?: boolean;
200
+ /**
201
+ * Override of the anti-resend delay sent to the widget. Default: the
202
+ * `resend_after_seconds` returned by the API (account's OTP settings), then 60.
203
+ * The API still enforces the real delay.
204
+ */
205
+ resendAfterSeconds?: number;
206
+ /**
207
+ * Override of the code length / alphabet sent to the widget. Default: the
208
+ * `code_length` / `code_type` returned by the API (account's OTP settings).
209
+ * Only needed with an API older than 1.2.0.
210
+ */
211
+ codeLength?: number;
212
+ codeType?: CodeType;
213
+ /**
214
+ * DEVELOPMENT ONLY: return the `test_code` of a `lsk_test_` key to the
215
+ * browser. Refused when NODE_ENV is "production". Never available with a live key.
216
+ */
217
+ exposeTestCode?: boolean;
218
+ /** Origins allowed to call the endpoints. Default: the request's own host only. */
219
+ allowedOrigins?: string[];
220
+ /**
221
+ * Your anti-abuse checks before a billed SMS is sent (rate limit per IP or
222
+ * account, captcha, allow-list…). Return false to refuse (403 `forbidden`).
223
+ */
224
+ beforeSend?: (context: BeforeSendContext) => boolean | undefined | Promise<boolean | undefined>;
225
+ /**
226
+ * Called once the code is verified: this is where you trust the phone number
227
+ * (log the user in, mark the number as verified…). Its return value is sent
228
+ * to the browser as `data` (keep it JSON and non-secret). The browser event
229
+ * alone must never be trusted.
230
+ */
231
+ onVerified: (context: VerifiedContext) => unknown;
232
+ /** Called with unexpected and upstream errors, for your logs (no key, no code, no otp_id inside). */
233
+ onError?: (error: unknown, context: {
234
+ action: 'send' | 'verify';
235
+ }) => void;
236
+ }
237
+ type FetchHandler = (request: Request) => Promise<Response>;
238
+ interface ExpressLikeRequest extends AsyncIterable<unknown> {
239
+ method?: string;
240
+ headers: Record<string, string | string[] | undefined>;
241
+ body?: unknown;
242
+ originalUrl?: string;
243
+ url?: string;
244
+ protocol?: string;
245
+ }
246
+ interface ExpressLikeResponse {
247
+ statusCode: number;
248
+ setHeader(name: string, value: string | string[]): unknown;
249
+ end(body?: string): unknown;
250
+ }
251
+ type ExpressHandler = (req: ExpressLikeRequest, res: ExpressLikeResponse, next: (error?: unknown) => void) => Promise<void>;
252
+ interface OtpHandlers {
253
+ /** Fetch API handler (Next.js App Router, Hono, Bun, Deno, edge). */
254
+ send: FetchHandler;
255
+ verify: FetchHandler;
256
+ /** Express / Connect / `node:http` handlers (with or without `express.json()`). */
257
+ express: {
258
+ send: ExpressHandler;
259
+ verify: ExpressHandler;
260
+ };
261
+ }
262
+ declare function createOtpHandlers(options: OtpHandlersOptions): OtpHandlers;
263
+
264
+ export { type BeforeSendContext, type CookieOptions, type ErrorBody, type ExpressHandler, type ExpressLikeRequest, type ExpressLikeResponse, type FetchHandler, type OtpClient, type OtpHandlers, type OtpHandlersOptions, type OtpSessionState, type OtpSessionStore, type SendSuccessBody, type ServerErrorCode, type VerifiedContext, type VerifyFailureBody, type VerifySuccessBody, createOtpHandlers, memoryStore };