UNPKG

@humanmark/sdk-js

Version:

Browser-native JavaScript SDK for Humanmark human verification challenges

1,048 lines (1,047 loc) 32.5 kB
const ENDPOINTS = { WAIT_CHALLENGE: "/api/v1/challenge/wait" }; const DEFAULT_BASE_URL = "https://humanmark.io"; function decodeChallengeToken(binary) { return _decodeChallengeToken(wrapByteBuffer(binary)); } function _decodeChallengeToken(bb) { let message = {}; end_of_message: while (!isAtEnd(bb)) { let tag = readVarint32(bb); switch (tag >>> 3) { case 0: break end_of_message; // optional uint32 version = 1; case 1: { message.version = readVarint32(bb) >>> 0; break; } // optional uint32 key_id = 2; case 2: { message.key_id = readVarint32(bb) >>> 0; break; } // optional uint32 issued_at = 3; case 3: { message.issued_at = readVarint32(bb) >>> 0; break; } // optional uint32 expires_at = 4; case 4: { message.expires_at = readVarint32(bb) >>> 0; break; } // optional string challenge = 5; case 5: { message.challenge = readString(bb, readVarint32(bb)); break; } // optional string shard = 6; case 6: { message.shard = readString(bb, readVarint32(bb)); break; } // optional string domain = 7; case 7: { message.domain = readString(bb, readVarint32(bb)); break; } default: skipUnknownField(bb, tag & 7); } } return message; } function skipUnknownField(bb, type) { switch (type) { case 0: while (readByte(bb) & 128) { } break; case 2: skip(bb, readVarint32(bb)); break; case 5: skip(bb, 4); break; case 1: skip(bb, 8); break; default: throw new Error("Unimplemented type: " + type); } } let f32 = new Float32Array(1); new Uint8Array(f32.buffer); let f64 = new Float64Array(1); new Uint8Array(f64.buffer); function wrapByteBuffer(bytes) { return { bytes, offset: 0, limit: bytes.length }; } function skip(bb, offset) { if (bb.offset + offset > bb.limit) { throw new Error("Skip past limit"); } bb.offset += offset; } function isAtEnd(bb) { return bb.offset >= bb.limit; } function advance(bb, count) { let offset = bb.offset; if (offset + count > bb.limit) { throw new Error("Read past limit"); } bb.offset += count; return offset; } function readString(bb, count) { let offset = advance(bb, count); let fromCharCode = String.fromCharCode; let bytes = bb.bytes; let invalid = "�"; let text = ""; for (let i = 0; i < count; i++) { let c1 = bytes[i + offset], c2, c3, c4, c; if ((c1 & 128) === 0) { text += fromCharCode(c1); } else if ((c1 & 224) === 192) { if (i + 1 >= count) text += invalid; else { c2 = bytes[i + offset + 1]; if ((c2 & 192) !== 128) text += invalid; else { c = (c1 & 31) << 6 | c2 & 63; if (c < 128) text += invalid; else { text += fromCharCode(c); i++; } } } } else if ((c1 & 240) == 224) { if (i + 2 >= count) text += invalid; else { c2 = bytes[i + offset + 1]; c3 = bytes[i + offset + 2]; if (((c2 | c3 << 8) & 49344) !== 32896) text += invalid; else { c = (c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63; if (c < 2048 || c >= 55296 && c <= 57343) text += invalid; else { text += fromCharCode(c); i += 2; } } } } else if ((c1 & 248) == 240) { if (i + 3 >= count) text += invalid; else { c2 = bytes[i + offset + 1]; c3 = bytes[i + offset + 2]; c4 = bytes[i + offset + 3]; if (((c2 | c3 << 8 | c4 << 16) & 12632256) !== 8421504) text += invalid; else { c = (c1 & 7) << 18 | (c2 & 63) << 12 | (c3 & 63) << 6 | c4 & 63; if (c < 65536 || c > 1114111) text += invalid; else { c -= 65536; text += fromCharCode((c >> 10) + 55296, (c & 1023) + 56320); i += 3; } } } } else text += invalid; } return text; } function readByte(bb) { return bb.bytes[advance(bb, 1)]; } function readVarint32(bb) { let c = 0; let value = 0; let b; do { b = readByte(bb); if (c < 32) value |= (b & 127) << c; c += 7; } while (b & 128); return value; } const HTTP_STATUS = { /** The server cannot or will not process the request due to an apparent client error */ BAD_REQUEST: 400, /** Invalid or missing API key */ UNAUTHORIZED: 401, /** API key does not have access to this resource */ FORBIDDEN: 403, /** Server timeout waiting for the request */ REQUEST_TIMEOUT: 408, /** The requested content has been permanently deleted */ GONE: 410, /** User has sent too many requests in a given amount of time */ TOO_MANY_REQUESTS: 429, /** Server has encountered a situation it doesn't know how to handle */ INTERNAL_SERVER_ERROR: 500 }; const HTTP_METHODS = { GET: "GET", POST: "POST" }; const HTTP_HEADERS = { CONTENT_TYPE: "Content-Type", API_KEY: "hm-api-key" }; var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => { ErrorCode2["INVALID_API_KEY"] = "invalid_api_key"; ErrorCode2["INVALID_CONFIG"] = "invalid_config"; ErrorCode2["NETWORK_ERROR"] = "network_error"; ErrorCode2["TIMEOUT"] = "timeout"; ErrorCode2["INVALID_RESPONSE"] = "invalid_response"; ErrorCode2["RATE_LIMITED"] = "rate_limited"; ErrorCode2["SERVER_ERROR"] = "server_error"; ErrorCode2["CHALLENGE_EXPIRED"] = "challenge_expired"; ErrorCode2["CHALLENGE_NOT_FOUND"] = "challenge_not_found"; ErrorCode2["INVALID_CHALLENGE_FORMAT"] = "invalid_challenge_format"; ErrorCode2["NO_ACTIVE_CHALLENGE"] = "no_active_challenge"; ErrorCode2["VERIFICATION_FAILED"] = "verification_failed"; ErrorCode2["NO_RECEIPT_RECEIVED"] = "no_receipt_received"; ErrorCode2["MODULE_LOAD_FAILED"] = "module_load_failed"; ErrorCode2["QR_CODE_GENERATION_FAILED"] = "qr_code_generation_failed"; ErrorCode2["USER_CANCELLED"] = "user_cancelled"; return ErrorCode2; })(ErrorCode || {}); class HumanmarkError extends Error { constructor(message, code, statusCode, metadata) { super(message); this.name = "HumanmarkError"; this.code = code; if (statusCode !== void 0) { this.statusCode = statusCode; } if (metadata !== void 0) { this.metadata = metadata; } this.timestamp = /* @__PURE__ */ new Date(); if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } /** * Returns a JSON representation of the error for logging */ toJSON() { return { name: this.name, message: this.message, code: this.code, timestamp: this.timestamp.toISOString(), ...this.stack && { stack: this.stack }, ...this.metadata && { metadata: { ...this.metadata, ...this.statusCode && { statusCode: this.statusCode } } } }; } } class HumanmarkConfigError extends HumanmarkError { constructor(message, code, metadata) { super(message, code, HTTP_STATUS.BAD_REQUEST, metadata); this.name = "HumanmarkConfigError"; } } class HumanmarkNetworkError extends HumanmarkError { constructor(message, code, statusCode, metadata) { super(message, code, statusCode, metadata); this.name = "HumanmarkNetworkError"; this.errorCategory = metadata?.errorCategory ?? "unknown"; this.isTemporary = this.errorCategory === "temporary"; } } class HumanmarkApiError extends HumanmarkError { constructor(message, code, statusCode, metadata) { super(message, code, statusCode ?? HTTP_STATUS.BAD_REQUEST, metadata); this.name = "HumanmarkApiError"; } } class HumanmarkVerificationError extends HumanmarkError { constructor(message, code, metadata) { super(message, code, HTTP_STATUS.BAD_REQUEST, metadata); this.name = "HumanmarkVerificationError"; } } class HumanmarkChallengeError extends HumanmarkError { constructor(message, code, challenge, metadata) { super(message, code, HTTP_STATUS.BAD_REQUEST, { ...metadata, challenge }); this.name = "HumanmarkChallengeError"; if (challenge !== void 0) { this.challenge = challenge; } } } function isHumanmarkError(error) { return error instanceof HumanmarkError; } function base64urlDecode(base64url) { const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/"); const padding = (4 - base64.length % 4) % 4; const padded = base64 + "=".repeat(padding); const binary = atob(padded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } function parseChallengeToken(token) { try { const parts = token.split("."); if (parts.length !== 2) { throw new Error("Invalid token format"); } const payload = parts[0]; if (!payload) { throw new Error("Invalid token format: missing payload"); } const bytes = base64urlDecode(payload); try { const challengeToken = decodeChallengeToken(bytes); const challenge = challengeToken.challenge; const shard = challengeToken.shard; const domain = challengeToken.domain; const expiresAt = challengeToken.expires_at; const issuedAt = challengeToken.issued_at; if (!shard || !challenge || !expiresAt) { throw new Error("Missing required token claims"); } const exp = expiresAt; const iat = issuedAt ?? void 0; const claims = { shard, challenge, exp, ...iat && { iat }, ...domain && { domain } }; return { token, claims }; } catch (protobufError) { try { const decoded = new TextDecoder().decode(bytes); const mockData = JSON.parse(decoded); if (mockData.shard && mockData.challenge && (mockData.exp ?? mockData.expiresAt ?? mockData.expires_at)) { const claims = { shard: mockData.shard, challenge: mockData.challenge, exp: mockData.exp ?? mockData.expires_at ?? (mockData.expiresAt ? mockData.expiresAt / 1e3 : 0), ...mockData.iat && { iat: mockData.iat }, ...mockData.issued_at && !mockData.iat && { iat: mockData.issued_at }, ...mockData.issuedAt && !mockData.iat && !mockData.issued_at && { iat: mockData.issuedAt / 1e3 }, ...mockData.domain && { domain: mockData.domain } }; return { token, claims }; } } catch { throw protobufError; } throw new Error("Invalid token format"); } } catch (error) { throw new HumanmarkChallengeError( `Invalid challenge token: ${error instanceof Error ? error.message : "Unknown error"}`, ErrorCode.INVALID_CHALLENGE_FORMAT ); } } function parseShardFromToken(token) { const parsed = parseChallengeToken(token); return parsed.claims.shard; } function parseChallengeFromToken(token) { const parsed = parseChallengeToken(token); return parsed.claims.challenge; } function getTokenExpiration(token) { const parsed = parseChallengeToken(token); return parsed.claims.exp * 1e3; } function isTokenExpired(token) { const expirationMs = getTokenExpiration(token); return Date.now() >= expirationMs; } function constructShardUrl(baseUrl, shard) { try { const url = new URL(baseUrl); url.hostname = `${shard}.${url.hostname}`; const result = url.toString(); if (result.endsWith("/") && !url.search && url.pathname === "/") { return result.slice(0, -1); } return result; } catch (error) { throw new HumanmarkNetworkError( `Failed to construct shard URL: ${error instanceof Error ? error.message : "Unknown error"}`, ErrorCode.NETWORK_ERROR ); } } const TIME_UNITS = { /** Milliseconds per second */ MS_PER_SECOND: 1e3, /** Milliseconds per minute */ MS_PER_MINUTE: 6e4 }; const RETRY_CONFIG = { /** Maximum number of retry attempts */ MAX_RETRIES: 20, /** Initial delay before first retry in milliseconds */ INITIAL_DELAY_MS: TIME_UNITS.MS_PER_SECOND, /** Factor by which to multiply the delay for each retry */ BACKOFF_FACTOR: 2, /** Random jitter factor to prevent thundering herd (±10%) */ JITTER_FACTOR: 0.1 }; const TIMEOUT_CONFIG = { /** Total timeout for wait challenge operation (10 minutes) */ WAIT_CHALLENGE_TOTAL_MS: 10 * TIME_UNITS.MS_PER_MINUTE, /** Single request timeout (must be > 25s server timeout) */ SINGLE_REQUEST_MS: 30 * TIME_UNITS.MS_PER_SECOND }; const SPECIAL_VALUES = { /** Return value indicating no expiry is set */ NO_EXPIRY: -1, /** Minimum value for time calculations */ MIN_TIME: 0 }; function isServerError(status) { return status >= HTTP_STATUS.INTERNAL_SERVER_ERROR && status < 600; } function isRetryableStatus(status) { return isServerError(status) || status === HTTP_STATUS.TOO_MANY_REQUESTS; } function isRetryableNetworkError(error) { if (!(error instanceof Error)) return false; if (error.name === "AbortError") return false; return error.name === "TypeError" || error.message?.includes("Failed to fetch") || error.message?.includes("Network request failed") || error.message?.includes("ERR_NETWORK") || error.message?.includes("ERR_INTERNET_DISCONNECTED") || error.message?.includes("ECONNREFUSED") || error.message?.includes("ETIMEDOUT") || error.message?.includes("ENOTFOUND"); } function categorizeNetworkError(error) { if (!(error instanceof Error)) return "unknown"; if (error.name === "AbortError") return "permanent"; if (error.message?.includes("Failed to fetch") || error.message?.includes("Network request failed") || error.message?.includes("ERR_NETWORK") || error.message?.includes("ERR_INTERNET_DISCONNECTED") || error.message?.includes("ECONNREFUSED") || error.message?.includes("ETIMEDOUT") || error.message?.includes("ENOTFOUND")) { return "temporary"; } if (error.message?.includes("ERR_CERT") || error.message?.includes("SSL") || error.message?.includes("TLS")) { return "permanent"; } if (error.message?.includes("CORS")) { return "permanent"; } return "unknown"; } function createFetchOptions(method, headers, body, signal) { const options = { method, headers }; return options; } function isOnline() { return typeof navigator !== "undefined" ? navigator.onLine : true; } function calculateRetryDelay(attempt) { const baseDelay = RETRY_CONFIG.INITIAL_DELAY_MS * Math.pow(RETRY_CONFIG.BACKOFF_FACTOR, attempt); const jitter = baseDelay * RETRY_CONFIG.JITTER_FACTOR * (Math.random() * 2 - 1); return Math.max(0, baseDelay + jitter); } async function delay(ms, signal) { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new DOMException("Aborted", "AbortError")); return; } const timeoutId = setTimeout(resolve, ms); const abortHandler = () => { clearTimeout(timeoutId); reject(new DOMException("Aborted", "AbortError")); }; signal?.addEventListener("abort", abortHandler, { once: true }); }); } class HumanmarkVerificationCancelledError extends HumanmarkError { constructor(message = "User cancelled verification") { super(message, ErrorCode.USER_CANCELLED); this.name = "HumanmarkVerificationCancelledError"; } } function createTimeoutError(operation) { const message = operation ? `${operation} timed out` : "Request timed out"; return new HumanmarkNetworkError(message, ErrorCode.TIMEOUT); } function createNetworkError(error) { if (error instanceof HumanmarkError) { return error; } const message = error instanceof Error ? error.message : "Network error occurred"; const category = categorizeNetworkError(error); return new HumanmarkNetworkError( message, ErrorCode.NETWORK_ERROR, void 0, { errorCategory: category } ); } function createApiErrorFromStatus(status, statusText) { let code; let message; switch (status) { case HTTP_STATUS.UNAUTHORIZED: case HTTP_STATUS.FORBIDDEN: code = ErrorCode.INVALID_API_KEY; message = `HTTP ${status}: ${statusText}`; break; case HTTP_STATUS.TOO_MANY_REQUESTS: code = ErrorCode.RATE_LIMITED; message = `HTTP ${status}: ${statusText}`; break; case HTTP_STATUS.GONE: code = ErrorCode.CHALLENGE_EXPIRED; message = "Challenge expired"; break; default: code = ErrorCode.SERVER_ERROR; message = `HTTP ${status}: ${statusText}`; } return new HumanmarkApiError(message, code, status); } function createCancelledError() { return new HumanmarkVerificationCancelledError("User cancelled verification"); } function createConfigError(message, field) { const metadata = { field }; return new HumanmarkConfigError(message, ErrorCode.INVALID_CONFIG, metadata); } function createInvalidChallengeError(challenge) { return new HumanmarkChallengeError( "Invalid challenge ID format", ErrorCode.INVALID_CHALLENGE_FORMAT, challenge ); } function createNoChallengeError() { return new HumanmarkChallengeError( "No active challenge available", ErrorCode.NO_ACTIVE_CHALLENGE ); } function createNoReceiptError() { return new HumanmarkVerificationError( "No receipt received from verification", ErrorCode.NO_RECEIPT_RECEIVED ); } class ApiClient { /** * Creates a new ApiClient instance * * @param baseUrl - Base URL for API requests (default: https://humanmark.io) */ constructor(baseUrl = DEFAULT_BASE_URL) { this.abortController = null; this.baseUrl = baseUrl; } /** * Cancels any ongoing API requests */ cancelPendingRequests() { if (this.abortController) { this.abortController.abort(); this.abortController = null; } } /** * Waits for challenge completion using long polling * * Continuously polls the server until: * - User completes verification (returns receipt) * - Challenge expires (410 status) * - Timeout is reached * * @param token - Challenge token containing shard and challenge * @param headers - Required API headers including api key * @param options - Optional request configuration * @returns Promise resolving to verification response with receipt * @throws {Error} On challenge expiry, timeout, or other errors */ async waitForChallengeToken(token, headers, options) { this.abortController = options?.signal ? new AbortController() : new AbortController(); if (options?.signal) { const abortHandler = () => this.abortController?.abort(); options.signal.addEventListener("abort", abortHandler, { once: true }); } try { const startTime = Date.now(); const timeout = options?.timeout ?? TIMEOUT_CONFIG.WAIT_CHALLENGE_TOTAL_MS; const shard = parseShardFromToken(token); const challenge = parseChallengeFromToken(token); const shardUrl = constructShardUrl(this.baseUrl, shard); let retryCount = 0; while (true) { try { const remainingTime = this.performPreRequestChecks( startTime, timeout ); if (retryCount > 0) { await this.waitForRetry(retryCount, startTime, timeout); } const requestTimeout = Math.min( remainingTime, TIMEOUT_CONFIG.SINGLE_REQUEST_MS ); const response = await this.makeRequest( `${shardUrl}${ENDPOINTS.WAIT_CHALLENGE}/${challenge}`, createFetchOptions(HTTP_METHODS.GET, { [HTTP_HEADERS.API_KEY]: headers[HTTP_HEADERS.API_KEY] }), requestTimeout ); const result = await this.handleWaitResponse(response); if (result === "retry") { retryCount = 0; continue; } return result; } catch (error) { if (!this.shouldRetryError(error, retryCount, startTime, timeout)) { throw this.mapNetworkError(error); } retryCount++; } } } finally { this.abortController = null; } } /** * Checks if the request has been cancelled * @throws {HumanmarkNetworkError} If request was cancelled */ checkCancellation() { if (this.abortController?.signal.aborted) { throw createNetworkError( new DOMException("Request cancelled", "AbortError") ); } } /** * Checks if we've exceeded the maximum time allowed * @throws {HumanmarkNetworkError} If timeout exceeded */ checkTimeout(startTime, maxTotalTime) { const elapsedTime = Date.now() - startTime; const remainingTime = maxTotalTime - elapsedTime; if (remainingTime <= 0) { throw createTimeoutError("Client request"); } return remainingTime; } /** * Waits before retrying with logging */ async waitForRetry(attempt, startTime, maxTotalTime) { const retryDelay = calculateRetryDelay(attempt); const elapsedTime = Date.now() - startTime; if (retryDelay > 0 && retryDelay + elapsedTime > maxTotalTime) { throw createTimeoutError("Client request"); } if (retryDelay > 0) { await delay(retryDelay, this.abortController?.signal); } } /** * Creates an abort controller linked to the main controller */ createLinkedAbortController(timeout) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const mainAbortHandler = () => controller.abort(); this.abortController?.signal.addEventListener("abort", mainAbortHandler, { once: true }); controller.timeoutId = timeoutId; return controller; } /** * Handles wait response and determines if retry is needed */ async handleWaitResponse(response) { if (response.status === HTTP_STATUS.REQUEST_TIMEOUT) { return "retry"; } if (response.ok) { return await this.parseJsonResponse(response); } if (isRetryableStatus(response.status)) { throw createApiErrorFromStatus(response.status, response.statusText); } this.handleResponseError(response); } /** * Makes a request with timeout and cancellation support */ async makeRequest(url, options, timeout) { const controller = this.createLinkedAbortController(timeout); try { return await fetch(url, { ...options, signal: controller.signal }); } finally { clearTimeout(controller.timeoutId); } } /** * Performs pre-request validation checks */ performPreRequestChecks(startTime, maxTime) { this.checkCancellation(); if (!isOnline()) { throw new HumanmarkNetworkError( "No internet connection", ErrorCode.NETWORK_ERROR, void 0, { isTemporary: true, errorCategory: "temporary" } ); } return this.checkTimeout(startTime, maxTime); } /** * Handles retry logic for network errors */ shouldRetryError(error, attempt, startTime, maxTime) { const elapsedTime = Date.now() - startTime; const hasTimeRemaining = elapsedTime < maxTime; const hasAttemptsRemaining = attempt < RETRY_CONFIG.MAX_RETRIES - 1; if (!hasTimeRemaining || !hasAttemptsRemaining) { return false; } if (error instanceof HumanmarkError) { if (error instanceof HumanmarkNetworkError && error.isTemporary) { return true; } if (error instanceof HumanmarkApiError && error.statusCode) { return isRetryableStatus(error.statusCode); } return false; } return isRetryableNetworkError(error); } /** * Maps errors to appropriate Humanmark error types */ mapNetworkError(error) { if (error instanceof HumanmarkError) { return error; } if (error instanceof Error && error.name === "AbortError") { return createTimeoutError(); } return createNetworkError(error); } /** * Handles response errors with appropriate error codes */ handleResponseError(response) { throw createApiErrorFromStatus(response.status, response.statusText); } /** * Parses JSON response safely */ async parseJsonResponse(response) { try { return await response.json(); } catch { throw new HumanmarkNetworkError( "Invalid JSON response from server", ErrorCode.INVALID_RESPONSE, response.status ); } } } class ChallengeManager { constructor() { this.token = null; } /** * Stores a challenge token * * @param token - Challenge token to store * @throws {HumanmarkChallengeError} If the token is invalid */ setChallengeToken(token) { try { parseChallengeToken(token); } catch { throw createInvalidChallengeError(token); } this.token = token; } /** * Retrieves the current challenge token if valid * * Automatically returns null for expired tokens. * * @returns Current challenge token or null if none exists or expired */ getCurrentToken() { if (!this.token || this.isExpired()) { return null; } return this.token; } /** * Checks if the current challenge token has expired * * @returns true if expired or no token, false if valid */ isExpired() { if (!this.token) { return true; } try { return isTokenExpired(this.token); } catch { return true; } } /** * Gets remaining time until challenge token expires * * @returns Milliseconds until expiry, 0 if expired, -1 if no token */ getTimeRemaining() { if (!this.token) { return SPECIAL_VALUES.NO_EXPIRY; } try { const expirationMs = getTokenExpiration(this.token); const remaining = expirationMs - Date.now(); return Math.max(SPECIAL_VALUES.MIN_TIME, remaining); } catch { return SPECIAL_VALUES.MIN_TIME; } } /** * Clears the stored challenge token */ clearChallengeToken() { this.token = null; } } class ThemeManager { /** * Initialize theme by setting data-hm-theme attribute * CSS handles all theme logic including auto theme via media queries */ static initialize(theme = "auto") { document.documentElement.setAttribute("data-hm-theme", theme ?? "auto"); } } class HumanmarkSdk { /** * Creates a new instance of HumanmarkSdk * * @param config - Configuration object for the SDK * @param config.apiKey - Your Humanmark API key (required) * @param config.challengeToken - Pre-created challenge token from your backend (required) * @param config.baseUrl - Base URL for API requests (optional, defaults to 'https://humanmark.io') * @param config.theme - Theme for the modal: 'light', 'dark', or 'auto' (optional, defaults to 'auto') * * @throws {Error} If configuration is invalid */ constructor(config) { this.uiManager = null; this.verificationInProgress = null; this.validateConfig(config); this.config = { ...config }; this.apiClient = new ApiClient(config.baseUrl ?? DEFAULT_BASE_URL); this.challengeManager = new ChallengeManager(); ThemeManager.initialize(config.theme); } validateConfig(config) { if (!config.apiKey || typeof config.apiKey !== "string") { throw new HumanmarkConfigError( "API key is required and must be a string", ErrorCode.INVALID_API_KEY ); } if (!config.challengeToken || typeof config.challengeToken !== "string") { throw createConfigError( "Challenge token is required and must be a string", "challengeToken" ); } } /** * Starts the verification process * * Shows a modal with either a QR code (desktop) or deep link button (mobile) * and waits for the user to complete verification in the Humanmark app. * * @returns Promise that resolves with the receipt * @throws {Error} If verification fails or times out * * @example * try { * const receipt = await sdk.verify(); * // Send receipt to your backend for validation * await submitToBackend(receipt); * } catch (error) { * console.error('Verification failed:', error); * } */ async verify() { const existingPromise = this.verificationInProgress; if (existingPromise) { return existingPromise; } const verificationPromise = this.performVerification(); this.verificationInProgress = verificationPromise; verificationPromise.finally(() => { if (this.verificationInProgress === verificationPromise) { this.verificationInProgress = null; } }).catch(() => { }); return verificationPromise; } async performVerification() { let verificationCompleted = false; const abortController = new AbortController(); try { this.initialize(); await this.showVerificationModal(); this.uiManager?.onModalClosed(() => { if (!verificationCompleted) { abortController.abort(); this.apiClient.cancelPendingRequests(); } }); const result = await this.waitForVerification(abortController.signal); verificationCompleted = true; if (this.uiManager) { const successAnimationComplete = new Promise((resolve) => { this.uiManager?.onSuccess(() => { this.cleanup(); resolve(); }); }); this.uiManager.showSuccess(); await successAnimationComplete; } else { this.cleanup(); } return result; } catch (error) { verificationCompleted = true; this.cleanup(); throw error; } } initialize() { this.challengeManager.setChallengeToken(this.config.challengeToken); } async showVerificationModal() { const token = this.challengeManager.getCurrentToken(); if (!token) { throw createNoChallengeError(); } if (!this.uiManager) { const { loadUIManager } = await import("./index-htvd8qd5.js"); const UIManagerClass = await loadUIManager(); this.uiManager = new UIManagerClass(); } await this.uiManager.showVerificationModal(token); } async waitForVerification(signal) { const token = this.challengeManager.getCurrentToken(); if (!token) { throw createNoChallengeError(); } if (this.challengeManager.isExpired()) { throw new HumanmarkApiError( "Challenge expired", ErrorCode.CHALLENGE_EXPIRED, 410 ); } if (signal.aborted) { throw createCancelledError(); } let abortHandler = null; try { const waitForAbort = new Promise((resolve) => { abortHandler = () => resolve(); signal.addEventListener("abort", abortHandler, { once: true }); }); const apiCallPromise = this.apiClient.waitForChallengeToken(token, { [HTTP_HEADERS.API_KEY]: this.config.apiKey }); const raceResult = await Promise.race([ apiCallPromise.then((result2) => ({ type: "success", result: result2 })), waitForAbort.then(() => ({ type: "aborted" })) ]); if (raceResult.type === "aborted") { this.apiClient.cancelPendingRequests(); throw createCancelledError(); } const result = raceResult.result; if (result.receipt) { return result.receipt; } else { throw createNoReceiptError(); } } finally { if (abortHandler) { signal.removeEventListener("abort", abortHandler); } } } /** * Cleans up resources after verification * Called automatically after verification completes or fails * @param immediate - Skip animations for immediate cleanup (useful for tests) */ cleanup(immediate = false) { this.apiClient.cancelPendingRequests(); this.challengeManager.clearChallengeToken(); this.uiManager?.hideModal(immediate); } } async function preloadUIComponents() { const { preloadUIComponents: preloadUIComponents2 } = await import("./index-htvd8qd5.js"); return preloadUIComponents2(); } export { ErrorCode as E, HumanmarkError as H, HumanmarkChallengeError as a, HumanmarkSdk as b, HumanmarkVerificationCancelledError as c, preloadUIComponents as d, getTokenExpiration as g, isHumanmarkError as i, parseChallengeToken as p }; //# sourceMappingURL=index-CjrzYJmo.js.map