otp-guard 0.1.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/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/index.cjs +435 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +259 -0
- package/dist/index.d.ts +259 -0
- package/dist/index.js +430 -0
- package/dist/index.js.map +1 -0
- package/dist/redis.cjs +71 -0
- package/dist/redis.cjs.map +1 -0
- package/dist/redis.d.cts +30 -0
- package/dist/redis.d.ts +30 -0
- package/dist/redis.js +69 -0
- package/dist/redis.js.map +1 -0
- package/dist/store-mOFmlT-6.d.cts +49 -0
- package/dist/store-mOFmlT-6.d.ts +49 -0
- package/package.json +75 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { O as OtpStore } from './store-mOFmlT-6.cjs';
|
|
2
|
+
export { a as OtpRecord } from './store-mOFmlT-6.cjs';
|
|
3
|
+
|
|
4
|
+
/** Duration in milliseconds, or a short string: "30s", "5m", "2h", "1d". */
|
|
5
|
+
type Duration = number | `${number}${'s' | 'm' | 'h' | 'd'}`;
|
|
6
|
+
/**
|
|
7
|
+
* Character set for generated codes.
|
|
8
|
+
*
|
|
9
|
+
* - `numeric` — digits only. The right default for SMS.
|
|
10
|
+
* - `alphanumeric` — excludes I, L, O, U, 0, 1 to avoid transcription errors
|
|
11
|
+
* when a code is read aloud or copied by hand.
|
|
12
|
+
* - `base32` — RFC 4648 alphabet, for codes that feed other systems.
|
|
13
|
+
*/
|
|
14
|
+
type Alphabet = 'numeric' | 'alphanumeric' | 'base32';
|
|
15
|
+
interface RateWindow {
|
|
16
|
+
count: number;
|
|
17
|
+
window: Duration;
|
|
18
|
+
}
|
|
19
|
+
type VerifyFailure = 'invalid' | 'expired' | 'locked' | 'bind_mismatch';
|
|
20
|
+
/** Metadata passed to observability hooks. */
|
|
21
|
+
interface HookEvent {
|
|
22
|
+
/**
|
|
23
|
+
* The raw identity. Treat as personal data: do not ship it to an APM or
|
|
24
|
+
* third-party log sink. Use `keyHash` for that.
|
|
25
|
+
*/
|
|
26
|
+
key: string;
|
|
27
|
+
/** Stable, non-reversible identifier derived from key and purpose. Safe to log. */
|
|
28
|
+
keyHash: string;
|
|
29
|
+
purpose: string;
|
|
30
|
+
scope?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Observability callbacks.
|
|
34
|
+
*
|
|
35
|
+
* Hooks are advisory. Exceptions thrown inside them are swallowed, so a
|
|
36
|
+
* broken metrics pipeline cannot break authentication.
|
|
37
|
+
*/
|
|
38
|
+
interface Hooks {
|
|
39
|
+
onIssue?(e: HookEvent): void;
|
|
40
|
+
onRateLimited?(e: HookEvent & {
|
|
41
|
+
reason: 'cooldown' | 'rate_limited';
|
|
42
|
+
/** Which of the three limits rejected the request. */
|
|
43
|
+
limit: 'cooldown' | 'key' | 'scope';
|
|
44
|
+
}): void;
|
|
45
|
+
onVerifyFailure?(e: HookEvent & {
|
|
46
|
+
reason: VerifyFailure;
|
|
47
|
+
attempts: number;
|
|
48
|
+
}): void;
|
|
49
|
+
onLockout?(e: HookEvent): void;
|
|
50
|
+
onStoreError?(e: {
|
|
51
|
+
operation: string;
|
|
52
|
+
error: unknown;
|
|
53
|
+
}): void;
|
|
54
|
+
}
|
|
55
|
+
interface OtpKitOptions {
|
|
56
|
+
store: OtpStore;
|
|
57
|
+
/**
|
|
58
|
+
* Server-side secret mixed into every hash and every store key. Rotating it
|
|
59
|
+
* invalidates all outstanding codes, which is a legitimate emergency lever.
|
|
60
|
+
*
|
|
61
|
+
* Minimum 16 characters. Generate with:
|
|
62
|
+
* `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
|
|
63
|
+
*
|
|
64
|
+
* Keep it out of the database the hashes live in — that separation is the
|
|
65
|
+
* entire point of a pepper.
|
|
66
|
+
*/
|
|
67
|
+
pepper: string;
|
|
68
|
+
/** Characters per code. 4–32. Default 6. */
|
|
69
|
+
length?: number;
|
|
70
|
+
/** Character set. Default `"numeric"`. */
|
|
71
|
+
alphabet?: Alphabet;
|
|
72
|
+
/** How long a code stays valid. Default `"5m"`. */
|
|
73
|
+
ttl?: Duration;
|
|
74
|
+
/** Failed attempts before the key is locked. Default 5. */
|
|
75
|
+
maxAttempts?: number;
|
|
76
|
+
/**
|
|
77
|
+
* How long a lockout persists. Independent of `ttl` on purpose: if the
|
|
78
|
+
* attempt counter expired alongside the code, a resend would refresh the
|
|
79
|
+
* attacker's guess budget and `maxAttempts` would be decorative.
|
|
80
|
+
* Default `"15m"`.
|
|
81
|
+
*/
|
|
82
|
+
lockoutWindow?: Duration;
|
|
83
|
+
/**
|
|
84
|
+
* How long an expired record is retained past `ttl` so that `verify` can
|
|
85
|
+
* answer `"expired"` rather than `"invalid"`.
|
|
86
|
+
*
|
|
87
|
+
* Without this the record dies with its TTL and an expired code is
|
|
88
|
+
* indistinguishable from a wrong one, which denies the user the only
|
|
89
|
+
* message that tells them what to do ("request a new code"). Retention is
|
|
90
|
+
* safe: validity is decided by `expiresAt`, not by the record's existence.
|
|
91
|
+
*
|
|
92
|
+
* Set to a small value to reduce storage. Default `"10m"`.
|
|
93
|
+
*/
|
|
94
|
+
expiredGrace?: Duration;
|
|
95
|
+
/** Minimum gap between issues for the same key. Default `"60s"`. */
|
|
96
|
+
resendCooldown?: Duration;
|
|
97
|
+
/** Cap on issues per key per window. Default 5 per hour. */
|
|
98
|
+
maxSendsPerKey?: RateWindow;
|
|
99
|
+
/**
|
|
100
|
+
* Cap on issues per caller-supplied scope (IP, device id, tenant).
|
|
101
|
+
* This is the limit that stops OTP-pumping fraud, where an attacker cycles
|
|
102
|
+
* through numbers to burn your SMS credit. Requires `scope` on `issue`.
|
|
103
|
+
* Default 20 per hour.
|
|
104
|
+
*/
|
|
105
|
+
maxSendsPerScope?: RateWindow;
|
|
106
|
+
/** Reject `key`, `purpose`, `scope`, or `bindToken` longer than this. Default 256. */
|
|
107
|
+
maxInputLength?: number;
|
|
108
|
+
/** Namespace prefix for all store keys. Default `"otp"`. */
|
|
109
|
+
prefix?: string;
|
|
110
|
+
/** Observability callbacks. */
|
|
111
|
+
hooks?: Hooks;
|
|
112
|
+
/** Injectable clock. Intended for tests. */
|
|
113
|
+
now?: () => number;
|
|
114
|
+
}
|
|
115
|
+
interface IssueInput {
|
|
116
|
+
/** Identity being verified — phone number, email, user id. */
|
|
117
|
+
key: string;
|
|
118
|
+
/**
|
|
119
|
+
* Purpose scope. A code issued for one purpose will not verify another,
|
|
120
|
+
* so a phished login code cannot be replayed against a payment.
|
|
121
|
+
* Default `"default"`.
|
|
122
|
+
*/
|
|
123
|
+
purpose?: string;
|
|
124
|
+
/** Rate-limit scope, typically `req.ip`. Enables `maxSendsPerScope`. */
|
|
125
|
+
scope?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Binds the code to a session or device. The same value must be presented
|
|
128
|
+
* at verify, which narrows the window for an attacker who intercepts the
|
|
129
|
+
* code but not the session.
|
|
130
|
+
*/
|
|
131
|
+
bindToken?: string;
|
|
132
|
+
}
|
|
133
|
+
type IssueResult = {
|
|
134
|
+
ok: true;
|
|
135
|
+
/**
|
|
136
|
+
* Plaintext code. Returned once and never stored. Send it; do not log
|
|
137
|
+
* it, and do not put it in an error message.
|
|
138
|
+
*/
|
|
139
|
+
code: string;
|
|
140
|
+
expiresAt: number;
|
|
141
|
+
/** Epoch millis before which a resend will be refused. */
|
|
142
|
+
resendAfter: number;
|
|
143
|
+
} | {
|
|
144
|
+
ok: false;
|
|
145
|
+
reason: 'cooldown' | 'rate_limited';
|
|
146
|
+
/** Epoch millis at which the caller may retry. */
|
|
147
|
+
retryAfter: number;
|
|
148
|
+
};
|
|
149
|
+
interface VerifyInput {
|
|
150
|
+
key: string;
|
|
151
|
+
code: string;
|
|
152
|
+
/** Must match the purpose the code was issued under. */
|
|
153
|
+
purpose?: string;
|
|
154
|
+
/** Required if the code was issued with a `bindToken`. */
|
|
155
|
+
bindToken?: string;
|
|
156
|
+
}
|
|
157
|
+
type VerifyResult = {
|
|
158
|
+
ok: true;
|
|
159
|
+
} | {
|
|
160
|
+
ok: false;
|
|
161
|
+
reason: VerifyFailure;
|
|
162
|
+
/** Attempts remaining before lockout, when knowable. */
|
|
163
|
+
attemptsLeft?: number;
|
|
164
|
+
};
|
|
165
|
+
interface OtpKit {
|
|
166
|
+
issue(input: IssueInput): Promise<IssueResult>;
|
|
167
|
+
verify(input: VerifyInput): Promise<VerifyResult>;
|
|
168
|
+
/** Clear the outstanding code, attempt counter, and lockout for a key. */
|
|
169
|
+
reset(key: string, purpose?: string): Promise<void>;
|
|
170
|
+
/** Release store-owned resources. */
|
|
171
|
+
close(): Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create an OTP issuer/verifier.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* import { createOtpKit, memoryStore } from 'otp-guard';
|
|
180
|
+
*
|
|
181
|
+
* const otp = createOtpKit({
|
|
182
|
+
* store: memoryStore(),
|
|
183
|
+
* pepper: process.env.OTP_PEPPER!,
|
|
184
|
+
* ttl: '5m',
|
|
185
|
+
* maxAttempts: 5,
|
|
186
|
+
* });
|
|
187
|
+
*
|
|
188
|
+
* const issued = await otp.issue({ key: '+919812345678', purpose: 'login' });
|
|
189
|
+
* if (issued.ok) await sms.send(issued.code);
|
|
190
|
+
*
|
|
191
|
+
* const result = await otp.verify({
|
|
192
|
+
* key: '+919812345678',
|
|
193
|
+
* code: '123456',
|
|
194
|
+
* purpose: 'login',
|
|
195
|
+
* });
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* @throws {OtpConfigError} if the options are invalid.
|
|
199
|
+
*/
|
|
200
|
+
declare function createOtpKit(options: OtpKitOptions): OtpKit;
|
|
201
|
+
|
|
202
|
+
interface MemoryStoreOptions {
|
|
203
|
+
/** Injectable clock. Intended for tests. */
|
|
204
|
+
now?: () => number;
|
|
205
|
+
/**
|
|
206
|
+
* Hard cap on retained entries. When exceeded, the soonest-expiring entries
|
|
207
|
+
* are dropped first. Prevents unbounded growth from codes that are issued
|
|
208
|
+
* and never verified. Default 100_000.
|
|
209
|
+
*/
|
|
210
|
+
maxEntries?: number;
|
|
211
|
+
/** Sweep interval for expired entries in ms. Default 60_000. 0 disables. */
|
|
212
|
+
sweepIntervalMs?: number;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Process-local store.
|
|
216
|
+
*
|
|
217
|
+
* Suitable for tests, CLIs, and single-instance apps. NOT suitable behind a
|
|
218
|
+
* load balancer: counters live in one process's heap, so N instances means N
|
|
219
|
+
* times your intended rate limit.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const otp = createOtpKit({ store: memoryStore(), pepper });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
declare function memoryStore(opts?: MemoryStoreOptions): OtpStore;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Thrown when the underlying store fails.
|
|
230
|
+
*
|
|
231
|
+
* otp-guard fails closed: a store outage means codes cannot be issued or
|
|
232
|
+
* verified, never that verification silently succeeds. Catch this at your
|
|
233
|
+
* route layer and return 503.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* try {
|
|
238
|
+
* await otp.verify({ key, code });
|
|
239
|
+
* } catch (err) {
|
|
240
|
+
* if (err instanceof OtpStoreError) return reply.status(503).send();
|
|
241
|
+
* throw err;
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
declare class OtpStoreError extends Error {
|
|
246
|
+
/** Which store operation failed, e.g. "acquire_cooldown". */
|
|
247
|
+
readonly operation: string;
|
|
248
|
+
constructor(operation: string, cause: unknown);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Thrown for caller mistakes: invalid configuration, malformed or oversized
|
|
252
|
+
* identifiers. Always a programming error, never a runtime condition to
|
|
253
|
+
* handle gracefully.
|
|
254
|
+
*/
|
|
255
|
+
declare class OtpConfigError extends Error {
|
|
256
|
+
constructor(message: string);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { type Alphabet, type Duration, type HookEvent, type Hooks, type IssueInput, type IssueResult, type MemoryStoreOptions, OtpConfigError, type OtpKit, type OtpKitOptions, OtpStore, OtpStoreError, type RateWindow, type VerifyFailure, type VerifyInput, type VerifyResult, createOtpKit, memoryStore };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { O as OtpStore } from './store-mOFmlT-6.js';
|
|
2
|
+
export { a as OtpRecord } from './store-mOFmlT-6.js';
|
|
3
|
+
|
|
4
|
+
/** Duration in milliseconds, or a short string: "30s", "5m", "2h", "1d". */
|
|
5
|
+
type Duration = number | `${number}${'s' | 'm' | 'h' | 'd'}`;
|
|
6
|
+
/**
|
|
7
|
+
* Character set for generated codes.
|
|
8
|
+
*
|
|
9
|
+
* - `numeric` — digits only. The right default for SMS.
|
|
10
|
+
* - `alphanumeric` — excludes I, L, O, U, 0, 1 to avoid transcription errors
|
|
11
|
+
* when a code is read aloud or copied by hand.
|
|
12
|
+
* - `base32` — RFC 4648 alphabet, for codes that feed other systems.
|
|
13
|
+
*/
|
|
14
|
+
type Alphabet = 'numeric' | 'alphanumeric' | 'base32';
|
|
15
|
+
interface RateWindow {
|
|
16
|
+
count: number;
|
|
17
|
+
window: Duration;
|
|
18
|
+
}
|
|
19
|
+
type VerifyFailure = 'invalid' | 'expired' | 'locked' | 'bind_mismatch';
|
|
20
|
+
/** Metadata passed to observability hooks. */
|
|
21
|
+
interface HookEvent {
|
|
22
|
+
/**
|
|
23
|
+
* The raw identity. Treat as personal data: do not ship it to an APM or
|
|
24
|
+
* third-party log sink. Use `keyHash` for that.
|
|
25
|
+
*/
|
|
26
|
+
key: string;
|
|
27
|
+
/** Stable, non-reversible identifier derived from key and purpose. Safe to log. */
|
|
28
|
+
keyHash: string;
|
|
29
|
+
purpose: string;
|
|
30
|
+
scope?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Observability callbacks.
|
|
34
|
+
*
|
|
35
|
+
* Hooks are advisory. Exceptions thrown inside them are swallowed, so a
|
|
36
|
+
* broken metrics pipeline cannot break authentication.
|
|
37
|
+
*/
|
|
38
|
+
interface Hooks {
|
|
39
|
+
onIssue?(e: HookEvent): void;
|
|
40
|
+
onRateLimited?(e: HookEvent & {
|
|
41
|
+
reason: 'cooldown' | 'rate_limited';
|
|
42
|
+
/** Which of the three limits rejected the request. */
|
|
43
|
+
limit: 'cooldown' | 'key' | 'scope';
|
|
44
|
+
}): void;
|
|
45
|
+
onVerifyFailure?(e: HookEvent & {
|
|
46
|
+
reason: VerifyFailure;
|
|
47
|
+
attempts: number;
|
|
48
|
+
}): void;
|
|
49
|
+
onLockout?(e: HookEvent): void;
|
|
50
|
+
onStoreError?(e: {
|
|
51
|
+
operation: string;
|
|
52
|
+
error: unknown;
|
|
53
|
+
}): void;
|
|
54
|
+
}
|
|
55
|
+
interface OtpKitOptions {
|
|
56
|
+
store: OtpStore;
|
|
57
|
+
/**
|
|
58
|
+
* Server-side secret mixed into every hash and every store key. Rotating it
|
|
59
|
+
* invalidates all outstanding codes, which is a legitimate emergency lever.
|
|
60
|
+
*
|
|
61
|
+
* Minimum 16 characters. Generate with:
|
|
62
|
+
* `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
|
|
63
|
+
*
|
|
64
|
+
* Keep it out of the database the hashes live in — that separation is the
|
|
65
|
+
* entire point of a pepper.
|
|
66
|
+
*/
|
|
67
|
+
pepper: string;
|
|
68
|
+
/** Characters per code. 4–32. Default 6. */
|
|
69
|
+
length?: number;
|
|
70
|
+
/** Character set. Default `"numeric"`. */
|
|
71
|
+
alphabet?: Alphabet;
|
|
72
|
+
/** How long a code stays valid. Default `"5m"`. */
|
|
73
|
+
ttl?: Duration;
|
|
74
|
+
/** Failed attempts before the key is locked. Default 5. */
|
|
75
|
+
maxAttempts?: number;
|
|
76
|
+
/**
|
|
77
|
+
* How long a lockout persists. Independent of `ttl` on purpose: if the
|
|
78
|
+
* attempt counter expired alongside the code, a resend would refresh the
|
|
79
|
+
* attacker's guess budget and `maxAttempts` would be decorative.
|
|
80
|
+
* Default `"15m"`.
|
|
81
|
+
*/
|
|
82
|
+
lockoutWindow?: Duration;
|
|
83
|
+
/**
|
|
84
|
+
* How long an expired record is retained past `ttl` so that `verify` can
|
|
85
|
+
* answer `"expired"` rather than `"invalid"`.
|
|
86
|
+
*
|
|
87
|
+
* Without this the record dies with its TTL and an expired code is
|
|
88
|
+
* indistinguishable from a wrong one, which denies the user the only
|
|
89
|
+
* message that tells them what to do ("request a new code"). Retention is
|
|
90
|
+
* safe: validity is decided by `expiresAt`, not by the record's existence.
|
|
91
|
+
*
|
|
92
|
+
* Set to a small value to reduce storage. Default `"10m"`.
|
|
93
|
+
*/
|
|
94
|
+
expiredGrace?: Duration;
|
|
95
|
+
/** Minimum gap between issues for the same key. Default `"60s"`. */
|
|
96
|
+
resendCooldown?: Duration;
|
|
97
|
+
/** Cap on issues per key per window. Default 5 per hour. */
|
|
98
|
+
maxSendsPerKey?: RateWindow;
|
|
99
|
+
/**
|
|
100
|
+
* Cap on issues per caller-supplied scope (IP, device id, tenant).
|
|
101
|
+
* This is the limit that stops OTP-pumping fraud, where an attacker cycles
|
|
102
|
+
* through numbers to burn your SMS credit. Requires `scope` on `issue`.
|
|
103
|
+
* Default 20 per hour.
|
|
104
|
+
*/
|
|
105
|
+
maxSendsPerScope?: RateWindow;
|
|
106
|
+
/** Reject `key`, `purpose`, `scope`, or `bindToken` longer than this. Default 256. */
|
|
107
|
+
maxInputLength?: number;
|
|
108
|
+
/** Namespace prefix for all store keys. Default `"otp"`. */
|
|
109
|
+
prefix?: string;
|
|
110
|
+
/** Observability callbacks. */
|
|
111
|
+
hooks?: Hooks;
|
|
112
|
+
/** Injectable clock. Intended for tests. */
|
|
113
|
+
now?: () => number;
|
|
114
|
+
}
|
|
115
|
+
interface IssueInput {
|
|
116
|
+
/** Identity being verified — phone number, email, user id. */
|
|
117
|
+
key: string;
|
|
118
|
+
/**
|
|
119
|
+
* Purpose scope. A code issued for one purpose will not verify another,
|
|
120
|
+
* so a phished login code cannot be replayed against a payment.
|
|
121
|
+
* Default `"default"`.
|
|
122
|
+
*/
|
|
123
|
+
purpose?: string;
|
|
124
|
+
/** Rate-limit scope, typically `req.ip`. Enables `maxSendsPerScope`. */
|
|
125
|
+
scope?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Binds the code to a session or device. The same value must be presented
|
|
128
|
+
* at verify, which narrows the window for an attacker who intercepts the
|
|
129
|
+
* code but not the session.
|
|
130
|
+
*/
|
|
131
|
+
bindToken?: string;
|
|
132
|
+
}
|
|
133
|
+
type IssueResult = {
|
|
134
|
+
ok: true;
|
|
135
|
+
/**
|
|
136
|
+
* Plaintext code. Returned once and never stored. Send it; do not log
|
|
137
|
+
* it, and do not put it in an error message.
|
|
138
|
+
*/
|
|
139
|
+
code: string;
|
|
140
|
+
expiresAt: number;
|
|
141
|
+
/** Epoch millis before which a resend will be refused. */
|
|
142
|
+
resendAfter: number;
|
|
143
|
+
} | {
|
|
144
|
+
ok: false;
|
|
145
|
+
reason: 'cooldown' | 'rate_limited';
|
|
146
|
+
/** Epoch millis at which the caller may retry. */
|
|
147
|
+
retryAfter: number;
|
|
148
|
+
};
|
|
149
|
+
interface VerifyInput {
|
|
150
|
+
key: string;
|
|
151
|
+
code: string;
|
|
152
|
+
/** Must match the purpose the code was issued under. */
|
|
153
|
+
purpose?: string;
|
|
154
|
+
/** Required if the code was issued with a `bindToken`. */
|
|
155
|
+
bindToken?: string;
|
|
156
|
+
}
|
|
157
|
+
type VerifyResult = {
|
|
158
|
+
ok: true;
|
|
159
|
+
} | {
|
|
160
|
+
ok: false;
|
|
161
|
+
reason: VerifyFailure;
|
|
162
|
+
/** Attempts remaining before lockout, when knowable. */
|
|
163
|
+
attemptsLeft?: number;
|
|
164
|
+
};
|
|
165
|
+
interface OtpKit {
|
|
166
|
+
issue(input: IssueInput): Promise<IssueResult>;
|
|
167
|
+
verify(input: VerifyInput): Promise<VerifyResult>;
|
|
168
|
+
/** Clear the outstanding code, attempt counter, and lockout for a key. */
|
|
169
|
+
reset(key: string, purpose?: string): Promise<void>;
|
|
170
|
+
/** Release store-owned resources. */
|
|
171
|
+
close(): Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create an OTP issuer/verifier.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* import { createOtpKit, memoryStore } from 'otp-guard';
|
|
180
|
+
*
|
|
181
|
+
* const otp = createOtpKit({
|
|
182
|
+
* store: memoryStore(),
|
|
183
|
+
* pepper: process.env.OTP_PEPPER!,
|
|
184
|
+
* ttl: '5m',
|
|
185
|
+
* maxAttempts: 5,
|
|
186
|
+
* });
|
|
187
|
+
*
|
|
188
|
+
* const issued = await otp.issue({ key: '+919812345678', purpose: 'login' });
|
|
189
|
+
* if (issued.ok) await sms.send(issued.code);
|
|
190
|
+
*
|
|
191
|
+
* const result = await otp.verify({
|
|
192
|
+
* key: '+919812345678',
|
|
193
|
+
* code: '123456',
|
|
194
|
+
* purpose: 'login',
|
|
195
|
+
* });
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* @throws {OtpConfigError} if the options are invalid.
|
|
199
|
+
*/
|
|
200
|
+
declare function createOtpKit(options: OtpKitOptions): OtpKit;
|
|
201
|
+
|
|
202
|
+
interface MemoryStoreOptions {
|
|
203
|
+
/** Injectable clock. Intended for tests. */
|
|
204
|
+
now?: () => number;
|
|
205
|
+
/**
|
|
206
|
+
* Hard cap on retained entries. When exceeded, the soonest-expiring entries
|
|
207
|
+
* are dropped first. Prevents unbounded growth from codes that are issued
|
|
208
|
+
* and never verified. Default 100_000.
|
|
209
|
+
*/
|
|
210
|
+
maxEntries?: number;
|
|
211
|
+
/** Sweep interval for expired entries in ms. Default 60_000. 0 disables. */
|
|
212
|
+
sweepIntervalMs?: number;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Process-local store.
|
|
216
|
+
*
|
|
217
|
+
* Suitable for tests, CLIs, and single-instance apps. NOT suitable behind a
|
|
218
|
+
* load balancer: counters live in one process's heap, so N instances means N
|
|
219
|
+
* times your intended rate limit.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const otp = createOtpKit({ store: memoryStore(), pepper });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
declare function memoryStore(opts?: MemoryStoreOptions): OtpStore;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Thrown when the underlying store fails.
|
|
230
|
+
*
|
|
231
|
+
* otp-guard fails closed: a store outage means codes cannot be issued or
|
|
232
|
+
* verified, never that verification silently succeeds. Catch this at your
|
|
233
|
+
* route layer and return 503.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* try {
|
|
238
|
+
* await otp.verify({ key, code });
|
|
239
|
+
* } catch (err) {
|
|
240
|
+
* if (err instanceof OtpStoreError) return reply.status(503).send();
|
|
241
|
+
* throw err;
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
declare class OtpStoreError extends Error {
|
|
246
|
+
/** Which store operation failed, e.g. "acquire_cooldown". */
|
|
247
|
+
readonly operation: string;
|
|
248
|
+
constructor(operation: string, cause: unknown);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Thrown for caller mistakes: invalid configuration, malformed or oversized
|
|
252
|
+
* identifiers. Always a programming error, never a runtime condition to
|
|
253
|
+
* handle gracefully.
|
|
254
|
+
*/
|
|
255
|
+
declare class OtpConfigError extends Error {
|
|
256
|
+
constructor(message: string);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { type Alphabet, type Duration, type HookEvent, type Hooks, type IssueInput, type IssueResult, type MemoryStoreOptions, OtpConfigError, type OtpKit, type OtpKitOptions, OtpStore, OtpStoreError, type RateWindow, type VerifyFailure, type VerifyInput, type VerifyResult, createOtpKit, memoryStore };
|