UNPKG

@bynn-intelligence/agemin-sdk

Version:

agemin.com SDK for age verification

1,356 lines (1,332 loc) 266 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Agemin = {})); })(this, (function (exports) { 'use strict'; /** * Create a DOM element with specified attributes and styles */ function createElement(tag, attributes, styles) { const element = document.createElement(tag); if (attributes) { Object.entries(attributes).forEach(([key, value]) => { element.setAttribute(key, value); }); } if (styles) { element.style.cssText = styles; } return element; } /** * Remove an element from the DOM */ function removeElement(elementOrId) { const element = document.getElementById(elementOrId) ; if (element && element.parentNode) { element.parentNode.removeChild(element); } } /** * Add styles to the document head */ function addStyles(id, styles) { if (!document.getElementById(id)) { const styleElement = createElement('style', { id }); styleElement.textContent = styles; document.head.appendChild(styleElement); } } /** * Build URL with query parameters */ function buildUrl(baseUrl, params) { const url = new URL(baseUrl); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (typeof value === 'object') { url.searchParams.append(key, JSON.stringify(value)); } else { url.searchParams.append(key, String(value)); } } }); return url.toString(); } /** * Parse message from postMessage event */ function parseMessage(event) { try { if (typeof event.data === 'object' && event.data.type) { return event.data; } return null; } catch { return null; } } /** * Check if origin is trusted */ function isTrustedOrigin(origin) { const trustedDomains = ['agemin.com', 'verify.agemin.com', 'localhost']; return trustedDomains.some(domain => origin.includes(domain)); } const SDK_VERSION = '"5.7.1"'; const DEFAULT_CONFIG = { baseUrl: 'https://verify.agemin.com', theme: 'auto', locale: 'auto', debug: false, allowSearchEngineBypass: false, searchEngineDetection: 'ua' }; const MODAL_STYLES = { overlay: ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999998; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(20px); animation: agemin-fade-in 0.3s ease-out; `, modal: ` position: relative; width: 90%; max-width: 500px; height: auto; min-height: 400px; max-height: 90vh; background: white; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow: hidden; z-index: 999999; animation: agemin-slide-up 0.3s ease-out; transition: height 0.3s ease-out; `, closeButton: ` position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border-radius: 50%; background: rgba(0, 0, 0, 0.1); border: none; font-size: 20px; cursor: pointer; z-index: 1000000; display: flex; align-items: center; justify-content: center; transition: all 0.2s; `, iframe: ` width: 100%; height: 100%; border: none; ` }; // Mobile-specific styles (fullscreen) const MOBILE_MODAL_STYLES = { overlay: ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; background: white; z-index: 999998; `, modal: ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; background: white; overflow: hidden; z-index: 999999; `, closeButton: ` position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border-radius: 50%; background: rgba(0, 0, 0, 0.05); border: none; font-size: 20px; cursor: pointer; z-index: 1000001; display: flex; align-items: center; justify-content: center; transition: all 0.2s; `, iframe: ` width: 100%; height: 100%; border: none; ` }; const SPINNER_STYLES = { container: ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1000002; `, spinner: ` width: 48px; height: 48px; border: 4px solid rgba(255, 255, 255, 0.3); border-top-color: white; border-radius: 50%; animation: agemin-spin 1s linear infinite; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); `, spinnerMobile: ` width: 56px; height: 56px; border: 4px solid rgba(0, 0, 0, 0.1); border-top-color: #3b82f6; border-radius: 50%; animation: agemin-spin 1s linear infinite; ` }; const ANIMATIONS = ` @keyframes agemin-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes agemin-slide-up { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes agemin-fade-out { from { opacity: 1; } to { opacity: 0; } } @keyframes agemin-slide-down { from { transform: translateY(0); opacity: 1; } to { transform: translateY(20px); opacity: 0; } } @keyframes agemin-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `; /** * Check if the current device is mobile */ /** * Check if the screen is small (mobile-sized) */ function isSmallScreen() { // Multiple checks for better mobile detection // Check viewport width const isNarrowViewport = window.innerWidth < 768; // Check for touch capability const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; // Check for mobile user agent as fallback const isMobileUA = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); // Check for tablet (wider viewport but still touch) const isTablet = window.innerWidth < 1024 && hasTouch; // Return true if any mobile condition is met return isNarrowViewport || (hasTouch && isMobileUA) || isTablet; } /** * Check if the browser supports required features */ function isSupported$1() { return typeof window !== 'undefined' && typeof window.postMessage === 'function' && typeof window.addEventListener === 'function'; } /** * Get the appropriate verification mode based on device */ function getDefaultMode() { // Always use modal for consistency across devices return 'modal'; } /** * Get the browser's language */ function getBrowserLanguage() { if (typeof navigator === 'undefined') return 'en'; // Try to get the most specific language first const lang = navigator.language || navigator.userLanguage || (navigator.languages && navigator.languages[0]) || 'en'; // Return the language code (e.g., 'en-US' -> 'en') return lang.split('-')[0].toLowerCase(); } /** * Check if browser has zero plugins (common in headless browsers) */ function isHeadlessPlugins() { if (typeof navigator === 'undefined') return false; return navigator.plugins.length === 0; } /** * Check if browser languages is empty (common in headless browsers) */ function isHeadlessLanguages() { if (typeof navigator === 'undefined') return false; // Check if languages is undefined, empty array, or empty string (for compatibility) return !navigator.languages || (Array.isArray(navigator.languages) && navigator.languages.length === 0) || navigator.languages === ''; } /** * Check for headless WebGL vendor/renderer signatures */ function isHeadlessWebGL() { if (typeof document === 'undefined') return false; try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl || !(gl instanceof WebGLRenderingContext)) return false; const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); if (!debugInfo) return false; const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL); const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); // Common headless browser signatures return (vendor === 'Brian Paul' && renderer === 'Mesa OffScreen') || (vendor === 'Google Inc.' && renderer === 'Google SwiftShader'); } catch (e) { // If WebGL check fails, assume not headless return false; } } /** * Combined headless browser detection */ function isHeadlessBrowser() { // Use lazy evaluation - stop checking after first positive return isHeadlessPlugins() || isHeadlessLanguages() || isHeadlessWebGL(); } /** * Check if browser supports cookies */ function supportsCookies() { if (typeof document === 'undefined') return true; // Assume support if not in browser try { // Try to set a test cookie document.cookie = 'agemin_test=1; path=/'; const enabled = document.cookie.indexOf('agemin_test=') !== -1; // Clean up test cookie if (enabled) { document.cookie = 'agemin_test=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; } return enabled; } catch (e) { // If cookie check fails, assume support return true; } } /** * User agent based bot detection */ function isUserAgentBot() { if (typeof navigator === 'undefined' || !navigator.userAgent) { return false; } const userAgent = navigator.userAgent.toLowerCase(); // Comprehensive list of search engine bot patterns const searchEnginePatterns = [ // Google 'googlebot', 'adsbot-google', 'mediapartners-google', 'google-inspectiontool', 'googlesecurityscanner', // Bing/Microsoft 'bingbot', 'bingpreview', 'msnbot', 'adidxbot', // Baidu 'baiduspider', 'baidu', // Yandex 'yandexbot', 'yandex', // DuckDuckGo 'duckduckbot', 'duckduckgo', // Yahoo/Verizon 'slurp', // Social Media 'facebookexternalhit', 'facebookcatalog', 'twitterbot', 'linkedinbot', 'whatsapp', 'telegrambot', 'discordbot', 'slackbot', // Other Major Crawlers 'applebot', // Apple 'ahrefsbot', // Ahrefs SEO 'semrushbot', // SEMrush SEO 'dotbot', // Moz 'rogerbot', // Moz 'seznambot', // Seznam 'petalbot', // Huawei 'mj12bot', // Majestic 'blexbot', // webmeup 'serpstatbot', // Serpstat 'screaming frog', // Screaming Frog SEO // Generic patterns 'bot', 'crawler', 'spider', 'scraper' ]; // Check if user agent contains any of the patterns return searchEnginePatterns.some(pattern => userAgent.includes(pattern)); } // Cache detection results to avoid repeated expensive operations let cachedDetectionResult = null; const CACHE_DURATION = 60000; // Cache for 1 minute /** * Check if the current user agent is a search engine bot/crawler * Used to allow search engines to bypass age verification for SEO purposes * * @param mode - Detection mode: 'ua' | 'headless' | 'cookies' | 'combined' | 'strict' */ function isSearchEngineBot(mode = 'ua') { // Check cache first if (cachedDetectionResult && cachedDetectionResult.mode === mode && Date.now() - cachedDetectionResult.timestamp < CACHE_DURATION) { return cachedDetectionResult.result; } let result = false; switch (mode) { case 'ua': // User agent only (fastest, default) result = isUserAgentBot(); break; case 'headless': // Headless browser detection only result = isHeadlessBrowser(); break; case 'cookies': // Cookie support check only (crawlers typically don't support cookies) result = !supportsCookies(); break; case 'combined': // Any detection method triggers (most inclusive) result = isUserAgentBot() || isHeadlessBrowser() || !supportsCookies(); break; case 'strict': // Requires multiple signals (reduces false positives) // Must have bot UA AND (headless OR no cookies) result = isUserAgentBot() && (isHeadlessBrowser() || !supportsCookies()); break; default: // Default to UA detection for backward compatibility result = isUserAgentBot(); } // Cache the result cachedDetectionResult = { mode, result, timestamp: Date.now() }; return result; } class Modal { constructor() { this.verificationWindow = null; this.escKeyHandler = null; this.loadingSpinner = null; this.setupStyles(); } setupStyles() { addStyles('agemin-styles', ANIMATIONS); } openIframe(url, onClose) { // Check if modal already exists if (document.getElementById('agemin-iframe')) { console.warn('Agemin SDK: Modal already exists, not creating another one'); return; } // Determine if we should use mobile styles (fullscreen) const isMobileView = isSmallScreen(); const styles = isMobileView ? MOBILE_MODAL_STYLES : MODAL_STYLES; // Create overlay const overlay = createElement('div', { id: 'agemin-overlay' }, styles.overlay); // Create loading spinner this.loadingSpinner = createElement('div', { id: 'agemin-spinner-container' }, SPINNER_STYLES.container); const spinner = createElement('div', { id: 'agemin-spinner' }, isMobileView ? SPINNER_STYLES.spinnerMobile : SPINNER_STYLES.spinner); this.loadingSpinner.appendChild(spinner); if (isMobileView) { // Mobile: fullscreen mode - overlay is the container // Create close button const closeBtn = createElement('button', { id: 'agemin-close-btn' }, styles.closeButton); closeBtn.innerHTML = '✕'; closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(0, 0, 0, 0.1)'; closeBtn.onmouseout = () => closeBtn.style.background = 'rgba(0, 0, 0, 0.05)'; closeBtn.onclick = () => { this.close(); if (onClose) onClose(); }; // Create iframe (initially hidden) const iframe = createElement('iframe', { id: 'agemin-iframe', src: url, allow: 'camera; microphone' }, styles.iframe + 'opacity: 0; transition: opacity 0.3s ease-out;'); this.verificationWindow = iframe; // Append directly to overlay for fullscreen overlay.appendChild(iframe); overlay.appendChild(this.loadingSpinner); // Add spinner overlay.appendChild(closeBtn); // Close button on top document.body.appendChild(overlay); // Prevent body scroll on mobile document.body.style.overflow = 'hidden'; } else { // Desktop: modal in center with overlay background const modal = createElement('div', { id: 'agemin-modal' }, styles.modal); // Create close button const closeBtn = createElement('button', {}, styles.closeButton); closeBtn.innerHTML = '✕'; closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(0, 0, 0, 0.2)'; closeBtn.onmouseout = () => closeBtn.style.background = 'rgba(0, 0, 0, 0.1)'; closeBtn.onclick = () => { this.close(); if (onClose) onClose(); }; // Create iframe (initially hidden) const iframe = createElement('iframe', { id: 'agemin-iframe', src: url, allow: 'camera; microphone' }, styles.iframe + 'opacity: 0; transition: opacity 0.3s ease-out;'); this.verificationWindow = iframe; // Assemble modal modal.appendChild(closeBtn); modal.appendChild(iframe); modal.appendChild(this.loadingSpinner); // Add spinner overlay.appendChild(modal); document.body.appendChild(overlay); } // NO overlay click handler - only close via X button or explicit actions // This prevents accidental closures // Close on escape key (desktop only - mobile users can't press escape) if (!isMobileView) { this.escKeyHandler = (e) => { if (e.key === 'Escape') { this.close(); if (onClose) onClose(); } }; document.addEventListener('keydown', this.escKeyHandler); } } close() { // Add closing animations const overlay = document.getElementById('agemin-overlay'); const modal = document.getElementById('agemin-modal'); if (overlay && modal) { overlay.style.animation = 'agemin-fade-out 0.3s ease-out'; modal.style.animation = 'agemin-slide-down 0.3s ease-out'; // Remove after animation setTimeout(() => { this.cleanup(); }, 300); } else { this.cleanup(); } } cleanup() { // Remove iframe overlay removeElement('agemin-overlay'); // Restore body scroll (was disabled on mobile) document.body.style.overflow = ''; // Remove event listener if (this.escKeyHandler) { document.removeEventListener('keydown', this.escKeyHandler); this.escKeyHandler = null; } this.verificationWindow = null; } getWindow() { return this.verificationWindow; } isOpen() { return document.getElementById('agemin-iframe') !== null; } updateHeight(height) { // Skip height updates on mobile since it's always fullscreen if (isSmallScreen()) { return; } const modal = document.getElementById('agemin-modal'); if (modal) { // Clamp height between min and max const minHeight = 400; const maxHeight = window.innerHeight * 0.9; // 90vh const clampedHeight = Math.min(Math.max(height, minHeight), maxHeight); // Apply the new height with smooth transition modal.style.height = `${clampedHeight}px`; } } hideLoading() { // Hide spinner with fade out if (this.loadingSpinner) { this.loadingSpinner.style.opacity = '0'; this.loadingSpinner.style.transition = 'opacity 0.3s ease-out'; // Remove spinner after animation setTimeout(() => { if (this.loadingSpinner && this.loadingSpinner.parentNode) { this.loadingSpinner.parentNode.removeChild(this.loadingSpinner); this.loadingSpinner = null; } }, 300); } // Show iframe with fade in const iframe = document.getElementById('agemin-iframe'); if (iframe) { iframe.style.opacity = '1'; } } } exports.MessageType = void 0; (function (MessageType) { MessageType["SUCCESS"] = "agemin:verification:success"; MessageType["ERROR"] = "agemin:verification:error"; MessageType["CANCEL"] = "agemin:verification:cancel"; MessageType["CLOSE"] = "agemin:verification:close"; MessageType["READY"] = "agemin:verification:ready"; MessageType["RESIZE"] = "agemin:verification:resize"; // New message types from app MessageType["APP_READY"] = "agemin:app:ready"; MessageType["APP_ERROR"] = "agemin:app:error"; MessageType["PROGRESS"] = "agemin:verification:progress"; MessageType["STATE_CHANGE"] = "agemin:state:change"; MessageType["USER_ACTION"] = "agemin:user:action"; MessageType["CONFIG_RECEIVED"] = "agemin:config:received"; })(exports.MessageType || (exports.MessageType = {})); /** * Cookie utility functions for managing age verification cookies */ /** * Set a cookie with the given name, value, and expiration */ function setCookie(name, value, expiresSeconds) { let expires = ''; if (expiresSeconds === null || expiresSeconds === 0) { // Session cookie - no expires attribute means it expires when browser closes expires = ''; } else if (expiresSeconds > 0) { // Convert seconds to milliseconds and create date const date = new Date(); date.setTime(date.getTime() + (expiresSeconds * 1000)); expires = '; expires=' + date.toUTCString(); } // Get the current domain (without subdomain for broader compatibility) const domain = window.location.hostname; const domainParts = domain.split('.'); let cookieDomain = ''; // For localhost or IP addresses, don't set domain if (domain === 'localhost' || /^[\d.]+$/.test(domain)) { cookieDomain = ''; } else if (domainParts.length >= 2) { // Set cookie for parent domain to work across subdomains cookieDomain = '; domain=.' + domainParts.slice(-2).join('.'); } // Build cookie string with security flags // SameSite=Lax allows the cookie to be sent with top-level navigations // which is needed for the redirect flow const cookieString = name + '=' + encodeURIComponent(value) + expires + cookieDomain + '; path=/; SameSite=Lax'; // Only add Secure flag if on HTTPS (not localhost) if (window.location.protocol === 'https:') { document.cookie = cookieString + '; Secure'; } else { document.cookie = cookieString; } } /** * Get a cookie value by name */ function getCookie(name) { const nameEQ = name + '='; const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { let cookie = cookies[i]; while (cookie.charAt(0) === ' ') { cookie = cookie.substring(1, cookie.length); } if (cookie.indexOf(nameEQ) === 0) { return decodeURIComponent(cookie.substring(nameEQ.length, cookie.length)); } } return null; } /** * Delete a cookie by name */ function deleteCookie(name) { // Delete cookie by setting expiration to past date setCookie(name, '', -1); // Also try to delete with current domain and parent domain const domain = window.location.hostname; document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=' + domain + ';'; // Try parent domain as well const domainParts = domain.split('.'); if (domainParts.length >= 2) { const parentDomain = '.' + domainParts.slice(-2).join('.'); document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=' + parentDomain + ';'; } } const encoder = new TextEncoder(); const decoder = new TextDecoder(); function concat(...buffers) { const size = buffers.reduce((acc, { length }) => acc + length, 0); const buf = new Uint8Array(size); let i = 0; for (const buffer of buffers) { buf.set(buffer, i); i += buffer.length; } return buf; } function decodeBase64(encoded) { if (Uint8Array.fromBase64) { return Uint8Array.fromBase64(encoded); } const binary = atob(encoded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } function decode(input) { if (Uint8Array.fromBase64) { return Uint8Array.fromBase64(typeof input === 'string' ? input : decoder.decode(input), { alphabet: 'base64url', }); } let encoded = input; if (encoded instanceof Uint8Array) { encoded = decoder.decode(encoded); } encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''); try { return decodeBase64(encoded); } catch { throw new TypeError('The input to be decoded is not correctly encoded.'); } } class JOSEError extends Error { static code = 'ERR_JOSE_GENERIC'; code = 'ERR_JOSE_GENERIC'; constructor(message, options) { super(message, options); this.name = this.constructor.name; Error.captureStackTrace?.(this, this.constructor); } } class JWTClaimValidationFailed extends JOSEError { static code = 'ERR_JWT_CLAIM_VALIDATION_FAILED'; code = 'ERR_JWT_CLAIM_VALIDATION_FAILED'; claim; reason; payload; constructor(message, payload, claim = 'unspecified', reason = 'unspecified') { super(message, { cause: { claim, reason, payload } }); this.claim = claim; this.reason = reason; this.payload = payload; } } class JWTExpired extends JOSEError { static code = 'ERR_JWT_EXPIRED'; code = 'ERR_JWT_EXPIRED'; claim; reason; payload; constructor(message, payload, claim = 'unspecified', reason = 'unspecified') { super(message, { cause: { claim, reason, payload } }); this.claim = claim; this.reason = reason; this.payload = payload; } } class JOSEAlgNotAllowed extends JOSEError { static code = 'ERR_JOSE_ALG_NOT_ALLOWED'; code = 'ERR_JOSE_ALG_NOT_ALLOWED'; } class JOSENotSupported extends JOSEError { static code = 'ERR_JOSE_NOT_SUPPORTED'; code = 'ERR_JOSE_NOT_SUPPORTED'; } class JWSInvalid extends JOSEError { static code = 'ERR_JWS_INVALID'; code = 'ERR_JWS_INVALID'; } class JWTInvalid extends JOSEError { static code = 'ERR_JWT_INVALID'; code = 'ERR_JWT_INVALID'; } class JWSSignatureVerificationFailed extends JOSEError { static code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; constructor(message = 'signature verification failed', options) { super(message, options); } } function unusable(name, prop = 'algorithm.name') { return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); } function isAlgorithm(algorithm, name) { return algorithm.name === name; } function getHashLength(hash) { return parseInt(hash.name.slice(4), 10); } function getNamedCurve$1(alg) { switch (alg) { case 'ES256': return 'P-256'; case 'ES384': return 'P-384'; case 'ES512': return 'P-521'; default: throw new Error('unreachable'); } } function checkUsage(key, usage) { if (!key.usages.includes(usage)) { throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); } } function checkSigCryptoKey(key, alg, usage) { switch (alg) { case 'HS256': case 'HS384': case 'HS512': { if (!isAlgorithm(key.algorithm, 'HMAC')) throw unusable('HMAC'); const expected = parseInt(alg.slice(2), 10); const actual = getHashLength(key.algorithm.hash); if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash'); break; } case 'RS256': case 'RS384': case 'RS512': { if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5')) throw unusable('RSASSA-PKCS1-v1_5'); const expected = parseInt(alg.slice(2), 10); const actual = getHashLength(key.algorithm.hash); if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash'); break; } case 'PS256': case 'PS384': case 'PS512': { if (!isAlgorithm(key.algorithm, 'RSA-PSS')) throw unusable('RSA-PSS'); const expected = parseInt(alg.slice(2), 10); const actual = getHashLength(key.algorithm.hash); if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash'); break; } case 'Ed25519': case 'EdDSA': { if (!isAlgorithm(key.algorithm, 'Ed25519')) throw unusable('Ed25519'); break; } case 'ES256': case 'ES384': case 'ES512': { if (!isAlgorithm(key.algorithm, 'ECDSA')) throw unusable('ECDSA'); const expected = getNamedCurve$1(alg); const actual = key.algorithm.namedCurve; if (actual !== expected) throw unusable(expected, 'algorithm.namedCurve'); break; } default: throw new TypeError('CryptoKey does not support this operation'); } checkUsage(key, usage); } function message(msg, actual, ...types) { types = types.filter(Boolean); if (types.length > 2) { const last = types.pop(); msg += `one of type ${types.join(', ')}, or ${last}.`; } else if (types.length === 2) { msg += `one of type ${types[0]} or ${types[1]}.`; } else { msg += `of type ${types[0]}.`; } if (actual == null) { msg += ` Received ${actual}`; } else if (typeof actual === 'function' && actual.name) { msg += ` Received function ${actual.name}`; } else if (typeof actual === 'object' && actual != null) { if (actual.constructor?.name) { msg += ` Received an instance of ${actual.constructor.name}`; } } return msg; } var invalidKeyInput = (actual, ...types) => { return message('Key must be ', actual, ...types); }; function withAlg(alg, actual, ...types) { return message(`Key for the ${alg} algorithm must be `, actual, ...types); } function isCryptoKey(key) { return key?.[Symbol.toStringTag] === 'CryptoKey'; } function isKeyObject(key) { return key?.[Symbol.toStringTag] === 'KeyObject'; } var isKeyLike = (key) => { return isCryptoKey(key) || isKeyObject(key); }; var isDisjoint = (...headers) => { const sources = headers.filter(Boolean); if (sources.length === 0 || sources.length === 1) { return true; } let acc; for (const header of sources) { const parameters = Object.keys(header); if (!acc || acc.size === 0) { acc = new Set(parameters); continue; } for (const parameter of parameters) { if (acc.has(parameter)) { return false; } acc.add(parameter); } } return true; }; function isObjectLike(value) { return typeof value === 'object' && value !== null; } var isObject = (input) => { if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') { return false; } if (Object.getPrototypeOf(input) === null) { return true; } let proto = input; while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto); } return Object.getPrototypeOf(input) === proto; }; var checkKeyLength = (alg, key) => { if (alg.startsWith('RS') || alg.startsWith('PS')) { const { modulusLength } = key.algorithm; if (typeof modulusLength !== 'number' || modulusLength < 2048) { throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); } } }; const getNamedCurve = (keyData) => { const patterns = Object.entries({ 'P-256': [0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07], 'P-384': [0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22], 'P-521': [0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23], }); const maxPatternLen = Math.max(...patterns.map(([, bytes]) => bytes.length)); for (let i = 0; i <= keyData.byteLength - maxPatternLen; i++) { for (const [curve, bytes] of patterns) { if (i <= keyData.byteLength - bytes.length) { if (keyData.subarray(i, i + bytes.length).every((byte, idx) => byte === bytes[idx])) { return curve; } } } } return undefined; }; const genericImport = async (keyFormat, keyData, alg, options) => { let algorithm; let keyUsages; const getSignatureUsages = () => (['verify'] ); const getEncryptionUsages = () => ['encrypt', 'wrapKey'] ; switch (alg) { case 'PS256': case 'PS384': case 'PS512': algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` }; keyUsages = getSignatureUsages(); break; case 'RS256': case 'RS384': case 'RS512': algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` }; keyUsages = getSignatureUsages(); break; case 'RSA-OAEP': case 'RSA-OAEP-256': case 'RSA-OAEP-384': case 'RSA-OAEP-512': algorithm = { name: 'RSA-OAEP', hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`, }; keyUsages = getEncryptionUsages(); break; case 'ES256': case 'ES384': case 'ES512': { const curveMap = { ES256: 'P-256', ES384: 'P-384', ES512: 'P-521' }; algorithm = { name: 'ECDSA', namedCurve: curveMap[alg] }; keyUsages = getSignatureUsages(); break; } case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': { const namedCurve = getNamedCurve(keyData); algorithm = namedCurve ? { name: 'ECDH', namedCurve } : { name: 'X25519' }; keyUsages = [] ; break; } case 'Ed25519': case 'EdDSA': algorithm = { name: 'Ed25519' }; keyUsages = getSignatureUsages(); break; default: throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value'); } return crypto.subtle.importKey(keyFormat, keyData, algorithm, (true ), keyUsages); }; const fromSPKI = (pem, alg, options) => { const keyData = decodeBase64(pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, '')); return genericImport('spki', keyData, alg); }; function subtleMapping(jwk) { let algorithm; let keyUsages; switch (jwk.kty) { case 'RSA': { switch (jwk.alg) { case 'PS256': case 'PS384': case 'PS512': algorithm = { name: 'RSA-PSS', hash: `SHA-${jwk.alg.slice(-3)}` }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'RS256': case 'RS384': case 'RS512': algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${jwk.alg.slice(-3)}` }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'RSA-OAEP': case 'RSA-OAEP-256': case 'RSA-OAEP-384': case 'RSA-OAEP-512': algorithm = { name: 'RSA-OAEP', hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`, }; keyUsages = jwk.d ? ['decrypt', 'unwrapKey'] : ['encrypt', 'wrapKey']; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } case 'EC': { switch (jwk.alg) { case 'ES256': algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ES384': algorithm = { name: 'ECDSA', namedCurve: 'P-384' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ES512': algorithm = { name: 'ECDSA', namedCurve: 'P-521' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': algorithm = { name: 'ECDH', namedCurve: jwk.crv }; keyUsages = jwk.d ? ['deriveBits'] : []; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } case 'OKP': { switch (jwk.alg) { case 'Ed25519': case 'EdDSA': algorithm = { name: 'Ed25519' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': algorithm = { name: jwk.crv }; keyUsages = jwk.d ? ['deriveBits'] : []; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } default: throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value'); } return { algorithm, keyUsages }; } var importJWK = async (jwk) => { if (!jwk.alg) { throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); } const { algorithm, keyUsages } = subtleMapping(jwk); const keyData = { ...jwk }; delete keyData.alg; delete keyData.use; return crypto.subtle.importKey('jwk', keyData, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages); }; async function importSPKI(spki, alg, options) { if (spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) { throw new TypeError('"spki" must be SPKI formatted string'); } return fromSPKI(spki, alg); } var validateCrit = (Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) => { if (joseHeader.crit !== undefined && protectedHeader?.crit === undefined) { throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); } if (!protectedHeader || protectedHeader.crit === undefined) { return new Set(); } if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) { throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); } let recognized; if (recognizedOption !== undefined) { recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]); } else { recognized = recognizedDefault; } for (const parameter of protectedHeader.crit) { if (!recognized.has(parameter)) { throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); } if (joseHeader[parameter] === undefined) { throw new Err(`Extension Header Parameter "${parameter}" is missing`); } if (recognized.get(parameter) && protectedHeader[parameter] === undefined) { throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); } } return new Set(protectedHeader.crit); }; var validateAlgorithms = (option, algorithms) => { if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string'))) { throw new TypeError(`"${option}" option must be an array of strings`); } if (!algorithms) { return undefined; } return new Set(algorithms); }; function isJWK(key) { return isObject(key) && typeof key.kty === 'string'; } function isPrivateJWK(key) { return key.kty !== 'oct' && typeof key.d === 'string'; } function isPublicJWK(key) { return key.kty !== 'oct' && typeof key.d === 'undefined'; } function isSecretJWK(key) { return key.kty === 'oct' && typeof key.k === 'string'; } let cache; const handleJWK = async (key, jwk, alg, freeze = false) => { cache ||= new WeakMap(); let cached = cache.get(key); if (cached?.[alg]) { return cached[alg]; } const cryptoKey = await importJWK({ ...jwk, alg }); if (freeze) Object.freeze(key); if (!cached) { cache.set(key, { [alg]: cryptoKey }); } else { cached[alg] = cryptoKey; } return cryptoKey; }; const handleKeyObject = (keyObject, alg) => { cache ||= new WeakMap(); let cached = cache.get(keyObject); if (cached?.[alg]) { return cached[alg]; } const isPublic = keyObject.type === 'public'; const extractable = isPublic ? true : false; let cryptoKey; if (keyObject.asymmetricKeyType === 'x25519') { switch (alg) { case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': break; default: throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, isPublic ? [] : ['deriveBits']); } if (keyObject.asymmetricKeyType === 'ed25519') { if (alg !== 'EdDSA' && alg !== 'Ed25519') { throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [ isPublic ? 'verify' : 'sign', ]); } if (keyObject.asymmetricKeyType === 'rsa') { let hash; switch (alg) { case 'RSA-OAEP': hash = 'SHA-1'; break; case 'RS256': case 'PS256': case 'RSA-OAEP-256': hash = 'SHA-256'; break; case 'RS384': case 'PS384': case 'RSA-OAEP-384': hash = 'SHA-384'; break; case 'RS512': case 'PS512': case 'RSA-OAEP-512': hash = 'SHA-512'; break; default: throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } if (alg.startsWith('RSA-OAEP')) { return keyObject.toCryptoKey({ name: 'RSA-OAEP', hash, }, extractable, isPublic ? ['encrypt'] : ['decrypt']); } cryptoKey = keyObject.toCryptoKey({ name: alg.startsWith('PS') ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5', hash, }, extractable, [isPublic ? 'verify' : 'sign']); } if (keyObject.asymmetricKeyType === 'ec') { const nist = new Map([ ['prime256v1', 'P-256'], ['secp384r1', 'P-384'], ['secp521r1', 'P-521'], ]); const namedCurve = nist.get(keyObject.asymmetricKeyDetails?.namedCurve); if (!namedCurve) { throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } if (alg === 'ES256' && namedCurve === 'P-256') { cryptoKey = keyObject.toCryptoKey({ name: 'ECDSA', namedCurve, }, extractable, [isPublic ? 'verify' : 'sign']); } if (alg === 'ES384' && namedCurve === 'P-384') { cryptoKey = keyObject.toCryptoKey({ name: 'ECDSA', namedCurve, }, extractable, [isPublic ? 'verify' : 'sign']); } if (alg === 'ES512' && namedCurve === 'P-521') { cryptoKey = keyObject.toCryptoKey({ name: 'ECDSA', namedCurve, }, extractable, [isPublic ? 'verify' : 'sign']); } if (alg.startsWith('ECDH-ES')) { cryptoKey = keyObject.toCryptoKey({ name: 'ECDH', namedCurve,