Web / JavaScript Shield SDK

The @akedly/shield package provides Proof-of-Work solving and Turnstile helpers for Akedly V1.2. Works in browsers (with Web Worker), React Native (batched main-thread), and Node.js (sync crypto).

Installation

Installation

BASH
npm install @akedly/shield

Or via CDN (no build step required):

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

When loaded via CDN, all exports are available on the global AkedlyShield object:

const { solvePow, getTurnstileToken } = window.AkedlyShield;

Quick Start

The complete V1.2 flow using the Shield SDK:

  1. Your backend exposes a challenge proxy
  2. The client calls the proxy, then solves PoW with solvePow()
  3. If required, the client gets a Turnstile token via getTurnstileToken()
  4. The client posts the proof to the backend, which forwards it to Akedly
  5. The client submits the OTP via the backend verify proxy

Complete Flow

JS
// Express proxy — put this on your server, not in the browser bundle.
import express from 'express';

const app = express();
app.use(express.json());
// Optional — only needed if you want per-end-user-IP rate limiting.
// Drop this line and the x-end-user-ip header below if you don't.
// Makes req.ip the real client IP behind a reverse proxy (1 = trust first hop).
app.set('trust proxy', 1);

app.get('/auth/akedly/challenge', async (_req, res) => {
  const r = await fetch(
    `https://api.akedly.io/api/v1.2/transactions/challenge` +
    `?APIKey=${process.env.AKEDLY_API_KEY}` +
    `&pipelineID=${process.env.AKEDLY_PIPELINE_ID}`
  );
  res.status(r.status).json(await r.json());
});

app.post('/auth/akedly/send', async (req, res) => {
  const { phoneNumber, powSolution, turnstileToken } = req.body;
  const r = await fetch('https://api.akedly.io/api/v1.2/transactions/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-end-user-ip': req.ip,
    },
    body: JSON.stringify({
      APIKey: process.env.AKEDLY_API_KEY,
      pipelineID: process.env.AKEDLY_PIPELINE_ID,
      verificationAddress: { phoneNumber },
      powSolution,
      turnstileToken,
    }),
  });
  res.status(r.status).json(await r.json());
});

app.post('/auth/akedly/verify', async (req, res) => {
  const { transactionReqID, otp } = req.body;
  const r = await fetch('https://api.akedly.io/api/v1.2/transactions/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ transactionReqID, otp }),
  });
  res.status(r.status).json(await r.json());
});

API Methods

solvePow(challenge, difficulty, options?)

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

  • Name
    challenge
    Type
    string
    Description

    64-character hex string from the server challenge response.

  • Name
    difficulty
    Type
    number
    Description

    Number of leading hex zeros required in the hash.

  • Name
    options.onProgress
    Type
    function
    Description

    Callback receiving the number of hashes checked so far. Useful for progress indicators.

  • Name
    options.useWorker
    Type
    'auto' | true | false
    Description

    Controls Web Worker usage. 'auto' (default) detects availability. Set false for Node.js or React Native.

Returns Promise<{ nonce: number }>.

new AkedlyShield(options?)

Reusable solver instance. Use when making multiple solve calls to avoid re-creating Workers.

  • Name
    options.useWorker
    Type
    'auto' | true | false
    Description

    Controls Web Worker usage. Default: 'auto'.

Instance methods:

  • Name
    .solve(challenge, difficulty, options?)
    Type
    method
    Description

    Same as solvePow but reuses the Worker instance. Returns Promise<{ nonce: number }>.

  • Name
    .terminate()
    Type
    method
    Description

    Kills the active Web Worker and releases the Blob URL. Call when done with the instance.

getTurnstileToken(siteKey, options?)

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

  • Name
    siteKey
    Type
    string
    Description

    Cloudflare Turnstile site key from the challenge response.

  • Name
    options.theme
    Type
    'light' | 'dark' | 'auto'
    Description

    Turnstile widget theme. Default: 'auto'.

  • Name
    options.size
    Type
    'normal' | 'compact'
    Description

    Widget size. Default: 'normal'.

Returns Promise<string> (the Turnstile token).

renderTurnstile(siteKey, container, options?)

Renders a Turnstile widget into a specific DOM element. Use when you want visible Turnstile placement.

  • Name
    siteKey
    Type
    string
    Description

    Cloudflare Turnstile site key.

  • Name
    container
    Type
    HTMLElement
    Description

    DOM element to render the widget into.

Returns Promise<string> (the Turnstile token).


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

The Web Worker approach runs PoW solving entirely off the main thread, preventing UI freezes during computation.


Platform Support

PlatformPoW SolverTurnstile
Browser (Worker)Web Worker (off-thread)getTurnstileToken()
Browser (no Worker)Batched main-threadgetTurnstileToken()
React NativeBatched main-threadUse bridge page
Node.jsSync with native cryptoN/A

