npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@akedly/shield

v1.1.0

Published

Client-side PoW solver, Turnstile helper, and hosted passkey ceremony launcher for Akedly Shield V1.2

Readme

@akedly/shield

Client-side Proof-of-Work solver, Turnstile helper, and hosted passkey ceremony launcher for Akedly Shield V1.2.

Installation

npm install @akedly/shield

Or via CDN:

<script src="https://unpkg.com/@akedly/shield/dist/akedly-shield.min.js"></script>

Quick Start

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 Reference

solvePow(challenge, difficulty, options?)

Convenience function that creates a solver, finds the nonce, and cleans up.

  • challenge — 64-char hex string from the server
  • difficulty — integer (number of leading hex zeros required)
  • options.onProgress(hashesChecked) — progress callback
  • options.useWorker — true, false, or 'auto' (default: 'auto')
  • Returns Promise<{ nonce: number }>

new AkedlyShield(options?)

Reusable solver instance.

  • options.useWorker — true, false, or 'auto' (default: 'auto')

.solve(challenge, difficulty, options?)

Same as solvePow but reuses the instance.

.terminate()

Kills the active Web Worker and releases the Blob URL.

getTurnstileToken(siteKey, options?)

Browser-only. Creates a hidden Turnstile widget, resolves with the token, and cleans up.

  • siteKey — Cloudflare Turnstile site key
  • options.theme — 'light', 'dark', or 'auto'
  • options.size — 'normal' or 'compact'
  • Returns Promise<string>

renderTurnstile(siteKey, container, options?)

Renders a Turnstile widget into a DOM element.

  • container — DOM element to render into
  • Returns Promise<string> (token)

openPasskeyCeremony(token, options?)

Browser-only. Opens the hosted V1.2 passkey ceremony (auth.akedly.io/pk) as a popup and resolves with the result the page relays back. Use it for both enroll (pass the enrollmentToken from a successful /verify) and auth (pass the ceremonyToken your backend gets from POST /auth-options).

Must be called inside the direct user-gesture handler (e.g. the click handler) or the browser will block the popup. If you need a backend round-trip to mint the token after the click (the auth case — /auth-options), open a blank window synchronously in the handler and hand it over via options.popup (see Recipe 2).

  • token — a ceremony token minted by your backend (enrollmentToken to enroll, ceremonyToken to authenticate)
  • options.ceremonyOrigin — override the hosted origin (default https://auth.akedly.io)
  • options.popup — a Window you opened synchronously in the click; the SDK navigates it instead of opening its own (use when the token is fetched after the gesture)
  • options.onResult(result) — side-channel callback (same payload as the resolved value)
  • options.onTelemetry(evt) — fire-and-forget funnel events (passkey_opened, passkey_verified, passkey_cancelled, …)
  • options.windowFeatures / options.windowName — window.open overrides
  • options.timeoutMs — auto-resolve with reason: 'timeout' after N ms (default 0 = no timeout)
  • Returns 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.
}

⚠️ Don't trust the raw verified flag — verify the resultToken. The message itself is a client signal and could be forged. But every verified outcome carries a resultToken: a compact value HMAC-signed with your account API key, so your backend confirms it offline — synchronously, with no polling and no round-trip to Akedly. See Verify the result below. (Two other channels also exist: if your pipeline sets backendCallbackURL, Akedly also fires that server-to-server callback when an authentication reaches /auth-verify — you do not opt into it, and because it is unsigned it is a delivery signal rather than proof. It does not fire for enrollment, nor for outcomes that never reach the server at all — closed, popup_blocked, unsupported, timeout — so do not wait on a webhook for those. There is also a GET /api/v1.2/transactions/passkey/result?token=<ceremonyToken> lookup — auth ceremonies only.)

The promise never rejects on a normal outcome (it resolves with a reason so you can fall back to OTP); it rejects only on a programming error (missing token).

isPasskeySupported()

