@bynn-intelligence/agemin-sdk
Version:
agemin.com SDK for age verification
1,500 lines (1,475 loc) • 241 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/**
* 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,
}, extractable, isPublic ? [] : ['deriveBits']);
}
}
if (!cryptoKey) {
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
}
if (!cached) {
cache.set(keyObject, { [alg]: cryptoKey });
}
else {
cached[alg] = cryptoKey;
}
return cryptoKey;
};
var normalizeKey = async (key, alg) => {
if (key instanceof Uint8Array) {
return key;
}
if (isCryptoKey(key)) {
return key;
}
if (isKeyObject(key)) {
if (key.type === 'secret') {
return key.export();
}
if ('toCryptoKey' in key && typeof key.toCryptoKey === 'function') {
try {
return handleKeyObject(key, alg);
}
catch (err) {
if (err instanceof TypeError) {
throw err;
}
}
}
let jwk = key.export({ format: 'jwk' });
return handleJWK(key, jwk, alg);
}
if (isJWK(key)) {
if (key.k) {
return decode(key.k);
}
return handleJWK(key, key, alg, true);
}
throw new Error('unreachable');
};
const tag = (key) => key?.[Symbol.toStringTag];
const jwkMatchesOp = (alg, key, usage) => {
if (key.use !== undefined) {
let expected;
switch (usage) {
case 'sign':
case 'verify':
expected = 'sig';
break;
case 'encrypt':
case 'decrypt':
expected = 'enc';
break;
}
if (key.use !== expected) {
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
}
}
if (key.alg !== undefined && key.alg !== alg) {
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
}
if (Array.isArray(key.key_ops)) {
let expectedKeyOp;
switch (true) {
case usage === 'verify':
case alg === 'dir':
case alg.includes('CBC-HS'):
expectedKeyOp = usage;
break;
case alg.startsWith('PBES2'):
expectedKeyOp = 'deriveBits';
break;
case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg):
if (!alg.includes('GCM') && alg.endsWith('KW')) {
expectedKeyOp = 'unwrapKey';
}
else {
expectedKeyOp = usage;
}
break;
case usage === 'encrypt':
expectedKeyOp = 'wrapKey';
break;
case usage === 'decrypt':
expectedKeyOp = alg.startsWith('RSA') ? 'unwrapKey' : 'deriveBits';
break;
}
if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) {
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
}
}
return true;
};
const symmetricTypeCheck = (alg, key, usage) => {
if (key instanceof Uint8Array)
return;
if (isJWK(key)) {
if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage))
return;
throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`);
}
if (!isKeyLike(key)) {
throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array'));
}
if (key.type !== 'secret') {
throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
}
};
const asymmetricTypeCheck = (alg, key, usage) => {
if (isJWK(key)) {
switch (usage) {
case 'decrypt':
case 'sign':
if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage))
return;
throw new TypeError(`JSON Web Key for this operation be a private JWK`);
case 'encrypt':
case 'verify':
if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage))
return;
throw new TypeError(`JSON Web Key for this operation be a public JWK`);
}
}
if (!isKeyLike(key)) {
throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key'));
}
if (key.type === 'secret') {
throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
}
if (key.type === 'public') {
switch (usage) {
case 'sign':
throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`);
case 'decrypt':
throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`);
}
}
if (key.type === 'private') {
switch (usage) {
case 'ver