Framework Examples

React

React Component

JS
components/OTPAuth.jsx
import { useState } from 'react';
import { solvePow, getTurnstileToken } from '@akedly/shield';

// This React component calls YOUR backend proxy (see Quick Start for the Node.js server).
// Never put AKEDLY_API_KEY or AKEDLY_PIPELINE_ID in REACT_APP_* — those ship to the browser.

function OTPAuth({ phoneNumber, onSuccess }) {
  const [transactionReqID, setTransactionReqID] = useState(null);
  const [otp, setOtp] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleSendOTP = async () => {
    setLoading(true);
    setError(null);
    try {
      // Get challenge from YOUR backend
      const challengeRes = await fetch('/auth/akedly/challenge');
      const { data } = await challengeRes.json();

      // Solve PoW
      const { nonce } = await solvePow(data.challenge, data.difficulty);

      // Get Turnstile token
      const turnstileToken = data.turnstile?.required
        ? await getTurnstileToken(data.turnstile.siteKey)
        : undefined;

      // Send proof via YOUR backend
      const sendRes = await fetch('/auth/akedly/send', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          phoneNumber,
          powSolution: { challengeToken: data.challengeToken, nonce },
          turnstileToken,
        }),
      });
      const result = await sendRes.json();
      setTransactionReqID(result.data.transactionReqID);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleVerify = async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch('/auth/akedly/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ transactionReqID, otp }),
      });
      const result = await res.json();
      if (result.status === 'success') onSuccess(result);
      else setError('Invalid OTP');
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  if (!transactionReqID) {
    return (
      <div>
        <button onClick={handleSendOTP} disabled={loading}>
          {loading ? 'Sending...' : 'Send OTP'}
        </button>
        {error && <p>{error}</p>}
      </div>
    );
  }

  return (
    <div>
      <input
        value={otp}
        onChange={(e) => setOtp(e.target.value)}
        placeholder="Enter 6-digit OTP"
        maxLength={6}
      />
      <button onClick={handleVerify} disabled={loading || otp.length < 6}>
        {loading ? 'Verifying...' : 'Verify'}
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

export default OTPAuth;

Next.js (Server Action + Client)

Next.js

JS
'use server';
import { headers } from 'next/headers';

export async function getChallenge() {
  const res = await fetch(
    `https://api.akedly.io/api/v1.2/transactions/challenge?APIKey=${process.env.AKEDLY_API_KEY}&pipelineID=${process.env.AKEDLY_PIPELINE_ID}`
  );
  return res.json();
}

export async function sendOTP(phoneNumber, powSolution, turnstileToken) {
  // Extract the END USER's IP (not this server's). Cloudflare sets
  // cf-connecting-ip; Vercel/ALBs set x-forwarded-for (first entry is the client).
  const h = await headers();
  const endUserIP =
    h.get('cf-connecting-ip') ||
    h.get('x-real-ip') ||
    h.get('x-forwarded-for')?.split(',')[0].trim();

  const res = await fetch('https://api.akedly.io/api/v1.2/transactions/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(endUserIP && { 'x-end-user-ip': endUserIP }),
    },
    body: JSON.stringify({
      APIKey: process.env.AKEDLY_API_KEY,
      pipelineID: process.env.AKEDLY_PIPELINE_ID,
      verificationAddress: { phoneNumber },
      powSolution,
      turnstileToken
    })
  });
  return res.json();
}

export async function verifyOTP(transactionReqID, otp) {
  const res = await fetch('https://api.akedly.io/api/v1.2/transactions/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ transactionReqID, otp }),
  });
  return res.json();
}

Vanilla JavaScript (CDN)

Still calls your backend proxy — only the Shield SDK is loaded from the CDN. Do not put your API key in a <script> tag.

CDN Usage

HTML
<script src="https://unpkg.com/@akedly/shield/dist/akedly-shield.min.js"></script>
<script>
  const { solvePow, getTurnstileToken } = window.AkedlyShield;

  async function authenticate(phone) {
    // 1. Get challenge from YOUR backend
    const res = await fetch('/auth/akedly/challenge');
    const { data } = await res.json();

    // 2. Solve PoW + (optional) Turnstile in the browser
    const { nonce } = await solvePow(data.challenge, data.difficulty);
    const turnstileToken = data.turnstile?.required
      ? await getTurnstileToken(data.turnstile.siteKey)
      : undefined;

    // 3. Send proof via YOUR backend
    const sendRes = await fetch('/auth/akedly/send', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        phoneNumber: phone,
        powSolution: { challengeToken: data.challengeToken, nonce },
        turnstileToken,
      }),
    });
    return sendRes.json();
  }
</script>

Was this page helpful?