@smsdora/otp 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 +138 -0
- package/dist/index.cjs +361 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +323 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var SmsdoraOtpError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
status;
|
|
8
|
+
requestId;
|
|
9
|
+
constructor(message, ctx) {
|
|
10
|
+
super(message, ctx.cause !== void 0 ? { cause: ctx.cause } : void 0);
|
|
11
|
+
this.name = new.target.name;
|
|
12
|
+
this.code = ctx.code;
|
|
13
|
+
this.status = ctx.status;
|
|
14
|
+
this.requestId = ctx.requestId;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var ConfigError = class extends SmsdoraOtpError {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message, { code: "config_error" });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var AuthError = class extends SmsdoraOtpError {
|
|
23
|
+
constructor(message, ctx = {}) {
|
|
24
|
+
super(message, { ...ctx, code: "auth_error" });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var ValidationError = class extends SmsdoraOtpError {
|
|
28
|
+
constructor(message, ctx = {}) {
|
|
29
|
+
super(message, { ...ctx, code: "validation_error" });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var NotFoundError = class extends SmsdoraOtpError {
|
|
33
|
+
constructor(message, ctx = {}) {
|
|
34
|
+
super(message, { ...ctx, code: "not_found" });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var RateLimitError = class extends SmsdoraOtpError {
|
|
38
|
+
retryAfterSeconds;
|
|
39
|
+
constructor(message, ctx = {}) {
|
|
40
|
+
super(message, { ...ctx, code: "rate_limited" });
|
|
41
|
+
this.retryAfterSeconds = ctx.retryAfterSeconds;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var DeliveryError = class extends SmsdoraOtpError {
|
|
45
|
+
constructor(message, ctx = {}) {
|
|
46
|
+
super(message, { ...ctx, code: "delivery_failed" });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var ServerError = class extends SmsdoraOtpError {
|
|
50
|
+
constructor(message, ctx = {}) {
|
|
51
|
+
super(message, { ...ctx, code: "server_error" });
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var NetworkError = class extends SmsdoraOtpError {
|
|
55
|
+
constructor(message, ctx = {}) {
|
|
56
|
+
super(message, { ...ctx, code: "network_error" });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var TokenVerificationError = class extends SmsdoraOtpError {
|
|
60
|
+
constructor(message, ctx = {}) {
|
|
61
|
+
super(message, { ...ctx, code: "token_invalid" });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/http.ts
|
|
66
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
67
|
+
var HttpClient = class {
|
|
68
|
+
constructor(config) {
|
|
69
|
+
this.config = config;
|
|
70
|
+
}
|
|
71
|
+
config;
|
|
72
|
+
async request(method, path, options = {}) {
|
|
73
|
+
const url = `${this.config.baseUrl.replace(/\/+$/, "")}${path}`;
|
|
74
|
+
const headers = {
|
|
75
|
+
"x-api-key": this.config.apiKey,
|
|
76
|
+
accept: "application/json"
|
|
77
|
+
};
|
|
78
|
+
let bodyStr;
|
|
79
|
+
if (options.body !== void 0) {
|
|
80
|
+
headers["content-type"] = "application/json";
|
|
81
|
+
bodyStr = JSON.stringify(options.body);
|
|
82
|
+
}
|
|
83
|
+
if (options.idempotencyKey) {
|
|
84
|
+
headers["idempotency-key"] = options.idempotencyKey;
|
|
85
|
+
}
|
|
86
|
+
let lastError;
|
|
87
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt += 1) {
|
|
88
|
+
if (attempt > 0) {
|
|
89
|
+
await delay(this.backoffMs(attempt, lastError));
|
|
90
|
+
}
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await this.fetchWithTimeout(url, method, headers, bodyStr);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
lastError = new NetworkError(
|
|
96
|
+
err instanceof Error && err.name === "AbortError" ? `Request timed out after ${this.config.timeoutMs}ms` : `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
97
|
+
{ cause: err }
|
|
98
|
+
);
|
|
99
|
+
if (attempt < this.config.maxRetries) continue;
|
|
100
|
+
throw lastError;
|
|
101
|
+
}
|
|
102
|
+
const envelope = await this.parseBody(response);
|
|
103
|
+
if (response.ok) {
|
|
104
|
+
return envelope?.data ?? envelope;
|
|
105
|
+
}
|
|
106
|
+
const error = this.toError(response, envelope);
|
|
107
|
+
if (RETRYABLE_STATUSES.has(response.status) && attempt < this.config.maxRetries) {
|
|
108
|
+
lastError = error;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
throw lastError ?? new NetworkError("Request failed");
|
|
114
|
+
}
|
|
115
|
+
async fetchWithTimeout(url, method, headers, body) {
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
118
|
+
try {
|
|
119
|
+
return await this.config.fetchImpl(url, {
|
|
120
|
+
method,
|
|
121
|
+
headers,
|
|
122
|
+
body,
|
|
123
|
+
signal: controller.signal
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async parseBody(response) {
|
|
130
|
+
const text = await response.text();
|
|
131
|
+
if (!text) return null;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(text);
|
|
134
|
+
} catch {
|
|
135
|
+
return { message: text };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
toError(response, envelope) {
|
|
139
|
+
const message = normalizeMessage(envelope) ?? `HTTP ${response.status}`;
|
|
140
|
+
const ctx = {
|
|
141
|
+
status: response.status,
|
|
142
|
+
requestId: response.headers.get("x-request-id") ?? void 0
|
|
143
|
+
};
|
|
144
|
+
switch (response.status) {
|
|
145
|
+
case 400:
|
|
146
|
+
return new ValidationError(message, ctx);
|
|
147
|
+
case 401:
|
|
148
|
+
case 403:
|
|
149
|
+
return new AuthError(message, ctx);
|
|
150
|
+
case 404:
|
|
151
|
+
return new NotFoundError(message, ctx);
|
|
152
|
+
case 429:
|
|
153
|
+
return new RateLimitError(message, {
|
|
154
|
+
...ctx,
|
|
155
|
+
retryAfterSeconds: envelope?.retryAfterSeconds ?? parseRetryAfter(response.headers.get("retry-after"))
|
|
156
|
+
});
|
|
157
|
+
case 502:
|
|
158
|
+
return new DeliveryError(message, ctx);
|
|
159
|
+
default:
|
|
160
|
+
return new ServerError(message, ctx);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
backoffMs(attempt, lastError) {
|
|
164
|
+
if (lastError instanceof RateLimitError && lastError.retryAfterSeconds) {
|
|
165
|
+
return Math.min(lastError.retryAfterSeconds * 1e3, 1e4);
|
|
166
|
+
}
|
|
167
|
+
const base = 200 * 2 ** (attempt - 1);
|
|
168
|
+
return Math.round(base * (0.5 + Math.random() * 0.5));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function normalizeMessage(envelope) {
|
|
172
|
+
if (!envelope?.message) return void 0;
|
|
173
|
+
return Array.isArray(envelope.message) ? envelope.message.join("; ") : envelope.message;
|
|
174
|
+
}
|
|
175
|
+
function parseRetryAfter(header) {
|
|
176
|
+
if (!header) return void 0;
|
|
177
|
+
const seconds = Number(header);
|
|
178
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
179
|
+
}
|
|
180
|
+
function delay(ms) {
|
|
181
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/token.ts
|
|
185
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
186
|
+
function base64UrlToBuffer(input) {
|
|
187
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
188
|
+
const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
|
|
189
|
+
return Buffer.from(padded + pad, "base64");
|
|
190
|
+
}
|
|
191
|
+
function verifyVerificationToken(token, secret) {
|
|
192
|
+
const parts = token.split(".");
|
|
193
|
+
if (parts.length !== 3) {
|
|
194
|
+
throw new TokenVerificationError("Malformed token");
|
|
195
|
+
}
|
|
196
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
197
|
+
let header;
|
|
198
|
+
try {
|
|
199
|
+
header = JSON.parse(base64UrlToBuffer(headerB64).toString("utf8"));
|
|
200
|
+
} catch {
|
|
201
|
+
throw new TokenVerificationError("Malformed token header");
|
|
202
|
+
}
|
|
203
|
+
if (header.alg !== "HS256") {
|
|
204
|
+
throw new TokenVerificationError(
|
|
205
|
+
`Unsupported token algorithm: ${header.alg ?? "unknown"}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const expected = createHmac("sha256", secret).update(`${headerB64}.${payloadB64}`).digest();
|
|
209
|
+
const actual = base64UrlToBuffer(signatureB64);
|
|
210
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
211
|
+
throw new TokenVerificationError("Invalid token signature");
|
|
212
|
+
}
|
|
213
|
+
let payload;
|
|
214
|
+
try {
|
|
215
|
+
payload = JSON.parse(base64UrlToBuffer(payloadB64).toString("utf8"));
|
|
216
|
+
} catch {
|
|
217
|
+
throw new TokenVerificationError("Malformed token payload");
|
|
218
|
+
}
|
|
219
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
220
|
+
if (typeof payload.exp === "number" && now >= payload.exp) {
|
|
221
|
+
throw new TokenVerificationError("Token has expired");
|
|
222
|
+
}
|
|
223
|
+
const nbf = payload.nbf;
|
|
224
|
+
if (typeof nbf === "number" && now < nbf) {
|
|
225
|
+
throw new TokenVerificationError("Token is not yet valid");
|
|
226
|
+
}
|
|
227
|
+
return payload;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/client.ts
|
|
231
|
+
var SmsdoraOtp = class {
|
|
232
|
+
http;
|
|
233
|
+
tokenSecret;
|
|
234
|
+
constructor(options) {
|
|
235
|
+
if (!options?.apiKey) {
|
|
236
|
+
throw new ConfigError("`apiKey` is required");
|
|
237
|
+
}
|
|
238
|
+
if (!options.baseUrl) {
|
|
239
|
+
throw new ConfigError("`baseUrl` is required");
|
|
240
|
+
}
|
|
241
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
242
|
+
if (typeof fetchImpl !== "function") {
|
|
243
|
+
throw new ConfigError(
|
|
244
|
+
"No global `fetch` available. Use Node >= 18, or pass a `fetch` implementation."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
this.tokenSecret = options.tokenSecret;
|
|
248
|
+
this.http = new HttpClient({
|
|
249
|
+
apiKey: options.apiKey,
|
|
250
|
+
baseUrl: options.baseUrl,
|
|
251
|
+
timeoutMs: options.timeoutMs ?? 1e4,
|
|
252
|
+
maxRetries: options.maxRetries ?? 2,
|
|
253
|
+
fetchImpl
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Send a one-time passcode to a phone number. */
|
|
257
|
+
async send(params) {
|
|
258
|
+
const { idempotencyKey, ...rest } = params;
|
|
259
|
+
return this.http.request("POST", "/otp/send", {
|
|
260
|
+
body: rest,
|
|
261
|
+
idempotencyKey: idempotencyKey ?? randomUUID()
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/** Re-deliver the code for an existing challenge (subject to cooldown/cap). */
|
|
265
|
+
async resend(params) {
|
|
266
|
+
return this.http.request("POST", "/otp/resend", {
|
|
267
|
+
body: params
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/** Verify a code. Returns a result; does NOT throw on a wrong code. */
|
|
271
|
+
async verify(params) {
|
|
272
|
+
return this.http.request("POST", "/otp/verify", {
|
|
273
|
+
body: params
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/** Fetch the current status of a challenge. */
|
|
277
|
+
async getStatus(challengeId) {
|
|
278
|
+
return this.http.request(
|
|
279
|
+
"GET",
|
|
280
|
+
`/otp/${encodeURIComponent(challengeId)}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Validate a verification token locally (no network). Requires `tokenSecret`
|
|
285
|
+
* in the client options. Use this in your auth flow to trust a verification
|
|
286
|
+
* without an extra round-trip.
|
|
287
|
+
*
|
|
288
|
+
* @throws {ConfigError} if no `tokenSecret` was configured.
|
|
289
|
+
* @throws {TokenVerificationError} if the token is invalid/expired.
|
|
290
|
+
*/
|
|
291
|
+
verifyToken(token) {
|
|
292
|
+
if (!this.tokenSecret) {
|
|
293
|
+
throw new ConfigError(
|
|
294
|
+
"`tokenSecret` must be set to verify tokens locally. Pass it in the client options, or use verifyTokenRemote()."
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return verifyVerificationToken(token, this.tokenSecret);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Validate a verification token on the server with single-use enforcement.
|
|
301
|
+
* Marks the token consumed so it cannot be replayed.
|
|
302
|
+
*/
|
|
303
|
+
async verifyTokenRemote(token) {
|
|
304
|
+
return this.http.request("POST", "/otp/verify-token", {
|
|
305
|
+
body: { token }
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
export {
|
|
310
|
+
AuthError,
|
|
311
|
+
ConfigError,
|
|
312
|
+
DeliveryError,
|
|
313
|
+
NetworkError,
|
|
314
|
+
NotFoundError,
|
|
315
|
+
RateLimitError,
|
|
316
|
+
ServerError,
|
|
317
|
+
SmsdoraOtp,
|
|
318
|
+
SmsdoraOtpError,
|
|
319
|
+
TokenVerificationError,
|
|
320
|
+
ValidationError,
|
|
321
|
+
verifyVerificationToken
|
|
322
|
+
};
|
|
323
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/errors.ts","../src/http.ts","../src/token.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { HttpClient } from './http.js';\nimport { ConfigError } from './errors.js';\nimport { verifyVerificationToken } from './token.js';\nimport type {\n OtpChallenge,\n ResendOtpParams,\n SendOtpParams,\n SmsdoraOtpOptions,\n TokenCheckResult,\n VerificationTokenPayload,\n VerifyOtpParams,\n VerifyResult,\n} from './types.js';\n\n/**\n * Client for the SmsDora OTP API.\n *\n * ```ts\n * const otp = new SmsdoraOtp({ apiKey: process.env.SMSDORA_KEY!, baseUrl });\n * const challenge = await otp.send({ recipient: '+14155550100', purpose: 'login' });\n * const result = await otp.verify({ challengeId: challenge.id, code });\n * if (result.verified) { ... }\n * ```\n */\nexport class SmsdoraOtp {\n private readonly http: HttpClient;\n private readonly tokenSecret?: string;\n\n constructor(options: SmsdoraOtpOptions) {\n if (!options?.apiKey) {\n throw new ConfigError('`apiKey` is required');\n }\n if (!options.baseUrl) {\n throw new ConfigError('`baseUrl` is required');\n }\n\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new ConfigError(\n 'No global `fetch` available. Use Node >= 18, or pass a `fetch` implementation.',\n );\n }\n\n this.tokenSecret = options.tokenSecret;\n this.http = new HttpClient({\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n timeoutMs: options.timeoutMs ?? 10_000,\n maxRetries: options.maxRetries ?? 2,\n fetchImpl,\n });\n }\n\n /** Send a one-time passcode to a phone number. */\n async send(params: SendOtpParams): Promise<OtpChallenge> {\n const { idempotencyKey, ...rest } = params;\n return this.http.request<OtpChallenge>('POST', '/otp/send', {\n body: rest,\n idempotencyKey: idempotencyKey ?? randomUUID(),\n });\n }\n\n /** Re-deliver the code for an existing challenge (subject to cooldown/cap). */\n async resend(params: ResendOtpParams): Promise<OtpChallenge> {\n return this.http.request<OtpChallenge>('POST', '/otp/resend', {\n body: params,\n });\n }\n\n /** Verify a code. Returns a result; does NOT throw on a wrong code. */\n async verify(params: VerifyOtpParams): Promise<VerifyResult> {\n return this.http.request<VerifyResult>('POST', '/otp/verify', {\n body: params,\n });\n }\n\n /** Fetch the current status of a challenge. */\n async getStatus(challengeId: string): Promise<OtpChallenge> {\n return this.http.request<OtpChallenge>(\n 'GET',\n `/otp/${encodeURIComponent(challengeId)}`,\n );\n }\n\n /**\n * Validate a verification token locally (no network). Requires `tokenSecret`\n * in the client options. Use this in your auth flow to trust a verification\n * without an extra round-trip.\n *\n * @throws {ConfigError} if no `tokenSecret` was configured.\n * @throws {TokenVerificationError} if the token is invalid/expired.\n */\n verifyToken(token: string): VerificationTokenPayload {\n if (!this.tokenSecret) {\n throw new ConfigError(\n '`tokenSecret` must be set to verify tokens locally. ' +\n 'Pass it in the client options, or use verifyTokenRemote().',\n );\n }\n return verifyVerificationToken(token, this.tokenSecret);\n }\n\n /**\n * Validate a verification token on the server with single-use enforcement.\n * Marks the token consumed so it cannot be replayed.\n */\n async verifyTokenRemote(token: string): Promise<TokenCheckResult> {\n return this.http.request<TokenCheckResult>('POST', '/otp/verify-token', {\n body: { token },\n });\n }\n}\n","/** Stable error codes — safe to branch on across SDK versions. */\nexport type SmsdoraOtpErrorCode =\n | 'config_error'\n | 'auth_error'\n | 'validation_error'\n | 'not_found'\n | 'rate_limited'\n | 'delivery_failed'\n | 'server_error'\n | 'network_error'\n | 'token_invalid';\n\nexport interface SmsdoraOtpErrorContext {\n code: SmsdoraOtpErrorCode;\n /** HTTP status, when the error originated from a response. */\n status?: number;\n /** Server request id, if provided. */\n requestId?: string;\n /** Underlying cause (e.g. a fetch error). */\n cause?: unknown;\n}\n\n/** Base class for every error the SDK throws. */\nexport class SmsdoraOtpError extends Error {\n readonly code: SmsdoraOtpErrorCode;\n readonly status?: number;\n readonly requestId?: string;\n\n constructor(message: string, ctx: SmsdoraOtpErrorContext) {\n super(message, ctx.cause !== undefined ? { cause: ctx.cause } : undefined);\n this.name = new.target.name;\n this.code = ctx.code;\n this.status = ctx.status;\n this.requestId = ctx.requestId;\n }\n}\n\n/** Missing/invalid client configuration (no apiKey, no tokenSecret, …). */\nexport class ConfigError extends SmsdoraOtpError {\n constructor(message: string) {\n super(message, { code: 'config_error' });\n }\n}\n\n/** 401 / 403 — bad or unauthorized API key, scope, or publishable restriction. */\nexport class AuthError extends SmsdoraOtpError {\n constructor(message: string, ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {}) {\n super(message, { ...ctx, code: 'auth_error' });\n }\n}\n\n/** 400 — invalid request parameters. */\nexport class ValidationError extends SmsdoraOtpError {\n constructor(message: string, ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {}) {\n super(message, { ...ctx, code: 'validation_error' });\n }\n}\n\n/** 404 — challenge/key not found. */\nexport class NotFoundError extends SmsdoraOtpError {\n constructor(message: string, ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {}) {\n super(message, { ...ctx, code: 'not_found' });\n }\n}\n\n/** 429 — a rate/abuse cap was hit. Inspect {@link retryAfterSeconds}. */\nexport class RateLimitError extends SmsdoraOtpError {\n readonly retryAfterSeconds?: number;\n constructor(\n message: string,\n ctx: Omit<SmsdoraOtpErrorContext, 'code'> & { retryAfterSeconds?: number } = {},\n ) {\n super(message, { ...ctx, code: 'rate_limited' });\n this.retryAfterSeconds = ctx.retryAfterSeconds;\n }\n}\n\n/** 502 — the code could not be delivered (no online device / FCM failure). */\nexport class DeliveryError extends SmsdoraOtpError {\n constructor(message: string, ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {}) {\n super(message, { ...ctx, code: 'delivery_failed' });\n }\n}\n\n/** 5xx — an unexpected server error. */\nexport class ServerError extends SmsdoraOtpError {\n constructor(message: string, ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {}) {\n super(message, { ...ctx, code: 'server_error' });\n }\n}\n\n/** Transport failure — connection refused, DNS, timeout, aborted. */\nexport class NetworkError extends SmsdoraOtpError {\n constructor(message: string, ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {}) {\n super(message, { ...ctx, code: 'network_error' });\n }\n}\n\n/** A verification token failed local or remote validation. */\nexport class TokenVerificationError extends SmsdoraOtpError {\n constructor(message: string, ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {}) {\n super(message, { ...ctx, code: 'token_invalid' });\n }\n}\n","import {\n AuthError,\n DeliveryError,\n NetworkError,\n NotFoundError,\n RateLimitError,\n ServerError,\n ValidationError,\n type SmsdoraOtpErrorContext,\n} from './errors.js';\n\nexport interface HttpClientConfig {\n apiKey: string;\n baseUrl: string;\n timeoutMs: number;\n maxRetries: number;\n fetchImpl: typeof fetch;\n}\n\ninterface RequestOptions {\n body?: unknown;\n idempotencyKey?: string;\n}\n\ninterface ApiEnvelope<T> {\n statusCode?: number;\n message?: string | string[];\n data?: T;\n retryAfterSeconds?: number;\n error?: string;\n}\n\nconst RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** Thin fetch wrapper: auth headers, timeout, retry with backoff, error mapping. */\nexport class HttpClient {\n constructor(private readonly config: HttpClientConfig) {}\n\n async request<T>(\n method: string,\n path: string,\n options: RequestOptions = {},\n ): Promise<T> {\n const url = `${this.config.baseUrl.replace(/\\/+$/, '')}${path}`;\n const headers: Record<string, string> = {\n 'x-api-key': this.config.apiKey,\n accept: 'application/json',\n };\n let bodyStr: string | undefined;\n if (options.body !== undefined) {\n headers['content-type'] = 'application/json';\n bodyStr = JSON.stringify(options.body);\n }\n if (options.idempotencyKey) {\n headers['idempotency-key'] = options.idempotencyKey;\n }\n\n let lastError: unknown;\n for (let attempt = 0; attempt <= this.config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n await delay(this.backoffMs(attempt, lastError));\n }\n\n let response: Response;\n try {\n response = await this.fetchWithTimeout(url, method, headers, bodyStr);\n } catch (err) {\n lastError = new NetworkError(\n err instanceof Error && err.name === 'AbortError'\n ? `Request timed out after ${this.config.timeoutMs}ms`\n : `Network request failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n { cause: err },\n );\n if (attempt < this.config.maxRetries) continue;\n throw lastError;\n }\n\n const envelope = await this.parseBody<T>(response);\n\n if (response.ok) {\n return (envelope?.data ?? (envelope as unknown as T)) as T;\n }\n\n const error = this.toError(response, envelope);\n if (RETRYABLE_STATUSES.has(response.status) && attempt < this.config.maxRetries) {\n lastError = error;\n continue;\n }\n throw error;\n }\n\n // Unreachable in practice — the loop always returns or throws.\n throw lastError ?? new NetworkError('Request failed');\n }\n\n private async fetchWithTimeout(\n url: string,\n method: string,\n headers: Record<string, string>,\n body: string | undefined,\n ): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);\n try {\n return await this.config.fetchImpl(url, {\n method,\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async parseBody<T>(response: Response): Promise<ApiEnvelope<T> | null> {\n const text = await response.text();\n if (!text) return null;\n try {\n return JSON.parse(text) as ApiEnvelope<T>;\n } catch {\n return { message: text };\n }\n }\n\n private toError<T>(response: Response, envelope: ApiEnvelope<T> | null) {\n const message = normalizeMessage(envelope) ?? `HTTP ${response.status}`;\n const ctx: Omit<SmsdoraOtpErrorContext, 'code'> = {\n status: response.status,\n requestId: response.headers.get('x-request-id') ?? undefined,\n };\n\n switch (response.status) {\n case 400:\n return new ValidationError(message, ctx);\n case 401:\n case 403:\n return new AuthError(message, ctx);\n case 404:\n return new NotFoundError(message, ctx);\n case 429:\n return new RateLimitError(message, {\n ...ctx,\n retryAfterSeconds:\n envelope?.retryAfterSeconds ??\n parseRetryAfter(response.headers.get('retry-after')),\n });\n case 502:\n return new DeliveryError(message, ctx);\n default:\n return new ServerError(message, ctx);\n }\n }\n\n private backoffMs(attempt: number, lastError: unknown): number {\n // Honor an explicit retry-after on rate limits (capped at 10s).\n if (lastError instanceof RateLimitError && lastError.retryAfterSeconds) {\n return Math.min(lastError.retryAfterSeconds * 1000, 10_000);\n }\n // Exponential backoff with full jitter: ~200ms, 400ms, 800ms…\n const base = 200 * 2 ** (attempt - 1);\n return Math.round(base * (0.5 + Math.random() * 0.5));\n }\n}\n\nfunction normalizeMessage<T>(envelope: ApiEnvelope<T> | null): string | undefined {\n if (!envelope?.message) return undefined;\n return Array.isArray(envelope.message)\n ? envelope.message.join('; ')\n : envelope.message;\n}\n\nfunction parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n return Number.isFinite(seconds) ? seconds : undefined;\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport { TokenVerificationError } from './errors.js';\nimport type { VerificationTokenPayload } from './types.js';\n\nfunction base64UrlToBuffer(input: string): Buffer {\n const padded = input.replace(/-/g, '+').replace(/_/g, '/');\n const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));\n return Buffer.from(padded + pad, 'base64');\n}\n\n/**\n * Verify a SmsDora verification token (HS256 JWT) locally — no network call.\n * Checks the signature against `secret` and enforces `exp` / `nbf`.\n *\n * @throws {TokenVerificationError} if malformed, wrong algorithm, bad\n * signature, or expired.\n */\nexport function verifyVerificationToken(\n token: string,\n secret: string,\n): VerificationTokenPayload {\n const parts = token.split('.');\n if (parts.length !== 3) {\n throw new TokenVerificationError('Malformed token');\n }\n const [headerB64, payloadB64, signatureB64] = parts as [\n string,\n string,\n string,\n ];\n\n let header: { alg?: string; typ?: string };\n try {\n header = JSON.parse(base64UrlToBuffer(headerB64).toString('utf8'));\n } catch {\n throw new TokenVerificationError('Malformed token header');\n }\n\n if (header.alg !== 'HS256') {\n throw new TokenVerificationError(\n `Unsupported token algorithm: ${header.alg ?? 'unknown'}`,\n );\n }\n\n const expected = createHmac('sha256', secret)\n .update(`${headerB64}.${payloadB64}`)\n .digest();\n const actual = base64UrlToBuffer(signatureB64);\n\n if (\n expected.length !== actual.length ||\n !timingSafeEqual(expected, actual)\n ) {\n throw new TokenVerificationError('Invalid token signature');\n }\n\n let payload: VerificationTokenPayload;\n try {\n payload = JSON.parse(base64UrlToBuffer(payloadB64).toString('utf8'));\n } catch {\n throw new TokenVerificationError('Malformed token payload');\n }\n\n const now = Math.floor(Date.now() / 1000);\n if (typeof payload.exp === 'number' && now >= payload.exp) {\n throw new TokenVerificationError('Token has expired');\n }\n const nbf = (payload as { nbf?: number }).nbf;\n if (typeof nbf === 'number' && now < nbf) {\n throw new TokenVerificationError('Token is not yet valid');\n }\n\n return payload;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smsdora/otp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official SmsDora OTP authentication SDK for Node.js — send and verify one-time passcodes over SMS.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.11.0",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.4.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"smsdora",
|
|
41
|
+
"otp",
|
|
42
|
+
"2fa",
|
|
43
|
+
"sms",
|
|
44
|
+
"authentication",
|
|
45
|
+
"passwordless",
|
|
46
|
+
"one-time-passcode",
|
|
47
|
+
"verification"
|
|
48
|
+
]
|
|
49
|
+
}
|