quickotp 2.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 ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 donginssam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # QuickOTP
2
+
3
+ [![NPM](https://img.shields.io/npm/v/quickotp.svg)](https://npmjs.org/package/quickotp)
4
+ [![NPM Downloads](https://img.shields.io/npm/dm/quickotp.svg)](https://npmjs.org/package/quickotp)
5
+ [![License](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/donginssam/quickotp/blob/master/LICENSE)
6
+
7
+ A lightweight OTP library for Node.js with zero runtime dependencies.
8
+ Generates and verifies TOTP/HOTP tokens compatible with Google Authenticator and similar apps.
9
+
10
+ ## Requirements
11
+
12
+ Node.js >= 18
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ npm install quickotp
18
+ # or
19
+ pnpm add quickotp
20
+ # or
21
+ yarn add quickotp
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### ESM
27
+
28
+ ```js
29
+ import { TOTP, HOTP } from "quickotp";
30
+ ```
31
+
32
+ ### CommonJS
33
+
34
+ ```js
35
+ const { TOTP, HOTP } = require("quickotp");
36
+ ```
37
+
38
+ ---
39
+
40
+ ### TOTP (Time-based OTP)
41
+
42
+ ```js
43
+ // Generate an otpauth:// URI to register with an authenticator app
44
+ const uri = TOTP.create("secretkey", "MyApp");
45
+ // → 'otpauth://totp/MyApp?secret=ONSWG4TFORQC...'
46
+
47
+ // Verify a token entered by the user
48
+ const valid = TOTP.verify("secretkey", "123456");
49
+ // → true or false
50
+ ```
51
+
52
+ ### HOTP (HMAC-based Counter OTP)
53
+
54
+ ```js
55
+ const uri = HOTP.create("secretkey", "MyApp");
56
+ // → 'otpauth://hotp/MyApp?secret=ONSWG4TFORQC...'
57
+
58
+ const valid = HOTP.verify("secretkey", "123456", 0); // counter must be a non-negative safe integer
59
+ // → true or false
60
+ ```
61
+
62
+ ### QR Code generation (optional)
63
+
64
+ The `.qrcode()` method requires the `qrcode` package to be installed separately:
65
+
66
+ ```sh
67
+ npm install qrcode
68
+ ```
69
+
70
+ ```js
71
+ const uri = TOTP.create("secretkey", "MyApp");
72
+ const dataUrl = await TOTP.qrcode(uri); // data:image/png;base64,...
73
+ ```
74
+
75
+ ---
76
+
77
+ ## API
78
+
79
+ ### `TOTP.create(key, label): string`
80
+
81
+ Returns an `otpauth://totp/` URI with the base32-encoded secret. Throws `TypeError` if `key` is empty.
82
+
83
+ ### `TOTP.verify(key, token, window?): boolean`
84
+
85
+ Verifies a TOTP token against the current time. `window` controls how many 30-second steps in either direction are accepted (default: `1`).
86
+ Throws `TypeError` or `RangeError` if `window` is not a non-negative integer.
87
+
88
+ ### `TOTP.qrcode(uri): Promise<string>`
89
+
90
+ Returns the URI encoded as a PNG data URL. Requires `qrcode` to be installed.
91
+
92
+ ### `HOTP.create(key, label): string`
93
+
94
+ Returns an `otpauth://hotp/` URI with the base32-encoded secret. Throws `TypeError` if `key` is empty.
95
+
96
+ ### `HOTP.verify(key, token, counter): boolean`
97
+
98
+ Verifies an HOTP token against the given counter value.
99
+ Throws `TypeError` or `RangeError` if `counter` is not a non-negative safe integer.
100
+
101
+ ### `HOTP.qrcode(uri): Promise<string>`
102
+
103
+ Returns the URI encoded as a PNG data URL. Requires `qrcode` to be installed.
104
+
105
+ ---
106
+
107
+ ### Author: [Dongin Lee](https://github.com/donginssam)
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var d=Object.create;var i=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var h=Object.getPrototypeOf,l=Object.prototype.hasOwnProperty;var P=(t,e)=>{for(var r in e)i(t,r,{get:e[r],enumerable:!0})},a=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of b(e))!l.call(t,n)&&n!==r&&i(t,n,{get:()=>e[n],enumerable:!(o=y(e,n))||o.enumerable});return t};var I=(t,e,r)=>(r=t!=null?d(h(t)):{},a(e||!t||!t.__esModule?i(r,"default",{value:t,enumerable:!0}):r,t)),w=t=>a(i({},"__esModule",{value:!0}),t);var x={};P(x,{HOTP:()=>E,TOTP:()=>q});module.exports=w(x);var f=require("crypto"),c="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",T=6,A=30;function N(t){let e="",r=0,o=0;for(let n of Buffer.from(t,"utf8"))for(o=o<<8|n,r+=8;r>=5;)e+=c[o>>>(r-=5)&31];return r>0&&(e+=c[o<<5-r&31]),e}function O(t){if(typeof t!="string"||t.length===0)throw new TypeError("key must be a non-empty string")}function p(t,e,r){if(typeof t!="number"||Number.isNaN(t))throw new TypeError(`${e} must be a number`);if(t<0||!(r?Number.isSafeInteger(t):Number.isInteger(t)))throw new RangeError(`${e} must be a non-negative${r?" safe":""} integer`)}function u(t,e,r){O(e);let o=new URLSearchParams({secret:N(e)});return`otpauth://${t}/${encodeURIComponent(r)}?${o}`}async function g(t){let e;try{e=await import("qrcode")}catch{throw new Error('qrcode is not installed. Run "pnpm add qrcode" to use this feature.')}return e.toDataURL(t)}function m(t,e,r=T){p(e,"counter",!0);let o=Buffer.alloc(8);o.writeBigUInt64BE(BigInt(e));let n=(0,f.createHmac)("sha1",t).update(o).digest(),s=n[n.length-1]&15;return(((n[s]&127)<<24|(n[s+1]&255)<<16|(n[s+2]&255)<<8|n[s+3]&255)%10**r).toString().padStart(r,"0")}var q={create:(t,e)=>u("totp",t,e),qrcode:g,verify(t,e,r=1){p(r,"window");let o=Math.floor(Date.now()/1e3/A);for(let n=-r;n<=r;n++)if(o+n>=0&&m(t,o+n)===e)return!0;return!1}},E={create:(t,e)=>u("hotp",t,e),qrcode:g,verify:(t,e,r)=>m(t,r)===e};0&&(module.exports={HOTP,TOTP});
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createHmac } from \"node:crypto\"\n\nconst base32Chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\"\nconst defaultDigits = 6\nconst totpPeriodSeconds = 30\n\ntype OtpType = \"totp\" | \"hotp\"\n\n/**\n * Shared methods for both {@link TotpAPI} and {@link HotpAPI}.\n */\ntype OtpAPI = {\n /**\n * Generates an `otpauth://` URI to register the secret with an authenticator app.\n * @param key - The shared secret (plain text; encoded to Base32 internally).\n * @param label - Account identifier shown in the authenticator (e.g. `\"alice@example.com\"`).\n * @returns An `otpauth://` URI string.\n * @throws {TypeError} If `key` is empty.\n */\n create(key: string, label: string): string\n\n /**\n * Converts an `otpauth://` URI into a Base64-encoded PNG data URL (QR code).\n * Requires the optional `qrcode` package (`pnpm add qrcode`).\n * @param uri - An `otpauth://` URI returned by `create`.\n * @returns A `data:image/png;base64,...` string ready for use in an `<img>` tag.\n * @throws {Error} If the `qrcode` package is not installed.\n * @example\n * const dataUrl = await TOTP.qrcode(uri)\n * // \"data:image/png;base64,...\"\n */\n qrcode(uri: string): Promise<string>\n}\n\n/**\n * API for Time-based One-Time Password (TOTP, RFC 6238).\n * Tokens rotate every 30 seconds based on the current time.\n */\ntype TotpAPI = OtpAPI & {\n /**\n * Checks whether a token is valid for the current time window.\n * @param key - The shared secret used when the URI was created.\n * @param token - The 6-digit token entered by the user.\n * @param window - Number of 30-second periods to accept on either side of the current period (default `1`).\n * @returns `true` if the token matches any counter in the allowed window.\n * @throws {TypeError} If `window` is not a number.\n * @throws {RangeError} If `window` is not a non-negative integer.\n * @example\n * const ok = TOTP.verify(\"mysecret\", \"123456\")\n * const lenient = TOTP.verify(\"mysecret\", \"123456\", 2) // ±2 periods\n */\n verify(key: string, token: string, window?: number): boolean\n}\n\n/**\n * API for HMAC-based One-Time Password (HOTP, RFC 4226).\n * Tokens are derived from an incrementing counter rather than the clock.\n */\ntype HotpAPI = OtpAPI & {\n /**\n * Checks whether a token is valid for a specific counter value.\n * @param key - The shared secret used when the URI was created.\n * @param token - The 6-digit token to verify.\n * @param counter - The counter value that was used to generate the token.\n * @returns `true` if the token matches the given counter.\n * @throws {TypeError} If `counter` is not a number.\n * @throws {RangeError} If `counter` is not a non-negative safe integer.\n * @example\n * const ok = HOTP.verify(\"mysecret\", \"123456\", 42)\n */\n verify(key: string, token: string, counter: number): boolean\n}\n\n/** Encodes a UTF-8 string to Base32 (RFC 4648) without padding. */\nfunction base32Encode(input: string): string {\n let result = \"\", bits = 0, value = 0\n for (const byte of Buffer.from(input, \"utf8\")) {\n value = (value << 8) | byte\n bits += 8\n while (bits >= 5) result += base32Chars[(value >>> (bits -= 5)) & 31]\n }\n if (bits > 0) result += base32Chars[(value << (5 - bits)) & 31]\n return result\n}\n\nfunction assertNonEmptyKey(key: string): void {\n if (typeof key !== \"string\" || key.length === 0)\n throw new TypeError(\"key must be a non-empty string\")\n}\n\nfunction assertNonNegativeInt(value: number, name: string, safe?: boolean): void {\n if (typeof value !== \"number\" || Number.isNaN(value))\n throw new TypeError(`${name} must be a number`)\n if (value < 0 || !(safe ? Number.isSafeInteger(value) : Number.isInteger(value)))\n throw new RangeError(`${name} must be a non-negative${safe ? \" safe\" : \"\"} integer`)\n}\n\nfunction createOtpAuthUri(type: OtpType, key: string, label: string): string {\n assertNonEmptyKey(key)\n const searchParams = new URLSearchParams({ secret: base32Encode(key) })\n return `otpauth://${type}/${encodeURIComponent(label)}?${searchParams}`\n}\n\nasync function createQrCode(uri: string): Promise<string> {\n let qrCode: typeof import(\"qrcode\")\n try {\n qrCode = (await import(\"qrcode\")) as typeof import(\"qrcode\")\n } catch {\n throw new Error(\n 'qrcode is not installed. Run \"pnpm add qrcode\" to use this feature.',\n )\n }\n return qrCode.toDataURL(uri)\n}\n\n/**\n * Computes an HOTP value per RFC 4226 §5.\n * @param key - Plain-text secret used as the HMAC-SHA1 key.\n * @param counter - 8-byte big-endian counter value.\n * @param digits - Number of digits in the output (default 6).\n */\nfunction computeHotp(key: string, counter: number, digits = defaultDigits): string {\n assertNonNegativeInt(counter, \"counter\", true)\n const buf = Buffer.alloc(8)\n buf.writeBigUInt64BE(BigInt(counter))\n const digest = createHmac(\"sha1\", key).update(buf).digest()\n const offset = digest[digest.length - 1] & 0x0f\n const code = ((digest[offset] & 0x7f) << 24) | ((digest[offset + 1] & 0xff) << 16) | ((digest[offset + 2] & 0xff) << 8) | (digest[offset + 3] & 0xff)\n return (code % 10 ** digits).toString().padStart(digits, \"0\")\n}\n\n/**\n * Time-based One-Time Password helpers (RFC 6238).\n * @example\n * import { TOTP } from \"quickotp\"\n * const uri = TOTP.create(\"mysecret\", \"alice@example.com\")\n * const ok = TOTP.verify(\"mysecret\", userInput)\n */\nconst totp: TotpAPI = {\n create: (key, label) => createOtpAuthUri(\"totp\", key, label),\n qrcode: createQrCode,\n verify(key: string, token: string, window = 1): boolean {\n assertNonNegativeInt(window, \"window\")\n const counter = Math.floor(Date.now() / 1000 / totpPeriodSeconds)\n for (let i = -window; i <= window; i++)\n if (counter + i >= 0 && computeHotp(key, counter + i) === token) return true\n return false\n },\n}\n\n/**\n * HMAC-based One-Time Password helpers (RFC 4226).\n * @example\n * import { HOTP } from \"quickotp\"\n * const uri = HOTP.create(\"mysecret\", \"alice@example.com\")\n * const ok = HOTP.verify(\"mysecret\", userInput, counter)\n */\nconst hotp: HotpAPI = {\n create: (key, label) => createOtpAuthUri(\"hotp\", key, label),\n qrcode: createQrCode,\n verify: (key, token, counter) => computeHotp(key, counter) === token,\n}\n\nexport { totp as TOTP, hotp as HOTP }\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,UAAAE,EAAA,SAAAC,IAAA,eAAAC,EAAAJ,GAAA,IAAAK,EAA2B,kBAErBC,EAAc,mCACdC,EAAgB,EAChBC,EAAoB,GAsE1B,SAASC,EAAaC,EAAuB,CAC3C,IAAIC,EAAS,GAAIC,EAAO,EAAGC,EAAQ,EACnC,QAAWC,KAAQ,OAAO,KAAKJ,EAAO,MAAM,EAG1C,IAFAG,EAASA,GAAS,EAAKC,EACvBF,GAAQ,EACDA,GAAQ,GAAGD,GAAUL,EAAaO,KAAWD,GAAQ,GAAM,EAAE,EAEtE,OAAIA,EAAO,IAAGD,GAAUL,EAAaO,GAAU,EAAID,EAAS,EAAE,GACvDD,CACT,CAEA,SAASI,EAAkBC,EAAmB,CAC5C,GAAI,OAAOA,GAAQ,UAAYA,EAAI,SAAW,EAC5C,MAAM,IAAI,UAAU,gCAAgC,CACxD,CAEA,SAASC,EAAqBJ,EAAeK,EAAcC,EAAsB,CAC/E,GAAI,OAAON,GAAU,UAAY,OAAO,MAAMA,CAAK,EACjD,MAAM,IAAI,UAAU,GAAGK,CAAI,mBAAmB,EAChD,GAAIL,EAAQ,GAAK,EAAEM,EAAO,OAAO,cAAcN,CAAK,EAAI,OAAO,UAAUA,CAAK,GAC5E,MAAM,IAAI,WAAW,GAAGK,CAAI,0BAA0BC,EAAO,QAAU,EAAE,UAAU,CACvF,CAEA,SAASC,EAAiBC,EAAeL,EAAaM,EAAuB,CAC3EP,EAAkBC,CAAG,EACrB,IAAMO,EAAe,IAAI,gBAAgB,CAAE,OAAQd,EAAaO,CAAG,CAAE,CAAC,EACtE,MAAO,aAAaK,CAAI,IAAI,mBAAmBC,CAAK,CAAC,IAAIC,CAAY,EACvE,CAEA,eAAeC,EAAaC,EAA8B,CACxD,IAAIC,EACJ,GAAI,CACFA,EAAU,KAAM,QAAO,QAAQ,CACjC,MAAQ,CACN,MAAM,IAAI,MACR,qEACF,CACF,CACA,OAAOA,EAAO,UAAUD,CAAG,CAC7B,CAQA,SAASE,EAAYX,EAAaY,EAAiBC,EAAStB,EAAuB,CACjFU,EAAqBW,EAAS,UAAW,EAAI,EAC7C,IAAME,EAAM,OAAO,MAAM,CAAC,EAC1BA,EAAI,iBAAiB,OAAOF,CAAO,CAAC,EACpC,IAAMG,KAAS,cAAW,OAAQf,CAAG,EAAE,OAAOc,CAAG,EAAE,OAAO,EACpDE,EAASD,EAAOA,EAAO,OAAS,CAAC,EAAI,GAE3C,SADeA,EAAOC,CAAM,EAAI,MAAS,IAAQD,EAAOC,EAAS,CAAC,EAAI,MAAS,IAAQD,EAAOC,EAAS,CAAC,EAAI,MAAS,EAAMD,EAAOC,EAAS,CAAC,EAAI,KACjI,IAAMH,GAAQ,SAAS,EAAE,SAASA,EAAQ,GAAG,CAC9D,CASA,IAAM1B,EAAgB,CACpB,OAAQ,CAACa,EAAKM,IAAUF,EAAiB,OAAQJ,EAAKM,CAAK,EAC3D,OAAQE,EACR,OAAOR,EAAaiB,EAAeC,EAAS,EAAY,CACtDjB,EAAqBiB,EAAQ,QAAQ,EACrC,IAAMN,EAAU,KAAK,MAAM,KAAK,IAAI,EAAI,IAAOpB,CAAiB,EAChE,QAAS2B,EAAI,CAACD,EAAQC,GAAKD,EAAQC,IACjC,GAAIP,EAAUO,GAAK,GAAKR,EAAYX,EAAKY,EAAUO,CAAC,IAAMF,EAAO,MAAO,GAC1E,MAAO,EACT,CACF,EASM/B,EAAgB,CACpB,OAAQ,CAACc,EAAKM,IAAUF,EAAiB,OAAQJ,EAAKM,CAAK,EAC3D,OAAQE,EACR,OAAQ,CAACR,EAAKiB,EAAOL,IAAYD,EAAYX,EAAKY,CAAO,IAAMK,CACjE","names":["index_exports","__export","hotp","totp","__toCommonJS","import_node_crypto","base32Chars","defaultDigits","totpPeriodSeconds","base32Encode","input","result","bits","value","byte","assertNonEmptyKey","key","assertNonNegativeInt","name","safe","createOtpAuthUri","type","label","searchParams","createQrCode","uri","qrCode","computeHotp","counter","digits","buf","digest","offset","token","window","i"]}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shared methods for both {@link TotpAPI} and {@link HotpAPI}.
3
+ */
4
+ type OtpAPI = {
5
+ /**
6
+ * Generates an `otpauth://` URI to register the secret with an authenticator app.
7
+ * @param key - The shared secret (plain text; encoded to Base32 internally).
8
+ * @param label - Account identifier shown in the authenticator (e.g. `"alice@example.com"`).
9
+ * @returns An `otpauth://` URI string.
10
+ * @throws {TypeError} If `key` is empty.
11
+ */
12
+ create(key: string, label: string): string;
13
+ /**
14
+ * Converts an `otpauth://` URI into a Base64-encoded PNG data URL (QR code).
15
+ * Requires the optional `qrcode` package (`pnpm add qrcode`).
16
+ * @param uri - An `otpauth://` URI returned by `create`.
17
+ * @returns A `data:image/png;base64,...` string ready for use in an `<img>` tag.
18
+ * @throws {Error} If the `qrcode` package is not installed.
19
+ * @example
20
+ * const dataUrl = await TOTP.qrcode(uri)
21
+ * // "data:image/png;base64,..."
22
+ */
23
+ qrcode(uri: string): Promise<string>;
24
+ };
25
+ /**
26
+ * API for Time-based One-Time Password (TOTP, RFC 6238).
27
+ * Tokens rotate every 30 seconds based on the current time.
28
+ */
29
+ type TotpAPI = OtpAPI & {
30
+ /**
31
+ * Checks whether a token is valid for the current time window.
32
+ * @param key - The shared secret used when the URI was created.
33
+ * @param token - The 6-digit token entered by the user.
34
+ * @param window - Number of 30-second periods to accept on either side of the current period (default `1`).
35
+ * @returns `true` if the token matches any counter in the allowed window.
36
+ * @throws {TypeError} If `window` is not a number.
37
+ * @throws {RangeError} If `window` is not a non-negative integer.
38
+ * @example
39
+ * const ok = TOTP.verify("mysecret", "123456")
40
+ * const lenient = TOTP.verify("mysecret", "123456", 2) // ±2 periods
41
+ */
42
+ verify(key: string, token: string, window?: number): boolean;
43
+ };
44
+ /**
45
+ * API for HMAC-based One-Time Password (HOTP, RFC 4226).
46
+ * Tokens are derived from an incrementing counter rather than the clock.
47
+ */
48
+ type HotpAPI = OtpAPI & {
49
+ /**
50
+ * Checks whether a token is valid for a specific counter value.
51
+ * @param key - The shared secret used when the URI was created.
52
+ * @param token - The 6-digit token to verify.
53
+ * @param counter - The counter value that was used to generate the token.
54
+ * @returns `true` if the token matches the given counter.
55
+ * @throws {TypeError} If `counter` is not a number.
56
+ * @throws {RangeError} If `counter` is not a non-negative safe integer.
57
+ * @example
58
+ * const ok = HOTP.verify("mysecret", "123456", 42)
59
+ */
60
+ verify(key: string, token: string, counter: number): boolean;
61
+ };
62
+ /**
63
+ * Time-based One-Time Password helpers (RFC 6238).
64
+ * @example
65
+ * import { TOTP } from "quickotp"
66
+ * const uri = TOTP.create("mysecret", "alice@example.com")
67
+ * const ok = TOTP.verify("mysecret", userInput)
68
+ */
69
+ declare const totp: TotpAPI;
70
+ /**
71
+ * HMAC-based One-Time Password helpers (RFC 4226).
72
+ * @example
73
+ * import { HOTP } from "quickotp"
74
+ * const uri = HOTP.create("mysecret", "alice@example.com")
75
+ * const ok = HOTP.verify("mysecret", userInput, counter)
76
+ */
77
+ declare const hotp: HotpAPI;
78
+
79
+ export { hotp as HOTP, totp as TOTP };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shared methods for both {@link TotpAPI} and {@link HotpAPI}.
3
+ */
4
+ type OtpAPI = {
5
+ /**
6
+ * Generates an `otpauth://` URI to register the secret with an authenticator app.
7
+ * @param key - The shared secret (plain text; encoded to Base32 internally).
8
+ * @param label - Account identifier shown in the authenticator (e.g. `"alice@example.com"`).
9
+ * @returns An `otpauth://` URI string.
10
+ * @throws {TypeError} If `key` is empty.
11
+ */
12
+ create(key: string, label: string): string;
13
+ /**
14
+ * Converts an `otpauth://` URI into a Base64-encoded PNG data URL (QR code).
15
+ * Requires the optional `qrcode` package (`pnpm add qrcode`).
16
+ * @param uri - An `otpauth://` URI returned by `create`.
17
+ * @returns A `data:image/png;base64,...` string ready for use in an `<img>` tag.
18
+ * @throws {Error} If the `qrcode` package is not installed.
19
+ * @example
20
+ * const dataUrl = await TOTP.qrcode(uri)
21
+ * // "data:image/png;base64,..."
22
+ */
23
+ qrcode(uri: string): Promise<string>;
24
+ };
25
+ /**
26
+ * API for Time-based One-Time Password (TOTP, RFC 6238).
27
+ * Tokens rotate every 30 seconds based on the current time.
28
+ */
29
+ type TotpAPI = OtpAPI & {
30
+ /**
31
+ * Checks whether a token is valid for the current time window.
32
+ * @param key - The shared secret used when the URI was created.
33
+ * @param token - The 6-digit token entered by the user.
34
+ * @param window - Number of 30-second periods to accept on either side of the current period (default `1`).
35
+ * @returns `true` if the token matches any counter in the allowed window.
36
+ * @throws {TypeError} If `window` is not a number.
37
+ * @throws {RangeError} If `window` is not a non-negative integer.
38
+ * @example
39
+ * const ok = TOTP.verify("mysecret", "123456")
40
+ * const lenient = TOTP.verify("mysecret", "123456", 2) // ±2 periods
41
+ */
42
+ verify(key: string, token: string, window?: number): boolean;
43
+ };
44
+ /**
45
+ * API for HMAC-based One-Time Password (HOTP, RFC 4226).
46
+ * Tokens are derived from an incrementing counter rather than the clock.
47
+ */
48
+ type HotpAPI = OtpAPI & {
49
+ /**
50
+ * Checks whether a token is valid for a specific counter value.
51
+ * @param key - The shared secret used when the URI was created.
52
+ * @param token - The 6-digit token to verify.
53
+ * @param counter - The counter value that was used to generate the token.
54
+ * @returns `true` if the token matches the given counter.
55
+ * @throws {TypeError} If `counter` is not a number.
56
+ * @throws {RangeError} If `counter` is not a non-negative safe integer.
57
+ * @example
58
+ * const ok = HOTP.verify("mysecret", "123456", 42)
59
+ */
60
+ verify(key: string, token: string, counter: number): boolean;
61
+ };
62
+ /**
63
+ * Time-based One-Time Password helpers (RFC 6238).
64
+ * @example
65
+ * import { TOTP } from "quickotp"
66
+ * const uri = TOTP.create("mysecret", "alice@example.com")
67
+ * const ok = TOTP.verify("mysecret", userInput)
68
+ */
69
+ declare const totp: TotpAPI;
70
+ /**
71
+ * HMAC-based One-Time Password helpers (RFC 4226).
72
+ * @example
73
+ * import { HOTP } from "quickotp"
74
+ * const uri = HOTP.create("mysecret", "alice@example.com")
75
+ * const ok = HOTP.verify("mysecret", userInput, counter)
76
+ */
77
+ declare const hotp: HotpAPI;
78
+
79
+ export { hotp as HOTP, totp as TOTP };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{createHmac as u}from"crypto";var i="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",g=6,m=30;function d(t){let e="",r=0,o=0;for(let n of Buffer.from(t,"utf8"))for(o=o<<8|n,r+=8;r>=5;)e+=i[o>>>(r-=5)&31];return r>0&&(e+=i[o<<5-r&31]),e}function y(t){if(typeof t!="string"||t.length===0)throw new TypeError("key must be a non-empty string")}function a(t,e,r){if(typeof t!="number"||Number.isNaN(t))throw new TypeError(`${e} must be a number`);if(t<0||!(r?Number.isSafeInteger(t):Number.isInteger(t)))throw new RangeError(`${e} must be a non-negative${r?" safe":""} integer`)}function c(t,e,r){y(e);let o=new URLSearchParams({secret:d(e)});return`otpauth://${t}/${encodeURIComponent(r)}?${o}`}async function f(t){let e;try{e=await import("qrcode")}catch{throw new Error('qrcode is not installed. Run "pnpm add qrcode" to use this feature.')}return e.toDataURL(t)}function p(t,e,r=g){a(e,"counter",!0);let o=Buffer.alloc(8);o.writeBigUInt64BE(BigInt(e));let n=u("sha1",t).update(o).digest(),s=n[n.length-1]&15;return(((n[s]&127)<<24|(n[s+1]&255)<<16|(n[s+2]&255)<<8|n[s+3]&255)%10**r).toString().padStart(r,"0")}var l={create:(t,e)=>c("totp",t,e),qrcode:f,verify(t,e,r=1){a(r,"window");let o=Math.floor(Date.now()/1e3/m);for(let n=-r;n<=r;n++)if(o+n>=0&&p(t,o+n)===e)return!0;return!1}},P={create:(t,e)=>c("hotp",t,e),qrcode:f,verify:(t,e,r)=>p(t,r)===e};export{P as HOTP,l as TOTP};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createHmac } from \"node:crypto\"\n\nconst base32Chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\"\nconst defaultDigits = 6\nconst totpPeriodSeconds = 30\n\ntype OtpType = \"totp\" | \"hotp\"\n\n/**\n * Shared methods for both {@link TotpAPI} and {@link HotpAPI}.\n */\ntype OtpAPI = {\n /**\n * Generates an `otpauth://` URI to register the secret with an authenticator app.\n * @param key - The shared secret (plain text; encoded to Base32 internally).\n * @param label - Account identifier shown in the authenticator (e.g. `\"alice@example.com\"`).\n * @returns An `otpauth://` URI string.\n * @throws {TypeError} If `key` is empty.\n */\n create(key: string, label: string): string\n\n /**\n * Converts an `otpauth://` URI into a Base64-encoded PNG data URL (QR code).\n * Requires the optional `qrcode` package (`pnpm add qrcode`).\n * @param uri - An `otpauth://` URI returned by `create`.\n * @returns A `data:image/png;base64,...` string ready for use in an `<img>` tag.\n * @throws {Error} If the `qrcode` package is not installed.\n * @example\n * const dataUrl = await TOTP.qrcode(uri)\n * // \"data:image/png;base64,...\"\n */\n qrcode(uri: string): Promise<string>\n}\n\n/**\n * API for Time-based One-Time Password (TOTP, RFC 6238).\n * Tokens rotate every 30 seconds based on the current time.\n */\ntype TotpAPI = OtpAPI & {\n /**\n * Checks whether a token is valid for the current time window.\n * @param key - The shared secret used when the URI was created.\n * @param token - The 6-digit token entered by the user.\n * @param window - Number of 30-second periods to accept on either side of the current period (default `1`).\n * @returns `true` if the token matches any counter in the allowed window.\n * @throws {TypeError} If `window` is not a number.\n * @throws {RangeError} If `window` is not a non-negative integer.\n * @example\n * const ok = TOTP.verify(\"mysecret\", \"123456\")\n * const lenient = TOTP.verify(\"mysecret\", \"123456\", 2) // ±2 periods\n */\n verify(key: string, token: string, window?: number): boolean\n}\n\n/**\n * API for HMAC-based One-Time Password (HOTP, RFC 4226).\n * Tokens are derived from an incrementing counter rather than the clock.\n */\ntype HotpAPI = OtpAPI & {\n /**\n * Checks whether a token is valid for a specific counter value.\n * @param key - The shared secret used when the URI was created.\n * @param token - The 6-digit token to verify.\n * @param counter - The counter value that was used to generate the token.\n * @returns `true` if the token matches the given counter.\n * @throws {TypeError} If `counter` is not a number.\n * @throws {RangeError} If `counter` is not a non-negative safe integer.\n * @example\n * const ok = HOTP.verify(\"mysecret\", \"123456\", 42)\n */\n verify(key: string, token: string, counter: number): boolean\n}\n\n/** Encodes a UTF-8 string to Base32 (RFC 4648) without padding. */\nfunction base32Encode(input: string): string {\n let result = \"\", bits = 0, value = 0\n for (const byte of Buffer.from(input, \"utf8\")) {\n value = (value << 8) | byte\n bits += 8\n while (bits >= 5) result += base32Chars[(value >>> (bits -= 5)) & 31]\n }\n if (bits > 0) result += base32Chars[(value << (5 - bits)) & 31]\n return result\n}\n\nfunction assertNonEmptyKey(key: string): void {\n if (typeof key !== \"string\" || key.length === 0)\n throw new TypeError(\"key must be a non-empty string\")\n}\n\nfunction assertNonNegativeInt(value: number, name: string, safe?: boolean): void {\n if (typeof value !== \"number\" || Number.isNaN(value))\n throw new TypeError(`${name} must be a number`)\n if (value < 0 || !(safe ? Number.isSafeInteger(value) : Number.isInteger(value)))\n throw new RangeError(`${name} must be a non-negative${safe ? \" safe\" : \"\"} integer`)\n}\n\nfunction createOtpAuthUri(type: OtpType, key: string, label: string): string {\n assertNonEmptyKey(key)\n const searchParams = new URLSearchParams({ secret: base32Encode(key) })\n return `otpauth://${type}/${encodeURIComponent(label)}?${searchParams}`\n}\n\nasync function createQrCode(uri: string): Promise<string> {\n let qrCode: typeof import(\"qrcode\")\n try {\n qrCode = (await import(\"qrcode\")) as typeof import(\"qrcode\")\n } catch {\n throw new Error(\n 'qrcode is not installed. Run \"pnpm add qrcode\" to use this feature.',\n )\n }\n return qrCode.toDataURL(uri)\n}\n\n/**\n * Computes an HOTP value per RFC 4226 §5.\n * @param key - Plain-text secret used as the HMAC-SHA1 key.\n * @param counter - 8-byte big-endian counter value.\n * @param digits - Number of digits in the output (default 6).\n */\nfunction computeHotp(key: string, counter: number, digits = defaultDigits): string {\n assertNonNegativeInt(counter, \"counter\", true)\n const buf = Buffer.alloc(8)\n buf.writeBigUInt64BE(BigInt(counter))\n const digest = createHmac(\"sha1\", key).update(buf).digest()\n const offset = digest[digest.length - 1] & 0x0f\n const code = ((digest[offset] & 0x7f) << 24) | ((digest[offset + 1] & 0xff) << 16) | ((digest[offset + 2] & 0xff) << 8) | (digest[offset + 3] & 0xff)\n return (code % 10 ** digits).toString().padStart(digits, \"0\")\n}\n\n/**\n * Time-based One-Time Password helpers (RFC 6238).\n * @example\n * import { TOTP } from \"quickotp\"\n * const uri = TOTP.create(\"mysecret\", \"alice@example.com\")\n * const ok = TOTP.verify(\"mysecret\", userInput)\n */\nconst totp: TotpAPI = {\n create: (key, label) => createOtpAuthUri(\"totp\", key, label),\n qrcode: createQrCode,\n verify(key: string, token: string, window = 1): boolean {\n assertNonNegativeInt(window, \"window\")\n const counter = Math.floor(Date.now() / 1000 / totpPeriodSeconds)\n for (let i = -window; i <= window; i++)\n if (counter + i >= 0 && computeHotp(key, counter + i) === token) return true\n return false\n },\n}\n\n/**\n * HMAC-based One-Time Password helpers (RFC 4226).\n * @example\n * import { HOTP } from \"quickotp\"\n * const uri = HOTP.create(\"mysecret\", \"alice@example.com\")\n * const ok = HOTP.verify(\"mysecret\", userInput, counter)\n */\nconst hotp: HotpAPI = {\n create: (key, label) => createOtpAuthUri(\"hotp\", key, label),\n qrcode: createQrCode,\n verify: (key, token, counter) => computeHotp(key, counter) === token,\n}\n\nexport { totp as TOTP, hotp as HOTP }\n"],"mappings":"AAAA,OAAS,cAAAA,MAAkB,SAE3B,IAAMC,EAAc,mCACdC,EAAgB,EAChBC,EAAoB,GAsE1B,SAASC,EAAaC,EAAuB,CAC3C,IAAIC,EAAS,GAAIC,EAAO,EAAGC,EAAQ,EACnC,QAAWC,KAAQ,OAAO,KAAKJ,EAAO,MAAM,EAG1C,IAFAG,EAASA,GAAS,EAAKC,EACvBF,GAAQ,EACDA,GAAQ,GAAGD,GAAUL,EAAaO,KAAWD,GAAQ,GAAM,EAAE,EAEtE,OAAIA,EAAO,IAAGD,GAAUL,EAAaO,GAAU,EAAID,EAAS,EAAE,GACvDD,CACT,CAEA,SAASI,EAAkBC,EAAmB,CAC5C,GAAI,OAAOA,GAAQ,UAAYA,EAAI,SAAW,EAC5C,MAAM,IAAI,UAAU,gCAAgC,CACxD,CAEA,SAASC,EAAqBJ,EAAeK,EAAcC,EAAsB,CAC/E,GAAI,OAAON,GAAU,UAAY,OAAO,MAAMA,CAAK,EACjD,MAAM,IAAI,UAAU,GAAGK,CAAI,mBAAmB,EAChD,GAAIL,EAAQ,GAAK,EAAEM,EAAO,OAAO,cAAcN,CAAK,EAAI,OAAO,UAAUA,CAAK,GAC5E,MAAM,IAAI,WAAW,GAAGK,CAAI,0BAA0BC,EAAO,QAAU,EAAE,UAAU,CACvF,CAEA,SAASC,EAAiBC,EAAeL,EAAaM,EAAuB,CAC3EP,EAAkBC,CAAG,EACrB,IAAMO,EAAe,IAAI,gBAAgB,CAAE,OAAQd,EAAaO,CAAG,CAAE,CAAC,EACtE,MAAO,aAAaK,CAAI,IAAI,mBAAmBC,CAAK,CAAC,IAAIC,CAAY,EACvE,CAEA,eAAeC,EAAaC,EAA8B,CACxD,IAAIC,EACJ,GAAI,CACFA,EAAU,KAAM,QAAO,QAAQ,CACjC,MAAQ,CACN,MAAM,IAAI,MACR,qEACF,CACF,CACA,OAAOA,EAAO,UAAUD,CAAG,CAC7B,CAQA,SAASE,EAAYX,EAAaY,EAAiBC,EAAStB,EAAuB,CACjFU,EAAqBW,EAAS,UAAW,EAAI,EAC7C,IAAME,EAAM,OAAO,MAAM,CAAC,EAC1BA,EAAI,iBAAiB,OAAOF,CAAO,CAAC,EACpC,IAAMG,EAAS1B,EAAW,OAAQW,CAAG,EAAE,OAAOc,CAAG,EAAE,OAAO,EACpDE,EAASD,EAAOA,EAAO,OAAS,CAAC,EAAI,GAE3C,SADeA,EAAOC,CAAM,EAAI,MAAS,IAAQD,EAAOC,EAAS,CAAC,EAAI,MAAS,IAAQD,EAAOC,EAAS,CAAC,EAAI,MAAS,EAAMD,EAAOC,EAAS,CAAC,EAAI,KACjI,IAAMH,GAAQ,SAAS,EAAE,SAASA,EAAQ,GAAG,CAC9D,CASA,IAAMI,EAAgB,CACpB,OAAQ,CAACjB,EAAKM,IAAUF,EAAiB,OAAQJ,EAAKM,CAAK,EAC3D,OAAQE,EACR,OAAOR,EAAakB,EAAeC,EAAS,EAAY,CACtDlB,EAAqBkB,EAAQ,QAAQ,EACrC,IAAMP,EAAU,KAAK,MAAM,KAAK,IAAI,EAAI,IAAOpB,CAAiB,EAChE,QAAS4B,EAAI,CAACD,EAAQC,GAAKD,EAAQC,IACjC,GAAIR,EAAUQ,GAAK,GAAKT,EAAYX,EAAKY,EAAUQ,CAAC,IAAMF,EAAO,MAAO,GAC1E,MAAO,EACT,CACF,EASMG,EAAgB,CACpB,OAAQ,CAACrB,EAAKM,IAAUF,EAAiB,OAAQJ,EAAKM,CAAK,EAC3D,OAAQE,EACR,OAAQ,CAACR,EAAKkB,EAAON,IAAYD,EAAYX,EAAKY,CAAO,IAAMM,CACjE","names":["createHmac","base32Chars","defaultDigits","totpPeriodSeconds","base32Encode","input","result","bits","value","byte","assertNonEmptyKey","key","assertNonNegativeInt","name","safe","createOtpAuthUri","type","label","searchParams","createQrCode","uri","qrCode","computeHotp","counter","digits","buf","digest","offset","totp","token","window","i","hotp"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "quickotp",
3
+ "version": "2.1.0",
4
+ "description": "A lightweight OTP library for Node.js with zero runtime dependencies.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git://github.com/donginssam/quickotp.git"
22
+ },
23
+ "author": "kimleedi",
24
+ "license": "MIT",
25
+ "bugs": {
26
+ "url": "https://github.com/donginssam/quickotp/issues"
27
+ },
28
+ "homepage": "https://github.com/donginssam/quickotp",
29
+ "keywords": [
30
+ "otp",
31
+ "qr",
32
+ "otpauth",
33
+ "googleauth",
34
+ "twofactor"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "peerDependencies": {
40
+ "qrcode": "^1.5.4"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "qrcode": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/js": "^10.0.1",
49
+ "@types/node": "^22.0.0",
50
+ "@types/qrcode": "^1.5.5",
51
+ "eslint": "^10.3.0",
52
+ "eslint-config-prettier": "^10.1.8",
53
+ "globals": "^17.6.0",
54
+ "prettier": "3.8.3",
55
+ "tsup": "^8.5.1",
56
+ "tsx": "^4.0.0",
57
+ "typescript": "^5.7.2",
58
+ "typescript-eslint": "^8.59.2"
59
+ },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "format": "prettier . --write",
63
+ "lint": "eslint .",
64
+ "lint:fix": "eslint . --fix",
65
+ "test": "node --import tsx/esm --test 'test/**/*.test.ts'"
66
+ }
67
+ }