@ackagent/web-sdk
v0.0.12
Published
TypeScript SDK for AckAgent - web-based authentication and signing with iOS Secure Enclave keys
Maintainers
Readme
AckAgent Web SDK
TypeScript SDK for web-based authentication and signing with iOS Secure Enclave keys. Use this SDK to integrate hardware-backed cryptographic signing and approval workflows into web applications.
Browser Requirements
- Chrome 113+
- Safari 17+
- Firefox 100+
- Requires WebCrypto API and IndexedDB support
Installation
npm install @ackagent/web-sdkQuick Start
1. Login with QR Code + SAS Verification
import { createLoginSession, storeAccount } from '@ackagent/web-sdk';
// Create a login session
const session = await createLoginSession({
relayUrl: 'https://relay.ackagent.com',
name: 'My Web App',
});
// Display QR code for user to scan with iOS app
showQRCode(session.qrCodeUrl);
// Set SAS callback (called when device keys are received)
session.onSASAvailable = (sas) => {
console.log('Verify on your iOS device:');
console.log('Emoji:', sas.emojiString);
console.log('Words:', sas.wordString);
};
// Wait for verification
const account = await session.waitForVerification();
// Store account in IndexedDB
await storeAccount(account);2. Request Web Approval
import { requestWebApproval, loadAccount } from '@ackagent/web-sdk';
// Load the stored account from IndexedDB
const account = await loadAccount(userId);
// Request approval from user's iOS device
const result = await requestWebApproval(account, {
title: 'Confirm Payment',
description: '$50.00 to Merchant Corp',
plaintext: `
Payment Authorization
Amount: $50.00
Recipient: Merchant Corp
`.trim(),
});
if (result.success) {
console.log('Signature:', result.signature);
console.log('Hash:', result.plaintextHash);
} else {
console.error('Failed:', result.errorMessage);
}API Reference
Authentication
login(options, onQRCode)
Complete QR code login flow with SAS verification.
interface LoginOptions {
relayUrl: string; // URL of the relay server
name: string; // Display name for this requester
expiresIn?: number; // Session expiration in seconds (default: 300)
}The onQRCode callback receives the QR code URL and expiration time. Display this as a QR code for the user to scan with their iOS app. Return true to continue waiting, false to cancel.
After the user scans the QR code and both sides compute matching SAS, the login completes and returns the account and pairing.
createLoginSession(options)
Create a login session with more control over the flow.
import { createLoginSession } from '@ackagent/web-sdk';
const session = await createLoginSession({
relayUrl: 'https://relay.ackagent.com',
name: 'My Web App',
});
// Display QR code
showQRCode(session.qrCodeUrl);
// Set SAS callback (called when device keys are received)
session.onSASAvailable = (sas) => {
console.log('Verify on your iOS device:');
console.log('Emoji:', sas.emojiString);
console.log('Words:', sas.wordString);
};
// Wait for verification
const result = await session.waitForVerification();generateQRCodeUrl(sessionId, publicKey)
Generate a QR code URL from session ID and public key.
const qrUrl = generateQRCodeUrl(sessionId, publicKeyBase64);Storage Functions
// Accounts
await storeAccount(account);
const account = await loadAccount(userId);
await deleteAccount(userId);
const accounts = await listAccounts();
// Clear all data
await clearAllData();Token Management
needsTokenRefresh(account)
Check if the account token needs refreshing (expires within 7 days).
refreshTokens(authServiceUrl, refreshToken, clientId)
Refresh tokens using the login service token endpoint.
updateAccountTokens(account, tokens)
Update an account with new tokens after refresh.
import {
needsTokenRefresh,
refreshTokens,
updateAccountTokens,
storeAccount,
loadAccount,
} from '@ackagent/web-sdk';
async function ensureValidToken(userId: string) {
const account = await loadAccount(userId);
if (!account) throw new Error('Account not found');
if (!needsTokenRefresh(account) || !account.refreshToken) {
return account;
}
const tokens = await refreshTokens(
'https://login.ackagent.com',
account.refreshToken,
'your-client-id'
);
const updated = updateAccountTokens(account, tokens);
await storeAccount(updated);
return updated;
}Web Approval
requestWebApproval(account, options, deviceId?, timeoutMs?)
Request approval from user's iOS device. Browser metadata is automatically collected.
interface WebApprovalOptions {
title: string; // Action-oriented title
description?: string; // Short description
plaintext: string; // Full content for user to review
keyId?: string; // Specific key to use
keyFingerprint?: string; // Key fingerprint to match
expiresIn?: number; // Expiration in seconds (default: 300)
}Returns:
interface WebApprovalResult {
success: boolean;
signature?: Uint8Array; // Signature over sha256(plaintext)
plaintextHash?: string; // Hash that was signed
errorCode?: SigningErrorCode;
errorMessage?: string;
}approveWeb(account, options, deviceId?, timeoutMs?)
Same as requestWebApproval but throws on error instead of returning a result.
SAS (Short Authentication String)
computeSAS(requesterPublicKey, devices)
Compute SAS for verification. Returns emoji and word representations.
interface SASResult {
emojiString: string; // e.g., "🎸🍎🐬⭐🌙"
wordString: string; // e.g., "guitar-apple-dolphin-star-moon"
bytes: Uint8Array; // Raw 5-byte SAS
}Attestation
createAttestationVerifier(options) / verifyAttestation(verifier, attestation)
Verify iOS App Attest attestations.
import { createAttestationVerifier, verifyAttestation } from '@ackagent/web-sdk';
const verifier = createAttestationVerifier({
teamId: 'YOUR_TEAM_ID',
bundleId: 'com.ackagent.ackagent',
environment: 'production', // or 'development'
});
const isValid = await verifyAttestation(verifier, attestationObject);Anonymous Attestation Replay Protection
For anonymous attestation pseudonym replay prevention, use NullifierStore and
its adapter interface:
import {
NullifierStore,
createAtomicNullifierStoreAdapter,
} from '@ackagent/web-sdk';
// Example Redis/DB-backed atomic callback.
// Return true when already spent, false when newly marked.
const adapter = createAtomicNullifierStoreAdapter(async (scope, pseudonym, ttlMs) => {
// Pseudocode:
// const key = `ackagent:nullifier:${scope}:${pseudonym}`;
// const set = await redis.set(key, "1", { NX: true, PX: ttlMs });
// return set !== "OK";
return false;
});
const nullifierStore = new NullifierStore({ adapter });Production guidance:
- Use an adapter backed by atomic compare-and-set semantics (Redis
SET NX PXor DB upsert with unique constraint). - Do not rely on browser
localStoragein clustered/server deployments. - Scope keys to tenant/org + request context to avoid accidental cross-domain replay coupling.
Anonymous Attestation Verification Input Formats
AnonymousAttestationVerifier.verify(...) accepts:
- Native
AnonymousAttestationpayloads - W3C/Data-Integrity envelopes (
W3CAnonymousAttestationEnvelope) usingproof.type = "DataIntegrityProof"andproof.cryptosuite = "bbs-2023"
Helpers exported by the SDK:
isW3CAnonymousAttestationEnvelope(value)parseW3CAnonymousAttestationEnvelope(envelope)toW3CAnonymousAttestationEnvelope(attestation)
Scoped Linking Prototype (AA-06)
The verifier supports an explicit, policy-gated scoped-linking mode for cross-device correlation experiments:
import {
AnonymousAttestationVerifier,
NullifierStore,
createAtomicNullifierStoreAdapter,
} from "@ackagent/web-sdk";
const verifier = new AnonymousAttestationVerifier();
const linkStore = new NullifierStore({
adapter: createAtomicNullifierStoreAdapter(async (scope, handle, ttlMs) => {
// Implement with Redis/DB compare-and-set semantics.
return false;
}),
});
const result = await verifier.verify(attestation, {
issuerPublicKey,
scopedLinking: {
scopeType: "organization",
scopeId: "org_123",
store: linkStore,
},
});Notes:
- Default behavior remains unlinkable unless
scopedLinkingis configured. - When enabled, attestation must include
scopedLinkingmetadata with matchingscopeTypeandscopeId. - Link records are namespaced as
ackagent_link:<scopeType>:<scopeId>.
Utilities
checkWebCryptoSupport()
Check if the browser supports required WebCrypto features.
import { checkWebCryptoSupport, UnsupportedBrowserError } from '@ackagent/web-sdk';
try {
await checkWebCryptoSupport();
} catch (e) {
if (e instanceof UnsupportedBrowserError) {
console.error('Browser not supported:', e.message);
}
}isIndexedDBAvailable()
Check if IndexedDB is available for storage.
VERSION
SDK version string.
Errors
All errors extend SignerError:
SigningError- General signing errorsSigningRejectedError- User rejected the requestSigningExpiredError- Signing request expiredNetworkError- Network/communication errorsCryptoError- Encryption/decryption errorsTimeoutError- Operation timed outUnsupportedBrowserError- Browser lacks required features
Security
The SDK implements end-to-end encryption:
- P-256 ECDH key exchange
- HKDF-SHA256 key derivation
- ChaCha20-Poly1305 authenticated encryption
- Forward secrecy via ephemeral keys per request
- SAS verification (5 symbols) to prevent MITM attacks during login
The relay server never sees plaintext payloads or signatures. Accounts and pairings are stored in IndexedDB with non-extractable CryptoKeys that cannot be exported from the browser.
License
MIT
