@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 +297 -0
- package/README.md +154 -0
- package/dist/binary/base64url.d.ts +12 -0
- package/dist/binary/cbor.d.ts +12 -0
- package/dist/binary/index.d.ts +3 -0
- package/dist/binary/reader.d.ts +19 -0
- package/dist/core/authentication.d.ts +5 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/registration.d.ts +5 -0
- package/dist/cose/coseParser.d.ts +5 -0
- package/dist/cose/index.d.ts +1 -0
- package/dist/errors/index.d.ts +30 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/options.d.ts +80 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/protocol/authData.d.ts +9 -0
- package/dist/protocol/clientData.d.ts +5 -0
- package/dist/protocol/der.d.ts +4 -0
- package/dist/protocol/extensions.d.ts +29 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/origin.d.ts +5 -0
- package/dist/types/cose.d.ts +30 -0
- package/dist/types/flags.d.ts +12 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/models.d.ts +103 -0
- package/package.json +54 -0
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
|
+
[](#)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](#)
|
|
8
|
+
[](#)
|
|
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,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 @@
|
|
|
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;
|
package/dist/index.d.ts
ADDED
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,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 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,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
|
+
}
|