@ekaone/nano-otp 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eka Prasetia <ekaone3033@gmail.com> (https://prasetia.me)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @ekaone/nano-otp
2
+
3
+ A tiny, zero-dependency One-Time Password (OTP) library for JavaScript and TypeScript.
4
+
5
+ **@ekaone/nano-otp** is designed to be lightweight, secure, and easy to integrate into modern applications. It uses Node.js built-in `crypto` for cryptographically secure random generation — no external dependencies required.
6
+
7
+ > ⚠️ **Status: Under Active Development**
8
+ > APIs and features may change before v1.0.0.
9
+ > Track progress at: https://github.com/ekaone/nano-otp
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # pnpm
17
+ pnpm add @ekaone/nano-otp
18
+
19
+ # npm
20
+ npm install @ekaone/nano-otp
21
+
22
+ # yarn
23
+ yarn add @ekaone/nano-otp
24
+
25
+ # bun
26
+ bun add @ekaone/nano-otp
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Usage
32
+
33
+ ### Generate an OTP
34
+
35
+ ```javascript
36
+ import { generate } from "@ekaone/nano-otp";
37
+
38
+ // default: 6-digit numeric OTP
39
+ const otp = generate();
40
+ console.log(otp);
41
+ // => "482910"
42
+
43
+ // custom length
44
+ const otp8 = generate({ length: 8 });
45
+ console.log(otp8);
46
+ // => "30571846"
47
+
48
+ // alpha charset (uppercase letters)
49
+ const otpAlpha = generate({ charset: "alpha" });
50
+ console.log(otpAlpha);
51
+ // => "KXRTBM"
52
+
53
+ // alphanumeric charset
54
+ const otpAlphaNum = generate({ charset: "alphanumeric", length: 8 });
55
+ console.log(otpAlphaNum);
56
+ // => "4BX9K2RZ"
57
+
58
+ // custom charset
59
+ const otpCustom = generate({ charset: "ABCDEF0123", length: 6 });
60
+ console.log(otpCustom);
61
+ // => "C3A0FB"
62
+ ```
63
+
64
+ ### Verify an OTP
65
+
66
+ ```javascript
67
+ import { verify } from "@ekaone/nano-otp";
68
+
69
+ // basic match
70
+ const isValid = verify({ input: "482910", code: "482910" });
71
+ console.log(isValid);
72
+ // => true
73
+
74
+ // mismatch
75
+ const isInvalid = verify({ input: "000000", code: "482910" });
76
+ console.log(isInvalid);
77
+ // => false
78
+
79
+ // with expiry (expiresAt is a Unix timestamp in milliseconds)
80
+ const isValidWithExpiry = verify({
81
+ input: "482910",
82
+ code: "482910",
83
+ expiresAt: Date.now() + 5 * 60 * 1000, // expires in 5 minutes
84
+ });
85
+ console.log(isValidWithExpiry);
86
+ // => true
87
+
88
+ // expired OTP
89
+ const isExpired = verify({
90
+ input: "482910",
91
+ code: "482910",
92
+ expiresAt: Date.now() - 1000, // already expired
93
+ });
94
+ console.log(isExpired);
95
+ // => false
96
+ ```
97
+
98
+ ### Generate a batch of OTPs
99
+
100
+ ```javascript
101
+ import { batch } from "@ekaone/nano-otp";
102
+
103
+ // generate 5 OTPs (may contain duplicates)
104
+ const otps = batch(5);
105
+ console.log(otps);
106
+ // => ["482910", "039471", "182930", "482910", "748201"]
107
+
108
+ // with options
109
+ const otpsAlpha = batch(3, { charset: "alpha", length: 8 });
110
+ console.log(otpsAlpha);
111
+ // => ["KXRTBMQZ", "LNVWACPD", "ERJHMKXB"]
112
+ ```
113
+
114
+ ### Generate a batch of unique OTPs
115
+
116
+ ```javascript
117
+ import { batchUnique } from "@ekaone/nano-otp";
118
+
119
+ // generate 5 guaranteed-unique OTPs
120
+ const uniqueOtps = batchUnique(5);
121
+ console.log(uniqueOtps);
122
+ // => ["482910", "039471", "182930", "748201", "910284"]
123
+ ```
124
+
125
+ ### Full workflow example
126
+
127
+ ```javascript
128
+ import { generate, verify } from "@ekaone/nano-otp";
129
+
130
+ // 1. generate and store OTP with a 10-minute expiry
131
+ const code = generate({ length: 6 });
132
+ const expiresAt = Date.now() + 10 * 60 * 1000;
133
+
134
+ console.log("Send this code to the user:", code);
135
+ // => "Send this code to the user: 482910"
136
+
137
+ // 2. later, verify what the user submitted
138
+ const userInput = "482910"; // submitted by user
139
+
140
+ const result = verify({ input: userInput, code, expiresAt });
141
+ console.log("Verification passed:", result);
142
+ // => "Verification passed: true"
143
+ ```
144
+
145
+ ---
146
+
147
+ ## API
148
+
149
+ ### `generate(options?)`
150
+
151
+ Generates a single cryptographically secure OTP.
152
+
153
+ ```ts
154
+ generate(options?: OTPOptions): string
155
+ ```
156
+
157
+ | Option | Type | Default | Description |
158
+ | --------- | ----------------------------------------- | ----------- | ---------------------------------------- |
159
+ | `length` | `number` | `6` | Length of the generated OTP |
160
+ | `charset` | `"numeric" \| "alpha" \| "alphanumeric" \| string` | `"numeric"` | Character set to use, or a custom string |
161
+
162
+ ---
163
+
164
+ ### `verify(options)`
165
+
166
+ Verifies an OTP using constant-time comparison to prevent timing attacks.
167
+
168
+ ```ts
169
+ verify(options: VerifyOptions): boolean
170
+ ```
171
+
172
+ | Option | Type | Required | Description |
173
+ | ----------- | -------- | -------- | -------------------------------------------------------- |
174
+ | `input` | `string` | ✅ | The OTP submitted by the user |
175
+ | `code` | `string` | ✅ | The original generated OTP to compare against |
176
+ | `expiresAt` | `number` | — | Unix timestamp (ms) after which the OTP is invalid |
177
+ | `now` | `number` | — | Override current time (ms) — useful for testing |
178
+
179
+ ---
180
+
181
+ ### `batch(count, options?)`
182
+
183
+ Generates multiple OTPs. May contain duplicates.
184
+
185
+ ```ts
186
+ batch(count: number, options?: OTPOptions): string[]
187
+ ```
188
+
189
+ | Parameter | Type | Description |
190
+ | --------- | ----------- | ---------------------------------- |
191
+ | `count` | `number` | Number of OTPs to generate |
192
+ | `options` | `OTPOptions`| Same options as `generate` |
193
+
194
+ ---
195
+
196
+ ### `batchUnique(count, options?)`
197
+
198
+ Generates multiple guaranteed-unique OTPs. Throws if the requested count exceeds the number of possible combinations for the given charset and length.
199
+
200
+ ```ts
201
+ batchUnique(count: number, options?: OTPOptions): string[]
202
+ ```
203
+
204
+ | Parameter | Type | Description |
205
+ | --------- | ------------ | ---------------------------------- |
206
+ | `count` | `number` | Number of unique OTPs to generate |
207
+ | `options` | `OTPOptions` | Same options as `generate` |
208
+
209
+ ---
210
+
211
+ ## Types
212
+
213
+ ```ts
214
+ interface OTPOptions {
215
+ length?: number;
216
+ charset?: "numeric" | "alpha" | "alphanumeric" | string;
217
+ }
218
+
219
+ interface VerifyOptions {
220
+ input: string;
221
+ code: string;
222
+ expiresAt?: number;
223
+ now?: number;
224
+ }
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Security
230
+
231
+ - Random generation uses Node.js `crypto.randomInt` — cryptographically secure, not `Math.random()`
232
+ - `verify` uses constant-time comparison (XOR-based) to prevent timing attacks
233
+ - Zero runtime dependencies — no supply chain risk from third-party packages
234
+
235
+ ---
236
+
237
+ ## License
238
+
239
+ MIT © Eka Prasetia — see [LICENSE](./LICENSE) for details.
240
+
241
+ ---
242
+
243
+ ## Links
244
+
245
+ - [NPM Package](https://www.npmjs.com/package/@ekaone/nano-otp)
246
+ - [GitHub Repository](https://github.com/ekaone/nano-otp)
247
+ - [Issue Tracker](https://github.com/ekaone/nano-otp/issues)
@@ -0,0 +1,35 @@
1
+ interface OTPOptions {
2
+ length?: number;
3
+ charset?: "numeric" | "alpha" | "alphanumeric" | string;
4
+ }
5
+ interface VerifyOptions {
6
+ input: string;
7
+ code: string;
8
+ expiresAt?: number;
9
+ now?: number;
10
+ }
11
+
12
+ declare function generate(options?: OTPOptions): string;
13
+
14
+ declare function batch(count: number, options?: OTPOptions): string[];
15
+
16
+ declare function batchUnique(count: number, options?: OTPOptions): string[];
17
+
18
+ declare function verify(options: VerifyOptions): boolean;
19
+
20
+ /**
21
+ * index.ts
22
+ * @description Main entry point for the nano-otp package
23
+ * @author Eka Prasetia
24
+ * @date 2024-06-01
25
+ * @version 1.0.0
26
+ */
27
+
28
+ declare const otp: {
29
+ generate: typeof generate;
30
+ batch: typeof batch;
31
+ batchUnique: typeof batchUnique;
32
+ verify: typeof verify;
33
+ };
34
+
35
+ export { type OTPOptions, type VerifyOptions, batch, batchUnique, generate, otp, verify };
@@ -0,0 +1,35 @@
1
+ interface OTPOptions {
2
+ length?: number;
3
+ charset?: "numeric" | "alpha" | "alphanumeric" | string;
4
+ }
5
+ interface VerifyOptions {
6
+ input: string;
7
+ code: string;
8
+ expiresAt?: number;
9
+ now?: number;
10
+ }
11
+
12
+ declare function generate(options?: OTPOptions): string;
13
+
14
+ declare function batch(count: number, options?: OTPOptions): string[];
15
+
16
+ declare function batchUnique(count: number, options?: OTPOptions): string[];
17
+
18
+ declare function verify(options: VerifyOptions): boolean;
19
+
20
+ /**
21
+ * index.ts
22
+ * @description Main entry point for the nano-otp package
23
+ * @author Eka Prasetia
24
+ * @date 2024-06-01
25
+ * @version 1.0.0
26
+ */
27
+
28
+ declare const otp: {
29
+ generate: typeof generate;
30
+ batch: typeof batch;
31
+ batchUnique: typeof batchUnique;
32
+ verify: typeof verify;
33
+ };
34
+
35
+ export { type OTPOptions, type VerifyOptions, batch, batchUnique, generate, otp, verify };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var m=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var P=(t,e)=>{for(var r in e)m(t,r,{get:e[r],enumerable:!0})},T=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of w(e))!x.call(t,o)&&o!==r&&m(t,o,{get:()=>e[o],enumerable:!(n=O(e,o))||n.enumerable});return t};var C=t=>T(m({},"__esModule",{value:!0}),t);var A={};P(A,{batch:()=>f,batchUnique:()=>u,generate:()=>s,otp:()=>d,verify:()=>h});module.exports=C(A);var g=require("crypto");var a="0123456789",l="ABCDEFGHIJKLMNOPQRSTUVWXYZ",E=a+l;function p(t={}){switch(t.charset){case"alpha":return l;case"alphanumeric":return E;case"numeric":return a;default:if(typeof t.charset=="string"){if(t.charset.length===0)throw new Error("charset must not be empty");return t.charset}return a}}function s(t={}){var o;let e=(o=t.length)!=null?o:6;if(!Number.isInteger(e)||e<1)throw new Error("length must be a positive integer");let r=p(t);if(r.length===0)throw new Error("charset must not be empty");let n="";for(let i=0;i<e;i++)n+=r[(0,g.randomInt)(0,r.length)];return n}function f(t,e={}){if(!Number.isInteger(t)||t<=0)throw new Error("batch count must be a positive integer");let r=[];for(let n=0;n<t;n++)r.push(s(e));return r}function y(t,e){if(t<1||e<1)return 0;let r=Math.pow(t,e);return Math.min(r,Number.MAX_SAFE_INTEGER)}function u(t,e={}){var c;if(!Number.isInteger(t)||t<=0)throw new Error("batchUnique count must be a positive integer");let r=(c=e.length)!=null?c:6,n=p(e),o=y(n.length,r);if(t>o)throw new Error(`Requested ${t} unique OTPs but only ${o} combinations possible with charset length ${n.length} and OTP length ${r}`);let i=new Set;for(;i.size<t;)i.add(s(e));return Array.from(i)}function b(t,e){if(t.length!==e.length)return!1;let r=0;for(let n=0;n<t.length;n++)r|=t.charCodeAt(n)^e.charCodeAt(n);return r===0}function h(t){let{input:e,code:r,expiresAt:n,now:o=Date.now()}=t;return!e||!r||n!==void 0&&o>n?!1:b(e,r)}var d={generate:s,batch:f,batchUnique:u,verify:h};0&&(module.exports={batch,batchUnique,generate,otp,verify});
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/core/generate.ts","../src/utils/resolveCharset.ts","../src/core/batch.ts","../src/utils/calculateMax.ts","../src/core/batchUnique.ts","../src/utils/safeCompare.ts","../src/core/verify.ts"],"sourcesContent":["/**\n * index.ts\n * @description Main entry point for the nano-otp package\n * @author Eka Prasetia\n * @date 2024-06-01\n * @version 1.0.0\n */\n\nimport { generate } from \"./core/generate\";\nimport { batch } from \"./core/batch\";\nimport { batchUnique } from \"./core/batchUnique\";\nimport { verify } from \"./core/verify\";\n\nexport const otp = {\n generate,\n batch,\n batchUnique,\n verify,\n};\n\nexport { generate, batch, batchUnique, verify };\nexport type { OTPOptions, VerifyOptions } from \"./types/otp.types\";\n","import { randomInt } from \"crypto\";\r\nimport type { OTPOptions } from \"../types/otp.types\";\r\nimport { resolveCharset } from \"../utils/resolveCharset\";\r\n\r\nexport function generate(options: OTPOptions = {}): string {\r\n const length = options.length ?? 6;\r\n\r\n if (!Number.isInteger(length) || length < 1) {\r\n throw new Error(\"length must be a positive integer\");\r\n }\r\n\r\n const charset = resolveCharset(options);\r\n\r\n if (charset.length === 0) {\r\n throw new Error(\"charset must not be empty\");\r\n }\r\n\r\n let result = \"\";\r\n\r\n for (let i = 0; i < length; i++) {\r\n result += charset[randomInt(0, charset.length)];\r\n }\r\n\r\n return result;\r\n}\r\n","import type { OTPOptions } from \"../types/otp.types\";\r\n\r\nconst NUMERIC = \"0123456789\";\r\nconst ALPHA = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\r\nconst ALPHANUMERIC = NUMERIC + ALPHA;\r\n\r\nexport function resolveCharset(options: OTPOptions = {}): string {\r\n switch (options.charset) {\r\n case \"alpha\":\r\n return ALPHA;\r\n case \"alphanumeric\":\r\n return ALPHANUMERIC;\r\n case \"numeric\":\r\n return NUMERIC;\r\n default:\r\n // custom charset string passed directly\r\n if (typeof options.charset === \"string\") {\r\n if (options.charset.length === 0) {\r\n throw new Error(\"charset must not be empty\");\r\n }\r\n return options.charset;\r\n }\r\n // undefined → fallback to numeric\r\n return NUMERIC;\r\n }\r\n}\r\n","import type { OTPOptions } from \"../types/otp.types\";\r\nimport { generate } from \"./generate\";\r\n\r\nexport function batch(count: number, options: OTPOptions = {}): string[] {\r\n if (!Number.isInteger(count) || count <= 0) {\r\n throw new Error(\"batch count must be a positive integer\");\r\n }\r\n\r\n const results: string[] = [];\r\n\r\n for (let i = 0; i < count; i++) {\r\n results.push(generate(options));\r\n }\r\n\r\n return results;\r\n}\r\n","export function calculateMaxCombinations(\r\n charsetLength: number,\r\n length: number,\r\n): number {\r\n if (charsetLength < 1 || length < 1) {\r\n return 0;\r\n }\r\n\r\n // Math.pow can exceed Number.MAX_SAFE_INTEGER for large inputs\r\n // (e.g. charsetLength=62, length=20 → ~7×10^35).\r\n // Cap at MAX_SAFE_INTEGER to keep comparisons in batchUnique meaningful.\r\n const result = Math.pow(charsetLength, length);\r\n return Math.min(result, Number.MAX_SAFE_INTEGER);\r\n}\r\n","import type { OTPOptions } from \"../types/otp.types\";\r\nimport { generate } from \"./generate\";\r\nimport { resolveCharset } from \"../utils/resolveCharset\";\r\nimport { calculateMaxCombinations } from \"../utils/calculateMax\";\r\n\r\nexport function batchUnique(count: number, options: OTPOptions = {}): string[] {\r\n if (!Number.isInteger(count) || count <= 0) {\r\n throw new Error(\"batchUnique count must be a positive integer\");\r\n }\r\n\r\n const length = options.length ?? 6;\r\n const charset = resolveCharset(options);\r\n const max = calculateMaxCombinations(charset.length, length);\r\n\r\n if (count > max) {\r\n throw new Error(\r\n `Requested ${count} unique OTPs but only ${max} combinations possible with charset length ${charset.length} and OTP length ${length}`,\r\n );\r\n }\r\n\r\n const results = new Set<string>();\r\n\r\n // Note: performance degrades as count approaches max combinations due to\r\n // increasing collision rate. Consider a shuffle-based approach for\r\n // high-density requests (count > max * 0.8).\r\n while (results.size < count) {\r\n results.add(generate(options));\r\n }\r\n\r\n return Array.from(results);\r\n}\r\n","/**\r\n * Constant-time string comparison to prevent timing attacks.\r\n *\r\n * Iterates the full length of both strings regardless of where\r\n * a mismatch occurs, so execution time does not reveal how many\r\n * characters matched. Note: length difference is detectable via\r\n * timing, but OTP length is typically public knowledge so this\r\n * is an acceptable tradeoff.\r\n */\r\nexport function safeCompare(a: string, b: string): boolean {\r\n if (a.length !== b.length) return false;\r\n\r\n let result = 0;\r\n\r\n for (let i = 0; i < a.length; i++) {\r\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\r\n }\r\n\r\n return result === 0;\r\n}\r\n","import type { VerifyOptions } from \"../types/otp.types\";\r\nimport { safeCompare } from \"../utils/safeCompare\";\r\n\r\nexport function verify(options: VerifyOptions): boolean {\r\n const { input, code, expiresAt, now = Date.now() } = options;\r\n\r\n if (!input || !code) {\r\n return false;\r\n }\r\n\r\n if (expiresAt !== undefined && now > expiresAt) {\r\n return false;\r\n }\r\n\r\n return safeCompare(input, code);\r\n}\r\n"],"mappings":"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","names":["index_exports","__export","batch","batchUnique","generate","otp","verify","__toCommonJS","import_crypto","NUMERIC","ALPHA","ALPHANUMERIC","resolveCharset","options","generate","options","_a","length","charset","resolveCharset","result","batch","count","options","results","i","generate","calculateMaxCombinations","charsetLength","length","result","batchUnique","count","options","_a","length","charset","resolveCharset","max","calculateMaxCombinations","results","generate","safeCompare","a","b","result","i","verify","options","input","code","expiresAt","now","safeCompare","otp","generate","batch","batchUnique","verify"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{randomInt as b}from"crypto";var m="0123456789",f="ABCDEFGHIJKLMNOPQRSTUVWXYZ",y=m+f;function p(t={}){switch(t.charset){case"alpha":return f;case"alphanumeric":return y;case"numeric":return m;default:if(typeof t.charset=="string"){if(t.charset.length===0)throw new Error("charset must not be empty");return t.charset}return m}}function i(t={}){var o;let e=(o=t.length)!=null?o:6;if(!Number.isInteger(e)||e<1)throw new Error("length must be a positive integer");let r=p(t);if(r.length===0)throw new Error("charset must not be empty");let n="";for(let s=0;s<e;s++)n+=r[b(0,r.length)];return n}function u(t,e={}){if(!Number.isInteger(t)||t<=0)throw new Error("batch count must be a positive integer");let r=[];for(let n=0;n<t;n++)r.push(i(e));return r}function h(t,e){if(t<1||e<1)return 0;let r=Math.pow(t,e);return Math.min(r,Number.MAX_SAFE_INTEGER)}function c(t,e={}){var a;if(!Number.isInteger(t)||t<=0)throw new Error("batchUnique count must be a positive integer");let r=(a=e.length)!=null?a:6,n=p(e),o=h(n.length,r);if(t>o)throw new Error(`Requested ${t} unique OTPs but only ${o} combinations possible with charset length ${n.length} and OTP length ${r}`);let s=new Set;for(;s.size<t;)s.add(i(e));return Array.from(s)}function l(t,e){if(t.length!==e.length)return!1;let r=0;for(let n=0;n<t.length;n++)r|=t.charCodeAt(n)^e.charCodeAt(n);return r===0}function g(t){let{input:e,code:r,expiresAt:n,now:o=Date.now()}=t;return!e||!r||n!==void 0&&o>n?!1:l(e,r)}var H={generate:i,batch:u,batchUnique:c,verify:g};export{u as batch,c as batchUnique,i as generate,H as otp,g as verify};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/generate.ts","../src/utils/resolveCharset.ts","../src/core/batch.ts","../src/utils/calculateMax.ts","../src/core/batchUnique.ts","../src/utils/safeCompare.ts","../src/core/verify.ts","../src/index.ts"],"sourcesContent":["import { randomInt } from \"crypto\";\r\nimport type { OTPOptions } from \"../types/otp.types\";\r\nimport { resolveCharset } from \"../utils/resolveCharset\";\r\n\r\nexport function generate(options: OTPOptions = {}): string {\r\n const length = options.length ?? 6;\r\n\r\n if (!Number.isInteger(length) || length < 1) {\r\n throw new Error(\"length must be a positive integer\");\r\n }\r\n\r\n const charset = resolveCharset(options);\r\n\r\n if (charset.length === 0) {\r\n throw new Error(\"charset must not be empty\");\r\n }\r\n\r\n let result = \"\";\r\n\r\n for (let i = 0; i < length; i++) {\r\n result += charset[randomInt(0, charset.length)];\r\n }\r\n\r\n return result;\r\n}\r\n","import type { OTPOptions } from \"../types/otp.types\";\r\n\r\nconst NUMERIC = \"0123456789\";\r\nconst ALPHA = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\r\nconst ALPHANUMERIC = NUMERIC + ALPHA;\r\n\r\nexport function resolveCharset(options: OTPOptions = {}): string {\r\n switch (options.charset) {\r\n case \"alpha\":\r\n return ALPHA;\r\n case \"alphanumeric\":\r\n return ALPHANUMERIC;\r\n case \"numeric\":\r\n return NUMERIC;\r\n default:\r\n // custom charset string passed directly\r\n if (typeof options.charset === \"string\") {\r\n if (options.charset.length === 0) {\r\n throw new Error(\"charset must not be empty\");\r\n }\r\n return options.charset;\r\n }\r\n // undefined → fallback to numeric\r\n return NUMERIC;\r\n }\r\n}\r\n","import type { OTPOptions } from \"../types/otp.types\";\r\nimport { generate } from \"./generate\";\r\n\r\nexport function batch(count: number, options: OTPOptions = {}): string[] {\r\n if (!Number.isInteger(count) || count <= 0) {\r\n throw new Error(\"batch count must be a positive integer\");\r\n }\r\n\r\n const results: string[] = [];\r\n\r\n for (let i = 0; i < count; i++) {\r\n results.push(generate(options));\r\n }\r\n\r\n return results;\r\n}\r\n","export function calculateMaxCombinations(\r\n charsetLength: number,\r\n length: number,\r\n): number {\r\n if (charsetLength < 1 || length < 1) {\r\n return 0;\r\n }\r\n\r\n // Math.pow can exceed Number.MAX_SAFE_INTEGER for large inputs\r\n // (e.g. charsetLength=62, length=20 → ~7×10^35).\r\n // Cap at MAX_SAFE_INTEGER to keep comparisons in batchUnique meaningful.\r\n const result = Math.pow(charsetLength, length);\r\n return Math.min(result, Number.MAX_SAFE_INTEGER);\r\n}\r\n","import type { OTPOptions } from \"../types/otp.types\";\r\nimport { generate } from \"./generate\";\r\nimport { resolveCharset } from \"../utils/resolveCharset\";\r\nimport { calculateMaxCombinations } from \"../utils/calculateMax\";\r\n\r\nexport function batchUnique(count: number, options: OTPOptions = {}): string[] {\r\n if (!Number.isInteger(count) || count <= 0) {\r\n throw new Error(\"batchUnique count must be a positive integer\");\r\n }\r\n\r\n const length = options.length ?? 6;\r\n const charset = resolveCharset(options);\r\n const max = calculateMaxCombinations(charset.length, length);\r\n\r\n if (count > max) {\r\n throw new Error(\r\n `Requested ${count} unique OTPs but only ${max} combinations possible with charset length ${charset.length} and OTP length ${length}`,\r\n );\r\n }\r\n\r\n const results = new Set<string>();\r\n\r\n // Note: performance degrades as count approaches max combinations due to\r\n // increasing collision rate. Consider a shuffle-based approach for\r\n // high-density requests (count > max * 0.8).\r\n while (results.size < count) {\r\n results.add(generate(options));\r\n }\r\n\r\n return Array.from(results);\r\n}\r\n","/**\r\n * Constant-time string comparison to prevent timing attacks.\r\n *\r\n * Iterates the full length of both strings regardless of where\r\n * a mismatch occurs, so execution time does not reveal how many\r\n * characters matched. Note: length difference is detectable via\r\n * timing, but OTP length is typically public knowledge so this\r\n * is an acceptable tradeoff.\r\n */\r\nexport function safeCompare(a: string, b: string): boolean {\r\n if (a.length !== b.length) return false;\r\n\r\n let result = 0;\r\n\r\n for (let i = 0; i < a.length; i++) {\r\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\r\n }\r\n\r\n return result === 0;\r\n}\r\n","import type { VerifyOptions } from \"../types/otp.types\";\r\nimport { safeCompare } from \"../utils/safeCompare\";\r\n\r\nexport function verify(options: VerifyOptions): boolean {\r\n const { input, code, expiresAt, now = Date.now() } = options;\r\n\r\n if (!input || !code) {\r\n return false;\r\n }\r\n\r\n if (expiresAt !== undefined && now > expiresAt) {\r\n return false;\r\n }\r\n\r\n return safeCompare(input, code);\r\n}\r\n","/**\n * index.ts\n * @description Main entry point for the nano-otp package\n * @author Eka Prasetia\n * @date 2024-06-01\n * @version 1.0.0\n */\n\nimport { generate } from \"./core/generate\";\nimport { batch } from \"./core/batch\";\nimport { batchUnique } from \"./core/batchUnique\";\nimport { verify } from \"./core/verify\";\n\nexport const otp = {\n generate,\n batch,\n batchUnique,\n verify,\n};\n\nexport { generate, batch, batchUnique, verify };\nexport type { OTPOptions, VerifyOptions } from \"./types/otp.types\";\n"],"mappings":"AAAA,OAAS,aAAAA,MAAiB,SCE1B,IAAMC,EAAU,aACVC,EAAQ,6BACRC,EAAeF,EAAUC,EAExB,SAASE,EAAeC,EAAsB,CAAC,EAAW,CAC/D,OAAQA,EAAQ,QAAS,CACvB,IAAK,QACH,OAAOH,EACT,IAAK,eACH,OAAOC,EACT,IAAK,UACH,OAAOF,EACT,QAEE,GAAI,OAAOI,EAAQ,SAAY,SAAU,CACvC,GAAIA,EAAQ,QAAQ,SAAW,EAC7B,MAAM,IAAI,MAAM,2BAA2B,EAE7C,OAAOA,EAAQ,OACjB,CAEA,OAAOJ,CACX,CACF,CDrBO,SAASK,EAASC,EAAsB,CAAC,EAAW,CAJ3D,IAAAC,EAKE,IAAMC,GAASD,EAAAD,EAAQ,SAAR,KAAAC,EAAkB,EAEjC,GAAI,CAAC,OAAO,UAAUC,CAAM,GAAKA,EAAS,EACxC,MAAM,IAAI,MAAM,mCAAmC,EAGrD,IAAMC,EAAUC,EAAeJ,CAAO,EAEtC,GAAIG,EAAQ,SAAW,EACrB,MAAM,IAAI,MAAM,2BAA2B,EAG7C,IAAIE,EAAS,GAEb,QAASC,EAAI,EAAGA,EAAIJ,EAAQI,IAC1BD,GAAUF,EAAQI,EAAU,EAAGJ,EAAQ,MAAM,CAAC,EAGhD,OAAOE,CACT,CErBO,SAASG,EAAMC,EAAeC,EAAsB,CAAC,EAAa,CACvE,GAAI,CAAC,OAAO,UAAUD,CAAK,GAAKA,GAAS,EACvC,MAAM,IAAI,MAAM,wCAAwC,EAG1D,IAAME,EAAoB,CAAC,EAE3B,QAASC,EAAI,EAAGA,EAAIH,EAAOG,IACzBD,EAAQ,KAAKE,EAASH,CAAO,CAAC,EAGhC,OAAOC,CACT,CCfO,SAASG,EACdC,EACAC,EACQ,CACR,GAAID,EAAgB,GAAKC,EAAS,EAChC,MAAO,GAMT,IAAMC,EAAS,KAAK,IAAIF,EAAeC,CAAM,EAC7C,OAAO,KAAK,IAAIC,EAAQ,OAAO,gBAAgB,CACjD,CCRO,SAASC,EAAYC,EAAeC,EAAsB,CAAC,EAAa,CAL/E,IAAAC,EAME,GAAI,CAAC,OAAO,UAAUF,CAAK,GAAKA,GAAS,EACvC,MAAM,IAAI,MAAM,8CAA8C,EAGhE,IAAMG,GAASD,EAAAD,EAAQ,SAAR,KAAAC,EAAkB,EAC3BE,EAAUC,EAAeJ,CAAO,EAChCK,EAAMC,EAAyBH,EAAQ,OAAQD,CAAM,EAE3D,GAAIH,EAAQM,EACV,MAAM,IAAI,MACR,aAAaN,CAAK,yBAAyBM,CAAG,8CAA8CF,EAAQ,MAAM,mBAAmBD,CAAM,EACrI,EAGF,IAAMK,EAAU,IAAI,IAKpB,KAAOA,EAAQ,KAAOR,GACpBQ,EAAQ,IAAIC,EAASR,CAAO,CAAC,EAG/B,OAAO,MAAM,KAAKO,CAAO,CAC3B,CCrBO,SAASE,EAAYC,EAAWC,EAAoB,CACzD,GAAID,EAAE,SAAWC,EAAE,OAAQ,MAAO,GAElC,IAAIC,EAAS,EAEb,QAASC,EAAI,EAAGA,EAAIH,EAAE,OAAQG,IAC5BD,GAAUF,EAAE,WAAWG,CAAC,EAAIF,EAAE,WAAWE,CAAC,EAG5C,OAAOD,IAAW,CACpB,CChBO,SAASE,EAAOC,EAAiC,CACtD,GAAM,CAAE,MAAAC,EAAO,KAAAC,EAAM,UAAAC,EAAW,IAAAC,EAAM,KAAK,IAAI,CAAE,EAAIJ,EAMrD,MAJI,CAACC,GAAS,CAACC,GAIXC,IAAc,QAAaC,EAAMD,EAC5B,GAGFE,EAAYJ,EAAOC,CAAI,CAChC,CCFO,IAAMI,EAAM,CACjB,SAAAC,EACA,MAAAC,EACA,YAAAC,EACA,OAAAC,CACF","names":["randomInt","NUMERIC","ALPHA","ALPHANUMERIC","resolveCharset","options","generate","options","_a","length","charset","resolveCharset","result","i","randomInt","batch","count","options","results","i","generate","calculateMaxCombinations","charsetLength","length","result","batchUnique","count","options","_a","length","charset","resolveCharset","max","calculateMaxCombinations","results","generate","safeCompare","a","b","result","i","verify","options","input","code","expiresAt","now","safeCompare","otp","generate","batch","batchUnique","verify"]}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@ekaone/nano-otp",
3
+ "version": "0.0.1",
4
+ "description": "A lightweight, zero-dependency TypeScript library for generating and validating OTPs",
5
+ "keywords": [
6
+ "otp",
7
+ "two-factor authentication",
8
+ "2fa",
9
+ "typescript",
10
+ "library",
11
+ "privacy",
12
+ "security",
13
+ "otp-generation",
14
+ "data-protection",
15
+ "typescript",
16
+ "library",
17
+ "privacy",
18
+ "security",
19
+ "data-protection"
20
+ ],
21
+ "author": {
22
+ "name": "Eka Prasetia",
23
+ "email": "ekaone3033@gmail.com",
24
+ "url": "https://prasetia.me"
25
+ },
26
+ "license": "MIT",
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.mjs",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.mjs",
38
+ "require": "./dist/index.js"
39
+ }
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/ekaone/nano-otp.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/ekaone/nano-otp/issues"
53
+ },
54
+ "homepage": "https://github.com/ekaone/nano-otp#readme",
55
+ "devDependencies": {
56
+ "@types/node": "^25.1.0",
57
+ "@vitest/coverage-v8": "^4.0.18",
58
+ "@vitest/ui": "^4.0.18",
59
+ "rimraf": "^6.0.0",
60
+ "tsup": "^8.5.1",
61
+ "typescript": "^5.7.0",
62
+ "vitest": "^4.0.18"
63
+ },
64
+ "scripts": {
65
+ "build": "tsup",
66
+ "dev": "tsup --watch",
67
+ "clean": "rimraf dist",
68
+ "typecheck": "tsc --noEmit",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "test:ui": "vitest --ui",
72
+ "test:coverage": "vitest run --coverage"
73
+ }
74
+ }