@akedly/shield

中文文档
1.1.0发布于 2026/08/04MITTypeScriptESM + CJS零依赖近 30 天下载 3828

客户端 PoW 解析器, Turnstile 帮助器, 并托管 Akedly Shield V1.2 的密码仪式启动器

中文使用文档
文档目录19 节
  1. 安装
  2. 快速启动
  3. API 参考
  4. solvePow(challenge, difficulty, options?)
  5. new AkedlyShield(options?)
  6. getTurnstileToken(siteKey, options?)
  7. renderTurnstile(siteKey, container, options?)
  8. openPasskeyCeremony(token, options?)
  9. isPasskeySupported()
  10. 平台支持
  11. 网络工人行为
  12. 算法
  13. 完整整合示例
  14. 密码(V1.2)
  15. 道具1——在检察官办公室成功核实后立即输入密码
  16. 道具2 — 用密码认证返回的用户
  17. 核查结果(无间隔,无投票)
  18. 道具 3 — 在取消/ 失败/ 缺少证据时返回检察官办公室
  19. 相关软件包

@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.popup a 包括: 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' (默认),库:

  1. 检查是否 Worker, (中文(简体) ), Blob,以及 URL.createObjectURL 备有
  2. 如果是,将解析器代码捆绑到 Blob URL Worker (不需要单独的文件)
  3. 如果无( 如 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 如果你想要可核实的入学结果. 主机页面 中继 a resultToken 仅改为a 服务器签名 返回目标,并携带入学标志 只有一个,如果你要求它。 关闭它和入学仍然成功——密码是创建的 但仪式决定了 verified:false, reason:"no_proof"因为每个Akedly SDK 拒绝调用未经核实的结果. 所以不要打开您的"密码已启用!" UI on result.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 共发布 4 个版本,首次发布于 2026/03/09;过去 12 个月发布了 4 个版本。

版本发布日期距今
1.1.0latest2026/08/043 个月前
1.0.22026/05/046 个月前
1.0.12026/03/097 个月前
1.0.02026/03/097 个月前

关于 @akedly/shield 的常见问题

@akedly/shield 是什么?

客户端 PoW 解析器, Turnstile 帮助器, 并托管 Akedly Shield V1.2 的密码仪式启动器

如何安装 @akedly/shield?

在项目目录运行 npm install @akedly/shield,或使用 pnpm add / yarn add / bun add。

@akedly/shield 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@akedly/shield 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@akedly/shield 可以商用吗?

@akedly/shield 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@akedly/shield 的下载量有多少?

根据 npm 官方下载统计,@akedly/shield 在 2026/10/06 – 2026/11/04 这 30 天内被下载 3,828 次,日均约 128 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@akedly/shield 还在维护吗?

最新版本 1.1.0 发布于 2026/08/04(3 个月前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/03/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。