@muljax/edge-passkey 1.0.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,297 @@
1
+ Mozilla Public License Version 2.0
2
+ ==================================
3
+
4
+ 1. Definitions
5
+ --------------
6
+
7
+ 1.1. "Contributor"
8
+ means each individual or entity that creates, contributes to the
9
+ creation of, or owns Existing Contributor Charge in a Modification.
10
+
11
+ 1.2. "Contributor Version"
12
+ means the combination of the Contributions of others (if any) used
13
+ by a Contributor and that particular Contributor's Contribution.
14
+
15
+ 1.3. "Contribution"
16
+ means Covered Software of a particular Contributor.
17
+
18
+ 1.4. "Covered Software"
19
+ means Source Code Form to which the initial Contributor has attached
20
+ the notice in Exhibit A, the Executable Form of such Source Code Form,
21
+ and Modifications of such Source Code Form, in each case including portions
22
+ thereof.
23
+
24
+ 1.5. "Incompatible With Secondary Licenses"
25
+ means
26
+ (a) that the initial Contributor has attached the notice described
27
+ in Exhibit B to the Covered Software; or
28
+
29
+ (b) that the Covered Software was made available under the terms of
30
+ version 1.1 or earlier of the License, but not also under the
31
+ terms of a Secondary License.
32
+
33
+ 1.6. "Executable Form"
34
+ means any form of the work other than Source Code Form.
35
+
36
+ 1.7. "Larger Work"
37
+ means a work that combines Covered Software with other material, in a separate
38
+ file or files, that is not Covered Software.
39
+
40
+ 1.8. "License"
41
+ means this document.
42
+
43
+ 1.9. "Licensable"
44
+ means having the right to grant, to the maximum extent possible, whether
45
+ at the time of the initial grant or subsequently, any and all of the
46
+ rights conveyed by this License.
47
+
48
+ 1.10. "Modifications"
49
+ means any of the following:
50
+ (a) any addition to or deletion from the contents of a file containing
51
+ Covered Software; or
52
+
53
+ (b) any new file in Source Code Form that contains any Covered Software.
54
+
55
+ 1.11. "Patent Claims" of a Contributor
56
+ means any patent claim(s), including without limitation, method, process,
57
+ and apparatus claims, in any patent Licensable by such Contributor that
58
+ would be infringed, but for the grant of the License, by the making,
59
+ using, selling, offering for sale, having made, import, or transfer of
60
+ either its Contributions or its Contributor Version.
61
+
62
+ 1.12. "Secondary License"
63
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
64
+ General Public License, Version 2.1, the GNU Affero General Public
65
+ License, Version 3.0, or any later versions of those licenses.
66
+
67
+ 1.13. "Source Code Form"
68
+ means the form of the work preferred for making modifications.
69
+
70
+ 1.14. "You" (or "Your")
71
+ means an individual or a legal entity exercising rights under this
72
+ License. For legal entities, "You" includes any entity that controls, is
73
+ controlled by, or is under common control with You. For purposes of this
74
+ definition, "control" means (a) the power, direct or indirect, to cause
75
+ the direction or management of such entity, whether by contract or
76
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
77
+ outstanding shares or beneficial ownership of such entity.
78
+
79
+ 2. License Grants and Conditions
80
+ --------------------------------
81
+
82
+ 2.1. Grants
83
+ Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive
84
+ license:
85
+ (a) under intellectual property rights (other than patent or trademark)
86
+ Licensable by such Contributor to use, reproduce, make available,
87
+ modify, display, perform, distribute, and otherwise exploit its
88
+ Contributions, either on an unmodified basis, with Modifications, or as
89
+ part of a Larger Work; and
90
+
91
+ (b) under Patent Claims of such Contributor to make, use, sell, offer for
92
+ sale, have made, import, and otherwise transfer either its Contributions
93
+ or its Contributor Version.
94
+
95
+ 2.2. Effective Date
96
+ The licenses granted in Section 2.1 with respect to any Contribution become
97
+ effective for each Contribution on the date the Contributor first distributes
98
+ such Contribution.
99
+
100
+ 2.3. Limitations on Grant Scope
101
+ The licenses granted in this Section 2 are the only rights granted under this
102
+ License. No additional terms or conditions will be implied. No other
103
+ intellectual property rights are granted under this License.
104
+
105
+ 2.4. Subsequent Licenses
106
+ No Contributor makes additional grants as a result of You distributing the
107
+ Covered Software under a subsequent version of this License (see Section 10.2)
108
+ or under the terms of a Secondary License (if permitted under the terms of
109
+ Section 3.3).
110
+
111
+ 2.5. Representation
112
+ Each Contributor represents that the Contributor believes its Contributions
113
+ are its original creation(s) or it has sufficient rights to grant the
114
+ rights to its Contributions conveyed by this License.
115
+
116
+ 2.6. Fair Use
117
+ This License is not intended to limit any rights You have under applicable
118
+ copyright doctrines of fair use, fair dealing, or other equivalents.
119
+
120
+ 2.7. Conditions
121
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
122
+ Section 2.1.
123
+
124
+ 3. Responsibilities
125
+ -------------------
126
+
127
+ 3.1. Availability of Source Code
128
+ All distribution of Covered Software in Source Code Form, including any
129
+ Modifications that You create or to which You contribute, must be under the
130
+ terms of this License. You must inform recipients that the Source Code Form of
131
+ the Covered Software is governed by the terms of this License, and how they
132
+ can obtain a copy of this License. You must make the Source Code Form of any
133
+ such Modifications publicly available under the terms of this License.
134
+
135
+ 3.2. Distribution of Executable Form
136
+ If You distribute Covered Software in Executable Form then:
137
+ (a) such Covered Software must also be made available in Source Code Form, as
138
+ described in Section 3.1, and You must inform recipients of the Executable
139
+ Form how they can obtain a copy of such Source Code Form by reasonable
140
+ means in a timely manner, at a charge no more than the cost of distribution
141
+ to the recipient; and
142
+
143
+ (b) You may distribute such Executable Form under the terms of this License,
144
+ or sublicense it under different terms, provided that the license for the
145
+ Executable Form does not attempt to limit or alter the recipients' rights
146
+ in the Source Code Form under this License.
147
+
148
+ 3.3. Distribution of a Larger Work
149
+ You may create and distribute a Larger Work under terms of Your choice,
150
+ provided that You also comply with the requirements of this License for the
151
+ Covered Software. If the Larger Work is a combination of Covered Software with
152
+ a work governed by one or more Secondary Licenses, and the Covered Software is
153
+ not Incompatible With Secondary Licenses, this License permits You to
154
+ additionally distribute such Covered Software under the terms of such Secondary
155
+ License(s), so that the recipient of the Larger Work may, at their option,
156
+ further distribute the Covered Software under the terms of either this License
157
+ or such Secondary License(s).
158
+
159
+ 3.4. Notices
160
+ You may not remove or alter the substance of any license notices (including
161
+ copyright notices, patent notices, disclaimers of warranty, or limitations of
162
+ liability) contained within the Source Code Form of the Covered Software,
163
+ except that You may alter any notices to the extent required to remedy known
164
+ factual inaccuracies.
165
+
166
+ 4. Inability to Comply Due to Statute or Regulation
167
+ ---------------------------------------------------
168
+
169
+ If it is impossible for You to comply with any of the terms of this License
170
+ with respect to some or all of the Covered Software due to statute, judicial
171
+ order, or regulation then You must: (a) comply with the terms of this License
172
+ to the maximum extent possible; and (b) describe the limitations and the code
173
+ they affect. Such description must be placed in a text file included with all
174
+ distributions of the Covered Software. Except to the extent prohibited by
175
+ statute or regulation, such description must be sufficiently detailed for a
176
+ recipient of ordinary skill to be able to understand it.
177
+
178
+ 5. Termination
179
+ --------------
180
+
181
+ 5.1. The rights granted under this License will terminate automatically if You
182
+ fail to comply with any of its terms. However, if You become compliant, then the
183
+ rights granted under this License from a particular Contributor are reinstated
184
+ (a) provisionally, unless and until such Contributor explicitly and finally
185
+ terminates Your grants, and (b) on an ongoing basis, if such Contributor fails
186
+ to notify You of the non-compliance by some reasonable means prior to 60 days
187
+ after You have come back into compliance. Moreover, Your grants from a
188
+ particular Contributor are reinstated on an ongoing basis if such Contributor
189
+ notifies You of the non-compliance by some reasonable means, this is the first
190
+ time You have received notice of non-compliance with this License from such
191
+ Contributor, and You become compliant prior to 30 days after Your receipt of the
192
+ notice.
193
+
194
+ 5.2. If You initiate litigation against any entity by asserting a patent
195
+ infringement claim (excluding declaratory judgment actions, counter-claims, and
196
+ cross-claims) alleging that a Contributor Version directly or indirectly
197
+ infringes any patent, then the rights granted to You by any and all
198
+ Contributors for the Covered Software under Section 2.1 of this License shall
199
+ terminate.
200
+
201
+ 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
202
+ license agreements (excluding distributors and resellers) which have been
203
+ validly granted by You or Your distributors under this License prior to
204
+ termination shall survive termination.
205
+
206
+ 6. Disclaimer of Warranty
207
+ -------------------------
208
+
209
+ Covered Software is provided under this License on an "as is" basis, without
210
+ warranty of any kind, either expressed, implied, or statutory, including,
211
+ without limitation, warranties that the Covered Software is free of defects,
212
+ merchantable, fit for a particular purpose or non-infringing. The entire
213
+ risk as to the quality and performance of the Covered Software is with You.
214
+ Should any Covered Software prove defective in any respect, You (not any
215
+ Contributor) assume the cost of any necessary servicing, repair, or
216
+ correction. This disclaimer of warranty constitutes an essential part of this
217
+ License. No use of any Covered Software is authorized under this License
218
+ except under this disclaimer.
219
+
220
+ 7. Limitation of Liability
221
+ --------------------------
222
+
223
+ Under no circumstances and under no legal theory, whether tort (including
224
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
225
+ distributes Covered Software as permitted above, be liable to You for any
226
+ direct, indirect, special, incidental, or exemplary damages of any character
227
+ including, without limitation, damages for lost profits, loss of goodwill,
228
+ work stoppage, computer failure or malfunction, or any and all other commercial
229
+ damages or losses, even if such party shall have been informed of the
230
+ possibility of such damages. This limitation of liability shall not apply to
231
+ liability for death or personal injury resulting from such party's negligence to
232
+ the extent applicable law prohibits such limitation. Some jurisdictions do not
233
+ allow the exclusion or limitation of incidental or exemplary damages, so this
234
+ exclusion and limitation may not apply to You.
235
+
236
+ 8. Litigation
237
+ -------------
238
+
239
+ Any litigation relating to this License may be brought only in the courts of a
240
+ jurisdiction where the defendant maintains its principal place of business and
241
+ such litigation shall be governed by laws of that jurisdiction, without
242
+ reference to its conflict-of-law provisions. Nothing in this Section shall
243
+ prevent a party's ability to bring cross-claims or counter-claims.
244
+
245
+ 9. Miscellaneous
246
+ ----------------
247
+
248
+ This License represents the complete agreement concerning the subject matter
249
+ hereof. If any provision of this License is held to be unenforceable, such
250
+ provision shall be reformed only to the extent necessary to make it
251
+ enforceable. Any law or regulation which provides that the language of a
252
+ contract shall be construed against the drafter shall not be used to construe
253
+ this License against a Contributor.
254
+
255
+ 10. Versions of the License
256
+ ---------------------------
257
+
258
+ 10.1. New Versions
259
+ Mozilla Foundation is the license steward. Except as provided in Section 10.3,
260
+ no one other than the license steward has the right to modify or publish new
261
+ versions of this License. Each version will be given a distinguishing version
262
+ number.
263
+
264
+ 10.2. Effect of New Versions
265
+ You may distribute the Covered Software under the terms of the version of the
266
+ License under which You originally received the Covered Software, or under the
267
+ terms of any subsequent version published by the license steward.
268
+
269
+ 10.3. Modified Versions
270
+ If you create software not governed by this License, and you want to create a
271
+ new license for such software, you may create and use a modified version of
272
+ this License if you rename the license and remove any references to the name of
273
+ the license steward (except to note that such modified license differs from
274
+ this License).
275
+
276
+ 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
277
+ If You choose to distribute Source Code Form that is Incompatible With Secondary
278
+ Licenses under the terms of this version of the License, the notice described
279
+ in Exhibit B of this License must be attached.
280
+
281
+ Exhibit A - Source Code Form License Notice
282
+ -------------------------------------------
283
+
284
+ This Source Code Form is subject to the terms of the Mozilla Public
285
+ License, v. 2.0. If a copy of the MPL was not distributed with this
286
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
287
+
288
+ If it is not possible to put the notice in a particular Source Code Form
289
+ file, then You may include the notice in a location (such as a LICENSE
290
+ file in a relevant directory) where a recipient would be likely to look
291
+ for such a notice.
292
+
293
+ Exhibit B - "Incompatible With Secondary Licenses" Notice
294
+ ---------------------------------------------------------
295
+
296
+ This Source Code Form is "Incompatible With Secondary Licenses", as
297
+ defined by the Mozilla Public License, v. 2.0.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # @muljax/edge-passkey
2
+
3
+ Zero-dependency, high-performance WebAuthn and Passkey verification engine designed specifically for edge runtimes (Cloudflare Workers, Bun, Deno, V8 Isolates, and Node.js).
4
+
5
+ [![Tests](https://img.shields.io/badge/tests-24%20passing-brightgreen)](#)
6
+ [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](LICENSE)
7
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-success)](#)
8
+ [![Bundle Size](https://img.shields.io/badge/bundle%20size-%3C3.8%20KB-blue)](#)
9
+
10
+ ---
11
+
12
+ ## Highlights
13
+
14
+ - **Zero Dependencies:** Pure TypeScript running directly on native Web Cryptography (`crypto.subtle`) and `DataView`.
15
+ - **Ultra Lightweight:** Less than 3.8 KB gzipped. Strips out legacy enterprise attestation bloat (X.509 chains, TPM 2.0, SafetyNet).
16
+ - **Sub-Millisecond Verification:** Verified in $< 0.8\text{ ms}$ on V8 isolates and Cloudflare Workers.
17
+ - **Modern Passkey Profile:** Native support for Apple iCloud Keychain, Google Password Manager, Windows Hello, 1Password, Bitwarden, and YubiKeys.
18
+ - **Cryptographic Suite:** Hardware-verified ES256 (P-256), EdDSA (Ed25519), RS256, PS256, ES384, and ES512.
19
+ - **Extensions & PRF:** Built-in parsing for `credProps` (discoverable credentials) and `prf` (hardware-derived E2EE keys).
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ # Bun
27
+ bun add @muljax/edge-passkey
28
+
29
+ # npm
30
+ npm install @muljax/edge-passkey
31
+
32
+ # pnpm
33
+ pnpm add @muljax/edge-passkey
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Quickstart
39
+
40
+ ### 1. Registration Ceremony
41
+
42
+ ```typescript
43
+ import {
44
+ generateRegistrationOptions,
45
+ verifyRegistration,
46
+ WebAuthnError,
47
+ } from "@muljax/edge-passkey";
48
+
49
+ // Step A: Generate Registration Options
50
+ const options = generateRegistrationOptions({
51
+ rpName: "Acme Corp",
52
+ rpId: "auth.example.com",
53
+ userName: "alex@example.com",
54
+ residentKey: "preferred",
55
+ userVerification: "preferred",
56
+ });
57
+
58
+ // Store `options.challenge` in session/cache (TTL: 2 min)
59
+
60
+ // Step B: Verify Registration Response from Browser
61
+ try {
62
+ const result = await verifyRegistration({
63
+ response: clientRegistrationPayload,
64
+ expectedChallenge: storedChallenge,
65
+ expectedOrigin: "https://auth.example.com",
66
+ expectedRpId: "auth.example.com",
67
+ });
68
+
69
+ console.log("Enrolled credential:", result.credential.id);
70
+ } catch (err) {
71
+ if (err instanceof WebAuthnError) {
72
+ console.error("WebAuthn verification error:", err.code, err.message);
73
+ }
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ### 2. Authentication Ceremony
80
+
81
+ ```typescript
82
+ import {
83
+ generateAuthenticationOptions,
84
+ verifyAuthentication,
85
+ WebAuthnError,
86
+ } from "@muljax/edge-passkey";
87
+
88
+ // Step A: Generate Authentication Options
89
+ const options = generateAuthenticationOptions({
90
+ rpId: "auth.example.com",
91
+ allowCredentials: [{ id: storedCredentialId }],
92
+ });
93
+
94
+ // Store `options.challenge` in session/cache (TTL: 2 min)
95
+
96
+ // Step B: Verify Assertion Response from Browser
97
+ try {
98
+ const result = await verifyAuthentication({
99
+ response: clientAuthenticationPayload,
100
+ expectedChallenge: storedChallenge,
101
+ expectedOrigin: "https://auth.example.com",
102
+ expectedRpId: "auth.example.com",
103
+ credentialPublicKey: storedPublicKey,
104
+ prevSignCount: storedSignCount,
105
+ });
106
+
107
+ console.log("Authenticated! User present:", result.userPresent);
108
+ } catch (err) {
109
+ if (err instanceof WebAuthnError) {
110
+ console.error("Authentication failed:", err.code, err.message);
111
+ }
112
+ }
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Error Handling
118
+
119
+ `@muljax/edge-passkey` provides typed error classes inheriting from `WebAuthnError`:
120
+
121
+ ```typescript
122
+ import {
123
+ WebAuthnError,
124
+ ChallengeMismatchError,
125
+ OriginMismatchError,
126
+ RpIdMismatchError,
127
+ CounterRollbackError,
128
+ UserPresenceError,
129
+ UserVerificationPolicyError,
130
+ InvalidSignatureError,
131
+ MalformedPayloadError,
132
+ } from "@muljax/edge-passkey";
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Development & Testing
138
+
139
+ ```bash
140
+ # Run unit & integration tests
141
+ bun test
142
+
143
+ # Run interactive testing server & UI
144
+ bun run dev
145
+ ```
146
+
147
+ ## License
148
+
149
+ This project is licensed under the **Mozilla Public License 2.0 (MPL-2.0)** - see the [LICENSE](LICENSE) file for details.
150
+
151
+ - **Derivative Works & Larger Works:** You may use and integrate this library into proprietary and commercial applications.
152
+ - **Modifications to Covered Files:** Any modifications made directly to this library's source files must be made publicly available under the MPL-2.0.
153
+ - **Attribution:** All distributions and derivative works must preserve original copyright and license notices.
154
+
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Decodes a Base64URL string into a Uint8Array.
3
+ */
4
+ export declare function base64URLToBytes(base64url: string): Uint8Array;
5
+ /**
6
+ * Encodes a Uint8Array into an unpadded Base64URL string.
7
+ */
8
+ export declare function bytesToBase64URL(bytes: Uint8Array): string;
9
+ /**
10
+ * Performs a constant-time byte equality check to prevent timing side-channels.
11
+ */
12
+ export declare function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean;
@@ -0,0 +1,12 @@
1
+ export type CBORValue = number | bigint | string | boolean | null | undefined | Uint8Array | CBORValue[] | Map<number | string, CBORValue>;
2
+ /**
3
+ * Decodes a CBOR buffer into its corresponding JavaScript value.
4
+ */
5
+ export declare function decodeCBOR(buffer: Uint8Array): CBORValue;
6
+ /**
7
+ * Decodes a CBOR item and returns the decoded value along with the exact raw byte slice consumed.
8
+ */
9
+ export declare function decodeCBORWithSlice(buffer: Uint8Array): {
10
+ value: CBORValue;
11
+ rawSlice: Uint8Array;
12
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./base64url";
2
+ export * from "./reader";
3
+ export * from "./cbor";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Bounds-checked sequential binary reader over an underlying DataView.
3
+ */
4
+ export declare class BinaryReader {
5
+ private readonly buffer;
6
+ private readonly view;
7
+ private offset;
8
+ constructor(buffer: Uint8Array);
9
+ get cursor(): number;
10
+ get remaining(): number;
11
+ hasBytes(count: number): boolean;
12
+ readUint8(): number;
13
+ readUint16(): number;
14
+ readUint32(): number;
15
+ readBigUint64(): bigint;
16
+ readBytes(length: number): Uint8Array;
17
+ seek(offset: number): void;
18
+ private assertAvailable;
19
+ }
@@ -0,0 +1,5 @@
1
+ import type { AuthenticationVerificationOptions, AuthenticationResult } from "@/types";
2
+ /**
3
+ * Verifies a WebAuthn authentication assertion response.
4
+ */
5
+ export declare function verifyAuthentication(options: AuthenticationVerificationOptions): Promise<AuthenticationResult>;
@@ -0,0 +1,2 @@
1
+ export * from "./registration";
2
+ export * from "./authentication";
@@ -0,0 +1,5 @@
1
+ import type { RegistrationVerificationOptions, RegistrationResult } from "@/types";
2
+ /**
3
+ * Verifies a WebAuthn registration response.
4
+ */
5
+ export declare function verifyRegistration(options: RegistrationVerificationOptions): Promise<RegistrationResult>;
@@ -0,0 +1,5 @@
1
+ import type { ParsedCOSEKey } from "@/types";
2
+ /**
3
+ * Imports a raw CBOR-encoded COSE Key into a native WebCrypto CryptoKey.
4
+ */
5
+ export declare function importCOSEPublicKey(coseBytes: Uint8Array): Promise<ParsedCOSEKey>;
@@ -0,0 +1 @@
1
+ export * from "./coseParser";
@@ -0,0 +1,30 @@
1
+ export type WebAuthnErrorCode = "CHALLENGE_MISMATCH" | "ORIGIN_MISMATCH" | "RP_ID_MISMATCH" | "USER_PRESENCE_NOT_SET" | "USER_VERIFICATION_FAILED" | "COUNTER_ROLLBACK" | "INVALID_SIGNATURE" | "INVALID_ATTESTATION" | "MALFORMED_PAYLOAD" | "UNSUPPORTED_ALGORITHM" | "KEY_PARSING_FAILED";
2
+ export declare class WebAuthnError extends Error {
3
+ readonly code: WebAuthnErrorCode;
4
+ readonly details?: Record<string, unknown>;
5
+ constructor(code: WebAuthnErrorCode, message: string, details?: Record<string, unknown>);
6
+ }
7
+ export declare class ChallengeMismatchError extends WebAuthnError {
8
+ constructor(expected: string, received: string);
9
+ }
10
+ export declare class OriginMismatchError extends WebAuthnError {
11
+ constructor(expected: string | string[], received: string);
12
+ }
13
+ export declare class RpIdMismatchError extends WebAuthnError {
14
+ constructor(expectedRpId: string);
15
+ }
16
+ export declare class UserPresenceError extends WebAuthnError {
17
+ constructor();
18
+ }
19
+ export declare class UserVerificationPolicyError extends WebAuthnError {
20
+ constructor();
21
+ }
22
+ export declare class CounterRollbackError extends WebAuthnError {
23
+ constructor(storedCount: number, receivedCount: number);
24
+ }
25
+ export declare class InvalidSignatureError extends WebAuthnError {
26
+ constructor(message?: string);
27
+ }
28
+ export declare class MalformedPayloadError extends WebAuthnError {
29
+ constructor(message: string, details?: Record<string, unknown>);
30
+ }
@@ -0,0 +1 @@
1
+ export * from "./options";
@@ -0,0 +1,80 @@
1
+ import { COSEAlgorithm } from "@/types";
2
+ export interface GenerateRegistrationOptionsArgs {
3
+ rpName: string;
4
+ rpId: string;
5
+ userName: string;
6
+ userID?: Uint8Array | string;
7
+ userDisplayName?: string;
8
+ challenge?: Uint8Array | string;
9
+ supportedAlgorithms?: COSEAlgorithm[];
10
+ attestationType?: "none" | "direct" | "enterprise";
11
+ residentKey?: "preferred" | "required" | "discouraged";
12
+ userVerification?: "preferred" | "required" | "discouraged";
13
+ authenticatorAttachment?: "platform" | "cross-platform";
14
+ timeout?: number;
15
+ excludeCredentials?: Array<{
16
+ id: Uint8Array | string;
17
+ transports?: AuthenticatorTransport[];
18
+ }>;
19
+ extensions?: Record<string, unknown>;
20
+ }
21
+ export interface PublicKeyCredentialCreationOptionsJSON {
22
+ challenge: string;
23
+ rp: {
24
+ name: string;
25
+ id: string;
26
+ };
27
+ user: {
28
+ id: string;
29
+ name: string;
30
+ displayName: string;
31
+ };
32
+ pubKeyCredParams: Array<{
33
+ type: "public-key";
34
+ alg: COSEAlgorithm;
35
+ }>;
36
+ timeout: number;
37
+ attestation: "none" | "direct" | "enterprise";
38
+ authenticatorSelection: {
39
+ residentKey?: "preferred" | "required" | "discouraged";
40
+ userVerification?: "preferred" | "required" | "discouraged";
41
+ authenticatorAttachment?: "platform" | "cross-platform";
42
+ };
43
+ excludeCredentials?: Array<{
44
+ id: string;
45
+ type: "public-key";
46
+ transports?: AuthenticatorTransport[];
47
+ }>;
48
+ extensions?: Record<string, unknown>;
49
+ }
50
+ /**
51
+ * Generates RFC-compliant PublicKeyCredentialCreationOptions for navigator.credentials.create().
52
+ */
53
+ export declare function generateRegistrationOptions(args: GenerateRegistrationOptionsArgs): PublicKeyCredentialCreationOptionsJSON;
54
+ export interface GenerateAuthenticationOptionsArgs {
55
+ rpId: string;
56
+ challenge?: Uint8Array | string;
57
+ timeout?: number;
58
+ userVerification?: "preferred" | "required" | "discouraged";
59
+ allowCredentials?: Array<{
60
+ id: Uint8Array | string;
61
+ transports?: AuthenticatorTransport[];
62
+ }>;
63
+ extensions?: Record<string, unknown>;
64
+ }
65
+ export interface PublicKeyCredentialRequestOptionsJSON {
66
+ challenge: string;
67
+ rpId: string;
68
+ timeout: number;
69
+ userVerification: "preferred" | "required" | "discouraged";
70
+ allowCredentials?: Array<{
71
+ id: string;
72
+ type: "public-key";
73
+ transports?: AuthenticatorTransport[];
74
+ }>;
75
+ extensions?: Record<string, unknown>;
76
+ }
77
+ /**
78
+ * Generates RFC-compliant PublicKeyCredentialRequestOptions for navigator.credentials.get().
79
+ */
80
+ export declare function generateAuthenticationOptions(args: GenerateAuthenticationOptionsArgs): PublicKeyCredentialRequestOptionsJSON;
@@ -0,0 +1,7 @@
1
+ export * from "./types";
2
+ export * from "./binary";
3
+ export * from "./cose";
4
+ export * from "./protocol";
5
+ export * from "./core";
6
+ export * from "./errors";
7
+ export * from "./helpers";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ var COSEAlgorithm;((COSEAlgorithm)=>{COSEAlgorithm[COSEAlgorithm["ES256"]=-7]="ES256";COSEAlgorithm[COSEAlgorithm["EdDSA"]=-8]="EdDSA";COSEAlgorithm[COSEAlgorithm["ES384"]=-35]="ES384";COSEAlgorithm[COSEAlgorithm["ES512"]=-36]="ES512";COSEAlgorithm[COSEAlgorithm["RS256"]=-257]="RS256";COSEAlgorithm[COSEAlgorithm["RS384"]=-258]="RS384";COSEAlgorithm[COSEAlgorithm["RS512"]=-259]="RS512";COSEAlgorithm[COSEAlgorithm["PS256"]=-37]="PS256"})(COSEAlgorithm||={});var COSEKeyType;((COSEKeyType)=>{COSEKeyType[COSEKeyType["OKP"]=1]="OKP";COSEKeyType[COSEKeyType["EC2"]=2]="EC2";COSEKeyType[COSEKeyType["RSA"]=3]="RSA"})(COSEKeyType||={});var COSECurve;((COSECurve)=>{COSECurve[COSECurve["P256"]=1]="P256";COSECurve[COSECurve["P384"]=2]="P384";COSECurve[COSECurve["P521"]=3]="P521";COSECurve[COSECurve["Ed25519"]=6]="Ed25519"})(COSECurve||={});function base64URLToBytes(base64url){let base64=base64url.replace(/-/g,"+").replace(/_/g,"/");while(base64.length%4!==0){base64+="="}const binaryString=atob(base64);const len=binaryString.length;const bytes=new Uint8Array(len);for(let i=0;i<len;i++){bytes[i]=binaryString.charCodeAt(i)}return bytes}function bytesToBase64URL(bytes){let binary="";const len=bytes.byteLength;for(let i=0;i<len;i++){binary+=String.fromCharCode(bytes[i])}return btoa(binary).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function timingSafeEqual(a,b){if(a.length!==b.length)return false;let result=0;for(let i=0;i<a.length;i++){result|=a[i]^b[i]}return result===0}class BinaryReader{buffer;view;offset=0;constructor(buffer){this.buffer=buffer;this.view=new DataView(buffer.buffer,buffer.byteOffset,buffer.byteLength)}get cursor(){return this.offset}get remaining(){return this.buffer.byteLength-this.offset}hasBytes(count){return this.offset+count<=this.buffer.byteLength}readUint8(){this.assertAvailable(1);const val=this.view.getUint8(this.offset);this.offset+=1;return val}readUint16(){this.assertAvailable(2);const val=this.view.getUint16(this.offset,false);this.offset+=2;return val}readUint32(){this.assertAvailable(4);const val=this.view.getUint32(this.offset,false);this.offset+=4;return val}readBigUint64(){this.assertAvailable(8);const val=this.view.getBigUint64(this.offset,false);this.offset+=8;return val}readBytes(length){this.assertAvailable(length);const slice=this.buffer.subarray(this.offset,this.offset+length);this.offset+=length;return slice}seek(offset){if(offset<0||offset>this.buffer.byteLength){throw new RangeError(`Invalid seek offset: ${offset}`)}this.offset=offset}assertAvailable(length){if(this.offset+length>this.buffer.byteLength){throw new RangeError(`Buffer underflow: Attempted to read ${length} bytes at offset ${this.offset} (length: ${this.buffer.byteLength})`)}}}function decodeCBOR(buffer){const reader=new BinaryReader(buffer);return parseCBORItem(reader)}function decodeCBORWithSlice(buffer){const reader=new BinaryReader(buffer);const start=reader.cursor;const value=parseCBORItem(reader);const end=reader.cursor;return{value,rawSlice:buffer.subarray(start,end)}}function parseCBORItem(reader){const initialByte=reader.readUint8();const majorType=initialByte>>5;const additionalInfo=initialByte&31;if(majorType===7){if(additionalInfo===20)return false;if(additionalInfo===21)return true;if(additionalInfo===22)return null;if(additionalInfo===23)return;return additionalInfo}const length=readLength(reader,additionalInfo);switch(majorType){case 0:return length;case 1:return typeof length==="bigint"?-1n-length:-1-Number(length);case 2:return reader.readBytes(Number(length));case 3:{const strBytes=reader.readBytes(Number(length));return new TextDecoder("utf-8").decode(strBytes)}case 4:{const arrLen=Number(length);const arr=new Array(arrLen);for(let i=0;i<arrLen;i++){arr[i]=parseCBORItem(reader)}return arr}case 5:{const mapLen=Number(length);const map=new Map;for(let i=0;i<mapLen;i++){const key=parseCBORItem(reader);if(typeof key!=="number"&&typeof key!=="string"){throw new Error(`Unsupported CBOR map key type: ${typeof key}`)}map.set(key,parseCBORItem(reader))}return map}default:throw new Error(`Unsupported CBOR Major Type: ${majorType}`)}}function readLength(reader,additionalInfo){if(additionalInfo<24)return additionalInfo;if(additionalInfo===24)return reader.readUint8();if(additionalInfo===25)return reader.readUint16();if(additionalInfo===26)return reader.readUint32();if(additionalInfo===27)return reader.readBigUint64();throw new Error(`Unsupported CBOR length info: ${additionalInfo}`)}async function importCOSEPublicKey(coseBytes){const map=decodeCBOR(coseBytes);if(!(map instanceof Map)){throw new Error("Invalid COSE Key: Root structure must be a CBOR Map")}const kty=map.get(1);const alg=map.get(3);if(!kty||!alg){throw new Error("Invalid COSE Key: Missing required kty (1) or alg (3) parameters")}if(kty===2){const crv=map.get(-1);const x=map.get(-2);const y=map.get(-3);if(!x||!y||!(x instanceof Uint8Array)||!(y instanceof Uint8Array)){throw new Error("Invalid COSE EC2 Key: Missing or invalid x/y coordinate bytes")}let namedCurve;let hashName;if(crv===1&&alg===-7){namedCurve="P-256";hashName="SHA-256"}else if(crv===2&&alg===-35){namedCurve="P-384";hashName="SHA-384"}else if(crv===3&&alg===-36){namedCurve="P-521";hashName="SHA-512"}else{throw new Error(`Unsupported EC2 Curve (${crv}) or Algorithm (${alg})`)}const rawPoint=new Uint8Array(1+x.length+y.length);rawPoint[0]=4;rawPoint.set(x,1);rawPoint.set(y,1+x.length);const algorithmParams={name:"ECDSA",namedCurve};const cryptoKey=await crypto.subtle.importKey("raw",rawPoint,algorithmParams,true,["verify"]);return{kty,alg,cryptoKey,algorithmParams:{...algorithmParams,hash:{name:hashName}}}}if(kty===1){const crv=map.get(-1);const x=map.get(-2);if(crv!==6||alg!==-8||!(x instanceof Uint8Array)){throw new Error("Unsupported OKP key: only Ed25519 (crv: 6, alg: -8) is supported")}const algorithmParams={name:"Ed25519"};const cryptoKey=await crypto.subtle.importKey("raw",x,algorithmParams,true,["verify"]);return{kty,alg,cryptoKey,algorithmParams}}if(kty===3){const n=map.get(-1);const e=map.get(-2);if(!n||!e||!(n instanceof Uint8Array)||!(e instanceof Uint8Array)){throw new Error("Invalid COSE RSA Key: Missing or invalid modulus or exponent")}let hashName="SHA-256";let jwkAlg="RS256";if(alg===-258){hashName="SHA-384";jwkAlg="RS384"}else if(alg===-259){hashName="SHA-512";jwkAlg="RS512"}else if(alg===-37){hashName="SHA-256";jwkAlg="PS256"}const keyAlgo=alg===-37?"RSA-PSS":"RSASSA-PKCS1-v1_5";const jwk={kty:"RSA",alg:jwkAlg,n:bytesToBase64URL(n),e:bytesToBase64URL(e),ext:true};const algorithmParams={name:keyAlgo,hash:{name:hashName}};const cryptoKey=await crypto.subtle.importKey("jwk",jwk,algorithmParams,true,["verify"]);return{kty,alg,cryptoKey,algorithmParams}}throw new Error(`Unsupported COSE Key Type: ${kty}`)}function parseAuthenticatorExtensions(extensionBytes){const{value,rawSlice}=decodeCBORWithSlice(extensionBytes);if(!(value instanceof Map)){return{extensions:{},bytesConsumed:rawSlice.byteLength}}const extensions={raw:value};const credPropsVal=value.get("credProps");if(credPropsVal instanceof Map){extensions.credProps={rk:Boolean(credPropsVal.get("rk"))}}const prfVal=value.get("prf");if(prfVal instanceof Map){const resultsVal=prfVal.get("results");if(resultsVal instanceof Map){const first=resultsVal.get("first");const second=resultsVal.get("second");extensions.prf={enabled:true,results:{first:first instanceof Uint8Array?first:undefined,second:second instanceof Uint8Array?second:undefined}}}else{extensions.prf={enabled:Boolean(prfVal.get("enabled")??true)}}}const largeBlobVal=value.get("largeBlob");if(largeBlobVal instanceof Map){const blob=largeBlobVal.get("blob");const written=largeBlobVal.get("written");extensions.largeBlob={supported:Boolean(largeBlobVal.get("supported")??true),blob:blob instanceof Uint8Array?blob:undefined,written:typeof written==="boolean"?written:undefined}}const appIdVal=value.get("appid");if(typeof appIdVal==="boolean"){extensions.appId=appIdVal}return{extensions,bytesConsumed:rawSlice.byteLength}}class WebAuthnError extends Error{code;details;constructor(code,message,details){super(message);this.name="WebAuthnError";this.code=code;this.details=details;Object.setPrototypeOf(this,new.target.prototype)}}class ChallengeMismatchError extends WebAuthnError{constructor(expected,received){super("CHALLENGE_MISMATCH",`Challenge mismatch. Expected '${expected}', received '${received}'`,{expected,received});this.name="ChallengeMismatchError"}}class OriginMismatchError extends WebAuthnError{constructor(expected,received){super("ORIGIN_MISMATCH",`Origin mismatch. Received origin '${received}' is not allowed`,{expected,received});this.name="OriginMismatchError"}}class RpIdMismatchError extends WebAuthnError{constructor(expectedRpId){super("RP_ID_MISMATCH",`RP ID hash in authenticatorData does not match expected RP ID '${expectedRpId}'`,{expectedRpId});this.name="RpIdMismatchError"}}class UserPresenceError extends WebAuthnError{constructor(){super("USER_PRESENCE_NOT_SET","User Presence (UP) flag was not set by authenticator");this.name="UserPresenceError"}}class UserVerificationPolicyError extends WebAuthnError{constructor(){super("USER_VERIFICATION_FAILED","User Verification (UV) was required but flag was not set by authenticator");this.name="UserVerificationPolicyError"}}class CounterRollbackError extends WebAuthnError{constructor(storedCount,receivedCount){super("COUNTER_ROLLBACK",`Authenticator counter rollback detected! Potential cloned authenticator. Stored: ${storedCount}, Received: ${receivedCount}`,{storedCount,receivedCount});this.name="CounterRollbackError"}}class InvalidSignatureError extends WebAuthnError{constructor(message="Cryptographic signature is invalid"){super("INVALID_SIGNATURE",message);this.name="InvalidSignatureError"}}class MalformedPayloadError extends WebAuthnError{constructor(message,details){super("MALFORMED_PAYLOAD",message,details);this.name="MalformedPayloadError"}}function parseAuthenticatorData(authDataBytes){if(authDataBytes.byteLength<37){throw new MalformedPayloadError(`authenticatorData too short: Expected >= 37 bytes, got ${authDataBytes.byteLength}`)}const reader=new BinaryReader(authDataBytes);const rpIdHash=reader.readBytes(32);const flagByte=reader.readUint8();const flags={up:(flagByte&1)!==0,uv:(flagByte&4)!==0,be:(flagByte&8)!==0,bs:(flagByte&16)!==0,at:(flagByte&64)!==0,ed:(flagByte&128)!==0,raw:flagByte};const signCount=reader.readUint32();let aaguid;let credentialId;let credentialPublicKey;let extensions;if(flags.at){if(reader.remaining<18){throw new MalformedPayloadError("authenticatorData has AT flag set but lacks attested credential data")}aaguid=reader.readBytes(16);const credIdLen=reader.readUint16();if(reader.remaining<credIdLen){throw new MalformedPayloadError(`authenticatorData credentialId truncated: expected ${credIdLen} bytes, remaining ${reader.remaining}`)}credentialId=reader.readBytes(credIdLen);const remainingBytes=authDataBytes.subarray(reader.cursor);const{rawSlice}=decodeCBORWithSlice(remainingBytes);credentialPublicKey=rawSlice;reader.seek(reader.cursor+rawSlice.byteLength)}if(flags.ed&&reader.remaining>0){const extBytes=authDataBytes.subarray(reader.cursor);const{extensions:parsedExts,bytesConsumed}=parseAuthenticatorExtensions(extBytes);extensions=parsedExts;reader.seek(reader.cursor+bytesConsumed)}return{rpIdHash,flags,signCount,aaguid,credentialId,credentialPublicKey,extensions,raw:authDataBytes}}function formatAAGUID(aaguid){const hex=Array.from(aaguid,(b)=>b.toString(16).padStart(2,"0")).join("");return`${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`}async function parseAndVerifyClientData(clientDataInput,expectedType){let rawJson;let rawBytes;if(typeof clientDataInput==="string"){if(clientDataInput.trim().startsWith("{")){rawJson=clientDataInput;rawBytes=new TextEncoder().encode(rawJson)}else{rawBytes=base64URLToBytes(clientDataInput);rawJson=new TextDecoder("utf-8").decode(rawBytes)}}else{rawBytes=clientDataInput;rawJson=new TextDecoder("utf-8").decode(rawBytes)}let parsed;try{parsed=JSON.parse(rawJson)}catch{throw new MalformedPayloadError("clientDataJSON is not valid JSON")}if(parsed.type!==expectedType){throw new MalformedPayloadError(`clientDataJSON type mismatch: expected '${expectedType}', received '${parsed.type}'`)}if(!parsed.challenge||typeof parsed.challenge!=="string"){throw new MalformedPayloadError("clientDataJSON missing challenge string")}if(!parsed.origin||typeof parsed.origin!=="string"){throw new MalformedPayloadError("clientDataJSON missing origin string")}const digestBuffer=await crypto.subtle.digest("SHA-256",rawBytes);const sha256=new Uint8Array(digestBuffer);return{type:parsed.type,challenge:parsed.challenge,origin:parsed.origin,crossOrigin:parsed.crossOrigin,rawJson,sha256}}function derToIEEEP1363(derSignature,keyCoordinateLength=32){if(derSignature.length===keyCoordinateLength*2){return derSignature}const reader=new BinaryReader(derSignature);const seqTag=reader.readUint8();if(seqTag!==48){throw new MalformedPayloadError(`Invalid DER signature: Expected SEQUENCE tag 0x30, got 0x${seqTag.toString(16)}`)}const seqLength=readDerLength(reader);if(seqLength>reader.remaining){throw new MalformedPayloadError("Invalid DER signature: SEQUENCE length exceeds available buffer")}const rBytes=readDerInteger(reader);const sBytes=readDerInteger(reader);const p1363=new Uint8Array(keyCoordinateLength*2);copyPaddedInteger(rBytes,p1363,0,keyCoordinateLength);copyPaddedInteger(sBytes,p1363,keyCoordinateLength,keyCoordinateLength);return p1363}function readDerLength(reader){const initial=reader.readUint8();if((initial&128)===0)return initial;const numOctets=initial&127;if(numOctets===1)return reader.readUint8();if(numOctets===2)return reader.readUint16();throw new MalformedPayloadError(`Unsupported DER multi-byte length: ${numOctets} octets`)}function readDerInteger(reader){const tag=reader.readUint8();if(tag!==2){throw new MalformedPayloadError(`Invalid DER signature: Expected INTEGER tag 0x02, got 0x${tag.toString(16)}`)}const length=readDerLength(reader);return reader.readBytes(length)}function copyPaddedInteger(source,target,targetOffset,targetLength){let src=source;while(src.length>targetLength&&src[0]===0){src=src.subarray(1)}if(src.length>targetLength){throw new MalformedPayloadError(`DER integer length (${src.length}) exceeds maximum expected coordinate length (${targetLength})`)}const paddingOffset=targetLength-src.length;target.fill(0,targetOffset,targetOffset+paddingOffset);target.set(src,targetOffset+paddingOffset)}async function verifyOrigin(receivedOrigin,expectedOrigin,allowCrossOrigin=false,crossOriginFlag){if(!allowCrossOrigin&&crossOriginFlag===true){throw new OriginMismatchError(Array.isArray(expectedOrigin)?expectedOrigin:typeof expectedOrigin==="string"?expectedOrigin:"custom validator",`${receivedOrigin} (crossOrigin flag was true)`)}if(typeof expectedOrigin==="function"){const isValid=await expectedOrigin(receivedOrigin);if(!isValid){throw new OriginMismatchError("custom validation predicate",receivedOrigin)}return}const allowedList=Array.isArray(expectedOrigin)?expectedOrigin:[expectedOrigin];const matched=allowedList.some((allowed)=>{if(allowed===receivedOrigin)return true;const normAllowed=allowed.replace(/\/+$/,"");const normReceived=receivedOrigin.replace(/\/+$/,"");if(normAllowed===normReceived)return true;if(normAllowed.startsWith("android:apk-key-hash:")&&normReceived===normAllowed){return true}if(normAllowed.startsWith("webauthn.bundle:")&&normReceived===normAllowed){return true}return false});if(!matched){throw new OriginMismatchError(allowedList,receivedOrigin)}}async function verifyRegistration(options){const{response,expectedChallenge,expectedOrigin,expectedRpId,requireUserVerification=false,allowCrossOrigin=false}=options;const clientData=await parseAndVerifyClientData(response.response.clientDataJSON,"webauthn.create");const expectedChallengeBytes=typeof expectedChallenge==="string"?new TextEncoder().encode(expectedChallenge):expectedChallenge;const clientChallengeBytes=new TextEncoder().encode(clientData.challenge);if(!timingSafeEqual(expectedChallengeBytes,clientChallengeBytes)&&expectedChallenge!==clientData.challenge){throw new ChallengeMismatchError(expectedChallenge,clientData.challenge)}await verifyOrigin(clientData.origin,expectedOrigin,allowCrossOrigin,clientData.crossOrigin);const attestationInput=response.response.attestationObject;const attestationBytes=typeof attestationInput==="string"?base64URLToBytes(attestationInput):attestationInput;const attestationMap=decodeCBOR(attestationBytes);if(!(attestationMap instanceof Map)){throw new MalformedPayloadError("Invalid attestationObject: Expected CBOR Map")}const rawAuthData=attestationMap.get("authData");if(!rawAuthData||!(rawAuthData instanceof Uint8Array)){throw new MalformedPayloadError("Invalid attestationObject: Missing authData bytes")}const authData=parseAuthenticatorData(rawAuthData);const expectedRpIdHash=new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(expectedRpId)));if(!timingSafeEqual(expectedRpIdHash,authData.rpIdHash)){throw new RpIdMismatchError(expectedRpId)}if(!authData.flags.up){throw new UserPresenceError}if(requireUserVerification&&!authData.flags.uv){throw new UserVerificationPolicyError}if(!authData.flags.at||!authData.credentialId||!authData.credentialPublicKey||!authData.aaguid){throw new MalformedPayloadError("Attested credential data missing in registration response")}const credentialIdB64=bytesToBase64URL(authData.credentialId);const aaguidFormatted=formatAAGUID(authData.aaguid);return{verified:true,credential:{id:credentialIdB64,publicKey:authData.credentialPublicKey,aaguid:aaguidFormatted,signCount:authData.signCount,backupEligible:authData.flags.be,backupState:authData.flags.bs},extensions:authData.extensions}}async function verifyAuthentication(options){const{response,expectedChallenge,expectedOrigin,expectedRpId,credentialPublicKey,prevSignCount=0,requireUserVerification=false,allowCrossOrigin=false}=options;const clientData=await parseAndVerifyClientData(response.response.clientDataJSON,"webauthn.get");const expectedChallengeBytes=typeof expectedChallenge==="string"?new TextEncoder().encode(expectedChallenge):expectedChallenge;const clientChallengeBytes=new TextEncoder().encode(clientData.challenge);if(!timingSafeEqual(expectedChallengeBytes,clientChallengeBytes)&&expectedChallenge!==clientData.challenge){throw new ChallengeMismatchError(expectedChallenge,clientData.challenge)}await verifyOrigin(clientData.origin,expectedOrigin,allowCrossOrigin,clientData.crossOrigin);const authDataInput=response.response.authenticatorData;const authDataBytes=typeof authDataInput==="string"?base64URLToBytes(authDataInput):authDataInput;const authData=parseAuthenticatorData(authDataBytes);const expectedRpIdHash=new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(expectedRpId)));if(!timingSafeEqual(expectedRpIdHash,authData.rpIdHash)){throw new RpIdMismatchError(expectedRpId)}if(!authData.flags.up){throw new UserPresenceError}if(requireUserVerification&&!authData.flags.uv){throw new UserVerificationPolicyError}if(authData.signCount>0||prevSignCount>0){if(authData.signCount<=prevSignCount&&!authData.flags.be){throw new CounterRollbackError(prevSignCount,authData.signCount)}}const signedData=new Uint8Array(authDataBytes.byteLength+clientData.sha256.byteLength);signedData.set(authDataBytes,0);signedData.set(clientData.sha256,authDataBytes.byteLength);const parsedKey=await importCOSEPublicKey(credentialPublicKey);const rawSigInput=response.response.signature;const rawSigBytes=typeof rawSigInput==="string"?base64URLToBytes(rawSigInput):rawSigInput;let normalizedSig=rawSigBytes;if(parsedKey.alg===-7||parsedKey.alg===-35||parsedKey.alg===-36){const coordLen=parsedKey.alg===-35?48:parsedKey.alg===-36?66:32;normalizedSig=derToIEEEP1363(rawSigBytes,coordLen)}const isValid=await crypto.subtle.verify(parsedKey.algorithmParams,parsedKey.cryptoKey,normalizedSig,signedData);if(!isValid){throw new InvalidSignatureError("Authentication signature verification failed")}return{verified:true,newSignCount:authData.signCount,userPresent:authData.flags.up,userVerified:authData.flags.uv,backupState:authData.flags.bs,extensions:authData.extensions}}function generateRegistrationOptions(args){const{rpName,rpId,userName,userDisplayName=userName.split("@")[0],timeout=60000,attestationType="none",residentKey="preferred",userVerification="preferred",authenticatorAttachment,supportedAlgorithms=[-7,-8,-257],excludeCredentials,extensions}=args;let challengeStr;if(!args.challenge){const raw=new Uint8Array(32);crypto.getRandomValues(raw);challengeStr=bytesToBase64URL(raw)}else if(typeof args.challenge==="string"){challengeStr=args.challenge}else{challengeStr=bytesToBase64URL(args.challenge)}let userIdStr;if(!args.userID){const rawId=new Uint8Array(16);crypto.getRandomValues(rawId);userIdStr=bytesToBase64URL(rawId)}else if(typeof args.userID==="string"){userIdStr=bytesToBase64URL(new TextEncoder().encode(args.userID))}else{userIdStr=bytesToBase64URL(args.userID)}const pubKeyCredParams=supportedAlgorithms.map((alg)=>({type:"public-key",alg}));const authenticatorSelection={residentKey,userVerification};if(authenticatorAttachment){authenticatorSelection.authenticatorAttachment=authenticatorAttachment}const options={challenge:challengeStr,rp:{name:rpName,id:rpId},user:{id:userIdStr,name:userName,displayName:userDisplayName},pubKeyCredParams,timeout,attestation:attestationType,authenticatorSelection};if(excludeCredentials&&excludeCredentials.length>0){options.excludeCredentials=excludeCredentials.map((cred)=>({id:typeof cred.id==="string"?cred.id:bytesToBase64URL(cred.id),type:"public-key",transports:cred.transports}))}if(extensions){options.extensions=extensions}return options}function generateAuthenticationOptions(args){const{rpId,timeout=60000,userVerification="preferred",allowCredentials,extensions}=args;let challengeStr;if(!args.challenge){const raw=new Uint8Array(32);crypto.getRandomValues(raw);challengeStr=bytesToBase64URL(raw)}else if(typeof args.challenge==="string"){challengeStr=args.challenge}else{challengeStr=bytesToBase64URL(args.challenge)}const options={challenge:challengeStr,rpId,timeout,userVerification};if(allowCredentials&&allowCredentials.length>0){options.allowCredentials=allowCredentials.map((cred)=>({id:typeof cred.id==="string"?cred.id:bytesToBase64URL(cred.id),type:"public-key",transports:cred.transports}))}if(extensions){options.extensions=extensions}return options}export{BinaryReader,COSEAlgorithm,COSECurve,COSEKeyType,ChallengeMismatchError,CounterRollbackError,InvalidSignatureError,MalformedPayloadError,OriginMismatchError,RpIdMismatchError,UserPresenceError,UserVerificationPolicyError,WebAuthnError,base64URLToBytes,bytesToBase64URL,decodeCBOR,decodeCBORWithSlice,derToIEEEP1363,formatAAGUID,generateAuthenticationOptions,generateRegistrationOptions,importCOSEPublicKey,parseAndVerifyClientData,parseAuthenticatorData,parseAuthenticatorExtensions,timingSafeEqual,verifyAuthentication,verifyOrigin,verifyRegistration};
@@ -0,0 +1,9 @@
1
+ import type { ParsedAuthData } from "@/types";
2
+ /**
3
+ * Parses raw binary authenticatorData, unpacking flags, counters, and credential structures.
4
+ */
5
+ export declare function parseAuthenticatorData(authDataBytes: Uint8Array): ParsedAuthData;
6
+ /**
7
+ * Formats a 16-byte AAGUID into a canonical UUID string (8-4-4-4-12).
8
+ */
9
+ export declare function formatAAGUID(aaguid: Uint8Array): string;
@@ -0,0 +1,5 @@
1
+ import type { ParsedClientData } from "@/types";
2
+ /**
3
+ * Validates the clientDataJSON string and computes its SHA-256 hash.
4
+ */
5
+ export declare function parseAndVerifyClientData(clientDataInput: string | Uint8Array, expectedType: "webauthn.create" | "webauthn.get"): Promise<ParsedClientData>;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Normalizes an ASN.1 DER ECDSA signature into a fixed-length IEEE P1363 (r || s) byte array.
3
+ */
4
+ export declare function derToIEEEP1363(derSignature: Uint8Array, keyCoordinateLength?: number): Uint8Array;
@@ -0,0 +1,29 @@
1
+ import { CBORValue } from "@/binary";
2
+ export interface CredentialPropertiesExtension {
3
+ rk?: boolean;
4
+ }
5
+ export interface PRFExtensionResults {
6
+ first?: Uint8Array;
7
+ second?: Uint8Array;
8
+ }
9
+ export interface ParsedExtensions {
10
+ credProps?: CredentialPropertiesExtension;
11
+ prf?: {
12
+ enabled?: boolean;
13
+ results?: PRFExtensionResults;
14
+ };
15
+ largeBlob?: {
16
+ supported?: boolean;
17
+ blob?: Uint8Array;
18
+ written?: boolean;
19
+ };
20
+ appId?: boolean;
21
+ raw?: Map<string | number, CBORValue>;
22
+ }
23
+ /**
24
+ * Parses trailing CBOR extension map from authenticatorData when the ED flag is present.
25
+ */
26
+ export declare function parseAuthenticatorExtensions(extensionBytes: Uint8Array): {
27
+ extensions: ParsedExtensions;
28
+ bytesConsumed: number;
29
+ };
@@ -0,0 +1,5 @@
1
+ export * from "./authData";
2
+ export * from "./clientData";
3
+ export * from "./der";
4
+ export * from "./origin";
5
+ export * from "./extensions";
@@ -0,0 +1,5 @@
1
+ export type ExpectedOrigin = string | string[] | ((origin: string) => boolean | Promise<boolean>);
2
+ /**
3
+ * Validates the clientData origin against allowed origins, wildcards, or mobile app facet identifiers.
4
+ */
5
+ export declare function verifyOrigin(receivedOrigin: string, expectedOrigin: ExpectedOrigin, allowCrossOrigin?: boolean, crossOriginFlag?: boolean): Promise<void>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Supported COSE algorithm identifiers.
3
+ */
4
+ export declare enum COSEAlgorithm {
5
+ ES256 = -7,
6
+ EdDSA = -8,
7
+ ES384 = -35,
8
+ ES512 = -36,
9
+ RS256 = -257,
10
+ RS384 = -258,
11
+ RS512 = -259,
12
+ PS256 = -37
13
+ }
14
+ /**
15
+ * Supported COSE key types.
16
+ */
17
+ export declare enum COSEKeyType {
18
+ OKP = 1,
19
+ EC2 = 2,
20
+ RSA = 3
21
+ }
22
+ /**
23
+ * Supported COSE elliptic curves.
24
+ */
25
+ export declare enum COSECurve {
26
+ P256 = 1,
27
+ P384 = 2,
28
+ P521 = 3,
29
+ Ed25519 = 6
30
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Bitfield flags parsed from authenticator data.
3
+ */
4
+ export interface AuthenticatorFlags {
5
+ up: boolean;
6
+ uv: boolean;
7
+ be: boolean;
8
+ bs: boolean;
9
+ at: boolean;
10
+ ed: boolean;
11
+ raw: number;
12
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./cose";
2
+ export * from "./flags";
3
+ export * from "./models";
@@ -0,0 +1,103 @@
1
+ import type { AuthenticatorFlags } from "./flags";
2
+ import type { COSEAlgorithm, COSEKeyType } from "./cose";
3
+ import type { ParsedExtensions } from "@/protocol/extensions";
4
+ import type { ExpectedOrigin } from "@/protocol/origin";
5
+ /**
6
+ * Parsed binary authenticator data structure.
7
+ */
8
+ export interface ParsedAuthData {
9
+ rpIdHash: Uint8Array;
10
+ flags: AuthenticatorFlags;
11
+ signCount: number;
12
+ aaguid?: Uint8Array;
13
+ credentialId?: Uint8Array;
14
+ credentialPublicKey?: Uint8Array;
15
+ extensions?: ParsedExtensions;
16
+ raw: Uint8Array;
17
+ }
18
+ /**
19
+ * Parsed and validated clientDataJSON payload.
20
+ */
21
+ export interface ParsedClientData {
22
+ type: string;
23
+ challenge: string;
24
+ origin: string;
25
+ crossOrigin?: boolean;
26
+ rawJson: string;
27
+ sha256: Uint8Array;
28
+ }
29
+ /**
30
+ * Imported WebCrypto public key with associated COSE metadata.
31
+ */
32
+ export interface ParsedCOSEKey {
33
+ kty: COSEKeyType;
34
+ alg: COSEAlgorithm;
35
+ cryptoKey: CryptoKey;
36
+ algorithmParams: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams;
37
+ }
38
+ /**
39
+ * Input parameters for verifying a WebAuthn registration response.
40
+ */
41
+ export interface RegistrationVerificationOptions {
42
+ response: {
43
+ id: string;
44
+ rawId: string;
45
+ response: {
46
+ clientDataJSON: string | Uint8Array;
47
+ attestationObject: string | Uint8Array;
48
+ };
49
+ };
50
+ expectedChallenge: string;
51
+ expectedOrigin: ExpectedOrigin;
52
+ expectedRpId: string;
53
+ requireUserVerification?: boolean;
54
+ allowCrossOrigin?: boolean;
55
+ }
56
+ /**
57
+ * Verification outcome containing the newly enrolled credential.
58
+ */
59
+ export interface RegistrationResult {
60
+ verified: boolean;
61
+ credential: {
62
+ id: string;
63
+ publicKey: Uint8Array;
64
+ aaguid: string;
65
+ signCount: number;
66
+ backupEligible: boolean;
67
+ backupState: boolean;
68
+ };
69
+ extensions?: ParsedExtensions;
70
+ }
71
+ /**
72
+ * Input parameters for verifying a WebAuthn authentication assertion response.
73
+ */
74
+ export interface AuthenticationVerificationOptions {
75
+ response: {
76
+ id: string;
77
+ rawId: string;
78
+ response: {
79
+ clientDataJSON: string | Uint8Array;
80
+ authenticatorData: string | Uint8Array;
81
+ signature: string | Uint8Array;
82
+ userHandle?: string;
83
+ };
84
+ };
85
+ expectedChallenge: string;
86
+ expectedOrigin: ExpectedOrigin;
87
+ expectedRpId: string;
88
+ credentialPublicKey: Uint8Array;
89
+ prevSignCount?: number;
90
+ requireUserVerification?: boolean;
91
+ allowCrossOrigin?: boolean;
92
+ }
93
+ /**
94
+ * Verification outcome of an authentication assertion.
95
+ */
96
+ export interface AuthenticationResult {
97
+ verified: boolean;
98
+ newSignCount: number;
99
+ userPresent: boolean;
100
+ userVerified: boolean;
101
+ backupState: boolean;
102
+ extensions?: ParsedExtensions;
103
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@muljax/edge-passkey",
3
+ "version": "1.0.0",
4
+ "description": "Zero-dependency, high-performance WebAuthn / Passkey verification engine for edge runtimes (Cloudflare Workers, Bun, Deno, Node)",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/muljax/edge-passkey.git"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/index.js",
16
+ "types": "./dist/index.d.ts"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "provenance": true
27
+ },
28
+ "scripts": {
29
+ "dev": "bun --watch server/index.ts",
30
+ "start": "bun server/index.ts",
31
+ "build": "bun build ./src/index.ts --outdir ./dist --target browser --minify-whitespace && tsc -p tsconfig.build.json",
32
+ "prepublishOnly": "bun test && bun run build",
33
+ "test": "bun test",
34
+ "bench": "bun benchmarks/benchmark.ts",
35
+ "typecheck": "tsc --noEmit"
36
+ },
37
+ "keywords": [
38
+ "webauthn",
39
+ "passkey",
40
+ "fido2",
41
+ "edge",
42
+ "cloudflare-workers",
43
+ "bun",
44
+ "webcrypto",
45
+ "zero-dependency"
46
+ ],
47
+ "author": "",
48
+ "license": "MPL-2.0",
49
+ "devDependencies": {
50
+ "@simplewebauthn/server": "^14.0.3",
51
+ "@types/bun": "latest",
52
+ "typescript": "^5.5.0"
53
+ }
54
+ }