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
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
Never ship your API key to the browser
Anything in your bundle is public — anyone can read it in DevTools. Keep APIKey and pipelineID on your backend and expose a thin proxy (/auth/akedly/challenge, /auth/akedly/send, /auth/akedly/verify) that the browser calls. The Backend tab below shows the minimal Node.js server; the Frontend tab shows the matching client. See the Next.js section further down for a server-actions variant.
Optional: per-end-user-IP rate limiting
Per-IP rate limiting is opt-in. If you want it, forward the end user's IP via the x-end-user-ip header — the IP that hit your backend from the browser, not your server's IP. The Express example below uses req.ip, which only returns the real client IP after app.set('trust proxy', 1) is configured for your reverse proxy (Cloudflare, AWS ALB, Nginx, Fly.io, Vercel). Skip this entirely if you don't need per-IP limiting — phone and pipeline limits always apply. See the V1.2 API reference for Next.js, Flask, and PHP extraction patterns.
The complete V1.2 flow using the Shield SDK:
- Your backend exposes a challenge proxy
- The client calls the proxy, then solves PoW with
solvePow() - If required, the client gets a Turnstile token via
getTurnstileToken() - The client posts the proof to the backend, which forwards it to Akedly
- The client submits the OTP via the backend verify proxy
Complete Flow
// 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. Setfalsefor 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
solvePowbut reuses the Worker instance. ReturnsPromise<{ 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:
- Checks if
Worker,Blob, andURL.createObjectURLare available - If yes, bundles the solver into a Blob URL Worker (no separate file needed)
- 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
| 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 |
Framework Examples
React
React Component
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
'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
<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>