@flun/webauthn-browser 2.0.2

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/dist/index.js ADDED
@@ -0,0 +1,270 @@
1
+ /* 基于[@simplewebauthn/browser@13.3.0] 开发 */
2
+ !function (e, t) {
3
+ "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd
4
+ ? define(["exports"], t) : t((e = "undefined" != typeof globalThis ? globalThis : e || self).flunWebAuthnBrowser = {})
5
+ }(this, e => {
6
+ "use strict";
7
+
8
+ const defaultPropDescriptor = { enumerable: true, configurable: true, writable: true, value: void 0 },
9
+ falsePromise = Promise.resolve(false),
10
+ // 将 ArrayBuffer 转换为 Base64URL 字符串
11
+ bufferToBase64URLString = (e) => {
12
+ const t = new Uint8Array(e);
13
+ let r = "";
14
+ for (const e of t) r += String.fromCharCode(e);
15
+ return btoa(r).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
16
+ },
17
+ // 将 Base64URL 字符串转换为 ArrayBuffer
18
+ base64URLStringToBuffer = (e) => {
19
+ const t = e.replace(/-/g, "+").replace(/_/g, "/"), r = (4 - t.length % 4) % 4, n = t.padEnd(t.length + r, "="),
20
+ o = atob(n), i = new ArrayBuffer(o.length), a = new Uint8Array(i);
21
+ for (let e = 0; e < o.length; e++) a[e] = o.charCodeAt(e);
22
+ return i
23
+ },
24
+ o = { stubThis: e => e },
25
+ // 检查浏览器是否支持 WebAuthn
26
+ browserSupportsWebAuthn = () =>
27
+ o.stubThis(void 0 !== globalThis?.PublicKeyCredential && "function" == typeof globalThis.PublicKeyCredential),
28
+ // 转换允许凭证中的 ID 字段
29
+ convertAllowCredential = e => {
30
+ const { id: t } = e;
31
+ return { ...e, id: base64URLStringToBuffer(t), transports: e.transports }
32
+ },
33
+ // 验证域名有效性
34
+ isValidDomain = e =>
35
+ "localhost" === e || /^((xn--[a-z0-9-]+|[a-z0-9]+(-[a-z0-9]+)*)\.)+([a-z]{2,}|xn--[a-z0-9-]+)$/i.test(e),
36
+
37
+ // WebAuthn 中止服务(用于取消进行中的认证/注册)
38
+ WebAuthnAbortService = new class {
39
+ constructor() {
40
+ Object.defineProperty(this, "controller", defaultPropDescriptor)
41
+ }
42
+ createNewAbortSignal() {
43
+ if (this.controller) {
44
+ const e = new Error("正在取消已有的 WebAuthn API 调用,然后进行新的调用");
45
+ e.name = "AbortError", this.controller.abort(e)
46
+ }
47
+ const e = new AbortController;
48
+ return this.controller = e, e.signal
49
+ }
50
+ cancelCeremony() {
51
+ if (this.controller) {
52
+ const e = new Error("正在取消 WebAuthn API 调用");
53
+ e.name = "AbortError", this.controller.abort(e), this.controller = void 0
54
+ }
55
+ }
56
+ },
57
+
58
+ authenticatorAttachmentValues = ["cross-platform", "platform"],
59
+ normalizeAuthenticatorAttachment = (e) => {
60
+ if (e && !(authenticatorAttachmentValues.indexOf(e) < 0)) return e
61
+ },
62
+ warnExtensionMishandling = (e, t) => {
63
+ console.warn(`拦截此 WebAuthn API 调用的浏览器扩展错误地实现了 ${e};请向扩展开发者报告此问题;\n`, t)
64
+ },
65
+ p = { stubThis: e => e },
66
+ // 检查浏览器是否支持 WebAuthn 自动填充
67
+ browserSupportsWebAuthnAutofill = () => {
68
+ if (!browserSupportsWebAuthn()) return p.stubThis(falsePromise);
69
+ const e = globalThis.PublicKeyCredential;
70
+ return void 0 === e?.isConditionalMediationAvailable ? p.stubThis(falsePromise)
71
+ : p.stubThis(e.isConditionalMediationAvailable())
72
+ };
73
+
74
+ // 自定义 WebAuthn 错误类
75
+ class WebAuthnError extends Error {
76
+ constructor({ message: e, code: t, cause: r, name: n }) {
77
+ super(e, { cause: r });
78
+ Object.defineProperty(this, "code", defaultPropDescriptor), this.name = n ?? r.name, this.code = t
79
+ }
80
+ };
81
+
82
+ // 导出内部工具(供测试/扩展使用)
83
+ e.WebAuthnAbortService = WebAuthnAbortService;
84
+ e._browserSupportsWebAuthnAutofillInternals = p;
85
+ e._browserSupportsWebAuthnInternals = o;
86
+ e.base64URLStringToBuffer = base64URLStringToBuffer;
87
+ e.bufferToBase64URLString = bufferToBase64URLString;
88
+ e.browserSupportsWebAuthn = browserSupportsWebAuthn;
89
+ e.browserSupportsWebAuthnAutofill = browserSupportsWebAuthnAutofill;
90
+ e.platformAuthenticatorIsAvailable = () => {
91
+ return browserSupportsWebAuthn() ? PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() : falsePromise;
92
+ };
93
+ e.WebAuthnError = WebAuthnError;
94
+
95
+ // 开始认证(登录)
96
+ e.startAuthentication = async e => {
97
+ if (!e.optionsJSON && e.challenge) {
98
+ console.warn(`startAuthentication() 调用方式不正确;将尝试继续使用提供的选项,但建议按照正确的调用结构重构;`);
99
+ e = { optionsJSON: e }
100
+ }
101
+ const { optionsJSON: o, useBrowserAutofill: c = !1, verifyBrowserAutofillInput: d = !0 } = e;
102
+ if (!browserSupportsWebAuthn()) throw new Error("当前浏览器不支持 WebAuthn");
103
+ let p, R;
104
+ if (0 !== o.allowCredentials?.length) p = o.allowCredentials?.map(convertAllowCredential);
105
+ const f = { ...o, challenge: base64URLStringToBuffer(o.challenge), allowCredentials: p }, b = {};
106
+ if (c) {
107
+ if (!await browserSupportsWebAuthnAutofill()) throw new Error("当前浏览器不支持 WebAuthn 自动填充");
108
+ if (document.querySelectorAll("input[autocomplete$='webauthn']").length < 1 && d)
109
+ throw new Error('未检测到任何 `autocomplete` 属性值以 "webauthn" 结尾的 <input> 元素');
110
+ b.mediation = "conditional", f.allowCredentials = []
111
+ }
112
+ b.publicKey = f, b.signal = WebAuthnAbortService.createNewAbortSignal();
113
+ try {
114
+ R = await navigator.credentials.get(b)
115
+ } catch (error) {
116
+ throw (({ error: e, options: t }) => {
117
+ const { publicKey: r } = t;
118
+ if (!r) throw new Error("options 缺少必需的 publicKey 属性");
119
+ if ("AbortError" === e.name) {
120
+ if (t.signal instanceof AbortSignal) return new WebAuthnError({
121
+ message: "认证流程收到了中止信号", code: "ERROR_CEREMONY_ABORTED", cause: e
122
+ })
123
+ } else {
124
+ if ("NotAllowedError" === e.name) return new WebAuthnError({
125
+ message: e.message, code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY", cause: e
126
+ });
127
+ if ("SecurityError" === e.name) {
128
+ const t = globalThis.location.hostname;
129
+ if (!isValidDomain(t)) return new WebAuthnError({
130
+ message: `${globalThis.location.hostname} 是无效域名`, code: "ERROR_INVALID_DOMAIN", cause: e
131
+ });
132
+ if (r.rpId !== t) return new WebAuthnError({
133
+ message: `RP ID "${r.rpId}" 对于当前域名无效`, code: "ERROR_INVALID_RP_ID", cause: e
134
+ })
135
+ } else if ("UnknownError" === e.name) return new WebAuthnError({
136
+ message: "验证器无法处理指定的选项或创建新的断言签名", code: "ERROR_AUTHENTICATOR_GENERAL_ERROR", cause: e
137
+ })
138
+ }
139
+ return e
140
+ })({ error, options: b })
141
+ }
142
+ if (!R) throw new Error("认证未完成");
143
+ const { id: g, rawId: w, response: A, type: E } = R;
144
+ let m;
145
+ if (A.userHandle) m = bufferToBase64URLString(A.userHandle);
146
+ return {
147
+ id: g,
148
+ rawId: bufferToBase64URLString(w),
149
+ response: {
150
+ authenticatorData: bufferToBase64URLString(A.authenticatorData),
151
+ clientDataJSON: bufferToBase64URLString(A.clientDataJSON),
152
+ signature: bufferToBase64URLString(A.signature),
153
+ userHandle: m
154
+ },
155
+ type: E,
156
+ clientExtensionResults: R.getClientExtensionResults(),
157
+ authenticatorAttachment: normalizeAuthenticatorAttachment(R.authenticatorAttachment)
158
+ }
159
+ };
160
+
161
+ // 开始注册(创建凭证)
162
+ e.startRegistration = async e => {
163
+ if (!e.optionsJSON && e.challenge) {
164
+ console.warn("startRegistration() 调用方式不正确;将尝试继续使用提供的选项,但建议按照正确的调用结构重构;");
165
+ e = { optionsJSON: e }
166
+ }
167
+ const { optionsJSON: o, useAutoRegister: c = !1 } = e;
168
+ if (!browserSupportsWebAuthn()) throw new Error("当前浏览器不支持 WebAuthn");
169
+ const h = {
170
+ ...o, challenge: base64URLStringToBuffer(o.challenge),
171
+ user: {
172
+ ...o.user, id: base64URLStringToBuffer(o.user.id)
173
+ },
174
+ excludeCredentials: o.excludeCredentials?.map(convertAllowCredential)
175
+ }, p = {};
176
+ let f;
177
+ if (c) p.mediation = "conditional";
178
+ p.publicKey = h, p.signal = WebAuthnAbortService.createNewAbortSignal();
179
+ try {
180
+ f = await navigator.credentials.create(p)
181
+ } catch (error) {
182
+ throw (({ error: e, options: t }) => {
183
+ const { publicKey: r } = t;
184
+ if (!r) throw new Error("options 缺少必需的 publicKey 属性");
185
+ if ("AbortError" === e.name) {
186
+ if (t.signal instanceof AbortSignal) return new WebAuthnError({
187
+ message: "注册流程收到了中止信号", code: "ERROR_CEREMONY_ABORTED", cause: e
188
+ })
189
+ } else if ("ConstraintError" === e.name) {
190
+ if (!0 === r.authenticatorSelection?.requireResidentKey) return new WebAuthnError({
191
+ message: "要求使用可发现凭证,但可用的验证器不支持",
192
+ code: "ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT", cause: e
193
+ });
194
+ if ("conditional" === t.mediation && "required" === r.authenticatorSelection?.userVerification)
195
+ return new WebAuthnError({
196
+ message: "自动注册时要求用户验证,但无法执行",
197
+ code: "ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE", cause: e
198
+ });
199
+ if ("required" === r.authenticatorSelection?.userVerification) return new WebAuthnError({
200
+ message: "要求用户验证,但可用的验证器不支持",
201
+ code: "ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT", cause: e
202
+ })
203
+ } else {
204
+ if ("InvalidStateError" === e.name) return new WebAuthnError({
205
+ message: "该验证器已被注册过", code: "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED", cause: e
206
+ });
207
+ if ("NotAllowedError" === e.name) return new WebAuthnError({
208
+ message: e.message, code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY", cause: e
209
+ });
210
+ if ("NotSupportedError" === e.name) {
211
+ if (0 === r.pubKeyCredParams.filter((e => "public-key" === e.type)).length) return new WebAuthnError({
212
+ message: 'pubKeyCredParams 中没有 type 为 "public-key" 的条目',
213
+ code: "ERROR_MALFORMED_PUBKEYCREDPARAMS", cause: e
214
+ });
215
+ return new WebAuthnError({
216
+ message: "没有可用的验证器支持指定的任何 pubKeyCredParams 算法",
217
+ code: "ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG", cause: e
218
+ })
219
+ }
220
+ if ("SecurityError" === e.name) {
221
+ const t = globalThis.location.hostname;
222
+ if (!isValidDomain(t)) return new WebAuthnError({
223
+ message: `${globalThis.location.hostname} 是无效域名`, code: "ERROR_INVALID_DOMAIN", cause: e
224
+ });
225
+ if (r.rp.id !== t) return new WebAuthnError({
226
+ message: `RP ID "${r.rp.id}" 对于当前域名无效`, code: "ERROR_INVALID_RP_ID", cause: e
227
+ })
228
+ } else if ("TypeError" === e.name) {
229
+ if (r.user.id.byteLength < 1 || r.user.id.byteLength > 64) return new WebAuthnError({
230
+ message: "用户 ID 长度不在1~64字节之间", code: "ERROR_INVALID_USER_ID_LENGTH", cause: e
231
+ })
232
+ } else if ("UnknownError" === e.name) return new WebAuthnError({
233
+ message: "验证器无法处理指定的选项或创建新的凭证", code: "ERROR_AUTHENTICATOR_GENERAL_ERROR", cause: e
234
+ })
235
+ }
236
+ return e
237
+ })({ error, options: p })
238
+ }
239
+ if (!f) throw new Error("注册未完成");
240
+ const { id: b, rawId: R, response: g, type: w } = f;
241
+ let A, E, m, y;
242
+ if ("function" == typeof g.getTransports) A = g.getTransports();
243
+ if ("function" == typeof g.getPublicKeyAlgorithm) {
244
+ try { E = g.getPublicKeyAlgorithm() }
245
+ catch (e) { warnExtensionMishandling("getPublicKeyAlgorithm()", e) }
246
+ }
247
+ if ("function" == typeof g.getPublicKey) {
248
+ try {
249
+ const e = g.getPublicKey();
250
+ null !== e && (m = bufferToBase64URLString(e))
251
+ } catch (e) { warnExtensionMishandling("getPublicKey()", e) }
252
+ }
253
+ if ("function" == typeof g.getAuthenticatorData) {
254
+ try { y = bufferToBase64URLString(g.getAuthenticatorData()) }
255
+ catch (e) { warnExtensionMishandling("getAuthenticatorData()", e) }
256
+ }
257
+ return {
258
+ id: b,
259
+ rawId: bufferToBase64URLString(R),
260
+ response: {
261
+ attestationObject: bufferToBase64URLString(g.attestationObject),
262
+ clientDataJSON: bufferToBase64URLString(g.clientDataJSON),
263
+ transports: A, publicKeyAlgorithm: E, publicKey: m, authenticatorData: y
264
+ },
265
+ type: w,
266
+ clientExtensionResults: f.getClientExtensionResults(),
267
+ authenticatorAttachment: normalizeAuthenticatorAttachment(f.authenticatorAttachment)
268
+ }
269
+ }
270
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 将 Base64URL 编码的字符串转换为数组缓冲区; 最适合在将凭据 ID 从 JSON 字符串转换为 ArrayBuffer 时使用,
3
+ * 例如在 allowCredentials 或 excludeCredentials 中;
4
+ * 与 `bufferToBase64URLString` 配对使用;
5
+ * - 查看定义:@see {@link base64URLStringToBuffer}
6
+ * @param {string} base64URLString - Base64URL 编码的字符串
7
+ * @returns {ArrayBuffer} 转换后的 ArrayBuffer
8
+ */
9
+ const base64URLStringToBuffer = base64URLString => {
10
+ // 从 Base64URL 转换为标准 Base64
11
+ const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/'),
12
+ /**
13
+ * 用 '=' 填充,直到长度是 4 的倍数
14
+ * (4 - (85 % 4 = 1) = 3) % 4 = 3 个填充
15
+ * (4 - (86 % 4 = 2) = 2) % 4 = 2 个填充
16
+ * (4 - (87 % 4 = 3) = 1) % 4 = 1 个填充
17
+ * (4 - (88 % 4 = 0) = 4) % 4 = 0 个填充
18
+ */
19
+ padLength = (4 - (base64.length % 4)) % 4, padded = base64.padEnd(base64.length + padLength, '='),
20
+ // 转换为二进制字符串并将二进制字符串转换为缓冲区
21
+ binary = atob(padded), buffer = new ArrayBuffer(binary.length), bytes = new Uint8Array(buffer);
22
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
23
+ return buffer;
24
+ }
25
+
26
+ /**
27
+ * 将给定的数组缓冲区转换为 Base64URL 编码的字符串;适用于将各种凭证响应的 ArrayBuffer
28
+ * 转换为字符串,以便作为 JSON 发送回服务器;
29
+ * 与 `base64URLStringToBuffer` 搭配使用
30
+ * - 查看定义:@see {@link bufferToBase64URLString}
31
+ * @param {BufferSource} buffer - 要转换的 ArrayBuffer 或 ArrayBufferView
32
+ * @returns {string} Base64URL 编码的字符串(不含填充符)
33
+ */
34
+ const bufferToBase64URLString = buffer => {
35
+ const bytes = new Uint8Array(buffer);
36
+ let str = '';
37
+ for (const charCode of bytes) str += String.fromCharCode(charCode);
38
+
39
+ const base64String = btoa(str);
40
+ return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
41
+ }
42
+
43
+ export { base64URLStringToBuffer, bufferToBase64URLString };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 使得在测试期间可以模拟返回值
3
+ * - 查看定义:@see {@link _browserSupportsWebAuthnInternals}
4
+ * @ignore 不要将其包含在文档输出中
5
+ * @type {{ stubThis: <T>(value: T) => T }}
6
+ */
7
+ const _browserSupportsWebAuthnInternals = { stubThis: value => value };
8
+
9
+ /**
10
+ * 判断浏览器是否支持 WebAuthn
11
+ * - 查看定义:@see {@link browserSupportsWebAuthn}
12
+ * @returns {boolean} 如果浏览器支持 WebAuthn 则返回 true,否则返回 false
13
+ */
14
+ const browserSupportsWebAuthn = () => {
15
+ return _browserSupportsWebAuthnInternals.stubThis(globalThis?.PublicKeyCredential !== undefined &&
16
+ typeof globalThis.PublicKeyCredential === 'function');
17
+ }
18
+
19
+ export { _browserSupportsWebAuthnInternals, browserSupportsWebAuthn };
@@ -0,0 +1,30 @@
1
+ import { browserSupportsWebAuthn } from './browserSupportsWebAuthn.js';
2
+
3
+ /**
4
+ * 使得在测试期间可以模拟返回值
5
+ * - 查看定义:@see {@link _browserSupportsWebAuthnAutofillInternals}
6
+ * @ignore 不要将其包含在文档输出中
7
+ * @type {{ stubThis: <T>(value: T) => T }}
8
+ */
9
+ const _browserSupportsWebAuthnAutofillInternals = { stubThis: value => value };
10
+
11
+ /**
12
+ * 判断浏览器是否支持条件式 UI,以便在浏览器的密码填充弹窗中向用户显示 WebAuthn 凭证;
13
+ * - 查看定义:@see {@link browserSupportsWebAuthnAutofill}
14
+ * @returns {Promise<boolean>} 一个解析为布尔值的 Promise,表示是否支持条件式 UI
15
+ */
16
+ const browserSupportsWebAuthnAutofill = () => {
17
+ if (!browserSupportsWebAuthn())
18
+ return _browserSupportsWebAuthnAutofillInternals.stubThis(Promise.resolve(false));
19
+
20
+ // 由于 TypeScript DOM 类型定义尚未包含 isConditionalMediationAvailable,这里直接通过属性访问进行检测,避免类型断言;
21
+ const globalPublicKeyCredential = globalThis.PublicKeyCredential;
22
+ if (globalPublicKeyCredential?.isConditionalMediationAvailable === undefined)
23
+ return _browserSupportsWebAuthnAutofillInternals.stubThis(Promise.resolve(false));
24
+
25
+ return _browserSupportsWebAuthnAutofillInternals.stubThis(
26
+ globalPublicKeyCredential.isConditionalMediationAvailable()
27
+ );
28
+ }
29
+
30
+ export { _browserSupportsWebAuthnAutofillInternals, browserSupportsWebAuthnAutofill };
@@ -0,0 +1,50 @@
1
+ import { isValidDomain } from './isValidDomain.js';
2
+ import { WebAuthnError } from './webAuthnError.js';
3
+
4
+ /**
5
+ * 尝试推断调用 `navigator.credentials.get()` 后引发错误的原因;
6
+ * - 查看定义:@see {@link identifyAuthenticationError}
7
+ *
8
+ * @param {Object} params - 参数对象
9
+ * @param {Error|DOMException} params.error - 原始错误对象
10
+ * @param {CredentialRequestOptions} params.options - navigator.credentials.get() 传入的选项
11
+ * @returns {Error|WebAuthnError} 解析后的错误对象(WebAuthnError 或原始错误)
12
+ */
13
+ const identifyAuthenticationError = ({ error, options }) => {
14
+ const { publicKey } = options;
15
+ if (!publicKey) throw Error('options 参数缺少必需的 publicKey 属性');
16
+
17
+ if (error.name === 'AbortError') {
18
+ if (options.signal instanceof AbortSignal)
19
+ // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (第 16 步)
20
+ return new WebAuthnError({ message: '身份验证收到了中止信号', code: 'ERROR_CEREMONY_ABORTED', cause: error });
21
+ }
22
+ else if (error.name === 'NotAllowedError')
23
+ /**
24
+ * 直接传递错误,平台对该错误的定义超出了规范范围,我们不想覆盖可能更有用的错误消息;
25
+ */
26
+ return new WebAuthnError({ message: error.message, code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY', cause: error });
27
+ else if (error.name === 'SecurityError') {
28
+ const effectiveDomain = globalThis.location.hostname;
29
+ if (!isValidDomain(effectiveDomain))
30
+ // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (第 5 步)
31
+ return new WebAuthnError({
32
+ message: `${globalThis.location.hostname} 是一个无效的域名`, code: 'ERROR_INVALID_DOMAIN', cause: error
33
+ });
34
+ else if (publicKey.rpId !== effectiveDomain)
35
+ // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (第 6 步)
36
+ return new WebAuthnError({
37
+ message: `信赖方 ID“${publicKey.rpId}”对此域名无效`, code: 'ERROR_INVALID_RP_ID', cause: error
38
+ });
39
+ }
40
+ else if (error.name === 'UnknownError')
41
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (第 1 步)
42
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (第 12 步)
43
+ return new WebAuthnError({
44
+ message: '身份验证器无法处理指定的选项或创建新的断言签名', code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR', cause: error
45
+ });
46
+
47
+ return error;
48
+ }
49
+
50
+ export { identifyAuthenticationError };
@@ -0,0 +1,99 @@
1
+ import { isValidDomain } from './isValidDomain.js';
2
+ import { WebAuthnError } from './webAuthnError.js';
3
+
4
+ /**
5
+ * 尝试推断调用 `navigator.credentials.create()` 后引发错误的原因
6
+ * - 查看定义: @see {@link identifyRegistrationError}
7
+ *
8
+ * @param {Object} params - 参数对象
9
+ * @param {Error} params.error - 原始错误对象
10
+ * @param {CredentialCreationOptions} params.options - 传递给 `navigator.credentials.create()` 的选项
11
+ * @returns {WebAuthnError|Error} 如果错误能被识别则返回包含更详细信息的 WebAuthnError,否则返回原始错误
12
+ */
13
+ const identifyRegistrationError = ({ error, options }) => {
14
+ const { publicKey } = options;
15
+ if (!publicKey) throw Error('options 参数缺少必需的 publicKey 属性');
16
+
17
+ if (error.name === 'AbortError') {
18
+ if (options.signal instanceof AbortSignal)
19
+ // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (第 16 步)
20
+ return new WebAuthnError({
21
+ message: '注册仪式收到了中止信号', code: 'ERROR_CEREMONY_ABORTED', cause: error,
22
+ });
23
+ }
24
+ else if (error.name === 'ConstraintError') {
25
+ if (publicKey.authenticatorSelection?.requireResidentKey === true)
26
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (第 4 步)
27
+ return new WebAuthnError({
28
+ message: '需要可发现凭证,但没有可用的身份验证器支持该功能',
29
+ code: 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT', cause: error,
30
+ });
31
+ // `mediation` 在 CredentialCreationOptions 中尚不存在,但截至 2024 年 9 月已有可能
32
+ else if (options.mediation === 'conditional' && publicKey.authenticatorSelection?.userVerification === 'required')
33
+ // https://w3c.github.io/webauthn/#sctn-createCredential (第 22.4 步)
34
+ return new WebAuthnError({
35
+ message: '自动注册期间要求用户验证,但无法执行', code: 'ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE', cause: error
36
+ });
37
+
38
+ else if (publicKey.authenticatorSelection?.userVerification === 'required')
39
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (第 5 步)
40
+ return new WebAuthnError({
41
+ message: '需要用户验证,但没有可用的身份验证器支持该功能',
42
+ code: 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT', cause: error
43
+ });
44
+ }
45
+ else if (error.name === 'InvalidStateError')
46
+ // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (第 20 步)
47
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (第 3 步)
48
+ return new WebAuthnError({
49
+ message: '该身份验证器此前已注册', code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED', cause: error
50
+ });
51
+ else if (error.name === 'NotAllowedError')
52
+ /**
53
+ * 直接传递错误;平台对该错误的定义超出了规范范围,保留原始错误消息;
54
+ */
55
+ return new WebAuthnError({ message: error.message, code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY', cause: error });
56
+ else if (error.name === 'NotSupportedError') {
57
+ const validPubKeyCredParams = publicKey.pubKeyCredParams.filter(param => param.type === 'public-key');
58
+ if (validPubKeyCredParams.length === 0)
59
+ // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (第 10 步)
60
+ return new WebAuthnError({
61
+ message: 'pubKeyCredParams 中没有类型为 "public-key" 的条目', code: 'ERROR_MALFORMED_PUBKEYCREDPARAMS', cause: error
62
+ });
63
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (第 2 步)
64
+ return new WebAuthnError({
65
+ message: '身份验证器不支持指定的任何 pubKeyCredParams 算法',
66
+ code: 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG', cause: error,
67
+ });
68
+ }
69
+ else if (error.name === 'SecurityError') {
70
+ const effectiveDomain = globalThis.location.hostname;
71
+ if (!isValidDomain(effectiveDomain))
72
+ // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (第 7 步)
73
+ return new WebAuthnError({
74
+ message: `${globalThis.location.hostname} 是一个无效的域名`, code: 'ERROR_INVALID_DOMAIN', cause: error
75
+ });
76
+ else if (publicKey.rp.id !== effectiveDomain)
77
+ // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (第 8 步)
78
+ return new WebAuthnError({
79
+ message: `信赖方 ID“${publicKey.rp.id}”对此域名无效`, code: 'ERROR_INVALID_RP_ID', cause: error
80
+ });
81
+ }
82
+ else if (error.name === 'TypeError') {
83
+ if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64)
84
+ // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (第 5 步)
85
+ return new WebAuthnError({
86
+ message: '用户 ID 长度不在 1~64 字节之间', code: 'ERROR_INVALID_USER_ID_LENGTH', cause: error
87
+ });
88
+ }
89
+ else if (error.name === 'UnknownError')
90
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (第 1 步)
91
+ // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (第 8 步)
92
+ return new WebAuthnError({
93
+ message: '身份验证器无法处理指定的选项,或无法创建新的凭证', code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR', cause: error
94
+ });
95
+
96
+ return error;
97
+ }
98
+
99
+ export { identifyRegistrationError };