Synchronous boolean — true when the browser exposes window.PublicKeyCredential. Use it to skip the passkey offer entirely on unsupported browsers (fall straight through to OTP).

Platform Support

| Platform | PoW Solver | Turnstile | |----------|-----------|-----------| | Browser (Worker) | Web Worker (off-thread) | getTurnstileToken() | | Browser (no Worker) | Batched main-thread | getTurnstileToken() | | React Native | Batched main-thread | Use bridge page | | Node.js | Sync with native crypto | N/A |

Web Worker Behavior

When useWorker is 'auto' (default), the library:

  1. Checks if Worker, Blob, and URL.createObjectURL are available
  2. If yes, bundles the solver code into a Blob URL Worker (no separate file needed)
  3. If no (e.g., React Native), falls back to batched main-thread solving with setTimeout(fn, 0) between batches to keep the UI responsive

Algorithm

hash = SHA256(challenge + ":" + String(nonce))   // hex digest
valid = hash.startsWith("0".repeat(difficulty))   // leading hex zeros

The solver increments nonce from 0 until a valid hash is found.

Full Integration Example

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();
}

Passkeys (V1.2)

openPasskeyCeremony is a stateless launcher — it never calls /challenge, /send, or /verify. You own those, exactly as you do today; passkeys add three customer-driven recipes on top. The hosted auth.akedly.io/pk page runs the WebAuthn ceremony (fully branded from your pipeline) and relays the result back. Always confirm server-side.

Recipe 1 — Enroll a passkey right after a successful OTP verify

A V1.2 /verify success now returns an additive enrollmentToken (valid ~2 min, single use). Offer enrollment immediately:

⚠️ Pass returnTarget to /verify if you want a verifiable enroll result. The hosted page relays a resultToken only to a server-signed return target, and the enrollment token carries one only if you asked for it. Omit it and enrollment still succeeds — the passkey is created and works — but the ceremony resolves verified:false, reason:"no_proof", because every Akedly SDK refuses to call an unproven result verified. So do not gate your "passkey enabled!" UI on result.verified unless you passed returnTarget; treat the enroll result as advisory otherwise, and let the next successful sign-in be the proof.

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.
}

Recipe 2 — Authenticate a returning user with their passkey

Your backend starts the ceremony by calling POST /auth-options (it carries the secret API key and clears the PoW/Turnstile gate), then hands the ceremonyToken to the browser:

⚠️ Web: include returnTarget.origin in the /auth-options call. The hosted page will never relay a signed resultToken to an unknown web origin — omit returnTarget and the popup posts to "*", so the gateway strips the token (result.resultToken arrives null and offline verification can't complete). Pass your site origin so the token is relayed. The enroll Enroll needs one too — corrected 2026-07-28. This line used to say "the enroll flow needs no token, and the native return-URL / bridge flows carry it without a returnTarget." Both halves were wrong. Enrollment's proof is relayed under exactly the same rule, so pass returnTarget to /verify (Recipe 1) or the enroll result arrives unproven. And of the native paths, only the WebView bridge carries the token without a returnTarget; return-URL redirects are stripped the same way on Android and 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
}

Verify the result (seamless, no polling)

Every verified ceremony resolves a resultToken: a compact, signed proof of the outcome. Forward it from your frontend to your backend; verify it there offline with your account API key — no call back to Akedly, no webhook to receive.

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
}

Treat the token like a one-time auth code: it is short-lived (~2 min) and you should accept it once. The old confirmation paths still work if you prefer them (backendCallbackURL, or a GET …/passkey/result?token= lookup).

Recipe 3 — Fall back to OTP on cancel / failure / missing proof

Every non-verified outcome is a reason you can branch on — fall straight through to your existing OTP 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 working end-to-end demo (Node backend driving the real V1.2 REST + a React front end that enrolls, authenticates, and falls back) lives in akedly-v12-test-apps/web — these recipes are extracted from it.

Related Packages