First Push

This commit is contained in:
喆OvO
2026-08-06 11:11:11 +08:00
commit 0fef309972
551 changed files with 15793 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
/* Passkey 中间件:统一所有 WebAuthn 调用(登录/注册/二次验证/管理员代配)。
*
* 根据 Agent 判定执行路径:
* - 浏览器 / 小窗(UA 不含 "CalDisplayApp" 且未注入标志)→ 直接调用
* navigator.credentials(标准 WebAuthn,Safari 小窗环境原生支持 Passkey)
* - iOS 套壳 App(注入 window.__CAL_DISPLAY_APP__ 或 UA 含 "CalDisplayApp")
* → 把 {op, options} 存入服务端一次性 stash,跳转 /passkey 小窗页面;
* App 拦截导航后用 ASWebAuthenticationSession 打开该页面,页面内完成
* WebAuthn 仪式,经 dashboard://passkey 回调由 App 回传结果。
*
* 所有返回的 credential 均为纯 base64url JSON(与浏览器 WebAuthn 结构一致),
* 调用方无需再做 ArrayBuffer 转换。
*/
(function (global) {
'use strict';
// iOS 套壳 App 判定:App 注入的全局标志优先,UA 标记兜底
var IS_IOS_APP = false;
try { IS_IOS_APP = window.__CAL_DISPLAY_APP__ === true; } catch (e) {}
if (!IS_IOS_APP) { IS_IOS_APP = /CalDisplayApp/i.test(navigator.userAgent || ''); }
var _pending = null; // {resolve, reject}
// ── base64url <-> ArrayBuffer ──
function b64ToBuf(b64) {
var base64 = b64.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) base64 += '=';
var binary = atob(base64);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
function bufToB64(buf) {
var bytes = new Uint8Array(buf);
var binary = '';
for (var i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// ── options 准备(浏览器模式:base64url → ArrayBuffer)──
function prepareCreation(options) {
var o = JSON.parse(JSON.stringify(options));
o.challenge = b64ToBuf(o.challenge);
if (o.user && o.user.id) o.user.id = b64ToBuf(o.user.id);
if (o.excludeCredentials) {
o.excludeCredentials = o.excludeCredentials.map(function (c) {
return { id: b64ToBuf(c.id), type: c.type };
});
}
return o;
}
function prepareAssertion(options) {
var o = JSON.parse(JSON.stringify(options));
o.challenge = b64ToBuf(o.challenge);
if (o.allowCredentials) {
o.allowCredentials = o.allowCredentials.map(function (c) {
return { id: b64ToBuf(c.id), type: c.type };
});
}
return o;
}
// ── 浏览器直接 WebAuthn ──
function webAuthnCreation(options) {
return navigator.credentials.create({ publicKey: prepareCreation(options) }).then(function (cred) {
return {
id: cred.id,
type: cred.type,
rawId: bufToB64(cred.rawId),
response: {
clientDataJSON: bufToB64(cred.response.clientDataJSON),
attestationObject: bufToB64(cred.response.attestationObject),
transports: cred.response.getTransports ? cred.response.getTransports() : []
}
};
});
}
function webAuthnAssertion(options) {
return navigator.credentials.get({ publicKey: prepareAssertion(options) }).then(function (cred) {
return {
id: cred.id,
type: cred.type,
rawId: bufToB64(cred.rawId),
response: {
authenticatorData: bufToB64(cred.response.authenticatorData),
clientDataJSON: bufToB64(cred.response.clientDataJSON),
signature: bufToB64(cred.response.signature),
userHandle: cred.response.userHandle ? bufToB64(cred.response.userHandle) : null
}
};
});
}
// ── iOS App:先把参数存入服务端一次性 stash,再跳转小窗页面 ──
// 小窗(ASWebAuthenticationSession)是独立的 ephemeral 会话,读不到主容器
// 的 sessionStorage,因此参数通过服务端短时暂存传递。
function iosDispatch(op, options) {
return fetch('/api/passkey/stash', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ op: op, options: options })
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.sid) throw new Error(d.error || 'Passkey 参数暂存失败');
var base = location.origin || ('https://' + location.host);
window.location.href = base + '/passkey?op=' + encodeURIComponent(op) + '&sid=' + encodeURIComponent(d.sid);
return new Promise(function (resolve, reject) {
_pending = { resolve: resolve, reject: reject };
// 兜底:小窗未完成/被取消时避免 promise 悬挂
setTimeout(function () {
if (_pending) {
_pending = null;
reject(new Error('Passkey 认证超时或已取消'));
}
}, 120000);
});
});
}
// ── 供原生层(evaluateJavaScript)调用的桥接方法 ──
function __iosComplete(credentialJson) {
var p = _pending;
_pending = null;
if (p) p.resolve(credentialJson);
}
function __iosCancel(message) {
var p = _pending;
_pending = null;
if (p) p.reject(new Error(message || 'Passkey 认证已取消'));
}
// ── 对外统一入口 ──
global.PasskeyBridge = {
creation: function (options) {
return IS_IOS_APP ? iosDispatch('creation', options) : webAuthnCreation(options);
},
assertion: function (options) {
return IS_IOS_APP ? iosDispatch('assertion', options) : webAuthnAssertion(options);
},
isIOSApp: function () { return IS_IOS_APP; },
__iosComplete: __iosComplete,
__iosCancel: __iosCancel
};
})(window);