@akedly/屏蔽
客户端验证-工作解析器,Turnstile帮助器,并托管了Akedly Shield V1.2的密码仪式启动器.
安装
npm install @akedly/shield
或者通过CDN:
<script src="https://unpkg.com/@akedly/shield/dist/akedly-shield.min.js"></script>
快速启动
import { solvePow } from '@akedly/shield';
// 1. Get challenge from your server
const res = await fetch('/api/v1.2/transactions/challenge?APIKey=...&pipelineID=...');
const { data } = await res.json();
// 2. Solve PoW
const { nonce } = await solvePow(data.challenge, data.difficulty);
// 3. Send OTP with solution
await fetch('/api/v1.2/transactions/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
APIKey: '...',
pipelineID: '...',
verificationAddress: { phoneNumber: '+201234567890' },
powSolution: { challengeToken: data.challengeToken, nonce }
})
});
API 参考
solvePow(challenge, difficulty, options?)
方便功能可以创建解子,找到非ce,并清理.
challenge服务器的64 char 十六进制字符串difficulty−整数(需为正六分数)options.onProgress(hashesChecked)——回调进度options.useWorker—— 说true, (中文(简体) ),false,或'auto'(默认 :'auto'(中文(简体) )- 回返
Promise<{ nonce: number }>
new AkedlyShield(options?)
可重复使用的解析器实例 .
options.useWorker—— 说true, (中文(简体) ),false,或'auto'(默认 :'auto'(中文(简体) )
.solve(challenge, difficulty, options?)
与 solvePow 但却重用这个例子.
.terminate()
杀死活动 Web Worker并发布 Blob URL.
getTurnstileToken(siteKey, options?)
只浏览器 。 创建隐藏的 Turnstile 部件, 用符号解决, 并清理 .
siteKey— Cloudflare Turnstile 站点密钥options.theme—— 说'light', (中文(简体) ),'dark',或'auto'options.size—— 说'normal'或者说'compact'- 回返
Promise<string>
renderTurnstile(siteKey, container, options?)
将 Turnstile 部件变成 DOM 元素 .
container- DOM 元素可制成- 回返
Promise<string>(铃声)
openPasskeyCeremony(token, options?)
只浏览器 。 打开托管的 V1.2 密钥仪式(auth.akedly.io/pk作为弹出和
以页面中继结果解决。 两个都用吧 注册l (通过
enrollmentToken 从成功的 /verify和 认证 (通过 ceremonyToken
后端从 POST /auth-options) (中文(简体) ).
必须在直接用户处理器内调用 (例如点击处理器)或 浏览器会屏蔽弹出 。 如果您需要后端回转来回 来铸造代币 之后 单击 认证 案例 -
/auth-options中同步打开空白窗口 处理器通过options.popup(见说明2).
token——由后端铸造的仪式代币(" your")enrollmentToken入学,ceremonyToken要认证)options.ceremonyOrigin— 覆盖主机源( 默认)https://auth.akedly.io(中文(简体) )options.popupa 包括:Window您在点击时同步打开; SDK 导航它而不是打开它自己的(在手势后获得令牌时使用)options.onResult(result)——侧道回调(有效载荷与解析值相同)options.onTelemetry(evt)火灾和遗忘漏斗事件(passkey_opened, (中文(简体) ),passkey_verified, (中文(简体) ),passkey_cancelled, (...) (中文(简体) )options.windowFeatures编号 :options.windowName—— 说window.open覆盖options.timeoutMs自动解决reason: 'timeout'在 N ms 之后( 默认)0= 没有超时)- 回返
Promise<PasskeyCeremonyResult>数字 :
{
verified: boolean; // true only on a completed, successful ceremony
supported: boolean; // false only when WebAuthn is unavailable
purpose: string | null; // 'enroll' | 'auth'
transactionId: string | null; // the passkey requestID
resultToken: string | null; // signed proof of a verified outcome — verify on YOUR backend
code: string | null;
reason: string | null; // null when verified; else 'closed' | 'popup_blocked'
// | 'unsupported' | 'timeout' | 'no_proof'
// | 'failed' | <server code>
// NOTE: /pk does not send a server `code` today, so `code` is
// always null and `reason` is only ever one of the local
// values above. Reserved in the contract, not a live branch.
}
⚠️ 别相信生来的东西
verified校验resultToken。 。 。 . 信息本身是 一个客户信号,并可以伪造。 但每个 已核实 结果包含一个resultToken数字 : 紧凑值 HMAC 用您的账户 API 密钥签名所以你的后端证实了 离线 —— 说 同步进行,没有投票,也没有往返到Akedly。见 校验结果 见下文。 (另外两个渠道) 存在: 如果您的管道集backendCallbackURL,凯德利 (单位:千美元) 点火服务器到服务器 调用时 认证 达到/auth-verify——你没有选择加入,因为 这是 未签名 这是送货信号而不是证据 没错 没有 射击入学, 也绝不是那些从未到达服务器的结果closed, (中文(简体) ),popup_blocked, (中文(简体) ),unsupported, (中文(简体) ),timeout因此,不要在网络上等待这些。 还有一个GET /api/v1.2/transactions/passkey/result?token=<ceremonyToken>寻找——只举行认证仪式. )
承诺 绝不拒绝正常的结果 (它以 reason 这样你就能摔倒了
它只在编程出错(丢失了令牌)时拒绝.
isPasskeySupported()
同步 boolean —— 说 true 当浏览器曝光时 window.PublicKeyCredential。使用它来
跳过不支持的浏览器提供的密码( 直接通过 OTP) .
平台支持
| 平台 | PoW 解决方案 | 转弯 |
|---|---|---|
| 浏览器 (工人) | 网络工人( 脱线) | getTurnstileToken() |
| 浏览器( 无工作) | 断开主线 | getTurnstileToken() |
| 原生反应 | 断开主线 | 使用桥页 |
| 节点.js | 与本地加密同步 | 不详 |
网络工人行为
何时 useWorker 实值 'auto' (默认),库:
- 检查是否
Worker, (中文(简体) ),Blob,以及URL.createObjectURL备有 - 如果是,将解析器代码捆绑到 Blob URL Worker (不需要单独的文件)
- 如果无( 如 React Industrial) , 则返回批量主线解析
setTimeout(fn, 0)在批次之间保持用户界面响应
算法
hash = SHA256(challenge + ":" + String(nonce)) // hex digest
valid = hash.startsWith("0".repeat(difficulty)) // leading hex zeros
解析器递增 nonce 从 0 到找到有效的散列.
完整整合示例
import { solvePow, getTurnstileToken } from '@akedly/shield';
async function sendOTP(phoneNumber, apiKey, pipelineID) {
// Step 1: Get challenge
const challengeRes = await fetch(
`https://api.akedly.io/api/v1.2/transactions/challenge?APIKey=${apiKey}&pipelineID=${pipelineID}`
);
const { data } = await challengeRes.json();
// Step 2: Solve PoW (if required)
let powSolution;
if (data.challengeRequired) {
const { nonce } = await solvePow(data.challenge, data.difficulty, {
onProgress: (checked) => console.log(`${checked} hashes checked...`)
});
powSolution = { challengeToken: data.challengeToken, nonce };
}
// Step 3: Get Turnstile token (if required)
let turnstileToken;
if (data.turnstile?.required) {
turnstileToken = await getTurnstileToken(data.turnstile.siteKey);
}
// Step 4: Send OTP
const sendRes = await fetch('https://api.akedly.io/api/v1.2/transactions/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
APIKey: apiKey,
pipelineID,
verificationAddress: { phoneNumber },
powSolution,
turnstileToken
})
});
return sendRes.json();
}
密码(V1.2)
openPasskeyCeremony 是一个 无国籍发射器 - 它从不打电话 /challenge, (中文(简体) ), /send,或
/verify。你拥有这些,就像你今天所做的一样;密码会添加三个客户驱动的食谱
在顶上。 主办单位 auth.akedly.io/pk 页面运行 WebAuthn 仪式(完全标出自您的
并转发结果. 总是确认服务器侧.
道具1——在检察官办公室成功核实后立即输入密码
A V1.2 数据 /verify 成功现在返回添加剂 enrollmentToken (有效~2分,单人)
使用) (中文(简体) ). 立即入学:
⚠️ 过
returnTarget改为/verify如果你想要可核实的入学结果. 主机页面 中继 aresultToken仅改为a 服务器签名 返回目标,并携带入学标志 只有一个,如果你要求它。 关闭它和入学仍然成功——密码是创建的 但仪式决定了verified:false, reason:"no_proof"因为每个Akedly SDK 拒绝调用未经核实的结果. 所以不要打开您的"密码已启用!" UI onresult.verified除非你通过了returnTarget· ; 将录取结果作为咨询 否则,并让下一个成功的签注成为证据.
import { openPasskeyCeremony, isPasskeySupported } from '@akedly/shield';
// ...your normal challenge -> (PoW/Turnstile) -> send -> verify flow...
const verify = await verifyOtp(requestID, otp); // your existing call
if (verify.data?.verified && verify.data?.enrollmentToken && isPasskeySupported()) {
// MUST run in the click handler of an "Enable Face ID / fingerprint" button.
const result = await openPasskeyCeremony(verify.data.enrollmentToken, {
onTelemetry: (e) => analytics.track(e.event, e.props)
});
if (result.verified) {
// Enrollment has no /result endpoint — the passkey now simply exists and is proven on
// the user's next sign-in. Update your own UI / record-keeping as you see fit.
await fetch('/api/passkey-enrolled', { method: 'POST', body: JSON.stringify({ transactionId: result.transactionId }) });
}
// result.verified === false (closed / unsupported / ...) => just don't show "enabled"; harmless.
}
道具2 — 用密码认证返回的用户
后端 以呼叫开始仪式 POST /auth-options (它带着秘密API)
键和清除PoW/Turnstile门,然后手拿 ceremonyToken 转到浏览器 :
⚠️ 网络:包括
returnTarget.origin输入/auth-options电话. 主机页面将 从不转发签名resultToken到未知的网络来源——省略returnTarget弹出 员额"*",所以门户 剥离符号 (单位:千美元)result.resultToken来了null财务报告和审定财务报表 离线核查不能完成). 传出你的地址来源,所以信使是中继的. 报名 进取也需要一个——更正为2026-07-28. 此行曾表示"招生流程不需要 符号,而原生回路-URL /桥道流在不带returnTarget两分之一 错了 录取的证明是完全一样的 所以通过returnTarget改为/verify(收录一)或收录结果未证实. 在本土道路上,只有 网络视频桥 不带符号returnTarget· ; 返回URL重定向被剥离 同样的方式 android和Flutter的电话.
// --- your backend ---
// POST https://api.akedly.io/api/v1.2/transactions/passkey/auth-options
// { APIKey, pipelineID, verificationAddress: { phoneNumber },
// returnTarget: { origin: 'https://your-site.example' }, // <-- REQUIRED for the web resultToken
// powSolution?, turnstileToken? }
// -> { data: { ceremonyToken, requestID } } (or a "no passkey" code => use OTP)
// --- your frontend (sign-in click handler) ---
// The token needs a backend round-trip, so open a blank popup NOW (in the gesture) and
// hand it to the SDK once the token is ready — a window.open after the await is blocked.
const popup = window.open('about:blank', 'akedly_passkey', 'width=480,height=680');
const { ceremonyToken } = await startAuth(phoneNumber); // your backend -> /auth-options
if (!ceremonyToken) { popup?.close(); return fallbackToOtp(); } // no passkey on file
const result = await openPasskeyCeremony(ceremonyToken, { popup });
if (result.verified) {
// Seamless + synchronous: hand the proof token to YOUR backend, which verifies it
// offline (no poll, no round-trip to Akedly) and creates the session. See "Verify the result".
await completeSignIn(result.resultToken);
} else {
await fallbackToOtp(); // Recipe 3
}
核查结果(无间隔,无投票)
每个 已核实 仪式决定 resultToken: 契约,签名证明
结果。 从前端转到后端; 在那里验证 离线 还有你的
账户 API 密钥 — 没有回电到 Akedly , 没有网页 hook 接收 .
pkrt1.<base64url(payload)>.<base64url(hmac)>
payload = { v, purpose, transactionId, pipelineId, verified, iat, exp }
hmac = HMAC_SHA256("pkrt1." + base64url(payload), YOUR_API_KEY)
// --- your backend --- (Node; ~12 lines, zero deps)
import crypto from 'node:crypto';
export function verifyAkedlyResult(token, apiKey) {
if (!apiKey) return null; // fail closed on an empty key
if (typeof token !== 'string' || !token.startsWith('pkrt1.')) return null;
const parts = token.slice('pkrt1.'.length).split('.');
if (parts.length !== 2) return null; // exactly two segments
const [data, sig] = parts;
const b64u = /^[A-Za-z0-9_-]+$/; // strict unpadded base64url
if (!b64u.test(data) || !b64u.test(sig)) return null;
// canonical form only — a slack-bit alias decodes to the same bytes and would bypass
// a string-keyed single-use check
if (Buffer.from(data, 'base64url').toString('base64url') !== data) return null;
if (Buffer.from(sig, 'base64url').toString('base64url') !== sig) return null;
const expected = crypto.createHmac('sha256', apiKey).update('pkrt1.' + data).digest();
const given = Buffer.from(sig, 'base64url');
if (expected.length !== given.length || !crypto.timingSafeEqual(expected, given)) return null;
let payload;
try { payload = JSON.parse(Buffer.from(data, 'base64url').toString()); } catch (e) { return null; }
if (!payload.exp || Date.now() > payload.exp) return null; // expired
if (payload.verified !== true) return null; // only a verified outcome is trustworthy
return payload; // verified + unexpired — bind transactionId + purpose to the ceremony you started
}
// POST /complete-sign-in { resultToken }
const claim = verifyAkedlyResult(req.body.resultToken, process.env.AKEDLY_API_KEY);
if (claim?.verified && claim.purpose === 'auth' && claim.transactionId === expectedTxId) {
createSession(user); // safe: the proof is signed by us, bound to this auth transaction
}
把信使当作一次性的认证代码: 它寿命短 (~2分) 你应该接受它
一次 一次吗 旧的确认路径仍然有效,如果你喜欢的话(backendCallbackURL,或一个
GET …/passkey/result?token= 寻取.
道具 3 — 在取消/ 失败/ 缺少证据时返回检察官办公室
每个未经核实的结果都是 reason 你可以继续... 直接掉下去
现有检察官办公室 send + 键 verify数字 :
const result = await openPasskeyCeremony(token);
if (!result.verified) {
// 'closed' | 'popup_blocked' | 'unsupported' | 'timeout' | 'no_proof' | 'failed'
// (<server code> is reserved in the contract but /pk never sends one today)
await runOtpFlow(); // your existing challenge -> send -> verify
}
A 类 工作端到端演示 (节点后端驱动真实的 V1.2 REST + a React 前端
入学、认证和返回)
akedly-v12-test-apps/web ——这些食谱是从中取出.
相关软件包
- 达特/弗卢特数字 :
akedly_shield - 快速( iOS)数字 :
AkedlyShield - 克特林 (Android) (英语)数字 :
com.akedly.shield