iOS / Swift Shield SDK
AkedlyShield provides Proof-of-Work solving and Turnstile token retrieval for Akedly V1.2 on iOS and macOS. Uses Swift concurrency (async/await) and CryptoKit for native performance.
Installation
Add via Swift Package Manager in Xcode:
- File > Add Package Dependencies
- Enter:
https://github.com/Akedly-Org/akedly-shield-swift - Select version
1.0.0or later
Or add to Package.swift:
dependencies: [
.package(url: "https://github.com/Akedly-Org/akedly-shield-swift", from: "1.0.0")
]
Requirements: iOS 13+ (CryptoKit)
Quick Start
Never ship your API key to an iOS app
App bundles can be inspected — anything compiled in is public. Keep APIKey and pipelineID on your backend and expose a thin proxy (/auth/akedly/challenge, /auth/akedly/send, /auth/akedly/verify) for the iOS app to call. The Backend tab below shows the minimal Node.js server; the Swift tab shows the matching client.
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 iOS device, 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). Do not read the device's public IP from within the iOS app — clients cannot reliably know their own public IP and can lie. Skip this entirely if you don't need per-IP limiting. See the V1.2 API reference for Next.js, Flask, and PHP extraction patterns.
Import AkedlyShield and use the async solvePow function to solve challenges on a background thread. For Turnstile, use AkedlyTurnstile which creates a hidden WKWebView. PoW and Turnstile run on-device; only non-sensitive proofs travel to your backend.
Quick Start
// Express proxy — deploy this on your server, not inside the iOS app.
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:) async -> Int
Asynchronous PoW solver using Swift concurrency. Executes on a background thread via Task.detached and yields every 10,000 iterations for cooperative cancellation.
- Name
challenge- Type
- String
- Description
64-character hex string from the server.
- Name
difficulty- Type
- Int
- Description
Number of leading hex zeros required.
Returns Int (the nonce).
solvePowSync(challenge:difficulty:) -> Int
Synchronous blocking solver. Use on background queues only.
- Name
challenge- Type
- String
- Description
64-character hex string from the server.
- Name
difficulty- Type
- Int
- Description
Number of leading hex zeros required.
Returns Int (the nonce). Blocks until found.
Blocking
solvePowSync blocks the calling thread. Never call on the main thread. Use solvePow (async) for most cases.
AkedlyTurnstile
Creates a hidden WKWebView to retrieve Cloudflare Turnstile tokens.
- Name
init(bridgeDomain:)- Type
- constructor
- Description
Optional bridge domain. Defaults to
turnstile.akedly.io.
- Name
getToken(siteKey:) async throws -> String- Type
- method
- Description
Loads the Turnstile bridge page and returns the verification token.
SwiftUI Example
SwiftUI
import SwiftUI
import AkedlyShield
// Calls YOUR backend proxy; see Quick Start for the Node.js server.
let backendUrl = "https://yourapp.com"
struct OTPView: View {
let phoneNumber: String
@State private var transactionReqID: String?
@State private var otp = ""
@State private var loading = false
@State private var error: String?
var body: some View {
VStack(spacing: 20) {
if transactionReqID == nil {
Text("Send OTP to \(phoneNumber)")
Button(loading ? "Sending..." : "Send OTP") {
Task { await sendOTP() }
}
.disabled(loading)
} else {
TextField("Enter 6-digit OTP", text: $otp)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
Button(loading ? "Verifying..." : "Verify") {
Task { await verifyOTP() }
}
.disabled(loading || otp.count < 6)
}
if let error {
Text(error).foregroundColor(.red)
}
}
.padding()
}
func sendOTP() async {
loading = true
error = nil
do {
let url = URL(string: "\(backendUrl)/auth/akedly/challenge")!
let (data, _) = try await URLSession.shared.data(from: url)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let d = json["data"] as! [String: Any]
let nonce = await solvePow(
challenge: d["challenge"] as! String,
difficulty: d["difficulty"] as! Int
)
var turnstileToken: String?
if let ts = d["turnstile"] as? [String: Any],
ts["required"] as? Bool == true {
turnstileToken = try await AkedlyTurnstile()
.getToken(siteKey: ts["siteKey"] as! String)
}
var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/send")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
var body: [String: Any] = [
"phoneNumber": phoneNumber,
"powSolution": ["challengeToken": d["challengeToken"]!, "nonce": nonce]
]
if let token = turnstileToken { body["turnstileToken"] = token }
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (resData, _) = try await URLSession.shared.data(for: request)
let result = try JSONSerialization.jsonObject(with: resData) as! [String: Any]
transactionReqID = (result["data"] as? [String: Any])?["transactionReqID"] as? String
} catch {
self.error = error.localizedDescription
}
loading = false
}
func verifyOTP() async {
loading = true
error = nil
do {
var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/verify")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: ["transactionReqID": transactionReqID!, "otp": otp])
let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if result["status"] as? String == "success" {
// Handle success
} else {
self.error = "Invalid OTP"
}
} catch {
self.error = error.localizedDescription
}
loading = false
}
}
UIKit Example
UIKit
import UIKit
import AkedlyShield
// Calls YOUR backend proxy; see Quick Start for the Node.js server.
let backendUrl = "https://yourapp.com"
class OTPViewController: UIViewController {
var phoneNumber: String = ""
private var transactionReqID: String?
func sendOTP() {
Task {
do {
let url = URL(string: "\(backendUrl)/auth/akedly/challenge")!
let (data, _) = try await URLSession.shared.data(from: url)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let d = json["data"] as! [String: Any]
let nonce = await solvePow(
challenge: d["challenge"] as! String,
difficulty: d["difficulty"] as! Int
)
var body: [String: Any] = [
"phoneNumber": phoneNumber,
"powSolution": ["challengeToken": d["challengeToken"]!, "nonce": nonce]
]
if let ts = d["turnstile"] as? [String: Any],
ts["required"] as? Bool == true {
let token = try await AkedlyTurnstile()
.getToken(siteKey: ts["siteKey"] as! String)
body["turnstileToken"] = token
}
var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/send")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (resData, _) = try await URLSession.shared.data(for: request)
let result = try JSONSerialization.jsonObject(with: resData) as! [String: Any]
transactionReqID = (result["data"] as? [String: Any])?["transactionReqID"] as? String
} catch {
print("Error: \(error)")
}
}
}
func verifyOTP(_ otp: String) {
guard let transactionReqID else { return }
Task {
var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/verify")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: ["transactionReqID": transactionReqID, "otp": otp])
let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if result["status"] as? String == "success" {
// Handle success
}
}
}
}