@openkey/sdk 0.12.0 → 0.12.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +110 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +110 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../core/src/pkce.ts","../../core/src/errors.ts","../../core/src/oauth.ts","../../core/src/native-delegation.ts","../src/nostr.ts","../src/popup-capability.ts"],"sourcesContent":["// OpenKey SDK - Browser client for third-party apps\n// Provides \"Sign with OpenKey\" functionality via popup or iframe\n\nimport {\n generateCodeVerifier,\n generateCodeChallenge,\n generateState,\n buildAuthorizationUrl,\n} from '@openkey/core';\nimport { OpenKeyNostr } from './nostr';\nimport { isEmbeddedAppWebView, isUsablePopup, popupUnavailableError } from './popup-capability';\n\nexport { OpenKeyNostr } from './nostr';\nexport type {\n UnsignedNostrEvent,\n SignedNostrEvent,\n NostrIdentity,\n NostrError,\n} from './nostr';\n\nexport interface EIP1193Provider {\n request(args: { method: string; params?: any[] }): Promise<any>;\n}\n\ninterface EIP6963ProviderInfo {\n uuid: string;\n name: string;\n icon: string;\n rdns: string;\n}\n\ninterface EIP6963ProviderDetail {\n info: EIP6963ProviderInfo;\n provider: EIP1193Provider;\n}\n\nexport type OpenKeyMode = 'iframe' | 'popup' | 'redirect';\n\nexport interface OpenKeyConfig {\n /** OpenKey host URL (default: https://openkey.so) */\n host?: string;\n /** OAuth API host URL (default: derived from host by prefixing 'api.') */\n oauthHost?: string;\n /** App identifier for display */\n appName?: string;\n /** UI mode: 'iframe' (default), 'popup', or 'redirect' */\n mode?: OpenKeyMode;\n /** App-provided wallet provider for external key signing */\n externalProvider?: EIP1193Provider;\n /**\n * Whether the client can use passkeys (WebAuthn). Default: true. Pass\n * false from clients where WebAuthn does not work (for example an\n * ad-hoc-signed desktop webview); the OpenKey UI then offers only email\n * and social sign-in.\n */\n passkeysSupported?: boolean;\n}\n\n/**\n * Query suffix telling the OpenKey widget that passkeys are unavailable.\n * Empty when passkeys are supported, so default URLs are unchanged.\n */\nexport function passkeysQueryFlag(passkeysSupported: boolean): string {\n return passkeysSupported ? '' : '&passkeys=false';\n}\n\nexport interface SignMessageRequest {\n message: string;\n keyId?: string; // If not provided, user selects\n}\n\nexport interface SignTypedDataRequest {\n domain: {\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: string;\n };\n types: Record<string, Array<{ name: string; type: string }>>;\n primaryType: string;\n message: Record<string, unknown>;\n keyId?: string;\n}\n\nexport interface SignResult {\n signature: string;\n address: string;\n}\n\nexport interface AuthResult {\n address: string;\n keyId: string;\n keyType: 'MANAGED' | 'EXTERNAL';\n}\n\n/**\n * Confirmation returned by {@link OpenKey.signOut}. `requestId` is generated\n * by the SDK and echoed by the OpenKey-owned widget, so callers can associate\n * the acknowledgement with the sign-out they initiated.\n */\nexport interface SignOutAcknowledgement {\n requestId: string;\n /**\n * Whether the widget confirmed that the previous bearer no longer\n * authenticates. `false` means the SDK is locally signed out, but remote\n * revocation could not be verified.\n */\n revoked: boolean;\n}\n\nexport interface OpenKeyError {\n code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';\n message: string;\n /** On `POPUP_BLOCKED`: `'embedded-webview'` when the page runs in an app WebView that cannot open popups. */\n reason?: 'embedded-webview';\n}\n\n// ======= OAuth 2.1 Types =======\n\n/** OAuth configuration for third-party apps */\nexport interface OAuthConfig {\n /** OAuth client_id (registered with OpenKey) */\n clientId: string;\n /** Redirect URI (must match registered URI) */\n redirectUri: string;\n /** State parameter for CSRF protection (auto-generated if not provided) */\n state?: string;\n}\n\n/** Result from OAuth authorization */\nexport interface OAuthResult {\n /** Authorization code (exchange for tokens) */\n code: string;\n /** State parameter (verify matches request) */\n state: string;\n}\n\n/** Response from token exchange */\nexport interface OAuthTokenResponse {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n id_token: string;\n scope: string;\n refresh_token?: string;\n}\n\n// ======= Versioned TinyCloud authorization protocol =======\n//\n// This protocol replaces the legacy `signMessage()` path for TinyCloud\n// SIWE-ReCap requests. It preserves the legacy exact-byte behavior for\n// existing callers: `signMessage()` continues to sign the caller's exact\n// bytes and OpenKey NEVER rewrites them. Callers who want to let the user\n// narrow the requested capabilities call `authorizeTinyCloud()` instead.\n//\n// The response carries `signedMessage` — the exact bytes the signature\n// verifies against. TinyCloud completes the session with these bytes, not\n// the caller's original request. See `NodeUserAuthorization` in the\n// js-sdk for the consumer side of this contract.\n\nexport interface TinyCloudAuthorizationRequestV1 {\n protocolVersion: 1;\n /** Suggested SIWE (may be edited server-side before signing). */\n siwe: string;\n /** The keyId the user picked, if the caller has one in mind. */\n keyId?: string;\n /**\n * The session-key JWK bound to `siwe`. Required so the server can\n * regenerate a narrowed SIWE tied to the SAME session key when the\n * user removes capabilities. Passing a mismatched jwk (or omitting it\n * when the widget attempts to narrow) causes the /authorize-sign call\n * to fail — never a silent fallback to the caller's exact bytes.\n */\n jwk?: Record<string, unknown>;\n /**\n * The TinyCloud host the resulting session will activate against.\n * Bound into the /authorize-sign-prepare context so /authorize-sign\n * cannot swap hosts server-side. Optional for callers that do not\n * plan to activate a delegation (they should still forward it when\n * known; the empty string is treated as \"unknown\").\n */\n host?: string;\n /** Optional presentation envelope (name, reason, manifest). */\n presentation?: CapabilityPresentationEnvelopeV1;\n}\n\nexport interface TinyCloudAuthorizationResultV1 {\n protocolVersion: 1;\n /** EIP-55 signer address. */\n address: string;\n /** Signature over `signedMessage`. */\n signature: string;\n /** The EXACT bytes the signature verifies against. Never the caller's original. */\n signedMessage: string;\n /** The action IDs the user (or default consent) selected. */\n selectedActionKeys: string[];\n /** Effective grant set after any narrowing. */\n permissions: Array<{\n service: string;\n space: string;\n path: string;\n actions: string[];\n }>;\n}\n\nexport interface CapabilityPresentationEnvelopeV1 {\n protocolVersion: 1;\n /** Human-readable requester name shown in the review. */\n displayName?: string;\n /**\n * Reason string the requester supplied for the delegation. Rendered as\n * caller-supplied (never as verified) unless a signed manifest carries\n * the same reason string.\n */\n reason?: string;\n /** Manifest ID + digest, when the app publishes a signed manifest. */\n manifestId?: string;\n manifestDigest?: string;\n /**\n * Full manifest payload(s) the caller wants surfaced in the review. The\n * receiving route validates and size-bounds this before it is used. This\n * is display-only: no metadata may expand authority beyond what the\n * ReCap already grants. Bounded to a reasonable size at the transport\n * boundary — oversized envelopes are dropped.\n */\n manifests?: Array<{\n name?: string;\n appId?: string;\n payload?: Record<string, unknown>;\n }>;\n}\n\n// Sol MAJOR-3: transport messages must carry `requestId` and\n// `protocolVersion` for the SDK to correlate concurrent flows. Legacy\n// (unversioned) messages are allowed for backward compatibility with\n// the plain signMessage path, but versioned flows (authorizeTinyCloud)\n// always carry both fields and the SDK matches them on response.\ntype MessageType =\n | { type: 'openkey:auth:request'; appName: string }\n | { type: 'openkey:auth:response'; success: true; address: string; keyId: string; keyType?: 'MANAGED' | 'EXTERNAL'; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:auth:response'; success: false; error: OpenKeyError; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:sign:request'; message: string; keyId?: string; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:sign:response'; success: true; signature: string; address: string; requestId?: string; protocolVersion?: number }\n | {\n type: 'openkey:sign:response';\n success: true;\n signature: string;\n address: string;\n /** Versioned: exact bytes the signature verifies against. */\n signedMessage?: string;\n /** Versioned: action IDs selected by the user. */\n selectedActionKeys?: string[];\n /** Versioned: effective grants after narrowing. */\n permissions?: Array<{\n service: string;\n space: string;\n path: string;\n actions: string[];\n }>;\n requestId?: string;\n protocolVersion?: number;\n }\n | { type: 'openkey:sign:response'; success: false; error: OpenKeyError; requestId?: string; protocolVersion?: number }\n // Sol MAJOR-3 (continuation): external-key review flow. The widget\n // renders the shared SigningApproval UI, calls /authorize-sign-prepare\n // and /authorize-sign-preview on the user's behalf, and — instead of\n // signing server-side with a managed key — hands the SDK back the\n // preview data so the SDK can invoke the user's wallet. The SDK then\n // completes /authorize-sign with the resulting `externalSignature`.\n | {\n type: 'openkey:externalSign:approve';\n success: true;\n requestId: string;\n protocolVersion: number;\n authorizationContextToken: string;\n previewApprovalToken: string;\n signedMessage: string;\n selectedActionIds: string[];\n address: string;\n }\n | {\n type: 'openkey:externalSign:approve';\n success: false;\n requestId: string;\n protocolVersion: number;\n error: OpenKeyError;\n }\n | { type: 'openkey:signTypedData:request'; data: SignTypedDataRequest; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:signTypedData:response'; success: true; signature: string; address: string; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:signTypedData:response'; success: false; error: OpenKeyError; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:link-wallet:request' }\n | { type: 'openkey:link-wallet:response'; success: true; address: string; keyId: string }\n | { type: 'openkey:link-wallet:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:link-wallet:delegate' }\n | { type: 'openkey:link-wallet:result'; success: true; address: string; keyId: string }\n | { type: 'openkey:link-wallet:result'; success: false; error: OpenKeyError }\n | { type: 'openkey:auth:use-external-wallet' }\n | { type: 'openkey:sign-out:request'; requestId: string; protocolVersion: 1 }\n | { type: 'openkey:sign-out:response'; success: true; requestId: string; protocolVersion: 1; revoked: boolean }\n | { type: 'openkey:sign-out:response'; success: false; requestId: string; protocolVersion: 1; error: OpenKeyError }\n | { type: 'openkey:resize'; height: number; protocolVersion?: number }\n | { type: 'openkey:ready'; protocolVersion?: number }\n | { type: 'openkey:close' };\n\ntype WidgetResponseMessage = Extract<MessageType, { success: boolean }>;\n\nconst DEFAULT_HOST = 'https://openkey.so';\nconst POPUP_WIDTH = 400;\nconst POPUP_HEIGHT = 600;\nconst DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes\nconst IFRAME_READY_TIMEOUT = 3000; // 3 seconds\nconst OAUTH_STORAGE_KEY = 'openkey_oauth';\n\n/**\n * Sol final continuation contract requirement 5: pure validation of an\n * incoming iframe resize message envelope. Exported so unit tests can\n * exercise every rejection branch — wrong requestId, wrong version,\n * malformed height — without booting the browser runtime that\n * `IframeModal` requires.\n *\n * Two modes, chosen by the modal's bound correlation:\n * - Versioned request bound (`requestId` AND `protocolVersion` set): the\n * resize MUST carry that exact pair.\n * - No versioned request (both null — `connect`, `signMessage`, and the\n * other unversioned flows): the widget sends a plain\n * `{ type, height }`. It is accepted only when it carries no\n * `requestId`. The caller has already checked that the message came\n * from this modal's iframe (`event.source`) on the OpenKey origin\n * (TC-647).\n * A half-bound correlation (only one of the two set) drops everything.\n *\n * Returns the height clamped to 85% of the viewport when acceptable, or\n * `null` when the message must be dropped.\n */\nexport function validateIframeResize(\n incoming: unknown,\n expected: {\n requestId: string | null;\n protocolVersion: number | null;\n viewportHeight: number;\n },\n): number | null {\n if (!incoming || typeof incoming !== 'object') return null;\n const data = incoming as Record<string, unknown>;\n if (data.type !== 'openkey:resize') return null;\n if (expected.requestId === null && expected.protocolVersion === null) {\n // No versioned request on this modal: a correlated resize cannot\n // belong to it.\n if (data.requestId !== undefined) return null;\n } else {\n if (expected.requestId === null || expected.protocolVersion === null) return null;\n if (typeof data.protocolVersion !== 'number' || data.protocolVersion !== expected.protocolVersion) {\n return null;\n }\n if (typeof data.requestId !== 'string' || data.requestId !== expected.requestId) {\n return null;\n }\n }\n if (typeof data.height !== 'number' || !Number.isFinite(data.height) || data.height <= 0) {\n return null;\n }\n return Math.min(data.height, Math.floor(expected.viewportHeight * 0.85));\n}\n\n/**\n * Sol MAJOR-4 (final): pure routing helper — decides whether an\n * `authorizeTinyCloud` call should route through the external-wallet\n * preview→sign→finalize flow or the managed-key widget-signs-server-side\n * flow. Exported so unit tests can exercise every branch\n * (explicit-external + external-session, explicit-external + external-\n * session with different keyId, no-explicit + external-session,\n * explicit-managed + managed-session, no-session) without booting the\n * browser runtime.\n *\n * The decision is authoritative from the ACTIVE session's keyType when\n * no explicit `requestKeyId` was supplied.\n *\n * Kept as a thin wrapper for backwards compatibility. New callers should\n * prefer `resolveAuthorizeTinyCloudRouting` which also considers the\n * request's explicit `keyId`.\n */\nexport function shouldRouteAuthorizeTinyCloudExternal(state: {\n lastAuth: { keyType?: 'MANAGED' | 'EXTERNAL' | null } | null;\n}): boolean {\n return state.lastAuth?.keyType === 'EXTERNAL';\n}\n\n/**\n * Sol MAJOR-4 (final continuation): resolve the target key's identity and\n * route by that key's type — NOT by `lastAuth.keyType` alone.\n *\n * Sol's rejection: when the caller supplies an explicit `requestKeyId`\n * that identifies a DIFFERENT key than `lastAuth.keyId` (address vs\n * internal id, or a genuinely different key), the SDK must\n * - resolve THAT key's type,\n * - route by that resolved type (external ⇒ wallet path, managed ⇒\n * server-signing path), and\n * - reject an inconsistent explicit pin whose resolved type contradicts\n * the caller's other implicit assumptions (e.g. `lastAuth` is\n * external but the caller pinned a managed key).\n *\n * Inputs:\n * - `lastAuth`: the connected-session key metadata (may be null on\n * boot; the widget renders a connect flow first in that case).\n * - `requestKeyId`: the caller's explicit pin (may be absent).\n * - `knownKeys`: any keys we have metadata for; the SDK's higher-level\n * `authorizeTinyCloud` populates this from the server before calling\n * the resolver so the pure helper is fully synchronous and testable.\n *\n * Return shape:\n * { route: 'external' | 'managed', resolvedKeyId: string | null,\n * resolvedKeyType: 'EXTERNAL' | 'MANAGED' | null }\n *\n * `resolvedKeyId` is null only when neither `requestKeyId` nor\n * `lastAuth.keyId` exists. When both exist and identify the same key\n * (matching id OR matching known-key type), the resolver returns that\n * shared identity. When both exist and identify different keys AND we\n * know both types AND those types disagree, the resolver throws\n * `KEY_ID_TYPE_MISMATCH` so no silent route coercion occurs.\n */\nexport interface AuthorizeTinyCloudRouting {\n route: 'external' | 'managed';\n resolvedKeyId: string | null;\n resolvedKeyType: 'EXTERNAL' | 'MANAGED' | null;\n}\n\nexport class KeyIdTypeMismatchError extends Error {\n code: 'KEY_ID_TYPE_MISMATCH';\n requestKeyId: string;\n lastAuthKeyId: string | null;\n requestKeyType: 'EXTERNAL' | 'MANAGED' | null;\n lastAuthKeyType: 'EXTERNAL' | 'MANAGED' | null;\n constructor(fields: {\n message: string;\n requestKeyId: string;\n lastAuthKeyId: string | null;\n requestKeyType: 'EXTERNAL' | 'MANAGED' | null;\n lastAuthKeyType: 'EXTERNAL' | 'MANAGED' | null;\n }) {\n super(fields.message);\n this.name = 'KeyIdTypeMismatchError';\n this.code = 'KEY_ID_TYPE_MISMATCH';\n this.requestKeyId = fields.requestKeyId;\n this.lastAuthKeyId = fields.lastAuthKeyId;\n this.requestKeyType = fields.requestKeyType;\n this.lastAuthKeyType = fields.lastAuthKeyType;\n }\n}\n\nexport function resolveAuthorizeTinyCloudRouting(state: {\n lastAuth: { keyId?: string | null; keyType?: 'MANAGED' | 'EXTERNAL' | null } | null;\n requestKeyId?: string | null;\n knownKeys?: ReadonlyArray<{ id: string; address?: string; keyType: 'MANAGED' | 'EXTERNAL' }>;\n}): AuthorizeTinyCloudRouting {\n const lastAuthKeyId = state.lastAuth?.keyId ?? null;\n const lastAuthKeyType = (state.lastAuth?.keyType ?? null) as\n | 'EXTERNAL'\n | 'MANAGED'\n | null;\n\n const rawRequestKeyId = state.requestKeyId ?? null;\n const requestKeyId = rawRequestKeyId && rawRequestKeyId.length > 0 ? rawRequestKeyId : null;\n\n const knownKeys = state.knownKeys ?? [];\n\n // Helper: resolve a keyId string into its known type. Matches by id\n // first, then by address (address is a valid public identifier the\n // caller may pass in place of the internal record id).\n const lookupType = (id: string): 'EXTERNAL' | 'MANAGED' | null => {\n for (const k of knownKeys) {\n if (k.id === id) return k.keyType;\n if (k.address && k.address.toLowerCase() === id.toLowerCase()) return k.keyType;\n }\n // Convention: `external:<address>` is the SDK's synthetic id for a\n // wallet-only session that never enrolled with OpenKey. Callers\n // sometimes pass the bare address in requestKeyId; treat both\n // forms as EXTERNAL.\n if (id.startsWith('external:')) return 'EXTERNAL';\n return null;\n };\n\n // Case A: no request pin — fall back to lastAuth.keyType.\n if (!requestKeyId) {\n const route: 'external' | 'managed' =\n lastAuthKeyType === 'EXTERNAL' ? 'external' : 'managed';\n return {\n route,\n resolvedKeyId: lastAuthKeyId,\n resolvedKeyType: lastAuthKeyType,\n };\n }\n\n // Case B: request pin present. Resolve its type.\n let requestKeyType = lookupType(requestKeyId);\n // If the pin string-matches lastAuth.keyId AND we did not find it in\n // knownKeys, adopt lastAuth's type (they identify the same key).\n if (!requestKeyType && lastAuthKeyId && requestKeyId === lastAuthKeyId) {\n requestKeyType = lastAuthKeyType;\n }\n\n // If both sides tell us a type and they disagree, hard fail —\n // conflicting pins.\n if (requestKeyType && lastAuthKeyType && requestKeyType !== lastAuthKeyType) {\n throw new KeyIdTypeMismatchError({\n message: `authorizeTinyCloud: request.keyId ${requestKeyId} resolves to ${requestKeyType} but active session key ${lastAuthKeyId} is ${lastAuthKeyType}; refuse to route silently`,\n requestKeyId,\n lastAuthKeyId,\n requestKeyType,\n lastAuthKeyType,\n });\n }\n\n // If we still do not know the request's type, fall back to\n // lastAuth.keyType — the widget will surface a connect prompt if the\n // key is unknown.\n const effectiveType: 'EXTERNAL' | 'MANAGED' | null =\n requestKeyType ?? lastAuthKeyType;\n\n return {\n route: effectiveType === 'EXTERNAL' ? 'external' : 'managed',\n resolvedKeyId: requestKeyId,\n resolvedKeyType: effectiveType,\n };\n}\n\nclass IframeModal {\n private root: HTMLDivElement;\n private shadow: ShadowRoot;\n private iframe: HTMLIFrameElement;\n private onClose: () => void;\n private onMessage: (data: MessageType) => void;\n private messageHandler: (event: MessageEvent) => void;\n private host: string;\n // Sol final continuation contract requirement 5: when the modal carries a\n // versioned request, iframe resize traffic MUST correlate to its\n // requestId AND protocolVersion. Set by `setExpectedCorrelation` as soon\n // as the modal is created. When both are null the flow is unversioned and\n // only uncorrelated resizes from this iframe are accepted (TC-647).\n private expectedRequestId: string | null = null;\n private expectedProtocolVersion: number | null = null;\n\n constructor(opts: { url: string; host: string; onClose: () => void; onMessage: (data: MessageType) => void }) {\n this.host = opts.host;\n this.onClose = opts.onClose;\n this.onMessage = opts.onMessage;\n\n const isMobile = window.matchMedia('(max-width: 639px)').matches;\n\n this.root = document.createElement('div');\n this.shadow = this.root.attachShadow({ mode: 'closed' });\n\n const style = document.createElement('style');\n style.textContent = `\n :host{all:initial}\n .ok-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.42);z-index:999999;display:flex;align-items:${isMobile ? 'flex-end' : 'center'};justify-content:center;animation:ok-fade-in 150ms ease-out}\n .ok-card{position:relative;background:#fafafa;width:${isMobile ? '100%' : '400px'};border-radius:${isMobile ? '16px 16px 0 0' : '16px'};box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);overflow:hidden;overflow-y:auto;animation:${isMobile ? 'ok-slide-up 250ms ease-out' : 'ok-scale-in 200ms ease-out'};max-height:90vh}\n .ok-close{position:absolute;top:8px;right:8px;width:24px;height:24px;border:none;background:transparent;color:#6b7280;font-size:18px;cursor:pointer;z-index:1;display:flex;align-items:center;justify-content:center;border-radius:4px}\n .ok-close:hover{color:#111827}\n .ok-handle{width:40px;height:4px;background:#d4d4d4;border-radius:2px;margin:8px auto 0}\n iframe{border:none;width:100%;height:400px;display:block;transition:height 200ms ease}\n .ok-backdrop.ok-exit{animation:ok-fade-out 150ms ease-in}\n .ok-backdrop.ok-exit .ok-card{animation:${isMobile ? 'ok-slide-down 200ms ease-in' : 'ok-scale-out 150ms ease-in'}}\n @keyframes ok-fade-in{from{opacity:0}to{opacity:1}}\n @keyframes ok-fade-out{from{opacity:1}to{opacity:0}}\n @keyframes ok-scale-in{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}}\n @keyframes ok-scale-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(0.95)}}\n @keyframes ok-slide-up{from{transform:translateY(100%)}to{transform:translateY(0)}}\n @keyframes ok-slide-down{from{transform:translateY(0)}to{transform:translateY(100%)}}\n `;\n\n const backdrop = document.createElement('div');\n backdrop.className = 'ok-backdrop';\n backdrop.addEventListener('click', (e) => {\n if (e.target === backdrop) this.close();\n });\n\n const card = document.createElement('div');\n card.className = 'ok-card';\n\n if (isMobile) {\n const handle = document.createElement('div');\n handle.className = 'ok-handle';\n card.appendChild(handle);\n }\n\n const closeBtn = document.createElement('button');\n closeBtn.className = 'ok-close';\n closeBtn.textContent = '\\u00d7';\n closeBtn.addEventListener('click', () => this.close());\n card.appendChild(closeBtn);\n\n this.iframe = document.createElement('iframe');\n this.iframe.src = opts.url;\n this.iframe.setAttribute('allow', 'clipboard-write *; publickey-credentials-get *; publickey-credentials-create *');\n this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox');\n card.appendChild(this.iframe);\n\n backdrop.appendChild(card);\n this.shadow.appendChild(style);\n this.shadow.appendChild(backdrop);\n\n this.messageHandler = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n if (event.source !== this.iframe.contentWindow) return;\n const data = event.data as MessageType;\n if (data.type === 'openkey:resize') {\n // Sol final continuation contract requirement 5: a versioned\n // request's resize MUST carry its EXACT requestId AND\n // protocolVersion; an unversioned flow accepts only uncorrelated\n // resizes (TC-647). Origin and source were checked above. Every\n // branch is covered by unit tests over `validateIframeResize`.\n const h = validateIframeResize(event.data, {\n requestId: this.expectedRequestId,\n protocolVersion: this.expectedProtocolVersion,\n viewportHeight: window.innerHeight,\n });\n if (h === null) return;\n this.iframe.style.height = `${h}px`;\n return;\n }\n // Sol MAJOR-9: openkey:close MUST also carry versioning when the\n // request was versioned. The close message is the only other one\n // that a stray frame could try to inject to abort a legitimate\n // approval. For simplicity we drop unversioned close on messages\n // arriving from an authenticated iframe source and let the SDK's\n // timeout eventually fire.\n if (data.type === 'openkey:close') {\n // No protocol-version requirement enforced here because a\n // versionless close is a legitimate cancel from any widget\n // version; source+origin validation earlier is sufficient.\n }\n this.onMessage(data);\n };\n window.addEventListener('message', this.messageHandler);\n document.body.appendChild(this.root);\n }\n\n postMessage(message: object) {\n this.iframe.contentWindow?.postMessage(message, this.host);\n }\n\n /**\n * Sol final continuation contract requirement 5: bind the active\n * request correlation so subsequent resize (and future correlated)\n * messages can be verified against `requestId` + `protocolVersion`.\n *\n * The outer flow calls this right after creating the modal for a\n * versioned request, before the iframe can send anything, so no\n * uncorrelated resize is ever accepted for that request. Passing `null`\n * for both arguments means the flow is unversioned; passing `null` for\n * only one drops every resize.\n */\n setExpectedCorrelation(requestId: string | null, protocolVersion: number | null): void {\n this.expectedRequestId = requestId;\n this.expectedProtocolVersion = protocolVersion;\n }\n\n destroy() {\n window.removeEventListener('message', this.messageHandler);\n this.root.remove();\n }\n\n private close() {\n const backdrop = this.shadow.querySelector('.ok-backdrop');\n if (backdrop) {\n backdrop.classList.add('ok-exit');\n setTimeout(() => {\n this.destroy();\n this.onClose();\n }, 200);\n } else {\n this.destroy();\n this.onClose();\n }\n }\n}\n\nclass WalletPicker {\n private root: HTMLDivElement;\n private shadow: ShadowRoot;\n\n constructor(opts: { providers: EIP6963ProviderDetail[]; onSelect: (provider: EIP1193Provider) => void; onCancel: () => void }) {\n this.root = document.createElement('div');\n this.shadow = this.root.attachShadow({ mode: 'closed' });\n\n const style = document.createElement('style');\n style.textContent = `\n :host{all:initial}\n .wp-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.42);z-index:1000000;display:flex;align-items:center;justify-content:center;animation:wp-fade 150ms ease-out}\n .wp-card{background:#fff;width:340px;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);padding:20px;animation:wp-scale 200ms ease-out}\n .wp-title{font:600 16px/1.3 system-ui,sans-serif;color:#111;margin:0 0 12px}\n .wp-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}\n .wp-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid #e5e7eb;border-radius:10px;background:transparent;cursor:pointer;font:14px/1.3 system-ui,sans-serif;color:#111;width:100%;text-align:left;transition:border-color 150ms}\n .wp-item:hover{border-color:#6366f1}\n .wp-icon{width:28px;height:28px;border-radius:6px}\n .wp-empty{font:14px/1.4 system-ui,sans-serif;color:#6b7280;text-align:center;padding:16px 0}\n .wp-cancel{display:block;width:100%;margin-top:12px;padding:8px;border:none;background:transparent;color:#6b7280;font:14px/1.3 system-ui,sans-serif;cursor:pointer;border-radius:8px}\n .wp-cancel:hover{background:#f3f4f6;color:#111}\n @keyframes wp-fade{from{opacity:0}to{opacity:1}}\n @keyframes wp-scale{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}}\n @media(prefers-color-scheme:dark){\n .wp-card{background:#1a1a1a;box-shadow:0 25px 50px -12px rgba(0,0,0,0.5)}\n .wp-title{color:#f3f4f6}\n .wp-item{border-color:#374151;color:#f3f4f6}\n .wp-item:hover{border-color:#818cf8}\n .wp-empty{color:#9ca3af}\n .wp-cancel{color:#9ca3af}\n .wp-cancel:hover{background:#262626;color:#f3f4f6}\n }\n `;\n\n const backdrop = document.createElement('div');\n backdrop.className = 'wp-backdrop';\n backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { this.destroy(); opts.onCancel(); } });\n\n const card = document.createElement('div');\n card.className = 'wp-card';\n\n const title = document.createElement('h2');\n title.className = 'wp-title';\n title.textContent = 'Select a wallet';\n card.appendChild(title);\n\n if (opts.providers.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'wp-empty';\n empty.textContent = 'No wallets detected';\n card.appendChild(empty);\n } else {\n const list = document.createElement('ul');\n list.className = 'wp-list';\n for (const { info, provider } of opts.providers) {\n const item = document.createElement('button');\n item.className = 'wp-item';\n if (info.icon) {\n const icon = document.createElement('img');\n icon.className = 'wp-icon';\n icon.src = info.icon;\n icon.alt = info.name;\n item.appendChild(icon);\n }\n const name = document.createElement('span');\n name.textContent = info.name;\n item.appendChild(name);\n item.addEventListener('click', () => { this.destroy(); opts.onSelect(provider); });\n list.appendChild(item);\n }\n card.appendChild(list);\n }\n\n const cancel = document.createElement('button');\n cancel.className = 'wp-cancel';\n cancel.textContent = 'Cancel';\n cancel.addEventListener('click', () => { this.destroy(); opts.onCancel(); });\n card.appendChild(cancel);\n\n backdrop.appendChild(card);\n this.shadow.appendChild(style);\n this.shadow.appendChild(backdrop);\n document.body.appendChild(this.root);\n }\n\n destroy() {\n this.root.remove();\n }\n}\n\nfunction showToast(message = 'Opening in new window\\u2026', variant: 'info' | 'error' = 'info') {\n const bg = variant === 'error' ? '#dc2626' : '#1f2937';\n const root = document.createElement('div');\n const shadow = root.attachShadow({ mode: 'closed' });\n shadow.innerHTML = `<style>:host{all:initial}.ok-toast{position:fixed;top:16px;right:16px;z-index:1000000;background:${bg};color:#f9fafb;padding:10px 16px;border-radius:8px;font:14px/1.4 system-ui,sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.15);animation:ok-t-in 200ms ease-out;max-width:360px}@keyframes ok-t-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}</style><div class=\"ok-toast\">${message}</div>`;\n document.body.appendChild(root);\n setTimeout(() => root.remove(), 4000);\n}\n\nclass ExternalWalletError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ExternalWalletError';\n }\n}\n\nexport class OpenKey {\n private host: string;\n private oauthHost: string;\n private appName: string;\n private mode: OpenKeyMode;\n private passkeysSupported: boolean;\n private config: OpenKeyConfig;\n private popup: Window | null = null;\n private lastAuth: AuthResult | null = null;\n private discoveredProviders: EIP6963ProviderDetail[] = [];\n /** Cancels widget flows that could outlive a sign-out on this instance. */\n private activeFlowCancellations = new Set<() => void>();\n /**\n * Nostr identity custody + signing (secp256k1 Schnorr / BIP-340). Kept\n * fully separate from the Ethereum flows above: it never reuses\n * `lastAuth`, and its widget messages never carry a session token -\n * the OpenKey session and any signing grants stay inside\n * the OpenKey-origin iframe for the whole lifetime of this client.\n */\n readonly nostr: OpenKeyNostr;\n\n constructor(config: OpenKeyConfig = {}) {\n this.config = config;\n this.host = config.host || DEFAULT_HOST;\n this.oauthHost = config.oauthHost || this.deriveOAuthHost(this.host);\n this.appName = config.appName || window.location.hostname;\n this.mode = config.mode ?? 'iframe';\n this.passkeysSupported = config.passkeysSupported ?? true;\n this.nostr = new OpenKeyNostr(this.host, this.passkeysSupported);\n\n // Listen for EIP-6963 wallet announcements\n if (typeof window !== 'undefined') {\n this.discoveredProviders = [];\n window.addEventListener('eip6963:announceProvider', (event: any) => {\n this.discoveredProviders.push(event.detail);\n });\n window.dispatchEvent(new Event('eip6963:requestProvider'));\n }\n }\n\n /**\n * Derive the OAuth API host from the main host by prefixing 'api.'\n * @param host - The main host URL\n * @returns The derived OAuth API host URL\n */\n private deriveOAuthHost(host: string): string {\n try {\n const url = new URL(host);\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n return host.replace(/\\/$/, '');\n }\n url.hostname = 'api.' + url.hostname;\n return url.toString().replace(/\\/$/, '');\n } catch {\n return 'https://api.openkey.so';\n }\n }\n\n /**\n * Sol MAJOR-4 (final continuation): fetch the caller's registered keys\n * so `resolveAuthorizeTinyCloudRouting` can resolve an explicit pin\n * whose keyId does not match the active session key. Called only when\n * the caller passes a `requestKeyId` we do not already recognise; the\n * common no-pin path skips the network round-trip.\n *\n * Failures do NOT block routing — we fall through with `knownKeys=[]`\n * so the resolver's lastAuth-based fallback still runs. If the caller\n * pinned a genuinely unknown key that turns out to be external, the\n * widget rejects the server-signing path and the SDK surfaces that\n * error from the finalize call rather than silently routing wrong.\n */\n private async fetchKnownKeysIfNeeded(\n requestKeyId: string | undefined,\n ): Promise<Array<{ id: string; address?: string; keyType: 'MANAGED' | 'EXTERNAL' }>> {\n if (!requestKeyId) return [];\n // If lastAuth already identifies this key (id or address), we do not\n // need a network round-trip — the resolver adopts lastAuth.keyType.\n const la = this.lastAuth;\n if (la?.keyId === requestKeyId) return [];\n if (la?.address && la.address.toLowerCase() === requestKeyId.toLowerCase()) return [];\n if (requestKeyId.startsWith('external:')) return [];\n try {\n const res = await fetch(`${this.oauthHost}/api/keys`, {\n method: 'GET',\n credentials: 'include',\n });\n if (!res.ok) return [];\n const body = (await res.json()) as {\n keys?: Array<{ id: string; address?: string; keyType: 'MANAGED' | 'EXTERNAL' }>;\n };\n return Array.isArray(body?.keys) ? body.keys : [];\n } catch {\n return [];\n }\n }\n\n /**\n * Connect to OpenKey and get user's wallet address\n * Opens auth flow for user to select/create a key\n */\n async connect(opts?: { mode?: OpenKeyMode }): Promise<AuthResult> {\n const result = await this.openFlow<AuthResult>('connect', {\n type: 'openkey:auth:request',\n appName: this.appName,\n }, opts?.mode);\n this.lastAuth = result;\n return result;\n }\n\n /**\n * Signs the active OpenKey account out through the OpenKey-owned widget.\n *\n * The OpenKey-owned widget revokes the OpenKey session it holds itself;\n * the SDK never holds an OpenKey session token. Local SDK state is cleared\n * before opening the widget, including when remote revocation is\n * unavailable.\n *\n * Rejects without waiting for the timeout when the widget cannot be shown\n * (code `POPUP_BLOCKED`): the iframe did not load and the popup fallback is\n * blocked, or the app is an embedded WebView that cannot open popups.\n * Remote revocation did not happen then, so clearing any app session is the\n * caller's decision.\n */\n async signOut(opts?: { mode?: OpenKeyMode }): Promise<SignOutAcknowledgement> {\n const requestId = `ok-signout-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n // Redirect flows intentionally never resolve. Sign-out needs to return a\n // correlated acknowledgement, so use the OpenKey popup boundary instead.\n const mode = this.resolveMode(opts?.mode) === 'redirect' ? 'popup' : opts?.mode;\n\n // Stale connect/sign widgets must not later overwrite this signed-out\n // instance with their responses.\n this.cancelActiveFlows();\n // A failed/blocked widget must never leave this SDK instance authenticated.\n this.lastAuth = null;\n\n const acknowledgement = await this.openFlow<SignOutAcknowledgement>(\n 'sign-out',\n {\n type: 'openkey:sign-out:request',\n requestId,\n protocolVersion: 1,\n },\n mode,\n );\n\n if (acknowledgement.requestId !== requestId) {\n throw new Error('OpenKey signOut received an acknowledgement for a different request');\n }\n return acknowledgement;\n }\n\n /**\n * Always null. OpenKey no longer gives its session token to the embedding\n * page (TC-688): the token unlocked OpenKey's key and account APIs, not\n * just the requesting app's flows.\n *\n * @deprecated Kept so existing callers compile. Third-party server-side\n * signing uses an OAuth access token from the OpenKey OAuth flow.\n */\n getSessionToken(): string | null {\n return null;\n }\n\n /**\n * Endpoint for a TinyCloud auto-sign signing strategy. `token` is always\n * null; see {@link OpenKey.getSessionToken}.\n *\n * @deprecated The endpoint accepts an OAuth access token as its bearer.\n */\n tinycloudSigningOptions(): { endpoint: string; token: string | null } {\n return {\n endpoint: `${this.oauthHost}/api/delegate/sign`,\n token: null,\n };\n }\n\n /**\n * Link an external wallet to the user's OpenKey account\n * Opens link-wallet widget flow\n */\n async linkWallet(opts?: { mode?: OpenKeyMode }): Promise<{ address: string; keyId: string }> {\n return this.openFlow<{ address: string; keyId: string }>('link-wallet', {\n type: 'openkey:link-wallet:request',\n }, opts?.mode);\n }\n\n /**\n * Sign a message with the user's OpenKey wallet\n * For external keys, routes directly to the user's wallet provider\n */\n async signMessage(request: SignMessageRequest, opts?: { mode?: OpenKeyMode }): Promise<SignResult> {\n if (this.lastAuth?.keyType === 'EXTERNAL') {\n return this.signWithExternalWallet(request);\n }\n return this.signWithOpenKey(request, opts?.mode);\n }\n\n /**\n * Versioned TinyCloud authorization protocol (v1).\n *\n * Unlike `signMessage()` (which is contractually byte-exact — the returned\n * signature always verifies against the caller's exact bytes), this method\n * permits the user to narrow the requested capabilities before signing.\n * OpenKey may therefore regenerate the SIWE. The response carries\n * `signedMessage` — the exact bytes the signature actually verifies against.\n *\n * TinyCloud completes the session with `signedMessage`, not the original\n * `siwe` passed in. Consumers MUST use the returned bytes and MUST NOT\n * assume the returned grants are a superset of what they requested — the\n * user is allowed to remove any grant that is not required.\n *\n * Legacy `signMessage()` callers are unaffected: their exact-byte contract\n * remains preserved and OpenKey never rewrites their bytes.\n */\n async authorizeTinyCloud(\n request: TinyCloudAuthorizationRequestV1,\n opts?: { mode?: OpenKeyMode },\n ): Promise<TinyCloudAuthorizationResultV1> {\n if (request.protocolVersion !== 1) {\n throw new Error(\n `authorizeTinyCloud only supports protocolVersion 1; got ${request.protocolVersion}`,\n );\n }\n if (typeof request.siwe !== 'string' || !request.siwe) {\n throw new Error('authorizeTinyCloud requires a non-empty SIWE');\n }\n // Sol MAJOR-3 (continuation) + Sol MAJOR-4 (final): route to the\n // external-wallet preview→sign→finalize flow whenever the ACTIVE\n // authentication session is against an EXTERNAL key. External keys\n // are held by the user's browser wallet — OpenKey does not have\n // the private material, so it cannot execute the managed-key\n // widget-signs-server-side path.\n //\n // Sol's final continuation rejection: the prior check\n // `this.lastAuth?.keyId === explicitKeyId && ... === 'EXTERNAL'`\n // let an explicitly-supplied external keyId enter the managed\n // widget path whenever the caller's `explicitKeyId` did not match\n // `lastAuth.keyId` string-for-string (for example: caller passes\n // the address; lastAuth carries the internal key record id — same\n // key, different identifier). A user cannot be authenticated with\n // two different active session keys at once — the ONE active\n // session's `keyType` is therefore authoritative for routing.\n //\n // Routing rules (revised):\n // 1. If `lastAuth?.keyType === 'EXTERNAL'`, route to external\n // (the wallet holds the material; the widget cannot sign\n // server-side either way).\n // 2. If `lastAuth?.keyType === 'MANAGED'`, route to managed.\n // 3. If `lastAuth` is missing (no active session), route to\n // managed — the widget renders a connect flow first, and\n // once the user connects the widget resolves keyType from\n // the fresh session; if that turns out to be EXTERNAL the\n // widget itself refuses to server-sign and asks the SDK to\n // re-enter via the external path.\n //\n // This resolves the key type authoritatively from the ONE active\n // session rather than relying on a fragile keyId string match.\n // See `shouldRouteAuthorizeTinyCloudExternal` for the full\n // routing-decision rationale and its unit tests for every branch.\n // Sol MAJOR-4 (final continuation): resolve the target key first\n // and route by ITS type, not by lastAuth.keyType alone. When the\n // caller pins a request.keyId that differs from the active session\n // key, we fetch the caller's key list from the server so the pure\n // `resolveAuthorizeTinyCloudRouting` helper can decide by resolved\n // type (or throw on conflicting pins).\n let routing: AuthorizeTinyCloudRouting;\n try {\n const knownKeys = await this.fetchKnownKeysIfNeeded(request.keyId);\n routing = resolveAuthorizeTinyCloudRouting({\n lastAuth: this.lastAuth,\n requestKeyId: request.keyId,\n knownKeys,\n });\n } catch (e) {\n if (e instanceof KeyIdTypeMismatchError) throw e;\n throw e instanceof Error\n ? e\n : new Error('authorizeTinyCloud: failed to resolve target key type');\n }\n if (routing.route === 'external') {\n return this.authorizeTinyCloudExternal(request, opts);\n }\n // Sol CRITICAL-1: Send a DISTINCT versioned sign request so the widget\n // knows to route through the server-authoritative /authorize-sign\n // endpoint. protocolVersion: 1 with the extended payload triggers the\n // server-authoritative narrowing path — the widget refuses to fall\n // back to legacy exact-byte signMessage() for a versioned request\n // whose origin is real.\n const requestId = `ok-auth-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n // Sol MAJOR-3: fall back to the connected key when the caller did not\n // pass an explicit keyId. Without this fallback the widget renders\n // 'Please connect first.' because it has no key context, and the\n // NodeUserAuthorization bridge never carries a keyId when the caller\n // only ran connect() — the widget path is otherwise unable to sign.\n const resolvedKeyId = routing.resolvedKeyId ?? request.keyId ?? this.lastAuth?.keyId;\n const raw = await this.openFlow<{\n signature: string;\n address: string;\n signedMessage?: string;\n selectedActionKeys?: string[];\n permissions?: TinyCloudAuthorizationResultV1['permissions'];\n }>(\n 'sign',\n {\n type: 'openkey:sign:request',\n requestId,\n protocolVersion: 1,\n message: request.siwe,\n keyId: resolvedKeyId,\n // Forward JWK so the widget can pass it to /authorize-sign for\n // narrowed-SIWE regeneration.\n jwk: request.jwk,\n // Forward the TinyCloud host so the widget can bind it into the\n // /authorize-sign-prepare context (Sol MAJOR-2).\n host: request.host,\n // Sol MAJOR-2 (envelope): forward the caller-supplied presentation\n // envelope so the widget can render honest provenance and the\n // /authorize-sign-prepare route can bind it into the authorization\n // context. The envelope is display-only; the widget transport\n // validates and size-bounds it before use and NEVER treats it as\n // verified unless the server actually origin-binds a manifest.\n presentation: request.presentation,\n },\n opts?.mode,\n );\n // Sol CRITICAL-1: NO silent fallback to `request.siwe`. If the widget\n // returned an authorization response without `signedMessage`, that is\n // a protocol violation and MUST fail — silently accepting the caller's\n // original bytes as `signedMessage` while selectedActionKeys/permissions\n // reflect a narrower set is exactly the bug this rewrite prevents.\n if (typeof raw.signedMessage !== 'string' || !raw.signedMessage) {\n throw new Error(\n 'authorizeTinyCloud: widget returned no signedMessage — signature would not correspond to the displayed authorization',\n );\n }\n if (!Array.isArray(raw.selectedActionKeys)) {\n throw new Error(\n 'authorizeTinyCloud: widget returned no selectedActionKeys — cannot verify displayed authorization matches the signed bytes',\n );\n }\n if (!Array.isArray(raw.permissions)) {\n throw new Error(\n 'authorizeTinyCloud: widget returned no permissions — cannot verify displayed authorization matches the signed bytes',\n );\n }\n return {\n protocolVersion: 1,\n address: raw.address,\n signature: raw.signature,\n signedMessage: raw.signedMessage,\n selectedActionKeys: raw.selectedActionKeys,\n permissions: raw.permissions,\n };\n }\n\n /**\n * Sol MAJOR-3 (continuation): authorizeTinyCloud path for external keys.\n * The wallet lives outside OpenKey, so the SDK opens the shared widget\n * for review + preview, then invokes the user's wallet to sign the\n * preview bytes, then finalizes server-side via /authorize-sign.\n *\n * Flow:\n * 1. Open the widget with `externalSign: true` — the widget renders\n * the shared SigningApproval UI, calls /authorize-sign-prepare,\n * calls /authorize-sign-preview after the user approves a\n * selection, and hands back `{ previewApprovalToken,\n * signedMessage, selectedActionIds, address, authorizationContextToken }`.\n * 2. SDK invokes the user's wallet on the returned signedMessage.\n * 3. SDK POSTs /authorize-sign with `externalSignature`.\n *\n * Legacy exact-byte signMessage callers are unaffected — this path is\n * only taken via authorizeTinyCloud (protocolVersion:1) when the target\n * key is EXTERNAL. Sol's rejection required this path to go through\n * the same review UI as managed keys.\n */\n private async authorizeTinyCloudExternal(\n request: TinyCloudAuthorizationRequestV1,\n opts?: { mode?: OpenKeyMode },\n ): Promise<TinyCloudAuthorizationResultV1> {\n const keyId = request.keyId ?? this.lastAuth?.keyId;\n if (!keyId) {\n throw new Error(\n 'authorizeTinyCloud (external) requires a keyId — call connect() first or pass request.keyId',\n );\n }\n const walletAddress = this.lastAuth?.address;\n if (!walletAddress) {\n throw new Error(\n 'authorizeTinyCloud (external) requires a connected wallet — call connect() first',\n );\n }\n // Sol MAJOR-3 (continuation): open the shared widget UI so the user\n // sees and can narrow the review, identical to the managed-key flow.\n const requestId = `ok-ext-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n const approval = await this.openFlow<{\n authorizationContextToken: string;\n previewApprovalToken: string;\n signedMessage: string;\n selectedActionIds: string[];\n address: string;\n }>(\n 'sign',\n {\n type: 'openkey:sign:request',\n requestId,\n protocolVersion: 1,\n message: request.siwe,\n keyId,\n jwk: request.jwk,\n host: request.host,\n // Sol MAJOR-2 (envelope): forward the caller-supplied presentation\n // envelope. Same rules as the managed path — display-only, size-\n // bounded at the transport, never treated as verified without\n // server-side origin-binding.\n presentation: request.presentation,\n // Sol MAJOR-3 (continuation): tell the widget to hand us back\n // the previewApproval instead of asking OpenKey to sign\n // server-side with a managed key. The widget will still render\n // the SAME SigningApproval component; it just diverges at the\n // \"sign\" step and emits `openkey:externalSign:approve` back\n // through the transport.\n externalSign: true,\n } as unknown as MessageType,\n opts?.mode,\n );\n if (!approval?.previewApprovalToken || !approval?.signedMessage) {\n throw new Error(\n 'authorizeTinyCloud (external): widget did not return a previewApprovalToken + signedMessage',\n );\n }\n if (\n approval.address &&\n approval.address.toLowerCase() !== walletAddress.toLowerCase()\n ) {\n throw new Error(\n `authorizeTinyCloud (external): widget preview address ${approval.address} does not match connected wallet ${walletAddress}`,\n );\n }\n // Wallet signs the exact bytes the widget previewed.\n const provider = await this.findWalletProvider(walletAddress);\n const hexMessage = this.toHex(approval.signedMessage);\n const walletSignature = (await provider.request({\n method: 'personal_sign',\n params: [hexMessage, walletAddress],\n })) as string;\n // Finalize — server verifies the wallet signature against the exact\n // same bytes it emitted for the preview.\n const finalizeRes = await fetch(\n `${this.oauthHost}/api/delegate/authorize-sign`,\n {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n authorizationContextToken: approval.authorizationContextToken,\n previewApprovalToken: approval.previewApprovalToken,\n selectedActionIds: approval.selectedActionIds,\n protocolVersion: 1,\n externalSignature: walletSignature,\n }),\n },\n );\n if (!finalizeRes.ok) {\n const body = await finalizeRes.json().catch(() => ({ error: `HTTP ${finalizeRes.status}` }));\n throw new Error(body.error || `authorize-sign failed (${finalizeRes.status})`);\n }\n const finalize = await finalizeRes.json();\n if (finalize.signedMessage !== approval.signedMessage) {\n throw new Error(\n 'authorizeTinyCloud (external): server signed different bytes than the preview showed',\n );\n }\n return {\n protocolVersion: 1,\n address: finalize.address,\n signature: finalize.signature,\n signedMessage: finalize.signedMessage,\n selectedActionKeys: finalize.selectedActionKeys ?? [],\n permissions: finalize.permissions ?? [],\n };\n }\n\n /**\n * Sign typed data (EIP-712) with the user's OpenKey wallet\n * For external keys, routes directly to the user's wallet provider\n */\n async signTypedData(request: SignTypedDataRequest, opts?: { mode?: OpenKeyMode }): Promise<SignResult> {\n if (this.lastAuth?.keyType === 'EXTERNAL') {\n return this.signTypedDataWithExternalWallet(request);\n }\n return this.signTypedDataWithOpenKey(request, opts?.mode);\n }\n\n private providerCandidates(): EIP1193Provider[] {\n const providers: EIP1193Provider[] = [];\n if (this.config.externalProvider) {\n providers.push(this.config.externalProvider);\n }\n providers.push(...this.discoveredProviders.map(({ provider }) => provider));\n if (typeof window !== 'undefined' && (window as any).ethereum) {\n providers.push((window as any).ethereum as EIP1193Provider);\n }\n return [...new Set(providers)];\n }\n\n private async providerHasAccount(\n provider: EIP1193Provider,\n method: 'eth_accounts' | 'eth_requestAccounts',\n targetAddress: string\n ): Promise<boolean> {\n try {\n const accounts = await provider.request({ method }) as string[];\n return accounts.some((account) => account.toLowerCase() === targetAddress.toLowerCase());\n } catch {\n return false;\n }\n }\n\n /**\n * Find a wallet provider that controls the given address.\n * Checks connected accounts first, then asks the wallet to authorize the\n * app origin if the external key was linked elsewhere.\n */\n async findWalletProvider(targetAddress: string): Promise<EIP1193Provider> {\n const providers = this.providerCandidates();\n\n for (const provider of providers) {\n if (await this.providerHasAccount(provider, 'eth_accounts', targetAddress)) {\n return provider;\n }\n }\n\n for (const provider of providers) {\n if (await this.providerHasAccount(provider, 'eth_requestAccounts', targetAddress)) {\n return provider;\n }\n }\n\n throw new ExternalWalletError('External wallet not found. Connect the wallet that owns this key and try again.');\n }\n\n private async signWithExternalWallet(request: SignMessageRequest): Promise<SignResult> {\n let provider: EIP1193Provider;\n try {\n provider = await this.findWalletProvider(this.lastAuth!.address);\n } catch (e) {\n showToast((e as Error).message, 'error');\n throw e;\n }\n try {\n const hexMessage = this.toHex(request.message);\n const signature = await provider.request({\n method: 'personal_sign',\n params: [hexMessage, this.lastAuth!.address],\n });\n return { signature: signature as string, address: this.lastAuth!.address };\n } catch (e: any) {\n const msg = e?.message?.includes('not been authorized')\n ? 'Wallet rejected the request. Approve the connection in your wallet and try again.'\n : e?.message || 'Signing failed';\n showToast(msg, 'error');\n throw new Error(msg);\n }\n }\n\n private async signTypedDataWithExternalWallet(request: SignTypedDataRequest): Promise<SignResult> {\n let provider: EIP1193Provider;\n try {\n provider = await this.findWalletProvider(this.lastAuth!.address);\n } catch (e) {\n showToast((e as Error).message, 'error');\n throw e;\n }\n try {\n const signature = await provider.request({\n method: 'eth_signTypedData_v4',\n params: [this.lastAuth!.address, JSON.stringify(request)],\n });\n return { signature: signature as string, address: this.lastAuth!.address };\n } catch (e: any) {\n const msg = e?.message?.includes('not been authorized')\n ? 'Wallet rejected the request. Approve the connection in your wallet and try again.'\n : e?.message || 'Signing failed';\n showToast(msg, 'error');\n throw new Error(msg);\n }\n }\n\n private signWithOpenKey(request: SignMessageRequest, mode?: OpenKeyMode): Promise<SignResult> {\n return this.openFlow<SignResult>('sign', {\n type: 'openkey:sign:request',\n message: request.message,\n keyId: request.keyId,\n }, mode);\n }\n\n private signTypedDataWithOpenKey(request: SignTypedDataRequest, mode?: OpenKeyMode): Promise<SignResult> {\n return this.openFlow<SignResult>('sign-typed-data', {\n type: 'openkey:signTypedData:request',\n data: request,\n }, mode);\n }\n\n private toHex(str: string): string {\n return '0x' + Array.from(new TextEncoder().encode(str)).map(b => b.toString(16).padStart(2, '0')).join('');\n }\n\n /**\n * Check if user is already authenticated\n */\n async isConnected(): Promise<boolean> {\n // Check for existing session via API\n try {\n const res = await fetch(`${this.host}/api/auth/session`, {\n credentials: 'include',\n });\n if (!res.ok) return false;\n const data = await res.json();\n return !!data.user;\n } catch {\n return false;\n }\n }\n\n // ======= OAuth 2.1 Provider Methods =======\n\n /**\n * OAuth 2.1 methods for third-party app authentication\n * Use this when your app has registered OAuth client credentials with OpenKey\n */\n oauth = {\n /**\n * Start OAuth authorization flow\n * Opens popup/redirect to OpenKey authorization endpoint\n * @returns Promise that resolves with authorization code after user consent\n */\n connect: async (config: OAuthConfig): Promise<OAuthResult> => {\n // Generate PKCE values\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n const state = config.state || generateState();\n\n // Store for later token exchange\n sessionStorage.setItem(\n OAUTH_STORAGE_KEY,\n JSON.stringify({ verifier, state })\n );\n\n // Build authorization URL\n const authUrl = buildAuthorizationUrl({\n host: this.oauthHost,\n clientId: config.clientId,\n redirectUri: config.redirectUri,\n codeChallenge: challenge,\n state,\n scopes: ['openid'],\n });\n\n return this.openOAuthFlow(authUrl, state, config.redirectUri);\n },\n\n /**\n * Exchange authorization code for tokens\n * Call this after receiving the callback with authorization code\n * @param code - Authorization code from callback URL\n * @param config - Same config used for connect()\n * @returns Promise that resolves with access_token, id_token, etc.\n */\n exchangeCode: async (\n code: string,\n config: OAuthConfig\n ): Promise<OAuthTokenResponse> => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) {\n throw new Error('No PKCE verifier found. Start a new OAuth flow.');\n }\n\n const { verifier } = JSON.parse(stored);\n\n const response = await fetch(`${this.oauthHost}/api/auth/oauth2/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n code_verifier: verifier,\n }),\n });\n\n // Clear stored verifier\n sessionStorage.removeItem(OAUTH_STORAGE_KEY);\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.error_description || 'Token exchange failed');\n }\n\n return response.json();\n },\n\n /**\n * Verify state parameter from callback\n * Call this before exchangeCode to prevent CSRF attacks\n * @param receivedState - State parameter from callback URL\n * @returns true if state matches, false otherwise\n */\n verifyState: (receivedState: string): boolean => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) return false;\n const { state } = JSON.parse(stored);\n return state === receivedState;\n },\n\n /**\n * Parse authorization response from callback URL\n * @param url - Callback URL (defaults to current window.location.href)\n * @returns Parsed code and state, or error\n */\n parseCallback: (\n url?: string\n ): { code: string; state: string } | { error: string; errorDescription?: string } => {\n const urlObj = new URL(url || window.location.href);\n const error = urlObj.searchParams.get('error');\n\n if (error) {\n return {\n error,\n errorDescription: urlObj.searchParams.get('error_description') || undefined,\n };\n }\n\n const code = urlObj.searchParams.get('code');\n const state = urlObj.searchParams.get('state');\n\n if (!code || !state) {\n return { error: 'missing_params', errorDescription: 'Missing code or state parameter' };\n }\n\n return { code, state };\n },\n };\n\n private openOAuthFlow(\n url: string,\n state: string,\n redirectUri: string\n ): Promise<OAuthResult> {\n return new Promise((resolve, reject) => {\n if (this.mode !== 'redirect') {\n this.openOAuthPopup(url, state, redirectUri, resolve, reject);\n } else {\n // Full page redirect - won't resolve, page navigates away\n window.location.href = url;\n }\n });\n }\n\n private openOAuthPopup(\n url: string,\n state: string,\n redirectUri: string,\n resolve: (value: OAuthResult) => void,\n reject: (error: OpenKeyError) => void\n ) {\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n const popup = window.open(\n url,\n 'openkey-oauth',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!isUsablePopup(popup)) {\n this.popup = null;\n const embedded = isEmbeddedAppWebView();\n if (!embedded) showToast('Popup was blocked. Please allow popups for this site.', 'error');\n reject(embedded\n ? popupUnavailableError(true)\n : { code: 'POPUP_BLOCKED', message: 'Popup was blocked. Please allow popups or use redirect mode.' });\n return;\n }\n this.popup = popup;\n\n const redirectHost = new URL(redirectUri).origin;\n\n // Poll for redirect back with code\n const pollInterval = setInterval(() => {\n try {\n if (this.popup?.closed) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n return;\n }\n\n // Check if redirected to callback URL\n const popupUrl = this.popup?.location?.href;\n if (popupUrl && popupUrl.startsWith(redirectHost)) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n\n const urlObj = new URL(popupUrl);\n const code = urlObj.searchParams.get('code');\n const returnedState = urlObj.searchParams.get('state');\n const error = urlObj.searchParams.get('error');\n\n this.popup?.close();\n\n if (error) {\n reject({\n code: 'UNAUTHORIZED',\n message: urlObj.searchParams.get('error_description') || error,\n });\n return;\n }\n\n if (returnedState !== state) {\n reject({\n code: 'STATE_MISMATCH',\n message: 'State mismatch - possible CSRF attack',\n });\n return;\n }\n\n if (code) {\n resolve({ code, state: returnedState });\n } else {\n reject({\n code: 'UNAUTHORIZED',\n message: 'Authorization failed - no code returned',\n });\n }\n }\n } catch {\n // Cross-origin - popup still on OpenKey domain, continue polling\n }\n }, 100);\n\n // Timeout\n const timeout = setTimeout(() => {\n clearInterval(pollInterval);\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'OAuth flow timed out',\n });\n }, DEFAULT_TIMEOUT);\n }\n\n private resolveMode(override?: OpenKeyMode): OpenKeyMode {\n return override ?? this.mode;\n }\n\n private hasDetectedEoa(): boolean {\n if (this.discoveredProviders.length > 0) return true;\n if (typeof window !== 'undefined' && (window as any).ethereum) return true;\n return false;\n }\n\n private async openFlow<T>(action: string, message: object, modeOverride?: OpenKeyMode): Promise<T> {\n const mode = this.resolveMode(modeOverride);\n const origin = encodeURIComponent(window.location.origin);\n const eoaFlag = action === 'connect' && this.hasDetectedEoa() ? '&hasEoa=true' : '';\n const passkeysFlag = passkeysQueryFlag(this.passkeysSupported);\n\n if (mode === 'popup') {\n const url = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}${passkeysFlag}`;\n return new Promise((resolve, reject) => this.openPopup(action, url, message, resolve, reject));\n }\n\n if (mode === 'redirect') {\n const url = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}${passkeysFlag}`;\n window.location.href = url;\n return new Promise(() => {}); // never resolves, page navigates\n }\n\n // iframe mode with auto-fallback\n const url = `${this.host}/widget/embed/${action}?origin=${origin}${eoaFlag}${passkeysFlag}`;\n return this.openIframeModal<T>(url, action, message, origin);\n }\n\n private openIframeModal<T>(url: string, action: string, message: object, origin: string): Promise<T> {\n // Sol MAJOR-3: capture the outgoing request's correlation IDs so\n // response handlers can require them to match. Legacy unversioned\n // requests carry neither and skip correlation.\n const outgoingRequestId = (message as { requestId?: unknown }).requestId;\n const outgoingProtocolVersion = (message as { protocolVersion?: unknown }).protocolVersion;\n const isVersionedRequest =\n typeof outgoingRequestId === 'string' &&\n typeof outgoingProtocolVersion === 'number' &&\n outgoingProtocolVersion >= 1;\n return new Promise((resolve, reject) => {\n let readyReceived = false;\n let settled = false;\n let modal: IframeModal | null = null;\n let cancel: () => void;\n\n const settle = (fn: () => void) => {\n if (settled) return;\n settled = true;\n fn();\n };\n\n const cleanup = () => {\n clearTimeout(readyTimeout);\n clearTimeout(overallTimeout);\n modal?.destroy();\n modal = null;\n this.activeFlowCancellations.delete(cancel);\n };\n\n modal = new IframeModal({\n url,\n host: this.host,\n onClose: () => {\n settle(() => {\n cleanup();\n reject({ code: 'USER_CANCELLED', message: 'User cancelled the request' } as OpenKeyError);\n });\n },\n onMessage: (data: MessageType) => {\n if (data.type === 'openkey:ready') {\n readyReceived = true;\n modal?.postMessage(message);\n return;\n }\n // Sol MAJOR-3: correlate versioned responses. If the request\n // was versioned, drop any response that doesn't match the\n // outgoing requestId/protocolVersion. If the request was\n // unversioned (legacy), accept any response.\n if (isVersionedRequest) {\n const respRequestId = (data as { requestId?: unknown }).requestId;\n const respVersion = (data as { protocolVersion?: unknown }).protocolVersion;\n if (respRequestId !== outgoingRequestId) return;\n if (respVersion !== outgoingProtocolVersion) return;\n }\n\n if (data.type === 'openkey:close') {\n settle(() => {\n cleanup();\n reject({ code: 'USER_CANCELLED', message: 'User cancelled the request' } as OpenKeyError);\n });\n return;\n }\n\n if (data.type === 'openkey:link-wallet:delegate') {\n this.handleWalletLinkDelegation(modal!);\n return;\n }\n\n if (data.type === 'openkey:auth:use-external-wallet') {\n cleanup();\n this.handleExternalWalletConnect<T>(resolve, reject);\n return;\n }\n\n if (this.isResponseForAction(action, data)) {\n settle(() => {\n cleanup();\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || 'MANAGED' } as T);\n } else if (data.type === 'openkey:link-wallet:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else if (data.type === 'openkey:sign-out:response') {\n resolve({ requestId: data.requestId, revoked: data.revoked } as T);\n } else if (data.type === 'openkey:externalSign:approve') {\n resolve({\n authorizationContextToken: (data as any).authorizationContextToken,\n previewApprovalToken: (data as any).previewApprovalToken,\n signedMessage: (data as any).signedMessage,\n selectedActionIds: (data as any).selectedActionIds,\n address: (data as any).address,\n } as T);\n } else {\n resolve({\n signature: (data as any).signature,\n address: (data as any).address,\n signedMessage: (data as any).signedMessage,\n selectedActionKeys: (data as any).selectedActionKeys,\n permissions: (data as any).permissions,\n } as T);\n }\n } else {\n reject(data.error);\n }\n });\n }\n },\n });\n\n // Sol final continuation contract requirement 5: bind a versioned\n // request's correlation before the iframe can deliver any message,\n // so every resize for it must carry the exact (requestId,\n // protocolVersion) pair. Unversioned flows stay unbound and accept\n // only uncorrelated resizes from this iframe (TC-647).\n if (isVersionedRequest) {\n modal.setExpectedCorrelation(\n outgoingRequestId as string,\n outgoingProtocolVersion as number,\n );\n }\n\n // Auto-fallback: if no openkey:ready within 3s, fall back to popup\n const readyTimeout = setTimeout(() => {\n if (readyReceived || settled) return;\n cleanup();\n if (isEmbeddedAppWebView()) {\n console.warn('OpenKey: iframe did not load in an embedded app WebView, which cannot open a popup. Add frame-src https://openkey.so to your CSP.');\n settle(() => reject(popupUnavailableError(true)));\n return;\n }\n console.warn('OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.');\n showToast();\n const popupUrl = `${this.host}/widget/${action}?origin=${origin}${passkeysQueryFlag(this.passkeysSupported)}`;\n this.openPopup(action, popupUrl, message, (val: any) => settle(() => resolve(val)), (err: any) => settle(() => reject(err)));\n }, IFRAME_READY_TIMEOUT);\n\n // Overall 5-minute timeout\n const overallTimeout = setTimeout(() => {\n settle(() => {\n cleanup();\n reject({ code: 'TIMEOUT', message: 'Request timed out' } as OpenKeyError);\n });\n }, DEFAULT_TIMEOUT);\n\n cancel = () => {\n settle(() => {\n cleanup();\n reject({ code: 'USER_CANCELLED', message: 'Superseded by sign-out' } as OpenKeyError);\n });\n };\n this.activeFlowCancellations.add(cancel);\n });\n }\n\n private isResponseForAction(action: string, data: MessageType): data is WidgetResponseMessage {\n const type = data.type;\n if (action === 'connect') return type === 'openkey:auth:response';\n if (action === 'sign') return type === 'openkey:sign:response' || type === 'openkey:externalSign:approve';\n if (action === 'sign-typed-data') return type === 'openkey:signTypedData:response';\n if (action === 'link-wallet') return type === 'openkey:link-wallet:response';\n if (action === 'sign-out') return type === 'openkey:sign-out:response';\n return false;\n }\n\n private handleWalletLinkDelegation(modal: IframeModal) {\n const sendResult = (result: MessageType) => modal.postMessage(result);\n\n new WalletPicker({\n providers: this.discoveredProviders,\n onCancel: () => {\n sendResult({ type: 'openkey:link-wallet:result', success: false, error: { code: 'USER_CANCELLED', message: 'Wallet linking cancelled' } });\n },\n onSelect: async (provider) => {\n try {\n const accounts = await provider.request({ method: 'eth_requestAccounts' }) as string[];\n const address = accounts[0];\n if (!address) throw new Error('No account returned');\n\n // Get challenge from API\n const challengeRes = await fetch(`${this.host}/api/keys/link/challenge`, {\n method: 'POST',\n credentials: 'include',\n });\n if (!challengeRes.ok) throw new Error('Failed to get challenge');\n const { message } = await challengeRes.json();\n\n // Sign challenge with wallet\n const hexMessage = this.toHex(message);\n const signature = await provider.request({\n method: 'personal_sign',\n params: [hexMessage, address],\n }) as string;\n\n // Submit to API\n const linkRes = await fetch(`${this.host}/api/keys/link`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ address, signature, message }),\n });\n if (!linkRes.ok) {\n const err = await linkRes.json();\n throw new Error(err.error || 'Failed to link wallet');\n }\n const { key } = await linkRes.json();\n\n sendResult({ type: 'openkey:link-wallet:result', success: true, address: key.address, keyId: key.id });\n } catch (e: any) {\n sendResult({ type: 'openkey:link-wallet:result', success: false, error: { code: 'UNKNOWN', message: e.message || 'Wallet linking failed' } });\n }\n },\n });\n }\n\n private handleExternalWalletConnect<T>(\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n new WalletPicker({\n providers: this.discoveredProviders,\n onCancel: () => {\n reject({ code: 'USER_CANCELLED', message: 'User cancelled wallet selection' });\n },\n onSelect: async (provider) => {\n try {\n const accounts = await provider.request({ method: 'eth_requestAccounts' }) as string[];\n const address = accounts[0];\n if (!address) throw new Error('No account returned');\n const result = { address, keyId: `external:${address}`, keyType: 'EXTERNAL' as const };\n this.lastAuth = result;\n resolve(result as T);\n } catch (e: any) {\n reject({ code: 'UNKNOWN', message: e.message || 'Failed to connect wallet' });\n }\n },\n });\n }\n\n private openPopup<T>(\n action: string,\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Calculate popup position\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n const popup = window.open(\n url,\n `openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!isUsablePopup(popup)) {\n this.popup = null;\n const embedded = isEmbeddedAppWebView();\n if (!embedded) showToast('Popup was blocked. Please allow popups for this site.', 'error');\n reject(popupUnavailableError(embedded));\n return;\n }\n this.popup = popup;\n\n let settled = false;\n let cancel: () => void;\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearInterval(pollClosed);\n clearTimeout(timeout);\n this.activeFlowCancellations.delete(cancel);\n if (this.popup === popup) this.popup = null;\n };\n\n const settle = (fn: () => void) => {\n if (settled) return;\n settled = true;\n cleanup();\n fn();\n };\n\n // Poll to detect popup close\n const pollClosed = setInterval(() => {\n if (popup.closed) {\n settle(() => reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n }));\n }\n }, 500);\n\n // Timeout\n const timeout = setTimeout(() => {\n settle(() => {\n popup.close();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n });\n }, DEFAULT_TIMEOUT);\n\n // Sol MAJOR-3: correlation IDs for versioned popups.\n const outgoingRequestId = (message as { requestId?: unknown }).requestId;\n const outgoingProtocolVersion = (message as { protocolVersion?: unknown }).protocolVersion;\n const isVersionedRequest =\n typeof outgoingRequestId === 'string' &&\n typeof outgoingProtocolVersion === 'number' &&\n outgoingProtocolVersion >= 1;\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n if (event.source !== popup) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n // Widget is ready, send our request\n popup.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n // Sol MAJOR-4: correlate close messages to the current versioned\n // request. A same-origin, same-source stale close from a\n // previous /widget/sign window MUST NOT tear down the active\n // request. Unversioned close messages are only accepted on\n // unversioned flows (legacy signMessage callers).\n if (isVersionedRequest) {\n const closeRequestId = (data as { requestId?: unknown }).requestId;\n const closeVersion = (data as { protocolVersion?: unknown }).protocolVersion;\n if (closeRequestId !== outgoingRequestId) return;\n if (closeVersion !== outgoingProtocolVersion) return;\n }\n settle(() => {\n popup.close();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n });\n return;\n }\n\n if (data.type === 'openkey:auth:use-external-wallet') {\n settle(() => {\n popup.close();\n this.handleExternalWalletConnect<T>(resolve, reject);\n });\n return;\n }\n\n // Handle responses\n if (this.isResponseForAction(action, data)) {\n // Sol MAJOR-3: drop cross-correlated responses on versioned flows.\n if (isVersionedRequest) {\n const respRequestId = (data as { requestId?: unknown }).requestId;\n const respVersion = (data as { protocolVersion?: unknown }).protocolVersion;\n if (respRequestId !== outgoingRequestId) return;\n if (respVersion !== outgoingProtocolVersion) return;\n }\n settle(() => {\n popup.close();\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || 'MANAGED' } as T);\n } else if (data.type === 'openkey:link-wallet:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else if (data.type === 'openkey:sign-out:response') {\n resolve({ requestId: data.requestId, revoked: data.revoked } as T);\n } else if (data.type === 'openkey:externalSign:approve') {\n // Sol MAJOR-3 (continuation): hand the preview payload back\n // to authorizeTinyCloudExternal so it can invoke the wallet.\n resolve({\n authorizationContextToken: (data as any).authorizationContextToken,\n previewApprovalToken: (data as any).previewApprovalToken,\n signedMessage: (data as any).signedMessage,\n selectedActionIds: (data as any).selectedActionIds,\n address: (data as any).address,\n } as T);\n } else {\n resolve({\n signature: (data as any).signature,\n address: (data as any).address,\n signedMessage: (data as any).signedMessage,\n selectedActionKeys: (data as any).selectedActionKeys,\n permissions: (data as any).permissions,\n } as T);\n }\n } else {\n reject(data.error);\n }\n });\n }\n };\n\n window.addEventListener('message', handleMessage);\n cancel = () => {\n settle(() => {\n popup.close();\n reject({ code: 'USER_CANCELLED', message: 'Superseded by sign-out' });\n });\n };\n this.activeFlowCancellations.add(cancel);\n }\n\n private cancelActiveFlows() {\n for (const cancel of [...this.activeFlowCancellations]) cancel();\n }\n\n /**\n * Disconnect and close any open flows\n */\n disconnect() {\n this.cancelActiveFlows();\n this.popup?.close();\n this.popup = null;\n }\n}\n\n/**\n * EIP-1193 compatible provider that wraps an OpenKey instance.\n * Transparently routes signing to either OpenKey (managed keys) or the user's\n * wallet (external keys).\n */\n\nexport class OpenKeyProvider implements EIP1193Provider {\n private openkey: OpenKey;\n private address: string;\n private keyId: string;\n private keyType: 'MANAGED' | 'EXTERNAL';\n\n constructor(openkey: OpenKey, authResult: AuthResult) {\n this.openkey = openkey;\n this.address = authResult.address;\n this.keyId = authResult.keyId;\n this.keyType = authResult.keyType;\n }\n\n async request({ method, params }: { method: string; params?: any[] }): Promise<any> {\n switch (method) {\n case 'eth_accounts':\n case 'eth_requestAccounts':\n return [this.address];\n\n case 'eth_chainId':\n return '0x1'; // mainnet\n\n case 'personal_sign': {\n if (this.keyType === 'EXTERNAL') {\n try {\n const provider = await this.openkey.findWalletProvider(this.address);\n return await provider.request({ method, params });\n } catch (e: any) {\n showToast(e?.message || 'External wallet signing failed', 'error');\n throw e;\n }\n }\n // Managed key: route through OpenKey\n const message = this.hexToString(params![0] as string);\n const result = await this.openkey.signMessage({ message, keyId: this.keyId });\n return result.signature;\n }\n\n case 'eth_signTypedData_v4': {\n if (this.keyType === 'EXTERNAL') {\n try {\n const provider = await this.openkey.findWalletProvider(this.address);\n return await provider.request({ method, params });\n } catch (e: any) {\n showToast(e?.message || 'External wallet signing failed', 'error');\n throw e;\n }\n }\n const data = JSON.parse(params![1] as string);\n const result = await this.openkey.signTypedData(data);\n return result.signature;\n }\n\n default:\n throw new Error(`Unsupported method: ${method}`);\n }\n }\n\n private hexToString(hex: string): string {\n const bytes = new Uint8Array(\n (hex.startsWith('0x') ? hex.slice(2) : hex).match(/.{1,2}/g)!.map(b => parseInt(b, 16))\n );\n return new TextDecoder().decode(bytes);\n }\n}\n\n// Default export for convenience\nexport default OpenKey;\n\n// Create singleton instance\nlet defaultInstance: OpenKey | null = null;\n\nexport function getOpenKey(config?: OpenKeyConfig): OpenKey {\n if (!defaultInstance || config) {\n defaultInstance = new OpenKey(config);\n }\n return defaultInstance;\n}\n","/**\n * PKCE (Proof Key for Code Exchange) utilities.\n *\n * Uses a pure-JS base64url encoder (no `btoa()`) so it works across\n * Node.js, browsers, and React Native (including Hermes < 0.73).\n *\n * The default SHA-256 implementation uses `crypto.subtle.digest`.\n * For runtimes without Web Crypto, pass a custom `SHA256Fn`.\n */\n\n/**\n * A function that computes the SHA-256 hash of the input string.\n * Return the raw 32-byte digest as a Uint8Array.\n */\nexport type SHA256Fn = (input: string) => Promise<Uint8Array>;\n\n// Standard base64 alphabet lookup table\nconst BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\n/**\n * Pure JS base64url encoding from a Uint8Array.\n * Does NOT use `btoa()` — safe for all runtimes.\n */\nexport function base64UrlEncode(buffer: Uint8Array): string {\n let base64 = '';\n const len = buffer.length;\n\n for (let i = 0; i < len; i += 3) {\n const b0 = buffer[i]!;\n const b1 = i + 1 < len ? buffer[i + 1]! : 0;\n const b2 = i + 2 < len ? buffer[i + 2]! : 0;\n\n const triplet = (b0 << 16) | (b1 << 8) | b2;\n\n base64 += BASE64_CHARS[(triplet >> 18) & 0x3f];\n base64 += BASE64_CHARS[(triplet >> 12) & 0x3f];\n base64 += i + 1 < len ? BASE64_CHARS[(triplet >> 6) & 0x3f] : '';\n base64 += i + 2 < len ? BASE64_CHARS[triplet & 0x3f] : '';\n }\n\n // Convert base64 to base64url: replace + with -, / with _, strip = padding\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\n\n/**\n * Pure JS base64url decoding into a Uint8Array.\n * Accepts unpadded base64url (RFC 4648 §5); ignores `=` padding.\n */\nexport function base64UrlDecode(input: string): Uint8Array {\n const base64 = input.replace(/-/g, '+').replace(/_/g, '/');\n const clean = base64.replace(/=+$/, '');\n const bytes = new Uint8Array(Math.floor((clean.length * 6) / 8));\n\n let accumulator = 0;\n let bits = 0;\n let out = 0;\n for (const char of clean) {\n const value = BASE64_CHARS.indexOf(char);\n if (value < 0) {\n throw new Error(`Invalid base64url character: ${char}`);\n }\n accumulator = (accumulator << 6) | value;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n bytes[out++] = (accumulator >> bits) & 0xff;\n }\n }\n return bytes;\n}\n\n/**\n * Generate a cryptographically random code verifier for PKCE.\n * Returns a 43-character base64url string (32 random bytes).\n */\nexport function generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\n/**\n * Default SHA-256 implementation using Web Crypto API.\n * Available in Node.js 18+, modern browsers, and Hermes 0.73+.\n */\nasync function defaultSha256(input: string): Promise<Uint8Array> {\n const encoder = new TextEncoder();\n const data = encoder.encode(input);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(digest);\n}\n/** SHA-256 using Web Crypto, exported for hashing proof credentials. */\nexport const sha256: SHA256Fn = defaultSha256;\n\n/**\n * Generate the PKCE code challenge from a code verifier.\n *\n * @param verifier - The code verifier string\n * @param sha256 - Optional custom SHA-256 function (for expo-crypto, react-native-quick-crypto, etc.)\n * @returns Base64url-encoded SHA-256 hash of the verifier\n */\nexport async function generateCodeChallenge(\n verifier: string,\n sha256Fn?: SHA256Fn,\n): Promise<string> {\n const hashFn = sha256Fn ?? defaultSha256;\n const digest = await hashFn(verifier);\n return base64UrlEncode(digest);\n}\n\n/**\n * Generate a cryptographically random state parameter.\n * Returns a 22-character base64url string (16 random bytes).\n */\nexport function generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n","/**\n * Unified error codes across all OpenKey SDKs.\n */\nexport type OpenKeyErrorCode =\n | 'USER_CANCELLED'\n | 'POPUP_BLOCKED'\n | 'TIMEOUT'\n | 'NO_KEY'\n | 'UNAUTHORIZED'\n | 'ACCESS_DENIED'\n | 'STATE_MISMATCH'\n | 'NETWORK_ERROR'\n | 'SERVER'\n | 'UNKNOWN';\n\n/**\n * Error class for all OpenKey SDK errors.\n */\nexport class OpenKeyError extends Error {\n constructor(\n public code: OpenKeyErrorCode,\n message: string,\n ) {\n super(message);\n this.name = 'OpenKeyError';\n }\n}\n","import type { AuthTokens } from './types';\nimport { OpenKeyError } from './errors';\n\n// ======= Authorization URL =======\n\nexport interface BuildAuthorizationUrlOptions {\n /** OpenKey host URL (e.g. https://openkey.so) */\n host: string;\n /** OAuth client ID */\n clientId: string;\n /** Redirect URI for callback */\n redirectUri: string;\n /** PKCE code challenge (base64url-encoded) */\n codeChallenge: string;\n /** State parameter for CSRF protection */\n state: string;\n /** OAuth scopes to request */\n scopes: string[];\n /** Authorization endpoint path (default: /api/auth/oauth2/authorize) */\n authorizePath?: string;\n}\n\n/**\n * Build the OAuth 2.1 authorization URL with PKCE parameters.\n */\nexport function buildAuthorizationUrl(options: BuildAuthorizationUrlOptions): string {\n const {\n host,\n clientId,\n redirectUri,\n codeChallenge,\n state,\n scopes,\n authorizePath = '/api/auth/oauth2/authorize',\n } = options;\n\n const url = new URL(authorizePath, host);\n url.searchParams.set('client_id', clientId);\n url.searchParams.set('redirect_uri', redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', codeChallenge);\n url.searchParams.set('code_challenge_method', 'S256');\n\n return url.toString();\n}\n\n// ======= Token Exchange =======\n\nexport interface ExchangeCodeOptions {\n /** OpenKey host URL */\n host: string;\n /** Authorization code from callback */\n code: string;\n /** Redirect URI (must match authorization request) */\n redirectUri: string;\n /** OAuth client ID */\n clientId: string;\n /** PKCE code verifier */\n codeVerifier: string;\n /** RFC 9700 resource indicator (optional) */\n resource?: string;\n /** Token endpoint path (default: /api/auth/oauth2/token) */\n tokenPath?: string;\n}\n\n/**\n * Exchange an authorization code for tokens.\n */\nexport async function exchangeAuthorizationCode(\n options: ExchangeCodeOptions,\n): Promise<AuthTokens> {\n const {\n host,\n code,\n redirectUri,\n clientId,\n codeVerifier,\n resource,\n tokenPath = '/api/auth/oauth2/token',\n } = options;\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n });\n\n if (resource) {\n body.set('resource', resource);\n }\n\n let response: Response;\n try {\n response = await fetch(`${host}${tokenPath}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n } catch (error) {\n throw new OpenKeyError(\n 'NETWORK_ERROR',\n error instanceof Error ? error.message : 'Network request failed',\n );\n }\n\n if (!response.ok) {\n let detail = '';\n try {\n detail = await response.text();\n } catch {\n // ignore\n }\n throw new OpenKeyError(\n 'UNKNOWN',\n `Token exchange failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`,\n );\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n return mapTokenResponse(data);\n}\n\n// ======= Token Refresh =======\n\nexport interface RefreshTokenOptions {\n /** OpenKey host URL */\n host: string;\n /** Refresh token */\n refreshToken: string;\n /** OAuth client ID */\n clientId: string;\n /** RFC 9700 resource indicator (optional) */\n resource?: string;\n /** Token endpoint path (default: /api/auth/oauth2/token) */\n tokenPath?: string;\n}\n\n/**\n * Refresh an access token using a refresh token.\n */\nexport async function refreshAccessToken(\n options: RefreshTokenOptions,\n): Promise<AuthTokens> {\n const {\n host,\n refreshToken,\n clientId,\n resource,\n tokenPath = '/api/auth/oauth2/token',\n } = options;\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n });\n\n if (resource) {\n body.set('resource', resource);\n }\n\n let response: Response;\n try {\n response = await fetch(`${host}${tokenPath}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n } catch (error) {\n throw new OpenKeyError(\n 'NETWORK_ERROR',\n error instanceof Error ? error.message : 'Network request failed',\n );\n }\n\n if (!response.ok) {\n let detail = '';\n try {\n detail = await response.text();\n } catch {\n // ignore\n }\n throw new OpenKeyError(\n 'UNKNOWN',\n `Token refresh failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`,\n );\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n return mapTokenResponse(data);\n}\n\n// ======= Helpers =======\n\n/**\n * Map a snake_case token response to the camelCase AuthTokens interface.\n */\nexport function mapTokenResponse(data: Record<string, unknown>): AuthTokens {\n return {\n accessToken: data.access_token as string,\n idToken: data.id_token as string,\n refreshToken: data.refresh_token as string | undefined,\n expiresIn: data.expires_in as number,\n };\n}\n\n/**\n * Parse an OAuth callback URL and extract the code and state parameters.\n * Returns null if the URL doesn't contain the expected parameters.\n */\nexport function parseOAuthCallback(url: string): { code: string; state: string } | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n\n const code = parsed.searchParams.get('code');\n const state = parsed.searchParams.get('state');\n\n if (!code || !state) {\n return null;\n }\n\n return { code, state };\n}\n","/**\n * Native TinyCloud delegation flow (TC-774 task S1).\n *\n * Platform-agnostic protocol helpers for native sign-in that returns a\n * TinyCloud delegation: RFC 8414 discovery, PKCE/PAR, an Ed25519 session key\n * (did:key), the OpenKey-Session-Proof compact JWS, callback parsing, token\n * response validation, and typed clients for the token, renew and revoke\n * endpoints. Both `@openkey/sdk-capacitor` and `@openkey/sdk-react-native`\n * build on this module.\n * Wire shapes follow `docs/native-tinycloud-delegation.md` (the O1 spec,\n * normative for O2–O5 and TC-774):\n * - PAR: `POST {metadata.pushed_authorization_request_endpoint}` (form)\n * - Token: `POST {metadata.token_endpoint}` (form)\n * - Renew: `POST {metadata.tinycloud_delegation_renew_endpoint}` (form)\n * - Revoke: `POST {metadata.tinycloud_delegation_revocation_endpoint}` (form)\n * - Proof header: `OpenKey-Session-Proof`; JWS `kid` is the session JWK's\n * RFC 7638 thumbprint, compared to the stored `sessionJkt`.\n */\n\nimport { ed25519 } from '@noble/curves/ed25519';\nimport { base64UrlEncode, base64UrlDecode, sha256, type SHA256Fn } from './pkce';\n\n// ======= Errors =======\n\n/** Error codes for the native delegation flow (spec \"SDK mapping\"). */\nexport type OpenKeyNativeErrorCode =\n | 'USER_CANCELLED'\n | 'ACCESS_DENIED'\n | 'STATE_MISMATCH'\n | 'CONSENT_REQUIRED'\n | 'INVALID_GRANT'\n | 'RENEWAL_CONFLICT'\n | 'RENEWAL_TOO_SOON'\n | 'SPACE_UNAVAILABLE'\n | 'INVALID_REQUEST'\n | 'TEMPORARILY_UNAVAILABLE'\n | 'NETWORK'\n | 'STORAGE'\n | 'SERVER'\n | 'NOT_SIGNED_IN'\n | 'UNAVAILABLE';\n\n/** Error class for the native delegation flow. */\nexport class OpenKeyNativeError extends Error {\n constructor(\n public code: OpenKeyNativeErrorCode,\n message: string,\n /** HTTP status when the failure came from the authorization server. */\n public status?: number,\n /** The server's `error` field when it returned a JSON OAuth error. */\n public serverError?: string,\n /** `Retry-After` in whole seconds, when the server sent it (429/503). */\n public retryAfterSeconds?: number,\n ) {\n super(message);\n this.name = 'OpenKeyNativeError';\n }\n\n /**\n * Set when a successful renew (or exchange) response was rejected after\n * the server already rotated the refresh token - delegation validation\n * or `hosting: \"failed\"`. The old token is dead; callers MUST persist\n * this value before reporting the error, or the session is lost.\n */\n public rotatedRefreshToken?: string;\n}\n\n// ======= Injectable fetch =======\n\n/**\n * Minimal `fetch` parameter surface so a stub can satisfy the type without\n * implementing the full `RequestInit`/`Response` contract.\n */\nexport interface NativeFetchInit {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface NativeFetchResponse {\n ok: boolean;\n status: number;\n /** Header lookup for `Retry-After`; case-insensitive like `Headers.get`. */\n headers?: { get(name: string): string | null };\n json(): Promise<unknown>;\n}\n\nexport type NativeFetch = (\n url: string,\n init?: NativeFetchInit,\n) => Promise<NativeFetchResponse>;\n\n/** Sleep function, injectable for tests. */\nexport type SleepFn = (ms: number) => Promise<void>;\n\n// Promise.withResolvers is newer than the ES2022 lib target and older\n// Hermes; keep the executor form.\nconst defaultSleep: SleepFn = (ms) =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/** Parse a `Retry-After` header as whole seconds; `undefined` if absent/invalid. */\nexport function parseRetryAfterSeconds(\n value: string | null | undefined,\n): number | undefined {\n if (value === null || value === undefined) return undefined;\n const seconds = Number.parseInt(value, 10);\n if (!Number.isFinite(seconds) || seconds < 0 || String(seconds) !== value.trim()) {\n return undefined;\n }\n return seconds;\n}\n\n// ======= Wire types =======\n\n/** `session_key` JWK inside `authorization_details` (public half only). */\nexport interface NativeSessionJwk {\n kty: 'OKP';\n crv: 'Ed25519';\n x: string;\n kid?: string;\n}\n\n/** One requested (or granted) TinyCloud capability entry. */\nexport interface NativeDelegationPermission {\n service: string;\n /** TinyCloud space; always `\"applications\"` for the current flow. */\n space: string;\n path?: string;\n actions: string[];\n}\n\n/** The `tinycloud_delegation` authorization-details element (PAR and renew). */\nexport interface TinyCloudDelegationRequest {\n type: 'tinycloud_delegation';\n session_key: NativeSessionJwk;\n permissions: NativeDelegationPermission[];\n ttl_seconds?: number;\n siwe_nonce?: string;\n}\n\n/**\n * The `tinycloud_delegation` object returned inside token and renew\n * responses. Field names mirror the server payload; `expiresAt` is accepted\n * as an ISO string or epoch seconds.\n */\nexport interface TinyCloudDelegation {\n version?: number;\n grantId?: string;\n address?: string;\n chainId?: number;\n ownerDid?: string;\n spaceId?: string;\n /** Verification method id — the session `did:key` with `#` fragment. */\n verificationMethod: string;\n siwe?: string;\n signature?: string;\n delegationHeader?: { Authorization: string };\n delegationCid?: string;\n issuedAt?: string;\n /** ISO timestamp or epoch seconds; must be in the future. */\n expiresAt: string | number;\n renewableUntil?: string | number;\n permissions: NativeDelegationPermission[];\n tinycloudHost: string;\n hosting?: 'existing' | 'created' | 'failed';\n}\n\n/** Validated RFC 8414 metadata for an OpenKey authorization server. */\nexport interface OpenKeyServerMetadata {\n /** The issuer URI exactly as configured (e.g. `https://api.openkey.so/api/auth`). */\n issuer: string;\n /** Origin of the issuer; endpoints are required to live on it. */\n origin: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n pushedAuthorizationRequestEndpoint: string;\n tinycloudDelegationRenewEndpoint: string;\n tinycloudDelegationRevocationEndpoint: string;\n}\n\n/** An Ed25519 session keypair plus its did:key and JWK serializations. */\nexport interface NativeSessionKeypair {\n /** `did:key:z…` (no fragment). */\n did: string;\n /**\n * Full verification-method id, `did:key:z…#z…`. This is the value the\n * server returns as `verificationMethod`. (The proof JWS `kid` is the\n * JWK's RFC 7638 thumbprint, not this value.)\n */\n keyId: string;\n /** Public JWK: `{kty: 'OKP', crv: 'Ed25519', x}` — the wire shape. */\n publicJwk: NativeSessionJwk;\n /** Private JWK: the public JWK plus `d`. */\n privateJwk: NativeSessionJwk & { d: string };\n}\n\n// ======= Constants =======\n\n/** Header carrying the session-proof compact JWS. */\nexport const SESSION_PROOF_HEADER = 'OpenKey-Session-Proof';\nexport const SESSION_PROOF_TYP = 'openkey-session-proof+jwt';\nexport const DELEGATION_SCOPE = 'tinycloud:delegation';\n/**\n * The permission entry every request must carry; the server refuses requests\n * without `tinycloud.capabilities/read`.\n */\nexport const CAPABILITIES_READ_PERMISSION: NativeDelegationPermission = {\n service: 'tinycloud.capabilities',\n space: 'applications',\n path: '',\n actions: ['tinycloud.capabilities/read'],\n};\n/** Scope set sent on PAR when no `extraScopes` are given. */\nexport const DEFAULT_DELEGATION_SCOPES = [\n 'openid',\n 'offline_access',\n DELEGATION_SCOPE,\n];\n\n// ======= Random helpers =======\n\n/**\n * Generate a nonce for `siwe_nonce` / `jti`. Alphanumeric output satisfies\n * the SIWE nonce grammar `[A-Za-z0-9]{8,64}`.\n */\nexport function generateNonce(length = 32): string {\n const alphabet =\n 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n const bytes = new Uint8Array(length);\n crypto.getRandomValues(bytes);\n let nonce = '';\n for (const byte of bytes) nonce += alphabet[byte % alphabet.length]!;\n return nonce;\n}\n\n// ======= Ed25519 session keys =======\n\nconst BASE58_ALPHABET =\n '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\nfunction base58btcEncode(bytes: Uint8Array): string {\n const digits = [0];\n for (const byte of bytes) {\n let carry = byte;\n for (let index = 0; index < digits.length; index += 1) {\n carry += digits[index]! << 8;\n digits[index] = carry % 58;\n carry = Math.floor(carry / 58);\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = Math.floor(carry / 58);\n }\n }\n let output = '';\n for (const byte of bytes) {\n if (byte !== 0) break;\n output += BASE58_ALPHABET[0]!;\n }\n for (let index = digits.length - 1; index >= 0; index -= 1) {\n output += BASE58_ALPHABET[digits[index]!]!;\n }\n return output;\n}\n\n/** `did:key` id for a raw Ed25519 public key: multicodec 0xed01 + base58btc. */\nexport function sessionDidForPublicKey(publicKey: Uint8Array): string {\n if (publicKey.length !== 32) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Ed25519 public key must be 32 bytes',\n );\n }\n const identifier = `z${base58btcEncode(\n new Uint8Array([0xed, 0x01, ...publicKey]),\n )}`;\n return `did:key:${identifier}`;\n}\n\n/** Generate a fresh Ed25519 session keypair. */\nexport function generateSessionKeypair(): NativeSessionKeypair {\n const secretKey = ed25519.utils.randomSecretKey();\n const publicKey = ed25519.getPublicKey(secretKey);\n const did = sessionDidForPublicKey(publicKey);\n const keyId = `${did}#${did.slice('did:key:'.length)}`;\n const publicJwk: NativeSessionJwk = {\n kty: 'OKP',\n crv: 'Ed25519',\n x: base64UrlEncode(publicKey),\n };\n return {\n did,\n keyId,\n publicJwk,\n privateJwk: { ...publicJwk, d: base64UrlEncode(secretKey) },\n };\n}\n\n/**\n * RFC 7638 JWK thumbprint of a session public JWK — the value the server\n * stores as `sessionJkt` and requires as the proof JWS `kid`.\n */\nexport async function sessionJktForPublicJwk(\n jwk: Pick<NativeSessionJwk, 'crv' | 'kty' | 'x'>,\n sha256Fn?: SHA256Fn,\n): Promise<string> {\n const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x });\n return base64UrlEncode(await (sha256Fn ?? sha256)(canonical));\n}\n\n/**\n * Rebuild a `NativeSessionKeypair` from a persisted private JWK (d + x).\n * Verifies that `x` is the public key belonging to `d`.\n */\nexport function sessionKeypairFromJwk(\n privateJwk: NativeSessionJwk & { d: string },\n): NativeSessionKeypair {\n const secretKey = base64UrlDecode(privateJwk.d);\n const publicKey = ed25519.getPublicKey(secretKey);\n const x = base64UrlEncode(publicKey);\n if (x !== privateJwk.x) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'session key JWK: x does not match d',\n );\n }\n const did = sessionDidForPublicKey(publicKey);\n const keyId = `${did}#${did.slice('did:key:'.length)}`;\n const publicJwk: NativeSessionJwk = { kty: 'OKP', crv: 'Ed25519', x };\n if (privateJwk.kid !== undefined) publicJwk.kid = privateJwk.kid;\n return {\n did,\n keyId,\n publicJwk,\n privateJwk,\n };\n}\n\n// ======= RFC 8414 discovery =======\n\n/**\n * Build the metadata document URL for an issuer per RFC 8414 §3.1: the\n * `/.well-known/oauth-authorization-server` prefix is inserted between the\n * host and the issuer's path component.\n */\nexport function discoveryUrlForIssuer(issuer: string): string {\n const issuerUrl = new URL(issuer);\n const path = issuerUrl.pathname.replace(/\\/+$/, '');\n return `${issuerUrl.origin}/.well-known/oauth-authorization-server${path}`;\n}\n\nconst REQUIRED_ENDPOINTS = [\n 'authorization_endpoint',\n 'token_endpoint',\n 'pushed_authorization_request_endpoint',\n 'tinycloud_delegation_renew_endpoint',\n 'tinycloud_delegation_revocation_endpoint',\n] as const;\n\n/**\n * Fetch and validate the authorization server metadata.\n *\n * Validation (plan §1.1): HTTPS issuer, `metadata.issuer === issuer`, and\n * every endpoint on the issuer's origin.\n */\nexport async function discoverOpenKeyServer(\n issuer: string,\n fetchFn: NativeFetch = fetch as unknown as NativeFetch,\n): Promise<OpenKeyServerMetadata> {\n let issuerUrl: URL;\n try {\n issuerUrl = new URL(issuer);\n } catch {\n throw new OpenKeyNativeError('SERVER', `issuer is not a URL: ${issuer}`);\n }\n if (issuerUrl.protocol !== 'https:') {\n throw new OpenKeyNativeError(\n 'SERVER',\n `issuer must use https: ${issuer}`,\n );\n }\n\n const metadataUrl = discoveryUrlForIssuer(issuer);\n let response: NativeFetchResponse;\n try {\n response = await fetchFn(metadataUrl, { method: 'GET' });\n } catch (error) {\n throw new OpenKeyNativeError(\n 'NETWORK',\n error instanceof Error ? error.message : 'Discovery request failed',\n );\n }\n if (!response.ok) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `Discovery failed: HTTP ${response.status}`,\n response.status,\n );\n }\n\n let metadata: Record<string, unknown>;\n try {\n metadata = (await response.json()) as Record<string, unknown>;\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Discovery response is not JSON',\n response.status,\n );\n }\n\n if (metadata.issuer !== issuer) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `metadata issuer \"${String(metadata.issuer)}\" does not match \"${issuer}\"`,\n );\n }\n\n const endpointErrors: string[] = [];\n const endpoints: Record<(typeof REQUIRED_ENDPOINTS)[number], string> =\n {} as never;\n for (const field of REQUIRED_ENDPOINTS) {\n const value = metadata[field];\n if (typeof value !== 'string' || value.length === 0) {\n endpointErrors.push(`missing ${field}`);\n continue;\n }\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n endpointErrors.push(`${field} is not a URL`);\n continue;\n }\n if (url.origin !== issuerUrl.origin || url.protocol !== 'https:') {\n endpointErrors.push(`${field} is not on the issuer origin`);\n continue;\n }\n endpoints[field] = value;\n }\n if (endpointErrors.length > 0) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `Invalid server metadata: ${endpointErrors.join('; ')}`,\n );\n }\n\n return {\n issuer,\n origin: issuerUrl.origin,\n authorizationEndpoint: endpoints.authorization_endpoint,\n tokenEndpoint: endpoints.token_endpoint,\n pushedAuthorizationRequestEndpoint:\n endpoints.pushed_authorization_request_endpoint,\n tinycloudDelegationRenewEndpoint:\n endpoints.tinycloud_delegation_renew_endpoint,\n tinycloudDelegationRevocationEndpoint:\n endpoints.tinycloud_delegation_revocation_endpoint,\n };\n}\n\n// ======= authorization_details =======\n\n/**\n * The exact permission array that goes on the wire: the caller's list with\n * `CAPABILITIES_READ_PERMISSION` prepended when missing. Subset validation\n * MUST run against this normalized array, not the raw caller list -\n * otherwise a server grant that includes `tinycloud.capabilities/read`\n * fails validation and a rotated refresh token is discarded.\n */\nexport function normalizeDelegationPermissions(\n permissions: NativeDelegationPermission[],\n): NativeDelegationPermission[] {\n const hasCapabilitiesRead = permissions.some(\n (permission) =>\n permission.service === CAPABILITIES_READ_PERMISSION.service &&\n permission.actions.includes(CAPABILITIES_READ_PERMISSION.actions[0]!),\n );\n return hasCapabilitiesRead\n ? permissions\n : [CAPABILITIES_READ_PERMISSION, ...permissions];\n}\n\n/**\n * Build the `authorization_details` array for a `tinycloud_delegation`\n * request. Used for both PAR and renew. The mandatory\n * `tinycloud.capabilities/read` entry (`CAPABILITIES_READ_PERMISSION`) is\n * prepended when the caller's permissions do not already include it.\n */\nexport function buildAuthorizationDetails(options: {\n sessionKey: NativeSessionKeypair;\n permissions: NativeDelegationPermission[];\n ttlSeconds?: number;\n siweNonce?: string;\n}): TinyCloudDelegationRequest[] {\n const detail: TinyCloudDelegationRequest = {\n type: 'tinycloud_delegation',\n session_key: options.sessionKey.publicJwk,\n permissions: normalizeDelegationPermissions(options.permissions),\n };\n if (options.ttlSeconds !== undefined) detail.ttl_seconds = options.ttlSeconds;\n if (options.siweNonce !== undefined) detail.siwe_nonce = options.siweNonce;\n return [detail];\n}\n\n// ======= PAR =======\n\nexport interface BuildParRequestOptions {\n clientId: string;\n redirectUri: string;\n state: string;\n codeChallenge: string;\n sessionKey: NativeSessionKeypair;\n permissions: NativeDelegationPermission[];\n ttlSeconds?: number;\n siweNonce?: string;\n loginHint?: string;\n loginMethod?: 'email' | 'passkey' | 'google';\n /** Extra scopes appended to `openid offline_access tinycloud:delegation`. */\n extraScopes?: string[];\n}\n\n/**\n * Build the form-encoded PAR body (spec: \"Pushed authorization request\").\n * The caller POSTs it to `metadata.pushedAuthorizationRequestEndpoint` —\n * the client cannot choose the host.\n */\nexport function buildParRequest(options: BuildParRequestOptions): {\n body: string;\n contentType: string;\n} {\n const scopes = options.extraScopes\n ? [...DEFAULT_DELEGATION_SCOPES, ...options.extraScopes]\n : DEFAULT_DELEGATION_SCOPES;\n\n const form = new URLSearchParams();\n form.set('client_id', options.clientId);\n form.set('response_type', 'code');\n form.set('redirect_uri', options.redirectUri);\n form.set('state', options.state);\n form.set('code_challenge', options.codeChallenge);\n form.set('code_challenge_method', 'S256');\n form.set('scope', scopes.join(' '));\n form.set(\n 'authorization_details',\n JSON.stringify(buildAuthorizationDetails(options)),\n );\n if (options.loginMethod !== undefined && !['email', 'passkey', 'google'].includes(options.loginMethod)) {\n throw new OpenKeyNativeError('INVALID_REQUEST', 'Invalid login_method');\n }\n if (options.loginHint !== undefined) {\n const hint = options.loginHint.trim();\n if (hint.length > 254 || /[\\s\\p{C}]/u.test(hint) || (hint.match(/@/g) ?? []).length !== 1 ||\n hint.startsWith('@') || hint.endsWith('@') || options.loginMethod === 'passkey' || options.loginMethod === 'google') {\n throw new OpenKeyNativeError('INVALID_REQUEST', 'Invalid login_hint');\n }\n form.set('login_hint', hint.toLowerCase());\n }\n if (options.loginMethod !== undefined) form.set('login_method', options.loginMethod);\n\n return {\n body: form.toString(),\n contentType: 'application/x-www-form-urlencoded',\n };\n}\n\nexport interface PushedAuthorizationResponse {\n requestUri: string;\n expiresIn: number;\n}\n\n/**\n * POST the PAR body and validate the `201` response.\n */\nexport async function sendParRequest(\n metadata: OpenKeyServerMetadata,\n options: BuildParRequestOptions,\n fetchFn: NativeFetch = fetch as unknown as NativeFetch,\n): Promise<PushedAuthorizationResponse> {\n const { body, contentType } = buildParRequest(options);\n const response = await postForm(\n metadata.pushedAuthorizationRequestEndpoint,\n body,\n contentType,\n fetchFn,\n );\n if (!response.ok) await throwEndpointError(response, 'PAR');\n const data = await readJson(response, 'PAR');\n const requestUri = data.request_uri;\n const expiresIn = data.expires_in;\n if (typeof requestUri !== 'string' || requestUri.length === 0) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'PAR response missing request_uri',\n response.status,\n );\n }\n return {\n requestUri,\n expiresIn: typeof expiresIn === 'number' ? expiresIn : 90,\n };\n}\n\n// ======= Authorize URL =======\n\n/**\n * Build the authorization URL the app opens in ASWebAuthenticationSession /\n * a Custom Tab: `authorization_endpoint?client_id&request_uri`.\n */\nexport function buildNativeAuthorizeUrl(options: {\n authorizationEndpoint: string;\n clientId: string;\n requestUri: string;\n}): string {\n const url = new URL(options.authorizationEndpoint);\n url.searchParams.set('client_id', options.clientId);\n url.searchParams.set('request_uri', options.requestUri);\n return url.toString();\n}\n\n// ======= Callback =======\n\nexport interface NativeCallbackResult {\n code: string;\n state: string;\n /** RFC 9207 issuer from the callback; verified equal to `issuer`. */\n iss: string;\n}\n\n/**\n * Parse the redirect callback URL.\n *\n * `iss` must equal the issuer (RFC 9207; the provider sends it on every\n * redirect, so a missing `iss` also fails) and `state` must match.\n * `error=access_denied` maps to `ACCESS_DENIED`; a state mismatch to\n * `STATE_MISMATCH`; a missing or wrong `iss` to `STATE_MISMATCH` too.\n */\nexport function parseNativeCallback(options: {\n url: string;\n expectedState: string;\n issuer: string;\n}): NativeCallbackResult {\n let parsed: URL;\n try {\n parsed = new URL(options.url);\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n // Never echo the callback URL - it may carry code/state.\n 'Callback URL is not parseable',\n );\n }\n\n let params = parsed.searchParams;\n // Some redirect handlers deliver the response in the fragment.\n if (!params.has('code') && !params.has('error') && parsed.hash.length > 1) {\n const fragment = parsed.hash.startsWith('#')\n ? parsed.hash.slice(1)\n : parsed.hash;\n params = new URLSearchParams(fragment);\n }\n\n const iss = params.get('iss');\n if (!iss || iss !== options.issuer) {\n throw new OpenKeyNativeError(\n 'STATE_MISMATCH',\n // Fixed message: iss is attacker-controlled and the URL carries\n // the auth code.\n 'Callback iss does not match the issuer',\n );\n }\n\n const state = params.get('state');\n if (!state || state !== options.expectedState) {\n throw new OpenKeyNativeError(\n 'STATE_MISMATCH',\n 'Callback state does not match the pending request',\n );\n }\n\n const error = params.get('error');\n if (error) {\n const description = params.get('error_description') ?? undefined;\n throw mapOAuthError(error, description);\n }\n\n const code = params.get('code');\n if (!code) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Callback has neither code nor error',\n );\n }\n\n return { code, state, iss };\n}\n\n/** Map an OAuth `error` value (callback or JSON error body) to a typed error. */\nfunction mapOAuthError(\n error: string,\n description: string | undefined,\n status?: number,\n retryAfterSeconds?: number,\n): OpenKeyNativeError {\n const message = description ? `${error}: ${description}` : error;\n switch (error) {\n case 'access_denied':\n return new OpenKeyNativeError('ACCESS_DENIED', message, status, error);\n case 'consent_required':\n return new OpenKeyNativeError('CONSENT_REQUIRED', message, status, error);\n case 'invalid_grant':\n case 'invalid_session_proof':\n // Terminal, never retried (spec SDK mapping).\n return new OpenKeyNativeError('INVALID_GRANT', message, status, error);\n case 'unauthorized':\n return new OpenKeyNativeError('NOT_SIGNED_IN', message, status, error);\n case 'renewal_conflict':\n return new OpenKeyNativeError('RENEWAL_CONFLICT', message, status, error);\n case 'space_unavailable':\n return new OpenKeyNativeError('SPACE_UNAVAILABLE', message, status, error);\n case 'renewal_too_soon':\n return new OpenKeyNativeError(\n 'RENEWAL_TOO_SOON',\n message,\n status,\n error,\n retryAfterSeconds,\n );\n case 'temporarily_unavailable':\n return new OpenKeyNativeError(\n 'TEMPORARILY_UNAVAILABLE',\n message,\n status,\n error,\n retryAfterSeconds,\n );\n default:\n return new OpenKeyNativeError('SERVER', message, status, error);\n }\n}\n\n// ======= Session proof JWS =======\n\nexport interface SessionProofOptions {\n sessionKey: NativeSessionKeypair;\n /** HTTP method of the request this proof authenticates (`htm`). */\n htm: string;\n /** Exact endpoint URL from metadata (`htu`). */\n htu: string;\n clientId: string;\n /** The credential being presented; hashed into `cred_hash`. */\n credential: string;\n /** For runtimes without Web Crypto. */\n sha256Fn?: SHA256Fn;\n}\n\n/**\n * Sign an `OpenKey-Session-Proof` compact JWS.\n *\n * Header: `{typ: \"openkey-session-proof+jwt\", alg: \"EdDSA\", kid}` where `kid`\n * is the RFC 7638 JWK thumbprint of the session public JWK — the value the\n * server stores as `sessionJkt` at PAR.\n * Payload: exactly `{jti, iat, htm, htu, client_id, cred_hash}` where\n * `cred_hash` is `b64url(sha256(credential))`.\n */\nexport async function signSessionProof(\n options: SessionProofOptions,\n): Promise<string> {\n const hash = options.sha256Fn ?? sha256;\n const header = {\n typ: SESSION_PROOF_TYP,\n alg: 'EdDSA',\n kid: await sessionJktForPublicJwk(options.sessionKey.publicJwk, hash),\n };\n const payload = {\n jti: generateNonce(),\n iat: Math.floor(Date.now() / 1000),\n htm: options.htm,\n htu: options.htu,\n client_id: options.clientId,\n cred_hash: base64UrlEncode(await hash(options.credential)),\n };\n\n const encoder = new TextEncoder();\n const signingInput = `${base64UrlEncode(\n encoder.encode(JSON.stringify(header)),\n )}.${base64UrlEncode(encoder.encode(JSON.stringify(payload)))}`;\n\n const secretKey = base64UrlDecode(options.sessionKey.privateJwk.d);\n const signature = ed25519.sign(encoder.encode(signingInput), secretKey);\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\n// ======= Token response validation =======\n\nfunction parseExpiresAt(value: string | number, field: string): number {\n if (typeof value === 'number') {\n // Seconds vs milliseconds heuristic: seconds values past ~2286 exceed 1e11.\n // Non-finite and non-positive values are malformed - reject (NaN <= now is\n // false and would otherwise pass the future check).\n if (!Number.isFinite(value) || value <= 0) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.${field} is not a valid epoch or ISO time`,\n );\n }\n return value > 1e11 ? value : value * 1000;\n }\n if (typeof value === 'string') {\n const parsed = Date.parse(value);\n if (!Number.isNaN(parsed)) return parsed;\n }\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.${field} is not a valid epoch or ISO time`,\n );\n}\n\n/** True when `grantedPath` is covered by `requestedPath` (prefix grants end in `/`). */\nfunction pathCoveredBy(\n grantedPath: string | undefined,\n requestedPath: string | undefined,\n): boolean {\n if (requestedPath === undefined || requestedPath === '') return true;\n if (grantedPath === undefined || grantedPath === '') return false;\n if (requestedPath.endsWith('/')) return grantedPath.startsWith(requestedPath);\n return grantedPath === requestedPath;\n}\n\n/** True when every granted permission is covered by a requested permission. */\nexport function isPermissionSubset(\n granted: NativeDelegationPermission[],\n requested: NativeDelegationPermission[],\n): boolean {\n return granted.every((grant) =>\n requested.some(\n (request) =>\n request.service === grant.service &&\n request.space === grant.space &&\n pathCoveredBy(grant.path, request.path) &&\n grant.actions.every((action) => request.actions.includes(action)),\n ),\n );\n}\n\nfunction isPermission(value: unknown): value is NativeDelegationPermission {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const permission = value as Record<string, unknown>;\n return (\n typeof permission.service === 'string' &&\n typeof permission.space === 'string' &&\n (permission.path === undefined ||\n typeof permission.path === 'string') &&\n Array.isArray(permission.actions) &&\n permission.actions.every((action) => typeof action === 'string')\n );\n}\n\nexport interface ValidateDelegationOptions {\n sessionKey: NativeSessionKeypair;\n /** The permission set sent in `authorization_details`, for the subset check. */\n requestedPermissions: NativeDelegationPermission[];\n /**\n * The TinyCloud node the client expects (spec: `tinycloudHost` must be\n * \"the host it expects\"). Required: validation fails closed without it.\n * Exact-match against `delegation.tinycloudHost`.\n */\n expectedTinycloudHost: string;\n}\n\n/**\n * Validate the `tinycloud_delegation` object from a token or renew response:\n * `verificationMethod` equals the session DID, `expiresAt` is in the future,\n * permissions are a subset of what was requested, and `tinycloudHost` is an\n * https URL. Throws `OpenKeyNativeError` (code `SERVER`) on any violation.\n */\nexport function validateTinyCloudDelegation(\n value: unknown,\n options: ValidateDelegationOptions,\n): TinyCloudDelegation {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'response missing tinycloud_delegation',\n );\n }\n const delegation = value as TinyCloudDelegation;\n\n if (delegation.verificationMethod !== options.sessionKey.keyId) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `verificationMethod \"${String(\n delegation.verificationMethod,\n )}\" does not match session key \"${options.sessionKey.keyId}\"`,\n );\n }\n\n if (delegation.expiresAt === undefined || delegation.expiresAt === null) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.expiresAt is missing',\n );\n }\n const expiresAt = parseExpiresAt(delegation.expiresAt, 'expiresAt');\n if (expiresAt <= Date.now()) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation is already expired',\n );\n }\n\n if (!Array.isArray(delegation.permissions)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.permissions is missing',\n );\n }\n if (!delegation.permissions.every(isPermission)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.permissions has an invalid entry',\n );\n }\n if (\n !isPermissionSubset(delegation.permissions, options.requestedPermissions)\n ) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'granted permissions are not a subset of the requested permissions',\n );\n }\n\n if (\n typeof delegation.tinycloudHost !== 'string' ||\n delegation.tinycloudHost.length === 0\n ) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.tinycloudHost is missing',\n );\n }\n let host: URL;\n try {\n host = new URL(delegation.tinycloudHost);\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.tinycloudHost is not a URL',\n );\n }\n if (host.protocol !== 'https:' && host.hostname !== 'localhost') {\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.tinycloudHost must be https: ${delegation.tinycloudHost}`,\n );\n }\n if (\n typeof options.expectedTinycloudHost !== 'string' ||\n options.expectedTinycloudHost.length === 0\n ) {\n // Fail closed: the host check must not silently vanish.\n throw new OpenKeyNativeError(\n 'SERVER',\n 'expectedTinycloudHost is required to validate tinycloud_delegation',\n );\n }\n if (delegation.tinycloudHost !== options.expectedTinycloudHost) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.tinycloudHost \"${delegation.tinycloudHost}\" is not the expected host \"${options.expectedTinycloudHost}\"`,\n );\n }\n\n return delegation;\n}\n\n// ======= Endpoint clients =======\n\nasync function postForm(\n url: string,\n body: string,\n contentType: string,\n fetchFn: NativeFetch,\n headers?: Record<string, string>,\n): Promise<NativeFetchResponse> {\n try {\n return await fetchFn(url, {\n method: 'POST',\n headers: { 'Content-Type': contentType, ...headers },\n body,\n });\n } catch (error) {\n throw new OpenKeyNativeError(\n 'NETWORK',\n error instanceof Error ? error.message : 'Network request failed',\n );\n }\n}\n\nasync function readJson(\n response: NativeFetchResponse,\n what: string,\n): Promise<Record<string, unknown>> {\n let data: unknown;\n try {\n data = await response.json();\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n `${what} response is not JSON (HTTP ${response.status})`,\n response.status,\n );\n }\n if (!data || typeof data !== 'object' || Array.isArray(data)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `${what} response is not a JSON object (HTTP ${response.status})`,\n response.status,\n );\n }\n return data as Record<string, unknown>;\n}\n\n/** On a non-ok response, map the JSON `error` field, else a status error. */\nasync function throwEndpointError(\n response: NativeFetchResponse,\n what: string,\n /**\n * Extra `error` values this endpoint maps to INVALID_GRANT (spec SDK\n * table: provider code-exchange failures are terminal). Scoped to the\n * token endpoint - PAR emits `invalid_request`/`invalid_client` for\n * different causes, which stay SERVER.\n */\n invalidGrantErrors?: readonly string[],\n): Promise<never> {\n const retryAfterSeconds = parseRetryAfterSeconds(\n response.headers?.get('Retry-After'),\n );\n try {\n const data = (await response.json()) as Record<string, unknown>;\n if (typeof data.error === 'string' && data.error.length > 0) {\n const description =\n typeof data.error_description === 'string'\n ? data.error_description\n : undefined;\n if (invalidGrantErrors?.includes(data.error)) {\n throw new OpenKeyNativeError(\n 'INVALID_GRANT',\n description ? `${data.error}: ${description}` : data.error,\n response.status,\n data.error,\n retryAfterSeconds,\n );\n }\n throw mapOAuthError(\n data.error,\n description,\n response.status,\n retryAfterSeconds,\n );\n }\n } catch (error) {\n if (error instanceof OpenKeyNativeError) throw error;\n // fall through to status error\n }\n throw new OpenKeyNativeError(\n 'SERVER',\n `${what} failed: HTTP ${response.status}`,\n response.status,\n undefined,\n retryAfterSeconds,\n );\n}\n\n// ----- Token exchange -----\n\nexport interface ExchangeNativeCodeOptions {\n metadata: OpenKeyServerMetadata;\n code: string;\n redirectUri: string;\n clientId: string;\n codeVerifier: string;\n sessionKey: NativeSessionKeypair;\n requestedPermissions: NativeDelegationPermission[];\n /** Expected TinyCloud node, exact-matched against `tinycloudHost`. Required - fails closed. */\n expectedTinycloudHost: string;\n fetchFn?: NativeFetch;\n sha256Fn?: SHA256Fn;\n /** Persist the issued refresh token before delegation validation or returning it. */\n onRefreshToken?: (refreshToken: string) => Promise<void>;\n}\n\nexport interface NativeTokenResult {\n accessToken: string;\n refreshToken: string;\n /** `expires_in`: access-token lifetime (300 s per spec). */\n expiresIn?: number;\n /** `expires_at`: access-token expiry in Unix seconds, when present. */\n accessTokenExpiresAt?: number;\n /** Raw `authorization_details` echo from the server, if present. */\n authorizationDetails?: unknown;\n /** Validated `tinycloud_delegation` payload. */\n delegation: TinyCloudDelegation;\n}\n\n/**\n * Exchange an authorization code for tokens plus the TinyCloud delegation\n * (spec: \"Code exchange\"). Sends `OpenKey-Session-Proof` with `cred_hash =\n * b64url(sha256(code))` and validates the delegation in the response.\n *\n * Never retried: any failed exchange spends the code — including a 503 —\n * so errors surface to the caller, which must start a fresh sign-in.\n * A successful response with `hosting: \"failed\"` throws `SPACE_UNAVAILABLE`.\n */\nexport async function exchangeDelegationCode(\n options: ExchangeNativeCodeOptions,\n): Promise<NativeTokenResult> {\n const fetchFn =\n options.fetchFn ?? (fetch as unknown as NativeFetch);\n const htu = options.metadata.tokenEndpoint;\n const proof = await signSessionProof({\n sessionKey: options.sessionKey,\n htm: 'POST',\n htu,\n clientId: options.clientId,\n credential: options.code,\n sha256Fn: options.sha256Fn,\n });\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code: options.code,\n redirect_uri: options.redirectUri,\n client_id: options.clientId,\n code_verifier: options.codeVerifier,\n });\n\n const response = await postForm(\n htu,\n body.toString(),\n 'application/x-www-form-urlencoded',\n fetchFn,\n { [SESSION_PROOF_HEADER]: proof },\n );\n if (!response.ok) {\n await throwEndpointError(response, 'Token exchange', [\n 'invalid_request',\n 'invalid_client',\n 'invalid_verification',\n ]);\n }\n\n const data = await readJson(response, 'Token exchange');\n const issuedRefreshToken = typeof data.refresh_token === 'string' ? data.refresh_token : undefined;\n if (issuedRefreshToken) await options.onRefreshToken?.(issuedRefreshToken);\n if (\n typeof data.access_token !== 'string' ||\n typeof data.refresh_token !== 'string'\n ) {\n const error = new OpenKeyNativeError(\n 'SERVER',\n 'Token response missing access_token or refresh_token',\n response.status,\n );\n error.rotatedRefreshToken = issuedRefreshToken;\n throw error;\n }\n\n let delegation: TinyCloudDelegation;\n try {\n delegation = validateTinyCloudDelegation(\n data.tinycloud_delegation,\n {\n sessionKey: options.sessionKey,\n // Same normalized array sent on PAR - includes capabilities/read.\n requestedPermissions: normalizeDelegationPermissions(\n options.requestedPermissions,\n ),\n expectedTinycloudHost: options.expectedTinycloudHost,\n },\n );\n // hosting: \"failed\" means the delegation can't reach the space:\n // terminal for this session (spec SDK mapping -> SPACE_UNAVAILABLE).\n if (delegation.hosting === 'failed') {\n throw new OpenKeyNativeError(\n 'SPACE_UNAVAILABLE',\n 'Delegation was issued but hosting the applications space failed',\n response.status,\n );\n }\n } catch (error) {\n // The code was exchanged: tokens are live server-side. Surface the\n // refresh token so the caller can persist it rather than lose the\n // session (it is valid even though this delegation was rejected).\n if (error instanceof OpenKeyNativeError) {\n error.rotatedRefreshToken = data.refresh_token;\n }\n throw error;\n }\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresIn:\n typeof data.expires_in === 'number' ? data.expires_in : undefined,\n accessTokenExpiresAt:\n typeof data.expires_at === 'number' ? data.expires_at : undefined,\n authorizationDetails: data.authorization_details,\n delegation,\n };\n}\n\n// ----- Renew -----\n\nexport interface RenewDelegationOptions {\n metadata: OpenKeyServerMetadata;\n clientId: string;\n refreshToken: string;\n sessionKey: NativeSessionKeypair;\n /** Permissions granted on the last delegation; used for the subset check. */\n requestedPermissions: NativeDelegationPermission[];\n /** Optional `siwe_nonce` form field (PAR rules: `[A-Za-z0-9]{8,64}`). */\n siweNonce?: string;\n /**\n * Optional permission subset sent as the `authorization_details` form\n * field (PAR shape, same session key, no `ttl_seconds`).\n */\n permissionsSubset?: NativeDelegationPermission[];\n /** Expected TinyCloud node, exact-matched against `tinycloudHost`. Required - fails closed. */\n expectedTinycloudHost: string;\n fetchFn?: NativeFetch;\n sha256Fn?: SHA256Fn;\n /** Injectable sleep for the spec-mandated `Retry-After` waits. */\n sleepFn?: SleepFn;\n}\n\nexport interface RenewDelegationResult {\n refreshToken: string;\n expiresIn?: number;\n delegation: TinyCloudDelegation;\n}\n\n/**\n * Renew a delegation (spec: \"Renewal\"): `POST\n * {metadata.tinycloudDelegationRenewEndpoint}` as form-encoded\n * `client_id=…&refresh_token=…[&siwe_nonce=…][&authorization_details=…]`\n * with `OpenKey-Session-Proof` where `cred_hash =\n * b64url(sha256(refresh_token))`.\n *\n * Per the spec's SDK mapping: on 429 `renewal_too_soon` waits\n * `Retry-After` seconds and retries once with the same token (no generic\n * backoff); on 503 `temporarily_unavailable` waits `Retry-After` and\n * retries once with a fresh proof. `invalid_session_proof`,\n * `invalid_grant`, `consent_required` and `access_denied` are terminal and\n * never retried. `hosting: \"failed\"` throws `SPACE_UNAVAILABLE`.\n *\n * The server rotates the refresh token on every success. If validation of\n * the returned delegation fails afterwards, the thrown error carries\n * `rotatedRefreshToken`: the old token is dead, so the caller MUST persist\n * it before reporting the error or the session is lost.\n */\nexport async function renewDelegation(\n options: RenewDelegationOptions,\n): Promise<RenewDelegationResult> {\n const fetchFn =\n options.fetchFn ?? (fetch as unknown as NativeFetch);\n const sleep = options.sleepFn ?? defaultSleep;\n const htu = options.metadata.tinycloudDelegationRenewEndpoint;\n\n // One normalized set: the exact array sent on the wire (including the\n // prepended capabilities/read) is what the grant is subset-checked\n // against - otherwise a 2xx renew that already rotated the refresh\n // token is rejected and the new token is lost.\n const requestedPermissions = normalizeDelegationPermissions(\n options.permissionsSubset ?? options.requestedPermissions,\n );\n // ttl_seconds; siwe_nonce travels as its own form field.\n const authorizationDetails = options.permissionsSubset\n ? buildAuthorizationDetails({\n sessionKey: options.sessionKey,\n permissions: options.permissionsSubset,\n })\n : undefined;\n\n const body = new URLSearchParams({\n client_id: options.clientId,\n refresh_token: options.refreshToken,\n });\n if (options.siweNonce !== undefined) {\n body.set('siwe_nonce', options.siweNonce);\n }\n if (authorizationDetails !== undefined) {\n body.set('authorization_details', JSON.stringify(authorizationDetails));\n }\n\n let retriedTooSoon = false;\n let retriedUnavailable = false;\n let response: NativeFetchResponse;\n for (;;) {\n // Fresh proof per attempt: `iat` must be within ±60 s.\n response = await postForm(\n htu,\n body.toString(),\n 'application/x-www-form-urlencoded',\n fetchFn,\n {\n [SESSION_PROOF_HEADER]: await signSessionProof({\n sessionKey: options.sessionKey,\n htm: 'POST',\n htu,\n clientId: options.clientId,\n credential: options.refreshToken,\n sha256Fn: options.sha256Fn,\n }),\n },\n );\n if (response.ok) break;\n\n let thrown: unknown;\n try {\n await throwEndpointError(response, 'Renew');\n } catch (caught) {\n thrown = caught;\n }\n if (!(thrown instanceof OpenKeyNativeError)) throw thrown;\n\n if (thrown.code === 'RENEWAL_TOO_SOON' && !retriedTooSoon) {\n retriedTooSoon = true;\n // Wait Retry-After seconds; the spec guarantees ≥ 1.\n await sleep(Math.max(thrown.retryAfterSeconds ?? 1, 1) * 1000);\n continue;\n }\n if (thrown.code === 'TEMPORARILY_UNAVAILABLE' && !retriedUnavailable) {\n retriedUnavailable = true;\n await sleep(Math.max(thrown.retryAfterSeconds ?? 2, 1) * 1000);\n continue;\n }\n throw thrown;\n }\n\n const data = await readJson(response, 'Renew');\n if (typeof data.refresh_token !== 'string') {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Renew response missing refresh_token',\n response.status,\n );\n }\n\n let delegation: TinyCloudDelegation;\n try {\n delegation = validateTinyCloudDelegation(data.tinycloud_delegation, {\n sessionKey: options.sessionKey,\n requestedPermissions,\n expectedTinycloudHost: options.expectedTinycloudHost,\n });\n if (delegation.hosting === 'failed') {\n throw new OpenKeyNativeError(\n 'SPACE_UNAVAILABLE',\n 'Renewed delegation could not reach the applications space',\n response.status,\n );\n }\n } catch (error) {\n // The refresh token was already rotated: never discard it. Surface it\n // on the error so the caller can persist it before reporting.\n if (error instanceof OpenKeyNativeError) {\n error.rotatedRefreshToken = data.refresh_token;\n }\n throw error;\n }\n\n return {\n refreshToken: data.refresh_token,\n expiresIn:\n typeof data.expires_in === 'number' ? data.expires_in : undefined,\n delegation,\n };\n}\n\n// ----- Revoke -----\n\nexport interface RevokeDelegationOptions {\n metadata: OpenKeyServerMetadata;\n clientId: string;\n refreshToken: string;\n sessionKey: NativeSessionKeypair;\n fetchFn?: NativeFetch;\n sha256Fn?: SHA256Fn;\n /** Injectable sleep for the spec-mandated `Retry-After` wait. */\n sleepFn?: SleepFn;\n}\n\n/**\n * Revoke a grant (spec: \"Native revocation\"): `POST\n * {metadata.tinycloudDelegationRevocationEndpoint}` as form-encoded\n * `client_id=…&refresh_token=…` with `OpenKey-Session-Proof` where\n * `cred_hash = b64url(sha256(refresh_token))`.\n *\n * Per the spec's SDK mapping: on 503 `temporarily_unavailable` waits\n * `Retry-After` and retries once with the same token and a fresh proof.\n * `401 invalid_session_proof` (bad proof or unknown token) is terminal —\n * it maps to `INVALID_GRANT` and is never retried.\n *\n * Callers (SDK `signOut`): wipe the local session key and tokens even when\n * this rejects with a terminal error — per spec, a terminal revoke failure\n * means the server-side grant is already unusable. Core owns no storage,\n * so the wipe lives in the SDK layer.\n */\nexport async function revokeDelegation(\n options: RevokeDelegationOptions,\n): Promise<void> {\n const fetchFn =\n options.fetchFn ?? (fetch as unknown as NativeFetch);\n const sleep = options.sleepFn ?? defaultSleep;\n const htu = options.metadata.tinycloudDelegationRevocationEndpoint;\n const body = new URLSearchParams({\n client_id: options.clientId,\n refresh_token: options.refreshToken,\n }).toString();\n\n let retriedUnavailable = false;\n for (;;) {\n const response = await postForm(\n htu,\n body,\n 'application/x-www-form-urlencoded',\n fetchFn,\n {\n [SESSION_PROOF_HEADER]: await signSessionProof({\n sessionKey: options.sessionKey,\n htm: 'POST',\n htu,\n clientId: options.clientId,\n credential: options.refreshToken,\n sha256Fn: options.sha256Fn,\n }),\n },\n );\n if (response.ok) return;\n\n let thrown: unknown;\n try {\n await throwEndpointError(response, 'Revoke');\n } catch (caught) {\n thrown = caught;\n }\n if (!(thrown instanceof OpenKeyNativeError)) throw thrown;\n if (\n thrown.code === 'TEMPORARILY_UNAVAILABLE' &&\n !retriedUnavailable\n ) {\n retriedUnavailable = true;\n await sleep(Math.max(thrown.retryAfterSeconds ?? 2, 1) * 1000);\n continue;\n }\n throw thrown;\n }\n}\n\n/**\n * True when the delegation should be renewed immediately — i.e. less than\n * the spec's renewal lead time remains on `expiresAt`. The lead is\n * `min(10 min, lifetime/4)` where `lifetime` comes from `issuedAt`; when\n * `issuedAt` is absent the full 10-minute lead is used. Call this after\n * sign-in (and on boot restore) to decide whether to renew now.\n */\nexport function delegationNeedsRenewalNow(\n delegation: Pick<TinyCloudDelegation, 'expiresAt' | 'issuedAt'>,\n now: number = Date.now(),\n): boolean {\n const expiresAt = parseExpiresAt(delegation.expiresAt, 'expiresAt');\n const issuedAt =\n typeof delegation.issuedAt === 'string'\n ? Date.parse(delegation.issuedAt)\n : Number.NaN;\n const lifetimeMs =\n Number.isFinite(issuedAt) && issuedAt < expiresAt\n ? expiresAt - issuedAt\n : Number.POSITIVE_INFINITY;\n const leadMs = Math.min(10 * 60 * 1000, lifetimeMs / 4);\n return expiresAt - now < leadMs;\n}\n","// OpenKey Nostr signer - browser client for third-party apps.\n//\n// Structurally separate from the Ethereum/EIP-1193 flows in index.ts:\n// - Nostr identities are secp256k1 Schnorr (BIP-340/NIP-01), never routed\n// through the ECDSA sign/signTypedData paths above.\n// - The OpenKey session and any signing grants never leave the\n// OpenKey-origin iframe. Unlike `openkey:auth:response`, none of the\n// postMessage payloads below ever carry a sessionToken.\n// - Every iframe created here uses a canonical, non-wildcard `host` origin\n// for both `postMessage` targeting and incoming-message verification\n// (`event.origin === host && event.source === iframe.contentWindow`).\n\n/** NIP-01 unsigned event template. */\nexport interface UnsignedNostrEvent {\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n}\n\n/** A fully signed NIP-01 event, as returned by OpenKey custody signing. */\nexport interface SignedNostrEvent extends UnsignedNostrEvent {\n id: string;\n sig: string;\n}\n\n/** Public descriptor for the user's OpenKey-custodied Nostr identity. Never includes the secret key. */\nexport interface NostrIdentity {\n keyId: string;\n pubkey: string;\n npub: string;\n}\n\nexport interface NostrError {\n code: 'USER_CANCELLED' | 'TIMEOUT' | 'INTERACTION_REQUIRED' | 'UNAUTHORIZED' | 'UNKNOWN';\n message: string;\n}\n\ntype NostrFrameMessage =\n | { type: 'openkey:ready'; protocolVersion: number }\n | { type: 'openkey:close'; requestId: string; protocolVersion: number }\n | { type: 'openkey:resize'; requestId: string; protocolVersion: number; height: number }\n | { type: 'openkey:nostr:show'; requestId: string; protocolVersion: number }\n | { type: 'openkey:nostr:connect:response'; requestId: string; protocolVersion: number; success: true; keyId: string; pubkey: string; npub: string }\n | { type: 'openkey:nostr:connect:response'; requestId: string; protocolVersion: number; success: false; error: NostrError }\n | { type: 'openkey:nostr:sign:response'; requestId: string; protocolVersion: number; success: true; event: SignedNostrEvent }\n | { type: 'openkey:nostr:sign:response'; requestId: string; protocolVersion: number; success: false; error: NostrError };\n\nconst NOSTR_FRAME_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes, matches the Ethereum widget flow\nconst NOSTR_PROTOCOL_VERSION = 1;\n\nfunction newRequestId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') {\n throw new Error('Secure request-id generation is unavailable.');\n }\n const bytes = new Uint32Array(4);\n crypto.getRandomValues(bytes);\n return `nostr-${Array.from(bytes, value => value.toString(16)).join('')}`;\n}\n\n/**\n * One ephemeral iframe per request. It is mounted off-screen at ~0 size and\n * only expanded into a visible, backdrop-covered modal when the OpenKey\n * widget page explicitly asks for it (`openkey:nostr:show`) - e.g. first-time\n * identity consent, a relay-scoped connect grant, or the first kind:9 message\n * to a new client origin. A silent signing request already covered by an\n * existing grant never sends `show`, so the user sees nothing at all.\n *\n * The hidden/visible states are pure CSS toggles on a container that always\n * holds the same `<iframe>` element - the iframe itself is never removed or\n * reparented, because doing so would reload it (and drop its in-flight\n * request state) in most browsers.\n */\nclass NostrFrame {\n private root: HTMLDivElement;\n private shadow: ShadowRoot;\n private iframe: HTMLIFrameElement;\n private outer: HTMLDivElement;\n private host: string;\n private messageHandler: (event: MessageEvent) => void = () => {};\n private revealed = false;\n private requestedHeight = 400;\n private viewportHandler = () => {\n this.syncViewport();\n this.applyFrameHeight();\n };\n\n constructor(host: string, url: string) {\n this.host = host;\n this.root = document.createElement('div');\n this.shadow = this.root.attachShadow({ mode: 'closed' });\n\n const style = document.createElement('style');\n style.textContent = `\n :host{all:initial}\n .nf-outer{position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}\n .nf-outer.nf-visible{position:fixed;top:var(--nf-viewport-top,0px);left:var(--nf-viewport-left,0px);width:var(--nf-viewport-width,100vw);height:var(--nf-viewport-height,100dvh);box-sizing:border-box;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));overflow:auto;overscroll-behavior:contain;opacity:1;pointer-events:auto;background:rgba(0,0,0,0.42);z-index:999999;display:flex;align-items:center;justify-content:center}\n .nf-card{width:1px;height:1px;overflow:hidden}\n .nf-visible .nf-card{background:#fafafa;width:min(400px,100%);height:auto;max-height:100%;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);overflow:auto;flex:0 1 auto}\n iframe{border:none;width:1px;height:1px;display:block;transition:height 200ms ease}\n .nf-visible iframe{width:100%;height:400px;max-height:100%}\n @media (prefers-reduced-motion:reduce){iframe{transition:none}}\n `;\n\n this.outer = document.createElement('div');\n this.outer.className = 'nf-outer';\n this.outer.setAttribute('role', 'dialog');\n this.outer.setAttribute('aria-modal', 'true');\n this.outer.setAttribute('aria-label', 'OpenKey approval');\n this.outer.setAttribute('aria-hidden', 'true');\n\n const card = document.createElement('div');\n card.className = 'nf-card';\n\n this.iframe = document.createElement('iframe');\n this.iframe.src = url;\n this.iframe.title = 'OpenKey approval';\n this.iframe.setAttribute('allow', 'clipboard-write *; publickey-credentials-get *; publickey-credentials-create *');\n this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox');\n\n card.appendChild(this.iframe);\n this.outer.appendChild(card);\n this.shadow.appendChild(style);\n this.shadow.appendChild(this.outer);\n document.body.appendChild(this.root);\n\n this.syncViewport();\n window.addEventListener('resize', this.viewportHandler);\n window.visualViewport?.addEventListener('resize', this.viewportHandler);\n window.visualViewport?.addEventListener('scroll', this.viewportHandler);\n }\n\n onMessage(handler: (data: NostrFrameMessage) => void) {\n this.messageHandler = (event: MessageEvent) => {\n // Canonical, non-wildcard origin only; exact window-identity match.\n if (event.origin !== this.host) return;\n if (event.source !== this.iframe.contentWindow) return;\n handler(event.data as NostrFrameMessage);\n };\n window.addEventListener('message', this.messageHandler);\n }\n\n reveal() {\n if (this.revealed) return;\n this.revealed = true;\n this.syncViewport();\n this.outer.classList.add('nf-visible');\n this.outer.setAttribute('aria-hidden', 'false');\n this.applyFrameHeight();\n }\n\n resize(height: number) {\n if (!Number.isFinite(height) || height <= 0) return;\n this.requestedHeight = height;\n if (this.revealed) this.applyFrameHeight();\n }\n\n private syncViewport() {\n const viewport = window.visualViewport;\n this.outer.style.setProperty('--nf-viewport-top', `${viewport?.offsetTop ?? 0}px`);\n this.outer.style.setProperty('--nf-viewport-left', `${viewport?.offsetLeft ?? 0}px`);\n this.outer.style.setProperty('--nf-viewport-width', `${viewport?.width ?? window.innerWidth}px`);\n this.outer.style.setProperty('--nf-viewport-height', `${viewport?.height ?? window.innerHeight}px`);\n }\n\n private applyFrameHeight() {\n if (!this.revealed) return;\n const viewportHeight = window.visualViewport?.height ?? window.innerHeight;\n const max = Math.max(1, Math.floor(viewportHeight - 32));\n this.iframe.style.height = `${Math.min(this.requestedHeight, max)}px`;\n }\n\n postMessage(message: object) {\n this.iframe.contentWindow?.postMessage(message, this.host);\n }\n\n destroy() {\n window.removeEventListener('message', this.messageHandler);\n window.removeEventListener('resize', this.viewportHandler);\n window.visualViewport?.removeEventListener('resize', this.viewportHandler);\n window.visualViewport?.removeEventListener('scroll', this.viewportHandler);\n this.root.remove();\n }\n}\n\n/**\n * Browser client for the OpenKey Nostr custody + signing flow. Get an\n * instance from `OpenKey#nostr` rather than constructing this directly.\n */\nexport class OpenKeyNostr {\n constructor(\n private host: string,\n private passkeysSupported: boolean = true,\n ) {}\n\n /**\n * Get-or-create the user's OpenKey-custodied Nostr identity. Always shows\n * a visible consent card (identity creation, or - when `relayUrl` is\n * given - a relay-scoped kind:22242/NIP-42 grant for that exact relay).\n * Never resolves with the secret key; only `keyId`/`pubkey`/`npub`.\n */\n async connect(opts?: { relayUrl?: string }): Promise<NostrIdentity> {\n const url = this.widgetUrl();\n return this.run<NostrIdentity>(\n url,\n { type: 'openkey:nostr:connect:request', relayUrl: opts?.relayUrl },\n (data, resolve, reject) => {\n if (data.type !== 'openkey:nostr:connect:response') return false;\n if (data.success) resolve({ keyId: data.keyId, pubkey: data.pubkey, npub: data.npub });\n else reject(data.error);\n return true;\n },\n );\n }\n\n /**\n * Sign a NIP-01 event template with the user's OpenKey Nostr key. Silent\n * (no visible UI) when an active grant already covers this client origin\n * + key + kind (+ exact relay, for kind 22242). Otherwise shows a visible\n * consent card before signing - required for every first kind:9 message to\n * a new origin, per NIP-29 channel-message policy.\n */\n async signEvent(keyId: string, event: UnsignedNostrEvent): Promise<SignedNostrEvent> {\n const url = this.widgetUrl();\n return this.run<SignedNostrEvent>(\n url,\n { type: 'openkey:nostr:sign:request', keyId, event },\n (data, resolve, reject) => {\n if (data.type !== 'openkey:nostr:sign:response') return false;\n if (data.success) resolve(data.event);\n else reject(data.error);\n return true;\n },\n );\n }\n\n private widgetUrl(): string {\n const origin = encodeURIComponent(window.location.origin);\n const passkeysFlag = this.passkeysSupported ? '' : '&passkeys=false';\n return `${this.host}/widget/embed/nostr/approve?origin=${origin}${passkeysFlag}`;\n }\n\n private run<T>(\n url: string,\n request: object,\n onData: (data: NostrFrameMessage, resolve: (v: T) => void, reject: (e: NostrError) => void) => boolean,\n ): Promise<T> {\n return new Promise((resolve, reject) => {\n let settled = false;\n const frame = new NostrFrame(this.host, url);\n const requestId = newRequestId();\n\n const settle = (fn: () => void) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeout);\n fn();\n frame.destroy();\n };\n\n frame.onMessage((data) => {\n if (!data || typeof (data as { type?: unknown }).type !== 'string') return;\n\n if (data.type === 'openkey:ready') {\n if (data.protocolVersion !== NOSTR_PROTOCOL_VERSION) return;\n frame.postMessage({ ...request, requestId, protocolVersion: NOSTR_PROTOCOL_VERSION });\n return;\n }\n if (\n data.requestId !== requestId ||\n data.protocolVersion !== NOSTR_PROTOCOL_VERSION\n ) return;\n if (data.type === 'openkey:resize') {\n frame.resize(data.height);\n return;\n }\n if (data.type === 'openkey:nostr:show') {\n frame.reveal();\n return;\n }\n if (data.type === 'openkey:close') {\n settle(() => reject({ code: 'USER_CANCELLED', message: 'Request closed' }));\n return;\n }\n\n onData(data, (v) => settle(() => resolve(v)), (e) => settle(() => reject(e)));\n });\n\n const timeout = setTimeout(() => {\n settle(() => reject({ code: 'TIMEOUT', message: 'Nostr request timed out' }));\n }, NOSTR_FRAME_TIMEOUT_MS);\n });\n }\n}\n","// Detects when a popup cannot work, so flows fail fast instead of waiting on\n// a window that will never answer (embedded app WebViews cannot open popups).\n\nexport interface PopupError {\n code: 'POPUP_BLOCKED';\n message: string;\n /** Set when the popup is unavailable because the page runs in an embedded app WebView. */\n reason?: 'embedded-webview';\n}\n\ntype PopupEnvironment = {\n Capacitor?: { isNativePlatform?: () => boolean };\n ReactNativeWebView?: unknown;\n navigator?: { userAgent?: string };\n};\n\n/** True inside a known embedded app WebView, where `window.open` cannot show a popup. */\nexport function isEmbeddedAppWebView(env?: PopupEnvironment): boolean {\n const target = env ?? (typeof window === 'undefined' ? undefined : (window as unknown as PopupEnvironment));\n if (!target) return false;\n\n if (target.Capacitor?.isNativePlatform?.() === true) return true;\n if (target.ReactNativeWebView) return true;\n\n const userAgent = target.navigator?.userAgent ?? '';\n // Known limitation: a WKWebView in iPadOS desktop content mode can report a\n // Macintosh UA that includes `Safari/`, which this heuristic cannot tell apart\n // from real Safari. Capacitor and React Native are still caught by the\n // capability checks above, and `isUsablePopup` still fails fast when\n // `window.open` returns null or a closed window.\n // Android WebView adds a `wv` token to the platform comment.\n if (/\\bAndroid\\b[^)]*;\\s*wv\\b/.test(userAgent)) return true;\n // iOS WKWebView sends an Apple WebKit UA without the `Safari/` token that\n // Safari and the iOS browsers built on WebKit (Chrome, Firefox, Edge) add.\n if (/\\b(iPhone|iPad|iPod)\\b/.test(userAgent) && /AppleWebKit/.test(userAgent) && !/Safari\\//.test(userAgent)) {\n return true;\n }\n return false;\n}\n\n/** `window.open` returns null when blocked, and can return an already-closed window. */\nexport function isUsablePopup(popup: Window | null | undefined): popup is Window {\n return !!popup && popup.closed !== true;\n}\n\nexport function popupUnavailableError(embeddedWebView: boolean): PopupError {\n if (embeddedWebView) {\n return {\n code: 'POPUP_BLOCKED',\n message: 'Popups are not available in this embedded app WebView. Make sure the OpenKey host is allowed by frame-src, or use native sign-in.',\n reason: 'embedded-webview',\n };\n }\n return { code: 'POPUP_BLOCKED', message: 'Popup was blocked. Please allow popups for this site.' };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBA,IAAMA,IAAe;AAMd,SAASC,EAAgBC,GAA4B;AAC1D,MAAIC,IAAS,IACPC,IAAMF,EAAO;AAEnB,WAAS,IAAI,GAAG,IAAIE,GAAK,KAAK,GAAG;AAC/B,QAAMC,IAAKH,EAAO,CAAC,GACbI,IAAK,IAAI,IAAIF,IAAMF,EAAO,IAAI,CAAC,IAAK,GACpCK,IAAK,IAAI,IAAIH,IAAMF,EAAO,IAAI,CAAC,IAAK,GAEpCM,IAAWH,KAAM,KAAOC,KAAM,IAAKC;AAEzCJ,SAAUH,EAAcQ,KAAW,KAAM,EAAI,GAC7CL,KAAUH,EAAcQ,KAAW,KAAM,EAAI,GAC7CL,KAAU,IAAI,IAAIC,IAAMJ,EAAcQ,KAAW,IAAK,EAAI,IAAI,IAC9DL,KAAU,IAAI,IAAIC,IAAMJ,EAAaQ,IAAU,EAAI,IAAI;EACzD;AAGA,SAAOL,EAAO,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG;AACtD;AAkCO,SAASM,IAA+B;AAC7C,MAAMC,IAAQ,IAAI,WAAW,EAAE;AAC/B,SAAA,OAAO,gBAAgBA,CAAK,GACrBC,EAAgBD,CAAK;AAC9B;AAMA,eAAeE,EAAcC,GAAoC;AAE/D,MAAMC,IADU,IAAI,YAAY,EACX,OAAOD,CAAK,GAC3BE,IAAS,MAAM,OAAO,OAAO,OAAO,WAAWD,CAAI;AACzD,SAAO,IAAI,WAAWC,CAAM;AAC9B;AAWA,eAAsBC,EACpBC,GACAC,GACiB;AAEjB,MAAMC,IAAS,OADAD,KAAYE,GACCH,CAAQ;AACpC,SAAOI,EAAgBF,CAAM;AAC/B;AAMO,SAASG,IAAwB;AACtC,MAAMC,IAAQ,IAAI,WAAW,EAAE;AAC/B,SAAA,OAAO,gBAAgBA,CAAK,GACrBF,EAAgBE,CAAK;AAC9B;AE9FO,SAASC,EAAsBC,GAA+C;AACnF,MAAM,EACJ,MAAAC,GACA,UAAAC,GACA,aAAAC,GACA,eAAAC,GACA,OAAAC,GACA,QAAAC,GACA,eAAAC,IAAgB,6BAClB,IAAIP,GAEEQ,IAAM,IAAI,IAAID,GAAeN,CAAI;AACvC,SAAAO,EAAI,aAAa,IAAI,aAAaN,CAAQ,GAC1CM,EAAI,aAAa,IAAI,gBAAgBL,CAAW,GAChDK,EAAI,aAAa,IAAI,iBAAiB,MAAM,GAC5CA,EAAI,aAAa,IAAI,SAASF,EAAO,KAAK,GAAG,CAAC,GAC9CE,EAAI,aAAa,IAAI,SAASH,CAAK,GACnCG,EAAI,aAAa,IAAI,kBAAkBJ,CAAa,GACpDI,EAAI,aAAa,IAAI,yBAAyB,MAAM,GAE7CA,EAAI,SAAS;AACtB;;;AEGA,IAAM,yBAAyB,IAAI,KAAK;AACxC,IAAM,yBAAyB;AAE/B,SAAS,eAAuB;AAC9B,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,oBAAoB,YAAY;AACjF,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACA,QAAM,QAAQ,IAAI,YAAY,CAAC;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,SAAS,MAAM,KAAK,OAAO,WAAS,MAAM,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AACzE;AAeA,IAAM,aAAN,MAAiB;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAgD,MAAM;AAAA,EAAC;AAAA,EACvD,WAAW;AAAA,EACX,kBAAkB;AAAA,EAClB,kBAAkB,MAAM;AAC9B,SAAK,aAAa;AAClB,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,YAAY,MAAc,KAAa;AACrC,SAAK,OAAO;AACZ,SAAK,OAAO,SAAS,cAAc,KAAK;AACxC,SAAK,SAAS,KAAK,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvD,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWpB,SAAK,QAAQ,SAAS,cAAc,KAAK;AACzC,SAAK,MAAM,YAAY;AACvB,SAAK,MAAM,aAAa,QAAQ,QAAQ;AACxC,SAAK,MAAM,aAAa,cAAc,MAAM;AAC5C,SAAK,MAAM,aAAa,cAAc,kBAAkB;AACxD,SAAK,MAAM,aAAa,eAAe,MAAM;AAE7C,UAAM,OAAO,SAAS,cAAc,KAAK;AACzC,SAAK,YAAY;AAEjB,SAAK,SAAS,SAAS,cAAc,QAAQ;AAC7C,SAAK,OAAO,MAAM;AAClB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,aAAa,SAAS,gFAAgF;AAClH,SAAK,OAAO,aAAa,WAAW,yFAAyF;AAE7H,SAAK,YAAY,KAAK,MAAM;AAC5B,SAAK,MAAM,YAAY,IAAI;AAC3B,SAAK,OAAO,YAAY,KAAK;AAC7B,SAAK,OAAO,YAAY,KAAK,KAAK;AAClC,aAAS,KAAK,YAAY,KAAK,IAAI;AAEnC,SAAK,aAAa;AAClB,WAAO,iBAAiB,UAAU,KAAK,eAAe;AACtD,WAAO,gBAAgB,iBAAiB,UAAU,KAAK,eAAe;AACtE,WAAO,gBAAgB,iBAAiB,UAAU,KAAK,eAAe;AAAA,EACxE;AAAA,EAEA,UAAU,SAA4C;AACpD,SAAK,iBAAiB,CAAC,UAAwB;AAE7C,UAAI,MAAM,WAAW,KAAK,KAAM;AAChC,UAAI,MAAM,WAAW,KAAK,OAAO,cAAe;AAChD,cAAQ,MAAM,IAAyB;AAAA,IACzC;AACA,WAAO,iBAAiB,WAAW,KAAK,cAAc;AAAA,EACxD;AAAA,EAEA,SAAS;AACP,QAAI,KAAK,SAAU;AACnB,SAAK,WAAW;AAChB,SAAK,aAAa;AAClB,SAAK,MAAM,UAAU,IAAI,YAAY;AACrC,SAAK,MAAM,aAAa,eAAe,OAAO;AAC9C,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,OAAO,QAAgB;AACrB,QAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG;AAC7C,SAAK,kBAAkB;AACvB,QAAI,KAAK,SAAU,MAAK,iBAAiB;AAAA,EAC3C;AAAA,EAEQ,eAAe;AACrB,UAAM,WAAW,OAAO;AACxB,SAAK,MAAM,MAAM,YAAY,qBAAqB,GAAG,UAAU,aAAa,CAAC,IAAI;AACjF,SAAK,MAAM,MAAM,YAAY,sBAAsB,GAAG,UAAU,cAAc,CAAC,IAAI;AACnF,SAAK,MAAM,MAAM,YAAY,uBAAuB,GAAG,UAAU,SAAS,OAAO,UAAU,IAAI;AAC/F,SAAK,MAAM,MAAM,YAAY,wBAAwB,GAAG,UAAU,UAAU,OAAO,WAAW,IAAI;AAAA,EACpG;AAAA,EAEQ,mBAAmB;AACzB,QAAI,CAAC,KAAK,SAAU;AACpB,UAAM,iBAAiB,OAAO,gBAAgB,UAAU,OAAO;AAC/D,UAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,iBAAiB,EAAE,CAAC;AACvD,SAAK,OAAO,MAAM,SAAS,GAAG,KAAK,IAAI,KAAK,iBAAiB,GAAG,CAAC;AAAA,EACnE;AAAA,EAEA,YAAY,SAAiB;AAC3B,SAAK,OAAO,eAAe,YAAY,SAAS,KAAK,IAAI;AAAA,EAC3D;AAAA,EAEA,UAAU;AACR,WAAO,oBAAoB,WAAW,KAAK,cAAc;AACzD,WAAO,oBAAoB,UAAU,KAAK,eAAe;AACzD,WAAO,gBAAgB,oBAAoB,UAAU,KAAK,eAAe;AACzE,WAAO,gBAAgB,oBAAoB,UAAU,KAAK,eAAe;AACzE,SAAK,KAAK,OAAO;AAAA,EACnB;AACF;AAMO,IAAM,eAAN,MAAmB;AAAA,EACxB,YACU,MACA,oBAA6B,MACrC;AAFQ;AACA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQH,MAAM,QAAQ,MAAsD;AAClE,UAAM,MAAM,KAAK,UAAU;AAC3B,WAAO,KAAK;AAAA,MACV;AAAA,MACA,EAAE,MAAM,iCAAiC,UAAU,MAAM,SAAS;AAAA,MAClE,CAAC,MAAM,SAAS,WAAW;AACzB,YAAI,KAAK,SAAS,iCAAkC,QAAO;AAC3D,YAAI,KAAK,QAAS,SAAQ,EAAE,OAAO,KAAK,OAAO,QAAQ,KAAK,QAAQ,MAAM,KAAK,KAAK,CAAC;AAAA,YAChF,QAAO,KAAK,KAAK;AACtB,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,UAAU,OAAe,OAAsD;AACnF,UAAM,MAAM,KAAK,UAAU;AAC3B,WAAO,KAAK;AAAA,MACV;AAAA,MACA,EAAE,MAAM,8BAA8B,OAAO,MAAM;AAAA,MACnD,CAAC,MAAM,SAAS,WAAW;AACzB,YAAI,KAAK,SAAS,8BAA+B,QAAO;AACxD,YAAI,KAAK,QAAS,SAAQ,KAAK,KAAK;AAAA,YAC/B,QAAO,KAAK,KAAK;AACtB,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,YAAoB;AAC1B,UAAM,SAAS,mBAAmB,OAAO,SAAS,MAAM;AACxD,UAAM,eAAe,KAAK,oBAAoB,KAAK;AACnD,WAAO,GAAG,KAAK,IAAI,sCAAsC,MAAM,GAAG,YAAY;AAAA,EAChF;AAAA,EAEQ,IACN,KACA,SACA,QACY;AACZ,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI,UAAU;AACd,YAAM,QAAQ,IAAI,WAAW,KAAK,MAAM,GAAG;AAC3C,YAAM,YAAY,aAAa;AAE/B,YAAM,SAAS,CAAC,OAAmB;AACjC,YAAI,QAAS;AACb,kBAAU;AACV,qBAAa,OAAO;AACpB,WAAG;AACH,cAAM,QAAQ;AAAA,MAChB;AAEA,YAAM,UAAU,CAAC,SAAS;AACxB,YAAI,CAAC,QAAQ,OAAQ,KAA4B,SAAS,SAAU;AAEpE,YAAI,KAAK,SAAS,iBAAiB;AACjC,cAAI,KAAK,oBAAoB,uBAAwB;AACrD,gBAAM,YAAY,EAAE,GAAG,SAAS,WAAW,iBAAiB,uBAAuB,CAAC;AACpF;AAAA,QACF;AACA,YACE,KAAK,cAAc,aACnB,KAAK,oBAAoB,uBACzB;AACF,YAAI,KAAK,SAAS,kBAAkB;AAClC,gBAAM,OAAO,KAAK,MAAM;AACxB;AAAA,QACF;AACA,YAAI,KAAK,SAAS,sBAAsB;AACtC,gBAAM,OAAO;AACb;AAAA,QACF;AACA,YAAI,KAAK,SAAS,iBAAiB;AACjC,iBAAO,MAAM,OAAO,EAAE,MAAM,kBAAkB,SAAS,iBAAiB,CAAC,CAAC;AAC1E;AAAA,QACF;AAEA,eAAO,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ,CAAC,CAAC,GAAG,CAAC,MAAM,OAAO,MAAM,OAAO,CAAC,CAAC,CAAC;AAAA,MAC9E,CAAC;AAED,YAAM,UAAU,WAAW,MAAM;AAC/B,eAAO,MAAM,OAAO,EAAE,MAAM,WAAW,SAAS,0BAA0B,CAAC,CAAC;AAAA,MAC9E,GAAG,sBAAsB;AAAA,IAC3B,CAAC;AAAA,EACH;AACF;;;ACxRO,SAAS,qBAAqB,KAAiC;AACpE,QAAM,SAAS,QAAQ,OAAO,WAAW,cAAc,SAAa;AACpE,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI,OAAO,WAAW,mBAAmB,MAAM,KAAM,QAAO;AAC5D,MAAI,OAAO,mBAAoB,QAAO;AAEtC,QAAM,YAAY,OAAO,WAAW,aAAa;AAOjD,MAAI,2BAA2B,KAAK,SAAS,EAAG,QAAO;AAGvD,MAAI,yBAAyB,KAAK,SAAS,KAAK,cAAc,KAAK,SAAS,KAAK,CAAC,WAAW,KAAK,SAAS,GAAG;AAC5G,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,SAAS,cAAc,OAAmD;AAC/E,SAAO,CAAC,CAAC,SAAS,MAAM,WAAW;AACrC;AAEO,SAAS,sBAAsB,iBAAsC;AAC1E,MAAI,iBAAiB;AACnB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,MACT,QAAQ;AAAA,IACV;AAAA,EACF;AACA,SAAO,EAAE,MAAM,iBAAiB,SAAS,wDAAwD;AACnG;;;ANQO,SAAS,kBAAkB,mBAAoC;AACpE,SAAO,oBAAoB,KAAK;AAClC;AAkPA,IAAM,eAAe;AACrB,IAAM,cAAc;AACpB,IAAM,eAAe;AACrB,IAAM,kBAAkB,IAAI,KAAK;AACjC,IAAM,uBAAuB;AAC7B,IAAM,oBAAoB;AAuBnB,SAAS,qBACd,UACA,UAKe;AACf,MAAI,CAAC,YAAY,OAAO,aAAa,SAAU,QAAO;AACtD,QAAM,OAAO;AACb,MAAI,KAAK,SAAS,iBAAkB,QAAO;AAC3C,MAAI,SAAS,cAAc,QAAQ,SAAS,oBAAoB,MAAM;AAGpE,QAAI,KAAK,cAAc,OAAW,QAAO;AAAA,EAC3C,OAAO;AACL,QAAI,SAAS,cAAc,QAAQ,SAAS,oBAAoB,KAAM,QAAO;AAC7E,QAAI,OAAO,KAAK,oBAAoB,YAAY,KAAK,oBAAoB,SAAS,iBAAiB;AACjG,aAAO;AAAA,IACT;AACA,QAAI,OAAO,KAAK,cAAc,YAAY,KAAK,cAAc,SAAS,WAAW;AAC/E,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,OAAO,KAAK,WAAW,YAAY,CAAC,OAAO,SAAS,KAAK,MAAM,KAAK,KAAK,UAAU,GAAG;AACxF,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM,SAAS,iBAAiB,IAAI,CAAC;AACzE;AAmBO,SAAS,sCAAsC,OAE1C;AACV,SAAO,MAAM,UAAU,YAAY;AACrC;AAyCO,IAAM,yBAAN,cAAqC,MAAM;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY,QAMT;AACD,UAAM,OAAO,OAAO;AACpB,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,eAAe,OAAO;AAC3B,SAAK,gBAAgB,OAAO;AAC5B,SAAK,iBAAiB,OAAO;AAC7B,SAAK,kBAAkB,OAAO;AAAA,EAChC;AACF;AAEO,SAAS,iCAAiC,OAInB;AAC5B,QAAM,gBAAgB,MAAM,UAAU,SAAS;AAC/C,QAAM,kBAAmB,MAAM,UAAU,WAAW;AAKpD,QAAM,kBAAkB,MAAM,gBAAgB;AAC9C,QAAM,eAAe,mBAAmB,gBAAgB,SAAS,IAAI,kBAAkB;AAEvF,QAAM,YAAY,MAAM,aAAa,CAAC;AAKtC,QAAM,aAAa,CAAC,OAA8C;AAChE,eAAW,KAAK,WAAW;AACzB,UAAI,EAAE,OAAO,GAAI,QAAO,EAAE;AAC1B,UAAI,EAAE,WAAW,EAAE,QAAQ,YAAY,MAAM,GAAG,YAAY,EAAG,QAAO,EAAE;AAAA,IAC1E;AAKA,QAAI,GAAG,WAAW,WAAW,EAAG,QAAO;AACvC,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,cAAc;AACjB,UAAM,QACJ,oBAAoB,aAAa,aAAa;AAChD,WAAO;AAAA,MACL;AAAA,MACA,eAAe;AAAA,MACf,iBAAiB;AAAA,IACnB;AAAA,EACF;AAGA,MAAI,iBAAiB,WAAW,YAAY;AAG5C,MAAI,CAAC,kBAAkB,iBAAiB,iBAAiB,eAAe;AACtE,qBAAiB;AAAA,EACnB;AAIA,MAAI,kBAAkB,mBAAmB,mBAAmB,iBAAiB;AAC3E,UAAM,IAAI,uBAAuB;AAAA,MAC/B,SAAS,qCAAqC,YAAY,gBAAgB,cAAc,2BAA2B,aAAa,OAAO,eAAe;AAAA,MACtJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAKA,QAAM,gBACJ,kBAAkB;AAEpB,SAAO;AAAA,IACL,OAAO,kBAAkB,aAAa,aAAa;AAAA,IACnD,eAAe;AAAA,IACf,iBAAiB;AAAA,EACnB;AACF;AAEA,IAAM,cAAN,MAAkB;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,oBAAmC;AAAA,EACnC,0BAAyC;AAAA,EAEjD,YAAY,MAAkG;AAC5G,SAAK,OAAO,KAAK;AACjB,SAAK,UAAU,KAAK;AACpB,SAAK,YAAY,KAAK;AAEtB,UAAM,WAAW,OAAO,WAAW,oBAAoB,EAAE;AAEzD,SAAK,OAAO,SAAS,cAAc,KAAK;AACxC,SAAK,SAAS,KAAK,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvD,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,cAAc;AAAA;AAAA,gHAEwF,WAAW,aAAa,QAAQ;AAAA,4DACpF,WAAW,SAAS,OAAO,kBAAkB,WAAW,kBAAkB,MAAM,4FAA4F,WAAW,+BAA+B,4BAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gDAM9P,WAAW,gCAAgC,4BAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASnH,UAAM,WAAW,SAAS,cAAc,KAAK;AAC7C,aAAS,YAAY;AACrB,aAAS,iBAAiB,SAAS,CAAC,MAAM;AACxC,UAAI,EAAE,WAAW,SAAU,MAAK,MAAM;AAAA,IACxC,CAAC;AAED,UAAM,OAAO,SAAS,cAAc,KAAK;AACzC,SAAK,YAAY;AAEjB,QAAI,UAAU;AACZ,YAAM,SAAS,SAAS,cAAc,KAAK;AAC3C,aAAO,YAAY;AACnB,WAAK,YAAY,MAAM;AAAA,IACzB;AAEA,UAAM,WAAW,SAAS,cAAc,QAAQ;AAChD,aAAS,YAAY;AACrB,aAAS,cAAc;AACvB,aAAS,iBAAiB,SAAS,MAAM,KAAK,MAAM,CAAC;AACrD,SAAK,YAAY,QAAQ;AAEzB,SAAK,SAAS,SAAS,cAAc,QAAQ;AAC7C,SAAK,OAAO,MAAM,KAAK;AACvB,SAAK,OAAO,aAAa,SAAS,gFAAgF;AAClH,SAAK,OAAO,aAAa,WAAW,yFAAyF;AAC7H,SAAK,YAAY,KAAK,MAAM;AAE5B,aAAS,YAAY,IAAI;AACzB,SAAK,OAAO,YAAY,KAAK;AAC7B,SAAK,OAAO,YAAY,QAAQ;AAEhC,SAAK,iBAAiB,CAAC,UAAwB;AAC7C,UAAI,MAAM,WAAW,KAAK,KAAM;AAChC,UAAI,MAAM,WAAW,KAAK,OAAO,cAAe;AAChD,YAAM,OAAO,MAAM;AACnB,UAAI,KAAK,SAAS,kBAAkB;AAMlC,cAAM,IAAI,qBAAqB,MAAM,MAAM;AAAA,UACzC,WAAW,KAAK;AAAA,UAChB,iBAAiB,KAAK;AAAA,UACtB,gBAAgB,OAAO;AAAA,QACzB,CAAC;AACD,YAAI,MAAM,KAAM;AAChB,aAAK,OAAO,MAAM,SAAS,GAAG,CAAC;AAC/B;AAAA,MACF;AAOA,UAAI,KAAK,SAAS,iBAAiB;AAAA,MAInC;AACA,WAAK,UAAU,IAAI;AAAA,IACrB;AACA,WAAO,iBAAiB,WAAW,KAAK,cAAc;AACtD,aAAS,KAAK,YAAY,KAAK,IAAI;AAAA,EACrC;AAAA,EAEA,YAAY,SAAiB;AAC3B,SAAK,OAAO,eAAe,YAAY,SAAS,KAAK,IAAI;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,uBAAuB,WAA0B,iBAAsC;AACrF,SAAK,oBAAoB;AACzB,SAAK,0BAA0B;AAAA,EACjC;AAAA,EAEA,UAAU;AACR,WAAO,oBAAoB,WAAW,KAAK,cAAc;AACzD,SAAK,KAAK,OAAO;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,UAAM,WAAW,KAAK,OAAO,cAAc,cAAc;AACzD,QAAI,UAAU;AACZ,eAAS,UAAU,IAAI,SAAS;AAChC,iBAAW,MAAM;AACf,aAAK,QAAQ;AACb,aAAK,QAAQ;AAAA,MACf,GAAG,GAAG;AAAA,IACR,OAAO;AACL,WAAK,QAAQ;AACb,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AACF;AAEA,IAAM,eAAN,MAAmB;AAAA,EACT;AAAA,EACA;AAAA,EAER,YAAY,MAAmH;AAC7H,SAAK,OAAO,SAAS,cAAc,KAAK;AACxC,SAAK,SAAS,KAAK,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvD,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBpB,UAAM,WAAW,SAAS,cAAc,KAAK;AAC7C,aAAS,YAAY;AACrB,aAAS,iBAAiB,SAAS,CAAC,MAAM;AAAE,UAAI,EAAE,WAAW,UAAU;AAAE,aAAK,QAAQ;AAAG,aAAK,SAAS;AAAA,MAAG;AAAA,IAAE,CAAC;AAE7G,UAAM,OAAO,SAAS,cAAc,KAAK;AACzC,SAAK,YAAY;AAEjB,UAAM,QAAQ,SAAS,cAAc,IAAI;AACzC,UAAM,YAAY;AAClB,UAAM,cAAc;AACpB,SAAK,YAAY,KAAK;AAEtB,QAAI,KAAK,UAAU,WAAW,GAAG;AAC/B,YAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,YAAM,YAAY;AAClB,YAAM,cAAc;AACpB,WAAK,YAAY,KAAK;AAAA,IACxB,OAAO;AACL,YAAM,OAAO,SAAS,cAAc,IAAI;AACxC,WAAK,YAAY;AACjB,iBAAW,EAAE,MAAM,SAAS,KAAK,KAAK,WAAW;AAC/C,cAAM,OAAO,SAAS,cAAc,QAAQ;AAC5C,aAAK,YAAY;AACjB,YAAI,KAAK,MAAM;AACb,gBAAM,OAAO,SAAS,cAAc,KAAK;AACzC,eAAK,YAAY;AACjB,eAAK,MAAM,KAAK;AAChB,eAAK,MAAM,KAAK;AAChB,eAAK,YAAY,IAAI;AAAA,QACvB;AACA,cAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,aAAK,cAAc,KAAK;AACxB,aAAK,YAAY,IAAI;AACrB,aAAK,iBAAiB,SAAS,MAAM;AAAE,eAAK,QAAQ;AAAG,eAAK,SAAS,QAAQ;AAAA,QAAG,CAAC;AACjF,aAAK,YAAY,IAAI;AAAA,MACvB;AACA,WAAK,YAAY,IAAI;AAAA,IACvB;AAEA,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,YAAY;AACnB,WAAO,cAAc;AACrB,WAAO,iBAAiB,SAAS,MAAM;AAAE,WAAK,QAAQ;AAAG,WAAK,SAAS;AAAA,IAAG,CAAC;AAC3E,SAAK,YAAY,MAAM;AAEvB,aAAS,YAAY,IAAI;AACzB,SAAK,OAAO,YAAY,KAAK;AAC7B,SAAK,OAAO,YAAY,QAAQ;AAChC,aAAS,KAAK,YAAY,KAAK,IAAI;AAAA,EACrC;AAAA,EAEA,UAAU;AACR,SAAK,KAAK,OAAO;AAAA,EACnB;AACF;AAEA,SAAS,UAAU,UAAU,+BAA+B,UAA4B,QAAQ;AAC9F,QAAM,KAAK,YAAY,UAAU,YAAY;AAC7C,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,QAAM,SAAS,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,SAAO,YAAY,oGAAoG,EAAE,kTAAkT,OAAO;AAClb,WAAS,KAAK,YAAY,IAAI;AAC9B,aAAW,MAAM,KAAK,OAAO,GAAG,GAAI;AACtC;AAEA,IAAM,sBAAN,cAAkC,MAAM;AAAA,EACtC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,UAAN,MAAc;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAuB;AAAA,EACvB,WAA8B;AAAA,EAC9B,sBAA+C,CAAC;AAAA;AAAA,EAEhD,0BAA0B,oBAAI,IAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ7C;AAAA,EAET,YAAY,SAAwB,CAAC,GAAG;AACtC,SAAK,SAAS;AACd,SAAK,OAAO,OAAO,QAAQ;AAC3B,SAAK,YAAY,OAAO,aAAa,KAAK,gBAAgB,KAAK,IAAI;AACnE,SAAK,UAAU,OAAO,WAAW,OAAO,SAAS;AACjD,SAAK,OAAO,OAAO,QAAQ;AAC3B,SAAK,oBAAoB,OAAO,qBAAqB;AACrD,SAAK,QAAQ,IAAI,aAAa,KAAK,MAAM,KAAK,iBAAiB;AAG/D,QAAI,OAAO,WAAW,aAAa;AACjC,WAAK,sBAAsB,CAAC;AAC5B,aAAO,iBAAiB,4BAA4B,CAAC,UAAe;AAClE,aAAK,oBAAoB,KAAK,MAAM,MAAM;AAAA,MAC5C,CAAC;AACD,aAAO,cAAc,IAAI,MAAM,yBAAyB,CAAC;AAAA,IAC3D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,gBAAgB,MAAsB;AAC5C,QAAI;AACF,YAAM,MAAM,IAAI,IAAI,IAAI;AACxB,UAAI,IAAI,aAAa,eAAe,IAAI,aAAa,aAAa;AAChE,eAAO,KAAK,QAAQ,OAAO,EAAE;AAAA,MAC/B;AACA,UAAI,WAAW,SAAS,IAAI;AAC5B,aAAO,IAAI,SAAS,EAAE,QAAQ,OAAO,EAAE;AAAA,IACzC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAc,uBACZ,cACmF;AACnF,QAAI,CAAC,aAAc,QAAO,CAAC;AAG3B,UAAM,KAAK,KAAK;AAChB,QAAI,IAAI,UAAU,aAAc,QAAO,CAAC;AACxC,QAAI,IAAI,WAAW,GAAG,QAAQ,YAAY,MAAM,aAAa,YAAY,EAAG,QAAO,CAAC;AACpF,QAAI,aAAa,WAAW,WAAW,EAAG,QAAO,CAAC;AAClD,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,SAAS,aAAa;AAAA,QACpD,QAAQ;AAAA,QACR,aAAa;AAAA,MACf,CAAC;AACD,UAAI,CAAC,IAAI,GAAI,QAAO,CAAC;AACrB,YAAM,OAAQ,MAAM,IAAI,KAAK;AAG7B,aAAO,MAAM,QAAQ,MAAM,IAAI,IAAI,KAAK,OAAO,CAAC;AAAA,IAClD,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAQ,MAAoD;AAChE,UAAM,SAAS,MAAM,KAAK,SAAqB,WAAW;AAAA,MACxD,MAAM;AAAA,MACN,SAAS,KAAK;AAAA,IAChB,GAAG,MAAM,IAAI;AACb,SAAK,WAAW;AAChB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAM,QAAQ,MAAgE;AAC5E,UAAM,YAAY,cAAc,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAGpF,UAAM,OAAO,KAAK,YAAY,MAAM,IAAI,MAAM,aAAa,UAAU,MAAM;AAI3E,SAAK,kBAAkB;AAEvB,SAAK,WAAW;AAEhB,UAAM,kBAAkB,MAAM,KAAK;AAAA,MACjC;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA,iBAAiB;AAAA,MACnB;AAAA,MACA;AAAA,IACF;AAEA,QAAI,gBAAgB,cAAc,WAAW;AAC3C,YAAM,IAAI,MAAM,qEAAqE;AAAA,IACvF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,kBAAiC;AAC/B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,0BAAsE;AACpE,WAAO;AAAA,MACL,UAAU,GAAG,KAAK,SAAS;AAAA,MAC3B,OAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAW,MAA4E;AAC3F,WAAO,KAAK,SAA6C,eAAe;AAAA,MACtE,MAAM;AAAA,IACR,GAAG,MAAM,IAAI;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,YAAY,SAA6B,MAAoD;AACjG,QAAI,KAAK,UAAU,YAAY,YAAY;AACzC,aAAO,KAAK,uBAAuB,OAAO;AAAA,IAC5C;AACA,WAAO,KAAK,gBAAgB,SAAS,MAAM,IAAI;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAM,mBACJ,SACA,MACyC;AACzC,QAAI,QAAQ,oBAAoB,GAAG;AACjC,YAAM,IAAI;AAAA,QACR,2DAA2D,QAAQ,eAAe;AAAA,MACpF;AAAA,IACF;AACA,QAAI,OAAO,QAAQ,SAAS,YAAY,CAAC,QAAQ,MAAM;AACrD,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAwCA,QAAI;AACJ,QAAI;AACF,YAAM,YAAY,MAAM,KAAK,uBAAuB,QAAQ,KAAK;AACjE,gBAAU,iCAAiC;AAAA,QACzC,UAAU,KAAK;AAAA,QACf,cAAc,QAAQ;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,GAAG;AACV,UAAI,aAAa,uBAAwB,OAAM;AAC/C,YAAM,aAAa,QACf,IACA,IAAI,MAAM,uDAAuD;AAAA,IACvE;AACA,QAAI,QAAQ,UAAU,YAAY;AAChC,aAAO,KAAK,2BAA2B,SAAS,IAAI;AAAA,IACtD;AAOA,UAAM,YAAY,WAAW,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAMjF,UAAM,gBAAgB,QAAQ,iBAAiB,QAAQ,SAAS,KAAK,UAAU;AAC/E,UAAM,MAAM,MAAM,KAAK;AAAA,MAOrB;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA,iBAAiB;AAAA,QACjB,SAAS,QAAQ;AAAA,QACjB,OAAO;AAAA;AAAA;AAAA,QAGP,KAAK,QAAQ;AAAA;AAAA;AAAA,QAGb,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOd,cAAc,QAAQ;AAAA,MACxB;AAAA,MACA,MAAM;AAAA,IACR;AAMA,QAAI,OAAO,IAAI,kBAAkB,YAAY,CAAC,IAAI,eAAe;AAC/D,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,MAAM,QAAQ,IAAI,kBAAkB,GAAG;AAC1C,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,MAAM,QAAQ,IAAI,WAAW,GAAG;AACnC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,MACL,iBAAiB;AAAA,MACjB,SAAS,IAAI;AAAA,MACb,WAAW,IAAI;AAAA,MACf,eAAe,IAAI;AAAA,MACnB,oBAAoB,IAAI;AAAA,MACxB,aAAa,IAAI;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBA,MAAc,2BACZ,SACA,MACyC;AACzC,UAAM,QAAQ,QAAQ,SAAS,KAAK,UAAU;AAC9C,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,UAAM,gBAAgB,KAAK,UAAU;AACrC,QAAI,CAAC,eAAe;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,UAAM,YAAY,UAAU,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAChF,UAAM,WAAW,MAAM,KAAK;AAAA,MAO1B;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA,iBAAiB;AAAA,QACjB,SAAS,QAAQ;AAAA,QACjB;AAAA,QACA,KAAK,QAAQ;AAAA,QACb,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,QAKd,cAAc,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOtB,cAAc;AAAA,MAChB;AAAA,MACA,MAAM;AAAA,IACR;AACA,QAAI,CAAC,UAAU,wBAAwB,CAAC,UAAU,eAAe;AAC/D,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QACE,SAAS,WACT,SAAS,QAAQ,YAAY,MAAM,cAAc,YAAY,GAC7D;AACA,YAAM,IAAI;AAAA,QACR,yDAAyD,SAAS,OAAO,oCAAoC,aAAa;AAAA,MAC5H;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,KAAK,mBAAmB,aAAa;AAC5D,UAAM,aAAa,KAAK,MAAM,SAAS,aAAa;AACpD,UAAM,kBAAmB,MAAM,SAAS,QAAQ;AAAA,MAC9C,QAAQ;AAAA,MACR,QAAQ,CAAC,YAAY,aAAa;AAAA,IACpC,CAAC;AAGD,UAAM,cAAc,MAAM;AAAA,MACxB,GAAG,KAAK,SAAS;AAAA,MACjB;AAAA,QACE,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB,2BAA2B,SAAS;AAAA,UACpC,sBAAsB,SAAS;AAAA,UAC/B,mBAAmB,SAAS;AAAA,UAC5B,iBAAiB;AAAA,UACjB,mBAAmB;AAAA,QACrB,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,CAAC,YAAY,IAAI;AACnB,YAAM,OAAO,MAAM,YAAY,KAAK,EAAE,MAAM,OAAO,EAAE,OAAO,QAAQ,YAAY,MAAM,GAAG,EAAE;AAC3F,YAAM,IAAI,MAAM,KAAK,SAAS,0BAA0B,YAAY,MAAM,GAAG;AAAA,IAC/E;AACA,UAAM,WAAW,MAAM,YAAY,KAAK;AACxC,QAAI,SAAS,kBAAkB,SAAS,eAAe;AACrD,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,MACL,iBAAiB;AAAA,MACjB,SAAS,SAAS;AAAA,MAClB,WAAW,SAAS;AAAA,MACpB,eAAe,SAAS;AAAA,MACxB,oBAAoB,SAAS,sBAAsB,CAAC;AAAA,MACpD,aAAa,SAAS,eAAe,CAAC;AAAA,IACxC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,SAA+B,MAAoD;AACrG,QAAI,KAAK,UAAU,YAAY,YAAY;AACzC,aAAO,KAAK,gCAAgC,OAAO;AAAA,IACrD;AACA,WAAO,KAAK,yBAAyB,SAAS,MAAM,IAAI;AAAA,EAC1D;AAAA,EAEQ,qBAAwC;AAC9C,UAAM,YAA+B,CAAC;AACtC,QAAI,KAAK,OAAO,kBAAkB;AAChC,gBAAU,KAAK,KAAK,OAAO,gBAAgB;AAAA,IAC7C;AACA,cAAU,KAAK,GAAG,KAAK,oBAAoB,IAAI,CAAC,EAAE,SAAS,MAAM,QAAQ,CAAC;AAC1E,QAAI,OAAO,WAAW,eAAgB,OAAe,UAAU;AAC7D,gBAAU,KAAM,OAAe,QAA2B;AAAA,IAC5D;AACA,WAAO,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC;AAAA,EAC/B;AAAA,EAEA,MAAc,mBACZ,UACA,QACA,eACkB;AAClB,QAAI;AACF,YAAM,WAAW,MAAM,SAAS,QAAQ,EAAE,OAAO,CAAC;AAClD,aAAO,SAAS,KAAK,CAAC,YAAY,QAAQ,YAAY,MAAM,cAAc,YAAY,CAAC;AAAA,IACzF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,mBAAmB,eAAiD;AACxE,UAAM,YAAY,KAAK,mBAAmB;AAE1C,eAAW,YAAY,WAAW;AAChC,UAAI,MAAM,KAAK,mBAAmB,UAAU,gBAAgB,aAAa,GAAG;AAC1E,eAAO;AAAA,MACT;AAAA,IACF;AAEA,eAAW,YAAY,WAAW;AAChC,UAAI,MAAM,KAAK,mBAAmB,UAAU,uBAAuB,aAAa,GAAG;AACjF,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,IAAI,oBAAoB,iFAAiF;AAAA,EACjH;AAAA,EAEA,MAAc,uBAAuB,SAAkD;AACrF,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,KAAK,mBAAmB,KAAK,SAAU,OAAO;AAAA,IACjE,SAAS,GAAG;AACV,gBAAW,EAAY,SAAS,OAAO;AACvC,YAAM;AAAA,IACR;AACA,QAAI;AACF,YAAM,aAAa,KAAK,MAAM,QAAQ,OAAO;AAC7C,YAAM,YAAY,MAAM,SAAS,QAAQ;AAAA,QACvC,QAAQ;AAAA,QACR,QAAQ,CAAC,YAAY,KAAK,SAAU,OAAO;AAAA,MAC7C,CAAC;AACD,aAAO,EAAE,WAAgC,SAAS,KAAK,SAAU,QAAQ;AAAA,IAC3E,SAAS,GAAQ;AACf,YAAM,MAAM,GAAG,SAAS,SAAS,qBAAqB,IAClD,sFACA,GAAG,WAAW;AAClB,gBAAU,KAAK,OAAO;AACtB,YAAM,IAAI,MAAM,GAAG;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,MAAc,gCAAgC,SAAoD;AAChG,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,KAAK,mBAAmB,KAAK,SAAU,OAAO;AAAA,IACjE,SAAS,GAAG;AACV,gBAAW,EAAY,SAAS,OAAO;AACvC,YAAM;AAAA,IACR;AACA,QAAI;AACF,YAAM,YAAY,MAAM,SAAS,QAAQ;AAAA,QACvC,QAAQ;AAAA,QACR,QAAQ,CAAC,KAAK,SAAU,SAAS,KAAK,UAAU,OAAO,CAAC;AAAA,MAC1D,CAAC;AACD,aAAO,EAAE,WAAgC,SAAS,KAAK,SAAU,QAAQ;AAAA,IAC3E,SAAS,GAAQ;AACf,YAAM,MAAM,GAAG,SAAS,SAAS,qBAAqB,IAClD,sFACA,GAAG,WAAW;AAClB,gBAAU,KAAK,OAAO;AACtB,YAAM,IAAI,MAAM,GAAG;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,gBAAgB,SAA6B,MAAyC;AAC5F,WAAO,KAAK,SAAqB,QAAQ;AAAA,MACvC,MAAM;AAAA,MACN,SAAS,QAAQ;AAAA,MACjB,OAAO,QAAQ;AAAA,IACjB,GAAG,IAAI;AAAA,EACT;AAAA,EAEQ,yBAAyB,SAA+B,MAAyC;AACvG,WAAO,KAAK,SAAqB,mBAAmB;AAAA,MAClD,MAAM;AAAA,MACN,MAAM;AAAA,IACR,GAAG,IAAI;AAAA,EACT;AAAA,EAEQ,MAAM,KAAqB;AACjC,WAAO,OAAO,MAAM,KAAK,IAAI,YAAY,EAAE,OAAO,GAAG,CAAC,EAAE,IAAI,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAAA,EAC3G;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAgC;AAEpC,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,IAAI,qBAAqB;AAAA,QACvD,aAAa;AAAA,MACf,CAAC;AACD,UAAI,CAAC,IAAI,GAAI,QAAO;AACpB,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,aAAO,CAAC,CAAC,KAAK;AAAA,IAChB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMN,SAAS,OAAO,WAA8C;AAE5D,YAAM,WAAW,EAAqB;AACtC,YAAM,YAAY,MAAM,EAAsB,QAAQ;AACtD,YAAM,QAAQ,OAAO,SAAS,EAAc;AAG5C,qBAAe;AAAA,QACb;AAAA,QACA,KAAK,UAAU,EAAE,UAAU,MAAM,CAAC;AAAA,MACpC;AAGA,YAAM,UAAU,EAAsB;AAAA,QACpC,MAAM,KAAK;AAAA,QACX,UAAU,OAAO;AAAA,QACjB,aAAa,OAAO;AAAA,QACpB,eAAe;AAAA,QACf;AAAA,QACA,QAAQ,CAAC,QAAQ;AAAA,MACnB,CAAC;AAED,aAAO,KAAK,cAAc,SAAS,OAAO,OAAO,WAAW;AAAA,IAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,cAAc,OACZ,MACA,WACgC;AAChC,YAAM,SAAS,eAAe,QAAQ,iBAAiB;AACvD,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,iDAAiD;AAAA,MACnE;AAEA,YAAM,EAAE,SAAS,IAAI,KAAK,MAAM,MAAM;AAEtC,YAAM,WAAW,MAAM,MAAM,GAAG,KAAK,SAAS,0BAA0B;AAAA,QACtE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,IAAI,gBAAgB;AAAA,UACxB,YAAY;AAAA,UACZ;AAAA,UACA,cAAc,OAAO;AAAA,UACrB,WAAW,OAAO;AAAA,UAClB,eAAe;AAAA,QACjB,CAAC;AAAA,MACH,CAAC;AAGD,qBAAe,WAAW,iBAAiB;AAE3C,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,QAAQ,MAAM,SAAS,KAAK;AAClC,cAAM,IAAI,MAAM,MAAM,qBAAqB,uBAAuB;AAAA,MACpE;AAEA,aAAO,SAAS,KAAK;AAAA,IACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,aAAa,CAAC,kBAAmC;AAC/C,YAAM,SAAS,eAAe,QAAQ,iBAAiB;AACvD,UAAI,CAAC,OAAQ,QAAO;AACpB,YAAM,EAAE,MAAM,IAAI,KAAK,MAAM,MAAM;AACnC,aAAO,UAAU;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,eAAe,CACb,QACmF;AACnF,YAAM,SAAS,IAAI,IAAI,OAAO,OAAO,SAAS,IAAI;AAClD,YAAM,QAAQ,OAAO,aAAa,IAAI,OAAO;AAE7C,UAAI,OAAO;AACT,eAAO;AAAA,UACL;AAAA,UACA,kBAAkB,OAAO,aAAa,IAAI,mBAAmB,KAAK;AAAA,QACpE;AAAA,MACF;AAEA,YAAM,OAAO,OAAO,aAAa,IAAI,MAAM;AAC3C,YAAM,QAAQ,OAAO,aAAa,IAAI,OAAO;AAE7C,UAAI,CAAC,QAAQ,CAAC,OAAO;AACnB,eAAO,EAAE,OAAO,kBAAkB,kBAAkB,kCAAkC;AAAA,MACxF;AAEA,aAAO,EAAE,MAAM,MAAM;AAAA,IACvB;AAAA,EACF;AAAA,EAEQ,cACN,KACA,OACA,aACsB;AACtB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI,KAAK,SAAS,YAAY;AAC5B,aAAK,eAAe,KAAK,OAAO,aAAa,SAAS,MAAM;AAAA,MAC9D,OAAO;AAEL,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,eACN,KACA,OACA,aACA,SACA,QACA;AACA,UAAM,OAAO,OAAO,WAAW,OAAO,aAAa,eAAe;AAClE,UAAM,MAAM,OAAO,WAAW,OAAO,cAAc,gBAAgB;AAEnE,UAAM,QAAQ,OAAO;AAAA,MACnB;AAAA,MACA;AAAA,MACA,SAAS,WAAW,WAAW,YAAY,SAAS,IAAI,QAAQ,GAAG;AAAA,IACrE;AAEA,QAAI,CAAC,cAAc,KAAK,GAAG;AACzB,WAAK,QAAQ;AACb,YAAM,WAAW,qBAAqB;AACtC,UAAI,CAAC,SAAU,WAAU,yDAAyD,OAAO;AACzF,aAAO,WACH,sBAAsB,IAAI,IAC1B,EAAE,MAAM,iBAAiB,SAAS,+DAA+D,CAAC;AACtG;AAAA,IACF;AACA,SAAK,QAAQ;AAEb,UAAM,eAAe,IAAI,IAAI,WAAW,EAAE;AAG1C,UAAM,eAAe,YAAY,MAAM;AACrC,UAAI;AACF,YAAI,KAAK,OAAO,QAAQ;AACtB,wBAAc,YAAY;AAC1B,uBAAa,OAAO;AACpB,iBAAO;AAAA,YACL,MAAM;AAAA,YACN,SAAS;AAAA,UACX,CAAC;AACD;AAAA,QACF;AAGA,cAAM,WAAW,KAAK,OAAO,UAAU;AACvC,YAAI,YAAY,SAAS,WAAW,YAAY,GAAG;AACjD,wBAAc,YAAY;AAC1B,uBAAa,OAAO;AAEpB,gBAAM,SAAS,IAAI,IAAI,QAAQ;AAC/B,gBAAM,OAAO,OAAO,aAAa,IAAI,MAAM;AAC3C,gBAAM,gBAAgB,OAAO,aAAa,IAAI,OAAO;AACrD,gBAAM,QAAQ,OAAO,aAAa,IAAI,OAAO;AAE7C,eAAK,OAAO,MAAM;AAElB,cAAI,OAAO;AACT,mBAAO;AAAA,cACL,MAAM;AAAA,cACN,SAAS,OAAO,aAAa,IAAI,mBAAmB,KAAK;AAAA,YAC3D,CAAC;AACD;AAAA,UACF;AAEA,cAAI,kBAAkB,OAAO;AAC3B,mBAAO;AAAA,cACL,MAAM;AAAA,cACN,SAAS;AAAA,YACX,CAAC;AACD;AAAA,UACF;AAEA,cAAI,MAAM;AACR,oBAAQ,EAAE,MAAM,OAAO,cAAc,CAAC;AAAA,UACxC,OAAO;AACL,mBAAO;AAAA,cACL,MAAM;AAAA,cACN,SAAS;AAAA,YACX,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,GAAG,GAAG;AAGN,UAAM,UAAU,WAAW,MAAM;AAC/B,oBAAc,YAAY;AAC1B,WAAK,OAAO,MAAM;AAClB,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,MACX,CAAC;AAAA,IACH,GAAG,eAAe;AAAA,EACpB;AAAA,EAEQ,YAAY,UAAqC;AACvD,WAAO,YAAY,KAAK;AAAA,EAC1B;AAAA,EAEQ,iBAA0B;AAChC,QAAI,KAAK,oBAAoB,SAAS,EAAG,QAAO;AAChD,QAAI,OAAO,WAAW,eAAgB,OAAe,SAAU,QAAO;AACtE,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,SAAY,QAAgB,SAAiB,cAAwC;AACjG,UAAM,OAAO,KAAK,YAAY,YAAY;AAC1C,UAAM,SAAS,mBAAmB,OAAO,SAAS,MAAM;AACxD,UAAM,UAAU,WAAW,aAAa,KAAK,eAAe,IAAI,iBAAiB;AACjF,UAAM,eAAe,kBAAkB,KAAK,iBAAiB;AAE7D,QAAI,SAAS,SAAS;AACpB,YAAMC,OAAM,GAAG,KAAK,IAAI,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO,GAAG,YAAY;AACnF,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW,KAAK,UAAU,QAAQA,MAAK,SAAS,SAAS,MAAM,CAAC;AAAA,IAC/F;AAEA,QAAI,SAAS,YAAY;AACvB,YAAMA,OAAM,GAAG,KAAK,IAAI,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO,GAAG,YAAY;AACnF,aAAO,SAAS,OAAOA;AACvB,aAAO,IAAI,QAAQ,MAAM;AAAA,MAAC,CAAC;AAAA,IAC7B;AAGA,UAAM,MAAM,GAAG,KAAK,IAAI,iBAAiB,MAAM,WAAW,MAAM,GAAG,OAAO,GAAG,YAAY;AACzF,WAAO,KAAK,gBAAmB,KAAK,QAAQ,SAAS,MAAM;AAAA,EAC7D;AAAA,EAEQ,gBAAmB,KAAa,QAAgB,SAAiB,QAA4B;AAInG,UAAM,oBAAqB,QAAoC;AAC/D,UAAM,0BAA2B,QAA0C;AAC3E,UAAM,qBACJ,OAAO,sBAAsB,YAC7B,OAAO,4BAA4B,YACnC,2BAA2B;AAC7B,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI,gBAAgB;AACpB,UAAI,UAAU;AACd,UAAI,QAA4B;AAChC,UAAI;AAEJ,YAAM,SAAS,CAAC,OAAmB;AACjC,YAAI,QAAS;AACb,kBAAU;AACV,WAAG;AAAA,MACL;AAEA,YAAM,UAAU,MAAM;AACpB,qBAAa,YAAY;AACzB,qBAAa,cAAc;AAC3B,eAAO,QAAQ;AACf,gBAAQ;AACR,aAAK,wBAAwB,OAAO,MAAM;AAAA,MAC5C;AAEA,cAAQ,IAAI,YAAY;AAAA,QACtB;AAAA,QACA,MAAM,KAAK;AAAA,QACX,SAAS,MAAM;AACb,iBAAO,MAAM;AACX,oBAAQ;AACR,mBAAO,EAAE,MAAM,kBAAkB,SAAS,6BAA6B,CAAiB;AAAA,UAC1F,CAAC;AAAA,QACH;AAAA,QACA,WAAW,CAAC,SAAsB;AAChC,cAAI,KAAK,SAAS,iBAAiB;AACjC,4BAAgB;AAChB,mBAAO,YAAY,OAAO;AAC1B;AAAA,UACF;AAKA,cAAI,oBAAoB;AACtB,kBAAM,gBAAiB,KAAiC;AACxD,kBAAM,cAAe,KAAuC;AAC5D,gBAAI,kBAAkB,kBAAmB;AACzC,gBAAI,gBAAgB,wBAAyB;AAAA,UAC/C;AAEA,cAAI,KAAK,SAAS,iBAAiB;AACjC,mBAAO,MAAM;AACX,sBAAQ;AACR,qBAAO,EAAE,MAAM,kBAAkB,SAAS,6BAA6B,CAAiB;AAAA,YAC1F,CAAC;AACD;AAAA,UACF;AAEA,cAAI,KAAK,SAAS,gCAAgC;AAChD,iBAAK,2BAA2B,KAAM;AACtC;AAAA,UACF;AAEA,cAAI,KAAK,SAAS,oCAAoC;AACpD,oBAAQ;AACR,iBAAK,4BAA+B,SAAS,MAAM;AACnD;AAAA,UACF;AAEA,cAAI,KAAK,oBAAoB,QAAQ,IAAI,GAAG;AAC1C,mBAAO,MAAM;AACX,sBAAQ;AACR,kBAAI,KAAK,SAAS;AAChB,oBAAI,KAAK,SAAS,yBAAyB;AACzC,0BAAQ,EAAE,SAAS,KAAK,SAAS,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,UAAU,CAAM;AAAA,gBAC/F,WAAW,KAAK,SAAS,gCAAgC;AACvD,0BAAQ,EAAE,SAAS,KAAK,SAAS,OAAO,KAAK,MAAM,CAAM;AAAA,gBAC3D,WAAW,KAAK,SAAS,6BAA6B;AACpD,0BAAQ,EAAE,WAAW,KAAK,WAAW,SAAS,KAAK,QAAQ,CAAM;AAAA,gBACnE,WAAW,KAAK,SAAS,gCAAgC;AACvD,0BAAQ;AAAA,oBACN,2BAA4B,KAAa;AAAA,oBACzC,sBAAuB,KAAa;AAAA,oBACpC,eAAgB,KAAa;AAAA,oBAC7B,mBAAoB,KAAa;AAAA,oBACjC,SAAU,KAAa;AAAA,kBACzB,CAAM;AAAA,gBACR,OAAO;AACL,0BAAQ;AAAA,oBACN,WAAY,KAAa;AAAA,oBACzB,SAAU,KAAa;AAAA,oBACvB,eAAgB,KAAa;AAAA,oBAC7B,oBAAqB,KAAa;AAAA,oBAClC,aAAc,KAAa;AAAA,kBAC7B,CAAM;AAAA,gBACR;AAAA,cACF,OAAO;AACL,uBAAO,KAAK,KAAK;AAAA,cACnB;AAAA,YACF,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAOD,UAAI,oBAAoB;AACtB,cAAM;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAGA,YAAM,eAAe,WAAW,MAAM;AACpC,YAAI,iBAAiB,QAAS;AAC9B,gBAAQ;AACR,YAAI,qBAAqB,GAAG;AAC1B,kBAAQ,KAAK,mIAAmI;AAChJ,iBAAO,MAAM,OAAO,sBAAsB,IAAI,CAAC,CAAC;AAChD;AAAA,QACF;AACA,gBAAQ,KAAK,sGAAsG;AACnH,kBAAU;AACV,cAAM,WAAW,GAAG,KAAK,IAAI,WAAW,MAAM,WAAW,MAAM,GAAG,kBAAkB,KAAK,iBAAiB,CAAC;AAC3G,aAAK,UAAU,QAAQ,UAAU,SAAS,CAAC,QAAa,OAAO,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,QAAa,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,MAC7H,GAAG,oBAAoB;AAGvB,YAAM,iBAAiB,WAAW,MAAM;AACtC,eAAO,MAAM;AACX,kBAAQ;AACR,iBAAO,EAAE,MAAM,WAAW,SAAS,oBAAoB,CAAiB;AAAA,QAC1E,CAAC;AAAA,MACH,GAAG,eAAe;AAElB,eAAS,MAAM;AACb,eAAO,MAAM;AACX,kBAAQ;AACR,iBAAO,EAAE,MAAM,kBAAkB,SAAS,yBAAyB,CAAiB;AAAA,QACtF,CAAC;AAAA,MACH;AACA,WAAK,wBAAwB,IAAI,MAAM;AAAA,IACzC,CAAC;AAAA,EACH;AAAA,EAEQ,oBAAoB,QAAgB,MAAkD;AAC5F,UAAM,OAAO,KAAK;AAClB,QAAI,WAAW,UAAW,QAAO,SAAS;AAC1C,QAAI,WAAW,OAAQ,QAAO,SAAS,2BAA2B,SAAS;AAC3E,QAAI,WAAW,kBAAmB,QAAO,SAAS;AAClD,QAAI,WAAW,cAAe,QAAO,SAAS;AAC9C,QAAI,WAAW,WAAY,QAAO,SAAS;AAC3C,WAAO;AAAA,EACT;AAAA,EAEQ,2BAA2B,OAAoB;AACrD,UAAM,aAAa,CAAC,WAAwB,MAAM,YAAY,MAAM;AAEpE,QAAI,aAAa;AAAA,MACf,WAAW,KAAK;AAAA,MAChB,UAAU,MAAM;AACd,mBAAW,EAAE,MAAM,8BAA8B,SAAS,OAAO,OAAO,EAAE,MAAM,kBAAkB,SAAS,2BAA2B,EAAE,CAAC;AAAA,MAC3I;AAAA,MACA,UAAU,OAAO,aAAa;AAC5B,YAAI;AACF,gBAAM,WAAW,MAAM,SAAS,QAAQ,EAAE,QAAQ,sBAAsB,CAAC;AACzE,gBAAM,UAAU,SAAS,CAAC;AAC1B,cAAI,CAAC,QAAS,OAAM,IAAI,MAAM,qBAAqB;AAGnD,gBAAM,eAAe,MAAM,MAAM,GAAG,KAAK,IAAI,4BAA4B;AAAA,YACvE,QAAQ;AAAA,YACR,aAAa;AAAA,UACf,CAAC;AACD,cAAI,CAAC,aAAa,GAAI,OAAM,IAAI,MAAM,yBAAyB;AAC/D,gBAAM,EAAE,QAAQ,IAAI,MAAM,aAAa,KAAK;AAG5C,gBAAM,aAAa,KAAK,MAAM,OAAO;AACrC,gBAAM,YAAY,MAAM,SAAS,QAAQ;AAAA,YACvC,QAAQ;AAAA,YACR,QAAQ,CAAC,YAAY,OAAO;AAAA,UAC9B,CAAC;AAGD,gBAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,kBAAkB;AAAA,YACxD,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,aAAa;AAAA,YACb,MAAM,KAAK,UAAU,EAAE,SAAS,WAAW,QAAQ,CAAC;AAAA,UACtD,CAAC;AACD,cAAI,CAAC,QAAQ,IAAI;AACf,kBAAM,MAAM,MAAM,QAAQ,KAAK;AAC/B,kBAAM,IAAI,MAAM,IAAI,SAAS,uBAAuB;AAAA,UACtD;AACA,gBAAM,EAAE,IAAI,IAAI,MAAM,QAAQ,KAAK;AAEnC,qBAAW,EAAE,MAAM,8BAA8B,SAAS,MAAM,SAAS,IAAI,SAAS,OAAO,IAAI,GAAG,CAAC;AAAA,QACvG,SAAS,GAAQ;AACf,qBAAW,EAAE,MAAM,8BAA8B,SAAS,OAAO,OAAO,EAAE,MAAM,WAAW,SAAS,EAAE,WAAW,wBAAwB,EAAE,CAAC;AAAA,QAC9I;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,4BACN,SACA,QACA;AACA,QAAI,aAAa;AAAA,MACf,WAAW,KAAK;AAAA,MAChB,UAAU,MAAM;AACd,eAAO,EAAE,MAAM,kBAAkB,SAAS,kCAAkC,CAAC;AAAA,MAC/E;AAAA,MACA,UAAU,OAAO,aAAa;AAC5B,YAAI;AACF,gBAAM,WAAW,MAAM,SAAS,QAAQ,EAAE,QAAQ,sBAAsB,CAAC;AACzE,gBAAM,UAAU,SAAS,CAAC;AAC1B,cAAI,CAAC,QAAS,OAAM,IAAI,MAAM,qBAAqB;AACnD,gBAAM,SAAS,EAAE,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,WAAoB;AACrF,eAAK,WAAW;AAChB,kBAAQ,MAAW;AAAA,QACrB,SAAS,GAAQ;AACf,iBAAO,EAAE,MAAM,WAAW,SAAS,EAAE,WAAW,2BAA2B,CAAC;AAAA,QAC9E;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,UACN,QACA,KACA,SACA,SACA,QACA;AAEA,UAAM,OAAO,OAAO,WAAW,OAAO,aAAa,eAAe;AAClE,UAAM,MAAM,OAAO,WAAW,OAAO,cAAc,gBAAgB;AAEnE,UAAM,QAAQ,OAAO;AAAA,MACnB;AAAA,MACA,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,MACzE,SAAS,WAAW,WAAW,YAAY,SAAS,IAAI,QAAQ,GAAG;AAAA,IACrE;AAEA,QAAI,CAAC,cAAc,KAAK,GAAG;AACzB,WAAK,QAAQ;AACb,YAAM,WAAW,qBAAqB;AACtC,UAAI,CAAC,SAAU,WAAU,yDAAyD,OAAO;AACzF,aAAO,sBAAsB,QAAQ,CAAC;AACtC;AAAA,IACF;AACA,SAAK,QAAQ;AAEb,QAAI,UAAU;AACd,QAAI;AACJ,UAAM,UAAU,MAAM;AACpB,aAAO,oBAAoB,WAAW,aAAa;AACnD,oBAAc,UAAU;AACxB,mBAAa,OAAO;AACpB,WAAK,wBAAwB,OAAO,MAAM;AAC1C,UAAI,KAAK,UAAU,MAAO,MAAK,QAAQ;AAAA,IACzC;AAEA,UAAM,SAAS,CAAC,OAAmB;AACjC,UAAI,QAAS;AACb,gBAAU;AACV,cAAQ;AACR,SAAG;AAAA,IACL;AAGA,UAAM,aAAa,YAAY,MAAM;AACnC,UAAI,MAAM,QAAQ;AAChB,eAAO,MAAM,OAAO;AAAA,UAClB,MAAM;AAAA,UACN,SAAS;AAAA,QACX,CAAC,CAAC;AAAA,MACJ;AAAA,IACF,GAAG,GAAG;AAGN,UAAM,UAAU,WAAW,MAAM;AAC/B,aAAO,MAAM;AACX,cAAM,MAAM;AACZ,eAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS;AAAA,QACX,CAAC;AAAA,MACH,CAAC;AAAA,IACH,GAAG,eAAe;AAGlB,UAAM,oBAAqB,QAAoC;AAC/D,UAAM,0BAA2B,QAA0C;AAC3E,UAAM,qBACJ,OAAO,sBAAsB,YAC7B,OAAO,4BAA4B,YACnC,2BAA2B;AAE7B,UAAM,gBAAgB,CAAC,UAAwB;AAC7C,UAAI,MAAM,WAAW,KAAK,KAAM;AAChC,UAAI,MAAM,WAAW,MAAO;AAE5B,YAAM,OAAO,MAAM;AAEnB,UAAI,KAAK,SAAS,iBAAiB;AAEjC,cAAM,YAAY,SAAS,KAAK,IAAI;AACpC;AAAA,MACF;AAEA,UAAI,KAAK,SAAS,iBAAiB;AAMjC,YAAI,oBAAoB;AACtB,gBAAM,iBAAkB,KAAiC;AACzD,gBAAM,eAAgB,KAAuC;AAC7D,cAAI,mBAAmB,kBAAmB;AAC1C,cAAI,iBAAiB,wBAAyB;AAAA,QAChD;AACA,eAAO,MAAM;AACX,gBAAM,MAAM;AACZ,iBAAO;AAAA,YACP,MAAM;AAAA,YACN,SAAS;AAAA,UACT,CAAC;AAAA,QACH,CAAC;AACD;AAAA,MACF;AAEA,UAAI,KAAK,SAAS,oCAAoC;AACpD,eAAO,MAAM;AACX,gBAAM,MAAM;AACZ,eAAK,4BAA+B,SAAS,MAAM;AAAA,QACrD,CAAC;AACD;AAAA,MACF;AAGA,UAAI,KAAK,oBAAoB,QAAQ,IAAI,GAAG;AAE1C,YAAI,oBAAoB;AACtB,gBAAM,gBAAiB,KAAiC;AACxD,gBAAM,cAAe,KAAuC;AAC5D,cAAI,kBAAkB,kBAAmB;AACzC,cAAI,gBAAgB,wBAAyB;AAAA,QAC/C;AACA,eAAO,MAAM;AACX,gBAAM,MAAM;AACZ,cAAI,KAAK,SAAS;AAClB,gBAAI,KAAK,SAAS,yBAAyB;AACzC,sBAAQ,EAAE,SAAS,KAAK,SAAS,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,UAAU,CAAM;AAAA,YAC/F,WAAW,KAAK,SAAS,gCAAgC;AACvD,sBAAQ,EAAE,SAAS,KAAK,SAAS,OAAO,KAAK,MAAM,CAAM;AAAA,YAC3D,WAAW,KAAK,SAAS,6BAA6B;AACpD,sBAAQ,EAAE,WAAW,KAAK,WAAW,SAAS,KAAK,QAAQ,CAAM;AAAA,YACnE,WAAW,KAAK,SAAS,gCAAgC;AAGvD,sBAAQ;AAAA,gBACN,2BAA4B,KAAa;AAAA,gBACzC,sBAAuB,KAAa;AAAA,gBACpC,eAAgB,KAAa;AAAA,gBAC7B,mBAAoB,KAAa;AAAA,gBACjC,SAAU,KAAa;AAAA,cACzB,CAAM;AAAA,YACR,OAAO;AACL,sBAAQ;AAAA,gBACN,WAAY,KAAa;AAAA,gBACzB,SAAU,KAAa;AAAA,gBACvB,eAAgB,KAAa;AAAA,gBAC7B,oBAAqB,KAAa;AAAA,gBAClC,aAAc,KAAa;AAAA,cAC7B,CAAM;AAAA,YACR;AAAA,UACA,OAAO;AACL,mBAAO,KAAK,KAAK;AAAA,UACnB;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,aAAa;AAChD,aAAS,MAAM;AACb,aAAO,MAAM;AACX,cAAM,MAAM;AACZ,eAAO,EAAE,MAAM,kBAAkB,SAAS,yBAAyB,CAAC;AAAA,MACtE,CAAC;AAAA,IACH;AACA,SAAK,wBAAwB,IAAI,MAAM;AAAA,EACzC;AAAA,EAEQ,oBAAoB;AAC1B,eAAW,UAAU,CAAC,GAAG,KAAK,uBAAuB,EAAG,QAAO;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa;AACX,SAAK,kBAAkB;AACvB,SAAK,OAAO,MAAM;AAClB,SAAK,QAAQ;AAAA,EACf;AACF;AAQO,IAAM,kBAAN,MAAiD;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAAkB,YAAwB;AACpD,SAAK,UAAU;AACf,SAAK,UAAU,WAAW;AAC1B,SAAK,QAAQ,WAAW;AACxB,SAAK,UAAU,WAAW;AAAA,EAC5B;AAAA,EAEA,MAAM,QAAQ,EAAE,QAAQ,OAAO,GAAqD;AAClF,YAAQ,QAAQ;AAAA,MACd,KAAK;AAAA,MACL,KAAK;AACH,eAAO,CAAC,KAAK,OAAO;AAAA,MAEtB,KAAK;AACH,eAAO;AAAA;AAAA,MAET,KAAK,iBAAiB;AACpB,YAAI,KAAK,YAAY,YAAY;AAC/B,cAAI;AACF,kBAAM,WAAW,MAAM,KAAK,QAAQ,mBAAmB,KAAK,OAAO;AACnE,mBAAO,MAAM,SAAS,QAAQ,EAAE,QAAQ,OAAO,CAAC;AAAA,UAClD,SAAS,GAAQ;AACf,sBAAU,GAAG,WAAW,kCAAkC,OAAO;AACjE,kBAAM;AAAA,UACR;AAAA,QACF;AAEA,cAAM,UAAU,KAAK,YAAY,OAAQ,CAAC,CAAW;AACrD,cAAM,SAAS,MAAM,KAAK,QAAQ,YAAY,EAAE,SAAS,OAAO,KAAK,MAAM,CAAC;AAC5E,eAAO,OAAO;AAAA,MAChB;AAAA,MAEA,KAAK,wBAAwB;AAC3B,YAAI,KAAK,YAAY,YAAY;AAC/B,cAAI;AACF,kBAAM,WAAW,MAAM,KAAK,QAAQ,mBAAmB,KAAK,OAAO;AACnE,mBAAO,MAAM,SAAS,QAAQ,EAAE,QAAQ,OAAO,CAAC;AAAA,UAClD,SAAS,GAAQ;AACf,sBAAU,GAAG,WAAW,kCAAkC,OAAO;AACjE,kBAAM;AAAA,UACR;AAAA,QACF;AACA,cAAM,OAAO,KAAK,MAAM,OAAQ,CAAC,CAAW;AAC5C,cAAM,SAAS,MAAM,KAAK,QAAQ,cAAc,IAAI;AACpD,eAAO,OAAO;AAAA,MAChB;AAAA,MAEA;AACE,cAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE;AAAA,IACnD;AAAA,EACF;AAAA,EAEQ,YAAY,KAAqB;AACvC,UAAM,QAAQ,IAAI;AAAA,OACf,IAAI,WAAW,IAAI,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,SAAS,EAAG,IAAI,OAAK,SAAS,GAAG,EAAE,CAAC;AAAA,IACxF;AACA,WAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AAAA,EACvC;AACF;AAGA,IAAO,gBAAQ;AAGf,IAAI,kBAAkC;AAE/B,SAAS,WAAW,QAAiC;AAC1D,MAAI,CAAC,mBAAmB,QAAQ;AAC9B,sBAAkB,IAAI,QAAQ,MAAM;AAAA,EACtC;AACA,SAAO;AACT;","names":["BASE64_CHARS","base64UrlEncode","buffer","base64","len","b0","b1","b2","triplet","generateCodeVerifier","array","base64UrlEncode","defaultSha256","input","data","digest","generateCodeChallenge","verifier","sha256Fn","digest","defaultSha256","base64UrlEncode","generateState","array","buildAuthorizationUrl","options","host","clientId","redirectUri","codeChallenge","state","scopes","authorizePath","url","url"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../core/src/pkce.ts","../../core/src/errors.ts","../../core/src/oauth.ts","../../core/src/native-delegation.ts","../src/nostr.ts","../src/popup-capability.ts"],"sourcesContent":["// OpenKey SDK - Browser client for third-party apps\n// Provides \"Sign with OpenKey\" functionality via popup or iframe\n\nimport {\n generateCodeVerifier,\n generateCodeChallenge,\n generateState,\n buildAuthorizationUrl,\n} from '@openkey/core';\nimport { OpenKeyNostr } from './nostr';\nimport { isEmbeddedAppWebView, isUsablePopup, popupUnavailableError } from './popup-capability';\n\nexport { OpenKeyNostr } from './nostr';\nexport type {\n UnsignedNostrEvent,\n SignedNostrEvent,\n NostrIdentity,\n NostrError,\n} from './nostr';\n\nexport interface EIP1193Provider {\n request(args: { method: string; params?: any[] }): Promise<any>;\n}\n\ninterface EIP6963ProviderInfo {\n uuid: string;\n name: string;\n icon: string;\n rdns: string;\n}\n\ninterface EIP6963ProviderDetail {\n info: EIP6963ProviderInfo;\n provider: EIP1193Provider;\n}\n\nexport type OpenKeyMode = 'iframe' | 'popup' | 'redirect';\n\nexport interface OpenKeyConfig {\n /** OpenKey host URL (default: https://openkey.so) */\n host?: string;\n /** OAuth API host URL (default: derived from host by prefixing 'api.') */\n oauthHost?: string;\n /** App identifier for display */\n appName?: string;\n /** UI mode: 'iframe' (default), 'popup', or 'redirect' */\n mode?: OpenKeyMode;\n /** App-provided wallet provider for external key signing */\n externalProvider?: EIP1193Provider;\n /**\n * Whether the client can use passkeys (WebAuthn). Default: true. Pass\n * false from clients where WebAuthn does not work (for example an\n * ad-hoc-signed desktop webview); the OpenKey UI then offers only email\n * and social sign-in.\n */\n passkeysSupported?: boolean;\n}\n\n/**\n * Query suffix telling the OpenKey widget that passkeys are unavailable.\n * Empty when passkeys are supported, so default URLs are unchanged.\n */\nexport function passkeysQueryFlag(passkeysSupported: boolean): string {\n return passkeysSupported ? '' : '&passkeys=false';\n}\n\nexport interface SignMessageRequest {\n message: string;\n keyId?: string; // If not provided, user selects\n}\n\nexport interface SignTypedDataRequest {\n domain: {\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: string;\n };\n types: Record<string, Array<{ name: string; type: string }>>;\n primaryType: string;\n message: Record<string, unknown>;\n keyId?: string;\n}\n\nexport interface SignResult {\n signature: string;\n address: string;\n}\n\nexport interface AuthResult {\n address: string;\n keyId: string;\n keyType: 'MANAGED' | 'EXTERNAL';\n}\n\n/**\n * Confirmation returned by {@link OpenKey.signOut}. `requestId` is generated\n * by the SDK and echoed by the OpenKey-owned widget, so callers can associate\n * the acknowledgement with the sign-out they initiated.\n */\nexport interface SignOutAcknowledgement {\n requestId: string;\n /**\n * Whether the widget confirmed that the previous bearer no longer\n * authenticates. `false` means the SDK is locally signed out, but remote\n * revocation could not be verified.\n */\n revoked: boolean;\n}\n\nexport interface OpenKeyError {\n code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';\n message: string;\n /** On `POPUP_BLOCKED`: `'embedded-webview'` when the page runs in an app WebView that cannot open popups. */\n reason?: 'embedded-webview';\n}\n\n// ======= OAuth 2.1 Types =======\n\n/** OAuth configuration for third-party apps */\nexport interface OAuthConfig {\n /** OAuth client_id (registered with OpenKey) */\n clientId: string;\n /** Redirect URI (must match registered URI) */\n redirectUri: string;\n /** State parameter for CSRF protection (auto-generated if not provided) */\n state?: string;\n}\n\n/** Result from OAuth authorization */\nexport interface OAuthResult {\n /** Authorization code (exchange for tokens) */\n code: string;\n /** State parameter (verify matches request) */\n state: string;\n}\n\n/** Response from token exchange */\nexport interface OAuthTokenResponse {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n id_token: string;\n scope: string;\n refresh_token?: string;\n}\n\n// ======= Versioned TinyCloud authorization protocol =======\n//\n// This protocol replaces the legacy `signMessage()` path for TinyCloud\n// SIWE-ReCap requests. It preserves the legacy exact-byte behavior for\n// existing callers: `signMessage()` continues to sign the caller's exact\n// bytes and OpenKey NEVER rewrites them. Callers who want to let the user\n// narrow the requested capabilities call `authorizeTinyCloud()` instead.\n//\n// The response carries `signedMessage` — the exact bytes the signature\n// verifies against. TinyCloud completes the session with these bytes, not\n// the caller's original request. See `NodeUserAuthorization` in the\n// js-sdk for the consumer side of this contract.\n\nexport interface TinyCloudAuthorizationRequestV1 {\n protocolVersion: 1;\n /** Suggested SIWE (may be edited server-side before signing). */\n siwe: string;\n /** The keyId the user picked, if the caller has one in mind. */\n keyId?: string;\n /**\n * The session-key JWK bound to `siwe`. Required so the server can\n * regenerate a narrowed SIWE tied to the SAME session key when the\n * user removes capabilities. Passing a mismatched jwk (or omitting it\n * when the widget attempts to narrow) causes the /authorize-sign call\n * to fail — never a silent fallback to the caller's exact bytes.\n */\n jwk?: Record<string, unknown>;\n /**\n * The TinyCloud host the resulting session will activate against.\n * Bound into the /authorize-sign-prepare context so /authorize-sign\n * cannot swap hosts server-side. Optional for callers that do not\n * plan to activate a delegation (they should still forward it when\n * known; the empty string is treated as \"unknown\").\n */\n host?: string;\n /** Optional presentation envelope (name, reason, manifest). */\n presentation?: CapabilityPresentationEnvelopeV1;\n}\n\nexport interface TinyCloudAuthorizationResultV1 {\n protocolVersion: 1;\n /** EIP-55 signer address. */\n address: string;\n /** Signature over `signedMessage`. */\n signature: string;\n /** The EXACT bytes the signature verifies against. Never the caller's original. */\n signedMessage: string;\n /** The action IDs the user (or default consent) selected. */\n selectedActionKeys: string[];\n /** Effective grant set after any narrowing. */\n permissions: Array<{\n service: string;\n space: string;\n path: string;\n actions: string[];\n }>;\n}\n\nexport interface CapabilityPresentationEnvelopeV1 {\n protocolVersion: 1;\n /** Human-readable requester name shown in the review. */\n displayName?: string;\n /**\n * Reason string the requester supplied for the delegation. Rendered as\n * caller-supplied (never as verified) unless a signed manifest carries\n * the same reason string.\n */\n reason?: string;\n /** Manifest ID + digest, when the app publishes a signed manifest. */\n manifestId?: string;\n manifestDigest?: string;\n /**\n * Full manifest payload(s) the caller wants surfaced in the review. The\n * receiving route validates and size-bounds this before it is used. This\n * is display-only: no metadata may expand authority beyond what the\n * ReCap already grants. Bounded to a reasonable size at the transport\n * boundary — oversized envelopes are dropped.\n */\n manifests?: Array<{\n name?: string;\n appId?: string;\n payload?: Record<string, unknown>;\n }>;\n}\n\n// Sol MAJOR-3: transport messages must carry `requestId` and\n// `protocolVersion` for the SDK to correlate concurrent flows. Legacy\n// (unversioned) messages are allowed for backward compatibility with\n// the plain signMessage path, but versioned flows (authorizeTinyCloud)\n// always carry both fields and the SDK matches them on response.\ntype MessageType =\n | { type: 'openkey:auth:request'; appName: string; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:auth:response'; success: true; address: string; keyId: string; keyType?: 'MANAGED' | 'EXTERNAL'; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:auth:response'; success: false; error: OpenKeyError; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:sign:request'; message: string; keyId?: string; requestId?: string; protocolVersion?: number; authorizationProtocolVersion?: 1 }\n | { type: 'openkey:sign:response'; success: true; signature: string; address: string; requestId?: string; protocolVersion?: number }\n | {\n type: 'openkey:sign:response';\n success: true;\n signature: string;\n address: string;\n /** Versioned: exact bytes the signature verifies against. */\n signedMessage?: string;\n /** Versioned: action IDs selected by the user. */\n selectedActionKeys?: string[];\n /** Versioned: effective grants after narrowing. */\n permissions?: Array<{\n service: string;\n space: string;\n path: string;\n actions: string[];\n }>;\n requestId?: string;\n protocolVersion?: number;\n }\n | { type: 'openkey:sign:response'; success: false; error: OpenKeyError; requestId?: string; protocolVersion?: number }\n // Sol MAJOR-3 (continuation): external-key review flow. The widget\n // renders the shared SigningApproval UI, calls /authorize-sign-prepare\n // and /authorize-sign-preview on the user's behalf, and — instead of\n // signing server-side with a managed key — hands the SDK back the\n // preview data so the SDK can invoke the user's wallet. The SDK then\n // completes /authorize-sign with the resulting `externalSignature`.\n | {\n type: 'openkey:externalSign:approve';\n success: true;\n requestId: string;\n protocolVersion: number;\n authorizationContextToken: string;\n previewApprovalToken: string;\n signedMessage: string;\n selectedActionIds: string[];\n address: string;\n }\n | {\n type: 'openkey:externalSign:approve';\n success: false;\n requestId: string;\n protocolVersion: number;\n error: OpenKeyError;\n }\n | { type: 'openkey:signTypedData:request'; data: SignTypedDataRequest; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:signTypedData:response'; success: true; signature: string; address: string; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:signTypedData:response'; success: false; error: OpenKeyError; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:link-wallet:request' }\n | { type: 'openkey:link-wallet:response'; success: true; address: string; keyId: string }\n | { type: 'openkey:link-wallet:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:link-wallet:delegate'; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:link-wallet:result'; success: true; address: string; keyId: string; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:link-wallet:result'; success: false; error: OpenKeyError; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:auth:use-external-wallet'; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:sign-out:request'; requestId: string; protocolVersion: 1 }\n | { type: 'openkey:sign-out:response'; success: true; requestId: string; protocolVersion: 1; revoked: boolean }\n | { type: 'openkey:sign-out:response'; success: false; requestId: string; protocolVersion: 1; error: OpenKeyError }\n | { type: 'openkey:resize'; height: number; requestId?: string; protocolVersion?: number; layoutId?: number }\n | { type: 'openkey:layout-applied'; height: number; requestId: string; protocolVersion: 2; layoutId: number }\n | { type: 'openkey:layout-ready'; requestId: string; protocolVersion: 2; layoutId: number }\n | { type: 'openkey:layout-invalidated'; requestId: string; protocolVersion: 2; layoutId: number }\n | { type: 'openkey:ready'; requestId?: string; protocolVersion?: number }\n | { type: 'openkey:close'; requestId?: string; protocolVersion?: number };\n\ntype WidgetResponseMessage = Extract<MessageType, { success: boolean }>;\n\nconst DEFAULT_HOST = 'https://openkey.so';\nconst POPUP_WIDTH = 400;\nconst POPUP_HEIGHT = 600;\nconst DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes\nconst IFRAME_READY_TIMEOUT = 3000; // 3 seconds\nconst OAUTH_STORAGE_KEY = 'openkey_oauth';\n\n/**\n * Sol final continuation contract requirement 5: pure validation of an\n * incoming iframe resize message envelope. Exported so unit tests can\n * exercise every rejection branch — wrong requestId, wrong version,\n * malformed height — without booting the browser runtime that\n * `IframeModal` requires.\n *\n * Two modes, chosen by the modal's bound correlation:\n * - Versioned request bound (`requestId` AND `protocolVersion` set): the\n * resize MUST carry that exact pair.\n * - No versioned request (both null — `connect`, `signMessage`, and the\n * other unversioned flows): the widget sends a plain\n * `{ type, height }`. It is accepted only when it carries no\n * `requestId`. The caller has already checked that the message came\n * from this modal's iframe (`event.source`) on the OpenKey origin\n * (TC-647).\n * A half-bound correlation (only one of the two set) drops everything.\n *\n * Returns the height clamped to 85% of the viewport when acceptable, or\n * `null` when the message must be dropped.\n */\nexport function validateIframeResize(\n incoming: unknown,\n expected: {\n requestId: string | null;\n protocolVersion: number | null;\n viewportHeight: number;\n },\n): number | null {\n if (!incoming || typeof incoming !== 'object') return null;\n const data = incoming as Record<string, unknown>;\n if (data.type !== 'openkey:resize') return null;\n if (expected.requestId === null && expected.protocolVersion === null) {\n // No versioned request on this modal: a correlated resize cannot\n // belong to it.\n if (data.requestId !== undefined) return null;\n } else {\n if (expected.requestId === null || expected.protocolVersion === null) return null;\n if (typeof data.protocolVersion !== 'number' || data.protocolVersion !== expected.protocolVersion) {\n return null;\n }\n if (typeof data.requestId !== 'string' || data.requestId !== expected.requestId) {\n return null;\n }\n }\n if (typeof data.height !== 'number' || !Number.isFinite(data.height) || data.height <= 0) {\n return null;\n }\n return Math.min(data.height, Math.floor(expected.viewportHeight * 0.85));\n}\n\n/**\n * Sol MAJOR-4 (final): pure routing helper — decides whether an\n * `authorizeTinyCloud` call should route through the external-wallet\n * preview→sign→finalize flow or the managed-key widget-signs-server-side\n * flow. Exported so unit tests can exercise every branch\n * (explicit-external + external-session, explicit-external + external-\n * session with different keyId, no-explicit + external-session,\n * explicit-managed + managed-session, no-session) without booting the\n * browser runtime.\n *\n * The decision is authoritative from the ACTIVE session's keyType when\n * no explicit `requestKeyId` was supplied.\n *\n * Kept as a thin wrapper for backwards compatibility. New callers should\n * prefer `resolveAuthorizeTinyCloudRouting` which also considers the\n * request's explicit `keyId`.\n */\nexport function shouldRouteAuthorizeTinyCloudExternal(state: {\n lastAuth: { keyType?: 'MANAGED' | 'EXTERNAL' | null } | null;\n}): boolean {\n return state.lastAuth?.keyType === 'EXTERNAL';\n}\n\n/**\n * Sol MAJOR-4 (final continuation): resolve the target key's identity and\n * route by that key's type — NOT by `lastAuth.keyType` alone.\n *\n * Sol's rejection: when the caller supplies an explicit `requestKeyId`\n * that identifies a DIFFERENT key than `lastAuth.keyId` (address vs\n * internal id, or a genuinely different key), the SDK must\n * - resolve THAT key's type,\n * - route by that resolved type (external ⇒ wallet path, managed ⇒\n * server-signing path), and\n * - reject an inconsistent explicit pin whose resolved type contradicts\n * the caller's other implicit assumptions (e.g. `lastAuth` is\n * external but the caller pinned a managed key).\n *\n * Inputs:\n * - `lastAuth`: the connected-session key metadata (may be null on\n * boot; the widget renders a connect flow first in that case).\n * - `requestKeyId`: the caller's explicit pin (may be absent).\n * - `knownKeys`: any keys we have metadata for; the SDK's higher-level\n * `authorizeTinyCloud` populates this from the server before calling\n * the resolver so the pure helper is fully synchronous and testable.\n *\n * Return shape:\n * { route: 'external' | 'managed', resolvedKeyId: string | null,\n * resolvedKeyType: 'EXTERNAL' | 'MANAGED' | null }\n *\n * `resolvedKeyId` is null only when neither `requestKeyId` nor\n * `lastAuth.keyId` exists. When both exist and identify the same key\n * (matching id OR matching known-key type), the resolver returns that\n * shared identity. When both exist and identify different keys AND we\n * know both types AND those types disagree, the resolver throws\n * `KEY_ID_TYPE_MISMATCH` so no silent route coercion occurs.\n */\nexport interface AuthorizeTinyCloudRouting {\n route: 'external' | 'managed';\n resolvedKeyId: string | null;\n resolvedKeyType: 'EXTERNAL' | 'MANAGED' | null;\n}\n\nexport class KeyIdTypeMismatchError extends Error {\n code: 'KEY_ID_TYPE_MISMATCH';\n requestKeyId: string;\n lastAuthKeyId: string | null;\n requestKeyType: 'EXTERNAL' | 'MANAGED' | null;\n lastAuthKeyType: 'EXTERNAL' | 'MANAGED' | null;\n constructor(fields: {\n message: string;\n requestKeyId: string;\n lastAuthKeyId: string | null;\n requestKeyType: 'EXTERNAL' | 'MANAGED' | null;\n lastAuthKeyType: 'EXTERNAL' | 'MANAGED' | null;\n }) {\n super(fields.message);\n this.name = 'KeyIdTypeMismatchError';\n this.code = 'KEY_ID_TYPE_MISMATCH';\n this.requestKeyId = fields.requestKeyId;\n this.lastAuthKeyId = fields.lastAuthKeyId;\n this.requestKeyType = fields.requestKeyType;\n this.lastAuthKeyType = fields.lastAuthKeyType;\n }\n}\n\nexport function resolveAuthorizeTinyCloudRouting(state: {\n lastAuth: { keyId?: string | null; keyType?: 'MANAGED' | 'EXTERNAL' | null } | null;\n requestKeyId?: string | null;\n knownKeys?: ReadonlyArray<{ id: string; address?: string; keyType: 'MANAGED' | 'EXTERNAL' }>;\n}): AuthorizeTinyCloudRouting {\n const lastAuthKeyId = state.lastAuth?.keyId ?? null;\n const lastAuthKeyType = (state.lastAuth?.keyType ?? null) as\n | 'EXTERNAL'\n | 'MANAGED'\n | null;\n\n const rawRequestKeyId = state.requestKeyId ?? null;\n const requestKeyId = rawRequestKeyId && rawRequestKeyId.length > 0 ? rawRequestKeyId : null;\n\n const knownKeys = state.knownKeys ?? [];\n\n // Helper: resolve a keyId string into its known type. Matches by id\n // first, then by address (address is a valid public identifier the\n // caller may pass in place of the internal record id).\n const lookupType = (id: string): 'EXTERNAL' | 'MANAGED' | null => {\n for (const k of knownKeys) {\n if (k.id === id) return k.keyType;\n if (k.address && k.address.toLowerCase() === id.toLowerCase()) return k.keyType;\n }\n // Convention: `external:<address>` is the SDK's synthetic id for a\n // wallet-only session that never enrolled with OpenKey. Callers\n // sometimes pass the bare address in requestKeyId; treat both\n // forms as EXTERNAL.\n if (id.startsWith('external:')) return 'EXTERNAL';\n return null;\n };\n\n // Case A: no request pin — fall back to lastAuth.keyType.\n if (!requestKeyId) {\n const route: 'external' | 'managed' =\n lastAuthKeyType === 'EXTERNAL' ? 'external' : 'managed';\n return {\n route,\n resolvedKeyId: lastAuthKeyId,\n resolvedKeyType: lastAuthKeyType,\n };\n }\n\n // Case B: request pin present. Resolve its type.\n let requestKeyType = lookupType(requestKeyId);\n // If the pin string-matches lastAuth.keyId AND we did not find it in\n // knownKeys, adopt lastAuth's type (they identify the same key).\n if (!requestKeyType && lastAuthKeyId && requestKeyId === lastAuthKeyId) {\n requestKeyType = lastAuthKeyType;\n }\n\n // If both sides tell us a type and they disagree, hard fail —\n // conflicting pins.\n if (requestKeyType && lastAuthKeyType && requestKeyType !== lastAuthKeyType) {\n throw new KeyIdTypeMismatchError({\n message: `authorizeTinyCloud: request.keyId ${requestKeyId} resolves to ${requestKeyType} but active session key ${lastAuthKeyId} is ${lastAuthKeyType}; refuse to route silently`,\n requestKeyId,\n lastAuthKeyId,\n requestKeyType,\n lastAuthKeyType,\n });\n }\n\n // If we still do not know the request's type, fall back to\n // lastAuth.keyType — the widget will surface a connect prompt if the\n // key is unknown.\n const effectiveType: 'EXTERNAL' | 'MANAGED' | null =\n requestKeyType ?? lastAuthKeyType;\n\n return {\n route: effectiveType === 'EXTERNAL' ? 'external' : 'managed',\n resolvedKeyId: requestKeyId,\n resolvedKeyType: effectiveType,\n };\n}\n\nclass IframeModal {\n private root: HTMLDivElement;\n private shadow: ShadowRoot;\n private iframe: HTMLIFrameElement;\n private card: HTMLDivElement;\n private onClose: () => void;\n private onMessage: (data: MessageType) => void;\n private messageHandler: (event: MessageEvent) => void;\n private host: string;\n // Sol final continuation contract requirement 5: when the modal carries a\n // versioned request, iframe resize traffic MUST correlate to its\n // requestId AND protocolVersion. Set by `setExpectedCorrelation` as soon\n // as the modal is created. When both are null the flow is unversioned and\n // only uncorrelated resizes from this iframe are accepted (TC-647).\n private expectedRequestId: string | null = null;\n private expectedProtocolVersion: number | null = null;\n private phaseV2 = false;\n private currentLayoutId = 0;\n private displayedHeight = 0;\n private hasDisplayedLayout = false;\n private revealTimer?: ReturnType<typeof setTimeout>;\n\n constructor(opts: { url: string; host: string; hideUntilLayout?: boolean; onClose: () => void; onMessage: (data: MessageType) => void }) {\n this.host = opts.host;\n this.onClose = opts.onClose;\n this.onMessage = opts.onMessage;\n\n const isMobile = window.matchMedia('(max-width: 639px)').matches;\n\n this.root = document.createElement('div');\n this.shadow = this.root.attachShadow({ mode: 'closed' });\n\n const style = document.createElement('style');\n style.textContent = `\n :host{all:initial}\n .ok-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.42);z-index:999999;display:flex;align-items:${isMobile ? 'flex-end' : 'center'};justify-content:center;animation:ok-fade-in 150ms ease-out}\n .ok-card{position:relative;background:#fafafa;width:${isMobile ? '100%' : '400px'};border-radius:${isMobile ? '16px 16px 0 0' : '16px'};box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);overflow:hidden;overflow-y:auto;animation:${isMobile ? 'ok-slide-up 250ms ease-out' : 'ok-scale-in 200ms ease-out'};max-height:90vh}\n .ok-close{position:absolute;top:8px;right:8px;width:24px;height:24px;border:none;background:transparent;color:#6b7280;font-size:18px;cursor:pointer;z-index:1;display:flex;align-items:center;justify-content:center;border-radius:4px}\n .ok-close:hover{color:#111827}\n .ok-handle{width:40px;height:4px;background:#d4d4d4;border-radius:2px;margin:8px auto 0}\n iframe{border:none;width:100%;height:400px;display:block;transition:height 200ms ease}\n .ok-backdrop.ok-exit{animation:ok-fade-out 150ms ease-in}\n .ok-backdrop.ok-exit .ok-card{animation:${isMobile ? 'ok-slide-down 200ms ease-in' : 'ok-scale-out 150ms ease-in'}}\n @keyframes ok-fade-in{from{opacity:0}to{opacity:1}}\n @keyframes ok-fade-out{from{opacity:1}to{opacity:0}}\n @keyframes ok-scale-in{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}}\n @keyframes ok-scale-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(0.95)}}\n @keyframes ok-slide-up{from{transform:translateY(100%)}to{transform:translateY(0)}}\n @keyframes ok-slide-down{from{transform:translateY(0)}to{transform:translateY(100%)}}\n `;\n\n const backdrop = document.createElement('div');\n backdrop.className = 'ok-backdrop';\n backdrop.addEventListener('click', (e) => {\n if (e.target === backdrop) this.close();\n });\n\n this.card = document.createElement('div');\n this.card.className = 'ok-card';\n if (opts.hideUntilLayout) {\n // Keep the iframe in the viewport and rendering: visibility:hidden\n // throttles its animation frames before the widget can measure review.\n this.card.inert = true;\n this.card.style.opacity = '0';\n this.card.style.pointerEvents = 'none';\n this.card.style.animation = 'none';\n }\n\n if (isMobile) {\n const handle = document.createElement('div');\n handle.className = 'ok-handle';\n this.card.appendChild(handle);\n }\n\n const closeBtn = document.createElement('button');\n closeBtn.className = 'ok-close';\n closeBtn.textContent = '\\u00d7';\n closeBtn.addEventListener('click', () => this.close());\n this.card.appendChild(closeBtn);\n\n this.iframe = document.createElement('iframe');\n this.iframe.src = opts.url;\n this.iframe.setAttribute('allow', 'clipboard-write *; publickey-credentials-get *; publickey-credentials-create *');\n this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox');\n if (opts.hideUntilLayout) {\n this.iframe.style.opacity = '0';\n this.iframe.style.pointerEvents = 'none';\n }\n this.card.appendChild(this.iframe);\n\n backdrop.appendChild(this.card);\n this.shadow.appendChild(style);\n this.shadow.appendChild(backdrop);\n\n this.messageHandler = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n if (event.source !== this.iframe.contentWindow) return;\n const data = event.data as MessageType;\n if (data.type === 'openkey:resize') {\n // Sol final continuation contract requirement 5: a versioned\n // request's resize MUST carry its EXACT requestId AND\n // protocolVersion; an unversioned flow accepts only uncorrelated\n // resizes (TC-647). Origin and source were checked above. Every\n // branch is covered by unit tests over `validateIframeResize`.\n const h = validateIframeResize(event.data, {\n requestId: this.expectedRequestId,\n protocolVersion: this.expectedProtocolVersion,\n viewportHeight: window.innerHeight,\n });\n if (h === null) return;\n if (this.phaseV2) {\n if (typeof data.layoutId !== 'number' || !Number.isSafeInteger(data.layoutId) ||\n data.layoutId <= this.currentLayoutId) return;\n this.currentLayoutId = data.layoutId;\n this.iframe.style.transition = 'none';\n }\n // Once Approve has been enabled, keep the visible surface fixed.\n // Later review revisions may invalidate consent, but must scroll\n // inside this frame rather than move the host card.\n if (!this.phaseV2 || !this.hasDisplayedLayout) {\n this.iframe.style.height = `${h}px`;\n if (this.phaseV2) this.displayedHeight = h;\n }\n if (this.phaseV2) {\n this.revealFrame();\n this.postMessage({\n type: 'openkey:layout-applied',\n requestId: this.expectedRequestId,\n protocolVersion: 2,\n layoutId: data.layoutId,\n height: this.hasDisplayedLayout ? this.displayedHeight : h,\n });\n }\n return;\n }\n if (data.type === 'openkey:layout-ready' || data.type === 'openkey:layout-invalidated') {\n if (!this.phaseV2 || data.requestId !== this.expectedRequestId ||\n data.protocolVersion !== this.expectedProtocolVersion ||\n data.layoutId !== this.currentLayoutId) return;\n if (data.type === 'openkey:layout-ready') {\n this.hasDisplayedLayout = true;\n }\n return;\n }\n // Origin and iframe source have been checked before forwarding any\n // ready, close or response to the phase-scoped flow.\n this.onMessage(data);\n };\n window.addEventListener('message', this.messageHandler);\n document.body.appendChild(this.root);\n }\n\n activatePhase(requestId: string, protocolVersion: 2): void {\n this.setExpectedCorrelation(requestId, protocolVersion);\n this.phaseV2 = true;\n // A blocked or slow layout must not strand Cancel behind an invisible\n // frame. This only reveals the UI; widget consent remains layout-gated.\n this.revealTimer = setTimeout(() => this.revealFrame(), 5000);\n }\n\n isVersionedPhase(): boolean {\n return this.phaseV2;\n }\n\n private revealFrame(): void {\n clearTimeout(this.revealTimer);\n this.revealTimer = undefined;\n this.card.inert = false;\n this.iframe.style.opacity = '1';\n this.iframe.style.pointerEvents = 'auto';\n this.card.style.opacity = '1';\n this.card.style.pointerEvents = 'auto';\n this.iframe.style.visibility = 'visible';\n this.card.style.visibility = 'visible';\n }\n\n revealLegacy(): void {\n this.revealFrame();\n }\n hasDisplayedCurrentPhase(): boolean {\n return this.hasDisplayedLayout;\n }\n\n\n postMessage(message: object) {\n this.iframe.contentWindow?.postMessage(message, this.host);\n }\n\n /**\n * Sol final continuation contract requirement 5: bind the active\n * request correlation so subsequent resize (and future correlated)\n * messages can be verified against `requestId` + `protocolVersion`.\n *\n * The outer flow calls this right after creating the modal for a\n * versioned request, before the iframe can send anything, so no\n * uncorrelated resize is ever accepted for that request. Passing `null`\n * for both arguments means the flow is unversioned; passing `null` for\n * only one drops every resize.\n */\n setExpectedCorrelation(requestId: string | null, protocolVersion: number | null): void {\n this.expectedRequestId = requestId;\n this.expectedProtocolVersion = protocolVersion;\n }\n\n destroy() {\n clearTimeout(this.revealTimer);\n window.removeEventListener('message', this.messageHandler);\n this.root.remove();\n }\n\n private close() {\n const backdrop = this.shadow.querySelector('.ok-backdrop');\n if (backdrop) {\n backdrop.classList.add('ok-exit');\n setTimeout(() => {\n this.destroy();\n this.onClose();\n }, 200);\n } else {\n this.destroy();\n this.onClose();\n }\n }\n}\n\nclass WalletPicker {\n private root: HTMLDivElement;\n private shadow: ShadowRoot;\n\n constructor(opts: { providers: EIP6963ProviderDetail[]; onSelect: (provider: EIP1193Provider) => void; onCancel: () => void }) {\n this.root = document.createElement('div');\n this.shadow = this.root.attachShadow({ mode: 'closed' });\n\n const style = document.createElement('style');\n style.textContent = `\n :host{all:initial}\n .wp-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.42);z-index:1000000;display:flex;align-items:center;justify-content:center;animation:wp-fade 150ms ease-out}\n .wp-card{background:#fff;width:340px;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);padding:20px;animation:wp-scale 200ms ease-out}\n .wp-title{font:600 16px/1.3 system-ui,sans-serif;color:#111;margin:0 0 12px}\n .wp-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}\n .wp-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid #e5e7eb;border-radius:10px;background:transparent;cursor:pointer;font:14px/1.3 system-ui,sans-serif;color:#111;width:100%;text-align:left;transition:border-color 150ms}\n .wp-item:hover{border-color:#6366f1}\n .wp-icon{width:28px;height:28px;border-radius:6px}\n .wp-empty{font:14px/1.4 system-ui,sans-serif;color:#6b7280;text-align:center;padding:16px 0}\n .wp-cancel{display:block;width:100%;margin-top:12px;padding:8px;border:none;background:transparent;color:#6b7280;font:14px/1.3 system-ui,sans-serif;cursor:pointer;border-radius:8px}\n .wp-cancel:hover{background:#f3f4f6;color:#111}\n @keyframes wp-fade{from{opacity:0}to{opacity:1}}\n @keyframes wp-scale{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}}\n @media(prefers-color-scheme:dark){\n .wp-card{background:#1a1a1a;box-shadow:0 25px 50px -12px rgba(0,0,0,0.5)}\n .wp-title{color:#f3f4f6}\n .wp-item{border-color:#374151;color:#f3f4f6}\n .wp-item:hover{border-color:#818cf8}\n .wp-empty{color:#9ca3af}\n .wp-cancel{color:#9ca3af}\n .wp-cancel:hover{background:#262626;color:#f3f4f6}\n }\n `;\n\n const backdrop = document.createElement('div');\n backdrop.className = 'wp-backdrop';\n backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { this.destroy(); opts.onCancel(); } });\n\n const card = document.createElement('div');\n card.className = 'wp-card';\n\n const title = document.createElement('h2');\n title.className = 'wp-title';\n title.textContent = 'Select a wallet';\n card.appendChild(title);\n\n if (opts.providers.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'wp-empty';\n empty.textContent = 'No wallets detected';\n card.appendChild(empty);\n } else {\n const list = document.createElement('ul');\n list.className = 'wp-list';\n for (const { info, provider } of opts.providers) {\n const item = document.createElement('button');\n item.className = 'wp-item';\n if (info.icon) {\n const icon = document.createElement('img');\n icon.className = 'wp-icon';\n icon.src = info.icon;\n icon.alt = info.name;\n item.appendChild(icon);\n }\n const name = document.createElement('span');\n name.textContent = info.name;\n item.appendChild(name);\n item.addEventListener('click', () => { this.destroy(); opts.onSelect(provider); });\n list.appendChild(item);\n }\n card.appendChild(list);\n }\n\n const cancel = document.createElement('button');\n cancel.className = 'wp-cancel';\n cancel.textContent = 'Cancel';\n cancel.addEventListener('click', () => { this.destroy(); opts.onCancel(); });\n card.appendChild(cancel);\n\n backdrop.appendChild(card);\n this.shadow.appendChild(style);\n this.shadow.appendChild(backdrop);\n document.body.appendChild(this.root);\n }\n\n destroy() {\n this.root.remove();\n }\n}\n\nfunction showToast(message = 'Opening in new window\\u2026', variant: 'info' | 'error' = 'info') {\n const bg = variant === 'error' ? '#dc2626' : '#1f2937';\n const root = document.createElement('div');\n const shadow = root.attachShadow({ mode: 'closed' });\n shadow.innerHTML = `<style>:host{all:initial}.ok-toast{position:fixed;top:16px;right:16px;z-index:1000000;background:${bg};color:#f9fafb;padding:10px 16px;border-radius:8px;font:14px/1.4 system-ui,sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.15);animation:ok-t-in 200ms ease-out;max-width:360px}@keyframes ok-t-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}</style><div class=\"ok-toast\">${message}</div>`;\n document.body.appendChild(root);\n setTimeout(() => root.remove(), 4000);\n}\n\nclass ExternalWalletError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ExternalWalletError';\n }\n}\n\nexport class OpenKey {\n private host: string;\n private oauthHost: string;\n private appName: string;\n private mode: OpenKeyMode;\n private passkeysSupported: boolean;\n private config: OpenKeyConfig;\n private popup: Window | null = null;\n private lastAuth: AuthResult | null = null;\n private discoveredProviders: EIP6963ProviderDetail[] = [];\n /** Cancels widget flows that could outlive a sign-out on this instance. */\n private activeFlowCancellations = new Set<() => void>();\n /**\n * Nostr identity custody + signing (secp256k1 Schnorr / BIP-340). Kept\n * fully separate from the Ethereum flows above: it never reuses\n * `lastAuth`, and its widget messages never carry a session token -\n * the OpenKey session and any signing grants stay inside\n * the OpenKey-origin iframe for the whole lifetime of this client.\n */\n readonly nostr: OpenKeyNostr;\n\n constructor(config: OpenKeyConfig = {}) {\n this.config = config;\n this.host = config.host || DEFAULT_HOST;\n this.oauthHost = config.oauthHost || this.deriveOAuthHost(this.host);\n this.appName = config.appName || window.location.hostname;\n this.mode = config.mode ?? 'iframe';\n this.passkeysSupported = config.passkeysSupported ?? true;\n this.nostr = new OpenKeyNostr(this.host, this.passkeysSupported);\n\n // Listen for EIP-6963 wallet announcements\n if (typeof window !== 'undefined') {\n this.discoveredProviders = [];\n window.addEventListener('eip6963:announceProvider', (event: any) => {\n this.discoveredProviders.push(event.detail);\n });\n window.dispatchEvent(new Event('eip6963:requestProvider'));\n }\n }\n\n /**\n * Derive the OAuth API host from the main host by prefixing 'api.'\n * @param host - The main host URL\n * @returns The derived OAuth API host URL\n */\n private deriveOAuthHost(host: string): string {\n try {\n const url = new URL(host);\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n return host.replace(/\\/$/, '');\n }\n url.hostname = 'api.' + url.hostname;\n return url.toString().replace(/\\/$/, '');\n } catch {\n return 'https://api.openkey.so';\n }\n }\n\n /**\n * Sol MAJOR-4 (final continuation): fetch the caller's registered keys\n * so `resolveAuthorizeTinyCloudRouting` can resolve an explicit pin\n * whose keyId does not match the active session key. Called only when\n * the caller passes a `requestKeyId` we do not already recognise; the\n * common no-pin path skips the network round-trip.\n *\n * Failures do NOT block routing — we fall through with `knownKeys=[]`\n * so the resolver's lastAuth-based fallback still runs. If the caller\n * pinned a genuinely unknown key that turns out to be external, the\n * widget rejects the server-signing path and the SDK surfaces that\n * error from the finalize call rather than silently routing wrong.\n */\n private async fetchKnownKeysIfNeeded(\n requestKeyId: string | undefined,\n ): Promise<Array<{ id: string; address?: string; keyType: 'MANAGED' | 'EXTERNAL' }>> {\n if (!requestKeyId) return [];\n // If lastAuth already identifies this key (id or address), we do not\n // need a network round-trip — the resolver adopts lastAuth.keyType.\n const la = this.lastAuth;\n if (la?.keyId === requestKeyId) return [];\n if (la?.address && la.address.toLowerCase() === requestKeyId.toLowerCase()) return [];\n if (requestKeyId.startsWith('external:')) return [];\n try {\n const res = await fetch(`${this.oauthHost}/api/keys`, {\n method: 'GET',\n credentials: 'include',\n });\n if (!res.ok) return [];\n const body = (await res.json()) as {\n keys?: Array<{ id: string; address?: string; keyType: 'MANAGED' | 'EXTERNAL' }>;\n };\n return Array.isArray(body?.keys) ? body.keys : [];\n } catch {\n return [];\n }\n }\n\n /**\n * Connect to OpenKey and get user's wallet address\n * Opens auth flow for user to select/create a key\n */\n async connect(opts?: { mode?: OpenKeyMode }): Promise<AuthResult> {\n const result = await this.openFlow<AuthResult>('connect', {\n type: 'openkey:auth:request',\n appName: this.appName,\n }, opts?.mode);\n this.lastAuth = result;\n return result;\n }\n\n /**\n * Signs the active OpenKey account out through the OpenKey-owned widget.\n *\n * The OpenKey-owned widget revokes the OpenKey session it holds itself;\n * the SDK never holds an OpenKey session token. Local SDK state is cleared\n * before opening the widget, including when remote revocation is\n * unavailable.\n *\n * Rejects without waiting for the timeout when the widget cannot be shown\n * (code `POPUP_BLOCKED`): the iframe did not load and the popup fallback is\n * blocked, or the app is an embedded WebView that cannot open popups.\n * Remote revocation did not happen then, so clearing any app session is the\n * caller's decision.\n */\n async signOut(opts?: { mode?: OpenKeyMode }): Promise<SignOutAcknowledgement> {\n const requestId = `ok-signout-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n // Redirect flows intentionally never resolve. Sign-out needs to return a\n // correlated acknowledgement, so use the OpenKey popup boundary instead.\n const mode = this.resolveMode(opts?.mode) === 'redirect' ? 'popup' : opts?.mode;\n\n // Stale connect/sign widgets must not later overwrite this signed-out\n // instance with their responses.\n this.cancelActiveFlows();\n // A failed/blocked widget must never leave this SDK instance authenticated.\n this.lastAuth = null;\n\n const acknowledgement = await this.openFlow<SignOutAcknowledgement>(\n 'sign-out',\n {\n type: 'openkey:sign-out:request',\n requestId,\n protocolVersion: 1,\n },\n mode,\n );\n\n if (acknowledgement.requestId !== requestId) {\n throw new Error('OpenKey signOut received an acknowledgement for a different request');\n }\n return acknowledgement;\n }\n\n /**\n * Always null. OpenKey no longer gives its session token to the embedding\n * page (TC-688): the token unlocked OpenKey's key and account APIs, not\n * just the requesting app's flows.\n *\n * @deprecated Kept so existing callers compile. Third-party server-side\n * signing uses an OAuth access token from the OpenKey OAuth flow.\n */\n getSessionToken(): string | null {\n return null;\n }\n\n /**\n * Endpoint for a TinyCloud auto-sign signing strategy. `token` is always\n * null; see {@link OpenKey.getSessionToken}.\n *\n * @deprecated The endpoint accepts an OAuth access token as its bearer.\n */\n tinycloudSigningOptions(): { endpoint: string; token: string | null } {\n return {\n endpoint: `${this.oauthHost}/api/delegate/sign`,\n token: null,\n };\n }\n\n /**\n * Link an external wallet to the user's OpenKey account\n * Opens link-wallet widget flow\n */\n async linkWallet(opts?: { mode?: OpenKeyMode }): Promise<{ address: string; keyId: string }> {\n return this.openFlow<{ address: string; keyId: string }>('link-wallet', {\n type: 'openkey:link-wallet:request',\n }, opts?.mode);\n }\n\n /**\n * Sign a message with the user's OpenKey wallet\n * For external keys, routes directly to the user's wallet provider\n */\n async signMessage(request: SignMessageRequest, opts?: { mode?: OpenKeyMode }): Promise<SignResult> {\n if (this.lastAuth?.keyType === 'EXTERNAL') {\n return this.signWithExternalWallet(request);\n }\n return this.signWithOpenKey(request, opts?.mode);\n }\n\n /**\n * Versioned TinyCloud authorization protocol (v1).\n *\n * Unlike `signMessage()` (which is contractually byte-exact — the returned\n * signature always verifies against the caller's exact bytes), this method\n * permits the user to narrow the requested capabilities before signing.\n * OpenKey may therefore regenerate the SIWE. The response carries\n * `signedMessage` — the exact bytes the signature actually verifies against.\n *\n * TinyCloud completes the session with `signedMessage`, not the original\n * `siwe` passed in. Consumers MUST use the returned bytes and MUST NOT\n * assume the returned grants are a superset of what they requested — the\n * user is allowed to remove any grant that is not required.\n *\n * Legacy `signMessage()` callers are unaffected: their exact-byte contract\n * remains preserved and OpenKey never rewrites their bytes.\n */\n async authorizeTinyCloud(\n request: TinyCloudAuthorizationRequestV1,\n opts?: { mode?: OpenKeyMode },\n ): Promise<TinyCloudAuthorizationResultV1> {\n if (request.protocolVersion !== 1) {\n throw new Error(\n `authorizeTinyCloud only supports protocolVersion 1; got ${request.protocolVersion}`,\n );\n }\n if (typeof request.siwe !== 'string' || !request.siwe) {\n throw new Error('authorizeTinyCloud requires a non-empty SIWE');\n }\n // Sol MAJOR-3 (continuation) + Sol MAJOR-4 (final): route to the\n // external-wallet preview→sign→finalize flow whenever the ACTIVE\n // authentication session is against an EXTERNAL key. External keys\n // are held by the user's browser wallet — OpenKey does not have\n // the private material, so it cannot execute the managed-key\n // widget-signs-server-side path.\n //\n // Sol's final continuation rejection: the prior check\n // `this.lastAuth?.keyId === explicitKeyId && ... === 'EXTERNAL'`\n // let an explicitly-supplied external keyId enter the managed\n // widget path whenever the caller's `explicitKeyId` did not match\n // `lastAuth.keyId` string-for-string (for example: caller passes\n // the address; lastAuth carries the internal key record id — same\n // key, different identifier). A user cannot be authenticated with\n // two different active session keys at once — the ONE active\n // session's `keyType` is therefore authoritative for routing.\n //\n // Routing rules (revised):\n // 1. If `lastAuth?.keyType === 'EXTERNAL'`, route to external\n // (the wallet holds the material; the widget cannot sign\n // server-side either way).\n // 2. If `lastAuth?.keyType === 'MANAGED'`, route to managed.\n // 3. If `lastAuth` is missing (no active session), route to\n // managed — the widget renders a connect flow first, and\n // once the user connects the widget resolves keyType from\n // the fresh session; if that turns out to be EXTERNAL the\n // widget itself refuses to server-sign and asks the SDK to\n // re-enter via the external path.\n //\n // This resolves the key type authoritatively from the ONE active\n // session rather than relying on a fragile keyId string match.\n // See `shouldRouteAuthorizeTinyCloudExternal` for the full\n // routing-decision rationale and its unit tests for every branch.\n // Sol MAJOR-4 (final continuation): resolve the target key first\n // and route by ITS type, not by lastAuth.keyType alone. When the\n // caller pins a request.keyId that differs from the active session\n // key, we fetch the caller's key list from the server so the pure\n // `resolveAuthorizeTinyCloudRouting` helper can decide by resolved\n // type (or throw on conflicting pins).\n let routing: AuthorizeTinyCloudRouting;\n try {\n const knownKeys = await this.fetchKnownKeysIfNeeded(request.keyId);\n routing = resolveAuthorizeTinyCloudRouting({\n lastAuth: this.lastAuth,\n requestKeyId: request.keyId,\n knownKeys,\n });\n } catch (e) {\n if (e instanceof KeyIdTypeMismatchError) throw e;\n throw e instanceof Error\n ? e\n : new Error('authorizeTinyCloud: failed to resolve target key type');\n }\n if (routing.route === 'external') {\n return this.authorizeTinyCloudExternal(request, opts);\n }\n // Sol CRITICAL-1: Send a DISTINCT versioned sign request so the widget\n // knows to route through the server-authoritative /authorize-sign\n // endpoint. protocolVersion: 1 with the extended payload triggers the\n // server-authoritative narrowing path — the widget refuses to fall\n // back to legacy exact-byte signMessage() for a versioned request\n // whose origin is real.\n const requestId = `ok-auth-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n // Sol MAJOR-3: fall back to the connected key when the caller did not\n // pass an explicit keyId. Without this fallback the widget renders\n // 'Please connect first.' because it has no key context, and the\n // NodeUserAuthorization bridge never carries a keyId when the caller\n // only ran connect() — the widget path is otherwise unable to sign.\n const resolvedKeyId = routing.resolvedKeyId ?? request.keyId ?? this.lastAuth?.keyId;\n const raw = await this.openFlow<{\n signature: string;\n address: string;\n signedMessage?: string;\n selectedActionKeys?: string[];\n permissions?: TinyCloudAuthorizationResultV1['permissions'];\n }>(\n 'sign',\n {\n type: 'openkey:sign:request',\n requestId,\n protocolVersion: 1,\n message: request.siwe,\n keyId: resolvedKeyId,\n // Forward JWK so the widget can pass it to /authorize-sign for\n // narrowed-SIWE regeneration.\n jwk: request.jwk,\n // Forward the TinyCloud host so the widget can bind it into the\n // /authorize-sign-prepare context (Sol MAJOR-2).\n host: request.host,\n // Sol MAJOR-2 (envelope): forward the caller-supplied presentation\n // envelope so the widget can render honest provenance and the\n // /authorize-sign-prepare route can bind it into the authorization\n // context. The envelope is display-only; the widget transport\n // validates and size-bounds it before use and NEVER treats it as\n // verified unless the server actually origin-binds a manifest.\n presentation: request.presentation,\n },\n opts?.mode,\n );\n // Sol CRITICAL-1: NO silent fallback to `request.siwe`. If the widget\n // returned an authorization response without `signedMessage`, that is\n // a protocol violation and MUST fail — silently accepting the caller's\n // original bytes as `signedMessage` while selectedActionKeys/permissions\n // reflect a narrower set is exactly the bug this rewrite prevents.\n if (typeof raw.signedMessage !== 'string' || !raw.signedMessage) {\n throw new Error(\n 'authorizeTinyCloud: widget returned no signedMessage — signature would not correspond to the displayed authorization',\n );\n }\n if (!Array.isArray(raw.selectedActionKeys)) {\n throw new Error(\n 'authorizeTinyCloud: widget returned no selectedActionKeys — cannot verify displayed authorization matches the signed bytes',\n );\n }\n if (!Array.isArray(raw.permissions)) {\n throw new Error(\n 'authorizeTinyCloud: widget returned no permissions — cannot verify displayed authorization matches the signed bytes',\n );\n }\n return {\n protocolVersion: 1,\n address: raw.address,\n signature: raw.signature,\n signedMessage: raw.signedMessage,\n selectedActionKeys: raw.selectedActionKeys,\n permissions: raw.permissions,\n };\n }\n\n /**\n * Sol MAJOR-3 (continuation): authorizeTinyCloud path for external keys.\n * The wallet lives outside OpenKey, so the SDK opens the shared widget\n * for review + preview, then invokes the user's wallet to sign the\n * preview bytes, then finalizes server-side via /authorize-sign.\n *\n * Flow:\n * 1. Open the widget with `externalSign: true` — the widget renders\n * the shared SigningApproval UI, calls /authorize-sign-prepare,\n * calls /authorize-sign-preview after the user approves a\n * selection, and hands back `{ previewApprovalToken,\n * signedMessage, selectedActionIds, address, authorizationContextToken }`.\n * 2. SDK invokes the user's wallet on the returned signedMessage.\n * 3. SDK POSTs /authorize-sign with `externalSignature`.\n *\n * Legacy exact-byte signMessage callers are unaffected — this path is\n * only taken via authorizeTinyCloud (protocolVersion:1) when the target\n * key is EXTERNAL. Sol's rejection required this path to go through\n * the same review UI as managed keys.\n */\n private async authorizeTinyCloudExternal(\n request: TinyCloudAuthorizationRequestV1,\n opts?: { mode?: OpenKeyMode },\n ): Promise<TinyCloudAuthorizationResultV1> {\n const keyId = request.keyId ?? this.lastAuth?.keyId;\n if (!keyId) {\n throw new Error(\n 'authorizeTinyCloud (external) requires a keyId — call connect() first or pass request.keyId',\n );\n }\n const walletAddress = this.lastAuth?.address;\n if (!walletAddress) {\n throw new Error(\n 'authorizeTinyCloud (external) requires a connected wallet — call connect() first',\n );\n }\n // Sol MAJOR-3 (continuation): open the shared widget UI so the user\n // sees and can narrow the review, identical to the managed-key flow.\n const requestId = `ok-ext-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n const approval = await this.openFlow<{\n authorizationContextToken: string;\n previewApprovalToken: string;\n signedMessage: string;\n selectedActionIds: string[];\n address: string;\n }>(\n 'sign',\n {\n type: 'openkey:sign:request',\n requestId,\n protocolVersion: 1,\n message: request.siwe,\n keyId,\n jwk: request.jwk,\n host: request.host,\n // Sol MAJOR-2 (envelope): forward the caller-supplied presentation\n // envelope. Same rules as the managed path — display-only, size-\n // bounded at the transport, never treated as verified without\n // server-side origin-binding.\n presentation: request.presentation,\n // Sol MAJOR-3 (continuation): tell the widget to hand us back\n // the previewApproval instead of asking OpenKey to sign\n // server-side with a managed key. The widget will still render\n // the SAME SigningApproval component; it just diverges at the\n // \"sign\" step and emits `openkey:externalSign:approve` back\n // through the transport.\n externalSign: true,\n } as unknown as MessageType,\n opts?.mode,\n );\n if (!approval?.previewApprovalToken || !approval?.signedMessage) {\n throw new Error(\n 'authorizeTinyCloud (external): widget did not return a previewApprovalToken + signedMessage',\n );\n }\n if (\n approval.address &&\n approval.address.toLowerCase() !== walletAddress.toLowerCase()\n ) {\n throw new Error(\n `authorizeTinyCloud (external): widget preview address ${approval.address} does not match connected wallet ${walletAddress}`,\n );\n }\n // Wallet signs the exact bytes the widget previewed.\n const provider = await this.findWalletProvider(walletAddress);\n const hexMessage = this.toHex(approval.signedMessage);\n const walletSignature = (await provider.request({\n method: 'personal_sign',\n params: [hexMessage, walletAddress],\n })) as string;\n // Finalize — server verifies the wallet signature against the exact\n // same bytes it emitted for the preview.\n const finalizeRes = await fetch(\n `${this.oauthHost}/api/delegate/authorize-sign`,\n {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n authorizationContextToken: approval.authorizationContextToken,\n previewApprovalToken: approval.previewApprovalToken,\n selectedActionIds: approval.selectedActionIds,\n protocolVersion: 1,\n externalSignature: walletSignature,\n }),\n },\n );\n if (!finalizeRes.ok) {\n const body = await finalizeRes.json().catch(() => ({ error: `HTTP ${finalizeRes.status}` }));\n throw new Error(body.error || `authorize-sign failed (${finalizeRes.status})`);\n }\n const finalize = await finalizeRes.json();\n if (finalize.signedMessage !== approval.signedMessage) {\n throw new Error(\n 'authorizeTinyCloud (external): server signed different bytes than the preview showed',\n );\n }\n return {\n protocolVersion: 1,\n address: finalize.address,\n signature: finalize.signature,\n signedMessage: finalize.signedMessage,\n selectedActionKeys: finalize.selectedActionKeys ?? [],\n permissions: finalize.permissions ?? [],\n };\n }\n\n /**\n * Sign typed data (EIP-712) with the user's OpenKey wallet\n * For external keys, routes directly to the user's wallet provider\n */\n async signTypedData(request: SignTypedDataRequest, opts?: { mode?: OpenKeyMode }): Promise<SignResult> {\n if (this.lastAuth?.keyType === 'EXTERNAL') {\n return this.signTypedDataWithExternalWallet(request);\n }\n return this.signTypedDataWithOpenKey(request, opts?.mode);\n }\n\n private providerCandidates(): EIP1193Provider[] {\n const providers: EIP1193Provider[] = [];\n if (this.config.externalProvider) {\n providers.push(this.config.externalProvider);\n }\n providers.push(...this.discoveredProviders.map(({ provider }) => provider));\n if (typeof window !== 'undefined' && (window as any).ethereum) {\n providers.push((window as any).ethereum as EIP1193Provider);\n }\n return [...new Set(providers)];\n }\n\n private async providerHasAccount(\n provider: EIP1193Provider,\n method: 'eth_accounts' | 'eth_requestAccounts',\n targetAddress: string\n ): Promise<boolean> {\n try {\n const accounts = await provider.request({ method }) as string[];\n return accounts.some((account) => account.toLowerCase() === targetAddress.toLowerCase());\n } catch {\n return false;\n }\n }\n\n /**\n * Find a wallet provider that controls the given address.\n * Checks connected accounts first, then asks the wallet to authorize the\n * app origin if the external key was linked elsewhere.\n */\n async findWalletProvider(targetAddress: string): Promise<EIP1193Provider> {\n const providers = this.providerCandidates();\n\n for (const provider of providers) {\n if (await this.providerHasAccount(provider, 'eth_accounts', targetAddress)) {\n return provider;\n }\n }\n\n for (const provider of providers) {\n if (await this.providerHasAccount(provider, 'eth_requestAccounts', targetAddress)) {\n return provider;\n }\n }\n\n throw new ExternalWalletError('External wallet not found. Connect the wallet that owns this key and try again.');\n }\n\n private async signWithExternalWallet(request: SignMessageRequest): Promise<SignResult> {\n let provider: EIP1193Provider;\n try {\n provider = await this.findWalletProvider(this.lastAuth!.address);\n } catch (e) {\n showToast((e as Error).message, 'error');\n throw e;\n }\n try {\n const hexMessage = this.toHex(request.message);\n const signature = await provider.request({\n method: 'personal_sign',\n params: [hexMessage, this.lastAuth!.address],\n });\n return { signature: signature as string, address: this.lastAuth!.address };\n } catch (e: any) {\n const msg = e?.message?.includes('not been authorized')\n ? 'Wallet rejected the request. Approve the connection in your wallet and try again.'\n : e?.message || 'Signing failed';\n showToast(msg, 'error');\n throw new Error(msg);\n }\n }\n\n private async signTypedDataWithExternalWallet(request: SignTypedDataRequest): Promise<SignResult> {\n let provider: EIP1193Provider;\n try {\n provider = await this.findWalletProvider(this.lastAuth!.address);\n } catch (e) {\n showToast((e as Error).message, 'error');\n throw e;\n }\n try {\n const signature = await provider.request({\n method: 'eth_signTypedData_v4',\n params: [this.lastAuth!.address, JSON.stringify(request)],\n });\n return { signature: signature as string, address: this.lastAuth!.address };\n } catch (e: any) {\n const msg = e?.message?.includes('not been authorized')\n ? 'Wallet rejected the request. Approve the connection in your wallet and try again.'\n : e?.message || 'Signing failed';\n showToast(msg, 'error');\n throw new Error(msg);\n }\n }\n\n private signWithOpenKey(request: SignMessageRequest, mode?: OpenKeyMode): Promise<SignResult> {\n return this.openFlow<SignResult>('sign', {\n type: 'openkey:sign:request',\n message: request.message,\n keyId: request.keyId ?? this.lastAuth?.keyId,\n }, mode);\n }\n\n private signTypedDataWithOpenKey(request: SignTypedDataRequest, mode?: OpenKeyMode): Promise<SignResult> {\n return this.openFlow<SignResult>('sign-typed-data', {\n type: 'openkey:signTypedData:request',\n data: request,\n }, mode);\n }\n\n private toHex(str: string): string {\n return '0x' + Array.from(new TextEncoder().encode(str)).map(b => b.toString(16).padStart(2, '0')).join('');\n }\n\n /**\n * Check if user is already authenticated\n */\n async isConnected(): Promise<boolean> {\n // Check for existing session via API\n try {\n const res = await fetch(`${this.host}/api/auth/session`, {\n credentials: 'include',\n });\n if (!res.ok) return false;\n const data = await res.json();\n return !!data.user;\n } catch {\n return false;\n }\n }\n\n // ======= OAuth 2.1 Provider Methods =======\n\n /**\n * OAuth 2.1 methods for third-party app authentication\n * Use this when your app has registered OAuth client credentials with OpenKey\n */\n oauth = {\n /**\n * Start OAuth authorization flow\n * Opens popup/redirect to OpenKey authorization endpoint\n * @returns Promise that resolves with authorization code after user consent\n */\n connect: async (config: OAuthConfig): Promise<OAuthResult> => {\n // Generate PKCE values\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n const state = config.state || generateState();\n\n // Store for later token exchange\n sessionStorage.setItem(\n OAUTH_STORAGE_KEY,\n JSON.stringify({ verifier, state })\n );\n\n // Build authorization URL\n const authUrl = buildAuthorizationUrl({\n host: this.oauthHost,\n clientId: config.clientId,\n redirectUri: config.redirectUri,\n codeChallenge: challenge,\n state,\n scopes: ['openid'],\n });\n\n return this.openOAuthFlow(authUrl, state, config.redirectUri);\n },\n\n /**\n * Exchange authorization code for tokens\n * Call this after receiving the callback with authorization code\n * @param code - Authorization code from callback URL\n * @param config - Same config used for connect()\n * @returns Promise that resolves with access_token, id_token, etc.\n */\n exchangeCode: async (\n code: string,\n config: OAuthConfig\n ): Promise<OAuthTokenResponse> => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) {\n throw new Error('No PKCE verifier found. Start a new OAuth flow.');\n }\n\n const { verifier } = JSON.parse(stored);\n\n const response = await fetch(`${this.oauthHost}/api/auth/oauth2/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n code_verifier: verifier,\n }),\n });\n\n // Clear stored verifier\n sessionStorage.removeItem(OAUTH_STORAGE_KEY);\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.error_description || 'Token exchange failed');\n }\n\n return response.json();\n },\n\n /**\n * Verify state parameter from callback\n * Call this before exchangeCode to prevent CSRF attacks\n * @param receivedState - State parameter from callback URL\n * @returns true if state matches, false otherwise\n */\n verifyState: (receivedState: string): boolean => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) return false;\n const { state } = JSON.parse(stored);\n return state === receivedState;\n },\n\n /**\n * Parse authorization response from callback URL\n * @param url - Callback URL (defaults to current window.location.href)\n * @returns Parsed code and state, or error\n */\n parseCallback: (\n url?: string\n ): { code: string; state: string } | { error: string; errorDescription?: string } => {\n const urlObj = new URL(url || window.location.href);\n const error = urlObj.searchParams.get('error');\n\n if (error) {\n return {\n error,\n errorDescription: urlObj.searchParams.get('error_description') || undefined,\n };\n }\n\n const code = urlObj.searchParams.get('code');\n const state = urlObj.searchParams.get('state');\n\n if (!code || !state) {\n return { error: 'missing_params', errorDescription: 'Missing code or state parameter' };\n }\n\n return { code, state };\n },\n };\n\n private openOAuthFlow(\n url: string,\n state: string,\n redirectUri: string\n ): Promise<OAuthResult> {\n return new Promise((resolve, reject) => {\n if (this.mode !== 'redirect') {\n this.openOAuthPopup(url, state, redirectUri, resolve, reject);\n } else {\n // Full page redirect - won't resolve, page navigates away\n window.location.href = url;\n }\n });\n }\n\n private openOAuthPopup(\n url: string,\n state: string,\n redirectUri: string,\n resolve: (value: OAuthResult) => void,\n reject: (error: OpenKeyError) => void\n ) {\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n const popup = window.open(\n url,\n 'openkey-oauth',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!isUsablePopup(popup)) {\n this.popup = null;\n const embedded = isEmbeddedAppWebView();\n if (!embedded) showToast('Popup was blocked. Please allow popups for this site.', 'error');\n reject(embedded\n ? popupUnavailableError(true)\n : { code: 'POPUP_BLOCKED', message: 'Popup was blocked. Please allow popups or use redirect mode.' });\n return;\n }\n this.popup = popup;\n\n const redirectHost = new URL(redirectUri).origin;\n\n // Poll for redirect back with code\n const pollInterval = setInterval(() => {\n try {\n if (this.popup?.closed) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n return;\n }\n\n // Check if redirected to callback URL\n const popupUrl = this.popup?.location?.href;\n if (popupUrl && popupUrl.startsWith(redirectHost)) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n\n const urlObj = new URL(popupUrl);\n const code = urlObj.searchParams.get('code');\n const returnedState = urlObj.searchParams.get('state');\n const error = urlObj.searchParams.get('error');\n\n this.popup?.close();\n\n if (error) {\n reject({\n code: 'UNAUTHORIZED',\n message: urlObj.searchParams.get('error_description') || error,\n });\n return;\n }\n\n if (returnedState !== state) {\n reject({\n code: 'STATE_MISMATCH',\n message: 'State mismatch - possible CSRF attack',\n });\n return;\n }\n\n if (code) {\n resolve({ code, state: returnedState });\n } else {\n reject({\n code: 'UNAUTHORIZED',\n message: 'Authorization failed - no code returned',\n });\n }\n }\n } catch {\n // Cross-origin - popup still on OpenKey domain, continue polling\n }\n }, 100);\n\n // Timeout\n const timeout = setTimeout(() => {\n clearInterval(pollInterval);\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'OAuth flow timed out',\n });\n }, DEFAULT_TIMEOUT);\n }\n\n private resolveMode(override?: OpenKeyMode): OpenKeyMode {\n return override ?? this.mode;\n }\n\n private hasDetectedEoa(): boolean {\n if (this.discoveredProviders.length > 0) return true;\n if (typeof window !== 'undefined' && (window as any).ethereum) return true;\n return false;\n }\n\n private async openFlow<T>(action: string, message: object, modeOverride?: OpenKeyMode): Promise<T> {\n const mode = this.resolveMode(modeOverride);\n const origin = encodeURIComponent(window.location.origin);\n const eoaFlag = action === 'connect' && this.hasDetectedEoa() ? '&hasEoa=true' : '';\n const passkeysFlag = passkeysQueryFlag(this.passkeysSupported);\n\n if (mode === 'popup') {\n const url = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}${passkeysFlag}`;\n return new Promise((resolve, reject) => this.openPopup(action, url, message, resolve, reject));\n }\n\n if (mode === 'redirect') {\n const url = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}${passkeysFlag}`;\n window.location.href = url;\n return new Promise(() => {}); // never resolves, page navigates\n }\n\n // The URL binds a fresh phase before the widget boots. A deployed older\n // widget can still answer with its legacy ready and keep the old protocol.\n // Without a selected key the existing unversioned sign flow remains in use.\n const selectedKeyId = action === 'sign' && 'keyId' in message ? message.keyId : null;\n const phase = action === 'connect' || (action === 'sign' && typeof selectedKeyId === 'string')\n ? { requestId: crypto.randomUUID(), protocolVersion: 2 as const }\n : null;\n const keyId = action === 'sign' && 'keyId' in message && typeof message.keyId === 'string'\n ? `&keyId=${encodeURIComponent(message.keyId)}` : '';\n const url = `${this.host}/widget/embed/${action}?origin=${origin}${eoaFlag}${passkeysFlag}${phase ? `&requestId=${encodeURIComponent(phase.requestId)}&protocolVersion=2${keyId}` : ''}`;\n return this.openIframeModal<T>(url, action, message, origin, phase);\n }\n\n private openIframeModal<T>(\n url: string,\n action: string,\n message: object,\n origin: string,\n phase: { requestId: string; protocolVersion: 2 } | null = null,\n ): Promise<T> {\n const outgoingRequestId = (message as { requestId?: unknown }).requestId;\n const outgoingProtocolVersion = (message as { protocolVersion?: unknown }).protocolVersion;\n const isVersionedRequest =\n typeof outgoingRequestId === 'string' &&\n typeof outgoingProtocolVersion === 'number' &&\n outgoingProtocolVersion >= 1;\n return new Promise((resolve, reject) => {\n let readyReceived = false;\n let settled = false;\n let modal: IframeModal | null = null;\n let cancel: () => void;\n\n const settle = (fn: () => void) => {\n if (settled) return;\n settled = true;\n fn();\n };\n\n const cleanup = () => {\n clearTimeout(readyTimeout);\n clearTimeout(overallTimeout);\n modal?.destroy();\n modal = null;\n this.activeFlowCancellations.delete(cancel);\n };\n\n modal = new IframeModal({\n url,\n host: this.host,\n hideUntilLayout: action === 'sign' && !!phase,\n onClose: () => {\n settle(() => {\n cleanup();\n reject({ code: 'USER_CANCELLED', message: 'User cancelled the request' } as OpenKeyError);\n });\n },\n onMessage: (data: MessageType) => {\n if (settled) return;\n if (data.type === 'openkey:ready') {\n if (readyReceived) return;\n if (phase && data.protocolVersion === 2) {\n if (data.requestId !== phase.requestId) return;\n modal?.activatePhase(phase.requestId, phase.protocolVersion);\n modal?.postMessage({\n ...message,\n ...phase,\n ...(action === 'sign' && isVersionedRequest && outgoingProtocolVersion === 1\n ? { authorizationProtocolVersion: 1 } : {}),\n });\n } else if (phase && data.protocolVersion !== undefined && data.protocolVersion !== 1) {\n return;\n } else {\n // Pre-v2 widgets retain their existing independent modal flow.\n modal?.setExpectedCorrelation(\n isVersionedRequest ? outgoingRequestId as string : null,\n isVersionedRequest ? outgoingProtocolVersion as number : null,\n );\n modal?.revealLegacy();\n modal?.postMessage(message);\n }\n readyReceived = true;\n return;\n }\n if (!readyReceived) return;\n const activeRequestId = phase && modal?.isVersionedPhase()\n ? phase.requestId : isVersionedRequest ? outgoingRequestId : null;\n const activeVersion = phase && modal?.isVersionedPhase()\n ? phase.protocolVersion : isVersionedRequest ? outgoingProtocolVersion : null;\n if (activeRequestId !== null) {\n if (!('requestId' in data) || data.requestId !== activeRequestId ||\n !('protocolVersion' in data) || data.protocolVersion !== activeVersion) return;\n }\n\n if (data.type === 'openkey:close') {\n settle(() => {\n cleanup();\n reject({ code: 'USER_CANCELLED', message: 'User cancelled the request' } as OpenKeyError);\n });\n return;\n }\n\n if (data.type === 'openkey:link-wallet:delegate') {\n this.handleWalletLinkDelegation(modal!, modal?.isVersionedPhase() ? phase : null);\n return;\n }\n\n if (data.type === 'openkey:auth:use-external-wallet') {\n cleanup();\n this.handleExternalWalletConnect<T>(resolve, reject);\n return;\n }\n\n // Consent is gated in the widget; do not discard an authorized\n // result merely because its \"Signing…\" state invalidated geometry.\n if (action === 'sign' && modal?.isVersionedPhase() &&\n !modal.hasDisplayedCurrentPhase() && this.isResponseForAction(action, data)) return;\n if (this.isResponseForAction(action, data)) {\n settle(() => {\n cleanup();\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || 'MANAGED' } as T);\n } else if (data.type === 'openkey:link-wallet:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else if (data.type === 'openkey:sign-out:response') {\n resolve({ requestId: data.requestId, revoked: data.revoked } as T);\n } else if (data.type === 'openkey:externalSign:approve') {\n resolve({\n authorizationContextToken: (data as any).authorizationContextToken,\n previewApprovalToken: (data as any).previewApprovalToken,\n signedMessage: (data as any).signedMessage,\n selectedActionIds: (data as any).selectedActionIds,\n address: (data as any).address,\n } as T);\n } else {\n resolve({\n signature: (data as any).signature,\n address: (data as any).address,\n signedMessage: (data as any).signedMessage,\n selectedActionKeys: (data as any).selectedActionKeys,\n permissions: (data as any).permissions,\n } as T);\n }\n } else {\n reject(data.error);\n }\n });\n }\n },\n });\n\n // Sol final continuation contract requirement 5: bind a versioned\n // request's correlation before the iframe can deliver any message,\n // so every resize for it must carry the exact (requestId,\n // protocolVersion) pair. Unversioned flows stay unbound and accept\n // only uncorrelated resizes from this iframe (TC-647).\n if (isVersionedRequest) {\n modal.setExpectedCorrelation(\n outgoingRequestId as string,\n outgoingProtocolVersion as number,\n );\n }\n\n // Auto-fallback: if no openkey:ready within 3s, fall back to popup\n const readyTimeout = setTimeout(() => {\n if (readyReceived || settled) return;\n cleanup();\n if (isEmbeddedAppWebView()) {\n console.warn('OpenKey: iframe did not load in an embedded app WebView, which cannot open a popup. Add frame-src https://openkey.so to your CSP.');\n settle(() => reject(popupUnavailableError(true)));\n return;\n }\n console.warn('OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.');\n showToast();\n const popupUrl = `${this.host}/widget/${action}?origin=${origin}${passkeysQueryFlag(this.passkeysSupported)}`;\n this.openPopup(action, popupUrl, message, (val: any) => settle(() => resolve(val)), (err: any) => settle(() => reject(err)));\n }, IFRAME_READY_TIMEOUT);\n\n // Overall 5-minute timeout\n const overallTimeout = setTimeout(() => {\n settle(() => {\n cleanup();\n reject({ code: 'TIMEOUT', message: 'Request timed out' } as OpenKeyError);\n });\n }, DEFAULT_TIMEOUT);\n\n cancel = () => {\n settle(() => {\n cleanup();\n reject({ code: 'USER_CANCELLED', message: 'Superseded by sign-out' } as OpenKeyError);\n });\n };\n this.activeFlowCancellations.add(cancel);\n });\n }\n\n private isResponseForAction(action: string, data: MessageType): data is WidgetResponseMessage {\n const type = data.type;\n if (action === 'connect') return type === 'openkey:auth:response';\n if (action === 'sign') return type === 'openkey:sign:response' || type === 'openkey:externalSign:approve';\n if (action === 'sign-typed-data') return type === 'openkey:signTypedData:response';\n if (action === 'link-wallet') return type === 'openkey:link-wallet:response';\n if (action === 'sign-out') return type === 'openkey:sign-out:response';\n return false;\n }\n\n private handleWalletLinkDelegation(modal: IframeModal, phase: { requestId: string; protocolVersion: 2 } | null = null) {\n const sendResult = (result: MessageType) => modal.postMessage({ ...result, ...phase });\n\n new WalletPicker({\n providers: this.discoveredProviders,\n onCancel: () => {\n sendResult({ type: 'openkey:link-wallet:result', success: false, error: { code: 'USER_CANCELLED', message: 'Wallet linking cancelled' } });\n },\n onSelect: async (provider) => {\n try {\n const accounts = await provider.request({ method: 'eth_requestAccounts' }) as string[];\n const address = accounts[0];\n if (!address) throw new Error('No account returned');\n\n // Get challenge from API\n const challengeRes = await fetch(`${this.host}/api/keys/link/challenge`, {\n method: 'POST',\n credentials: 'include',\n });\n if (!challengeRes.ok) throw new Error('Failed to get challenge');\n const { message } = await challengeRes.json();\n\n // Sign challenge with wallet\n const hexMessage = this.toHex(message);\n const signature = await provider.request({\n method: 'personal_sign',\n params: [hexMessage, address],\n }) as string;\n\n // Submit to API\n const linkRes = await fetch(`${this.host}/api/keys/link`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ address, signature, message }),\n });\n if (!linkRes.ok) {\n const err = await linkRes.json();\n throw new Error(err.error || 'Failed to link wallet');\n }\n const { key } = await linkRes.json();\n\n sendResult({ type: 'openkey:link-wallet:result', success: true, address: key.address, keyId: key.id });\n } catch (e: any) {\n sendResult({ type: 'openkey:link-wallet:result', success: false, error: { code: 'UNKNOWN', message: e.message || 'Wallet linking failed' } });\n }\n },\n });\n }\n\n private handleExternalWalletConnect<T>(\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n new WalletPicker({\n providers: this.discoveredProviders,\n onCancel: () => {\n reject({ code: 'USER_CANCELLED', message: 'User cancelled wallet selection' });\n },\n onSelect: async (provider) => {\n try {\n const accounts = await provider.request({ method: 'eth_requestAccounts' }) as string[];\n const address = accounts[0];\n if (!address) throw new Error('No account returned');\n const result = { address, keyId: `external:${address}`, keyType: 'EXTERNAL' as const };\n this.lastAuth = result;\n resolve(result as T);\n } catch (e: any) {\n reject({ code: 'UNKNOWN', message: e.message || 'Failed to connect wallet' });\n }\n },\n });\n }\n\n private openPopup<T>(\n action: string,\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Calculate popup position\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n const popup = window.open(\n url,\n `openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!isUsablePopup(popup)) {\n this.popup = null;\n const embedded = isEmbeddedAppWebView();\n if (!embedded) showToast('Popup was blocked. Please allow popups for this site.', 'error');\n reject(popupUnavailableError(embedded));\n return;\n }\n this.popup = popup;\n\n let settled = false;\n let cancel: () => void;\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearInterval(pollClosed);\n clearTimeout(timeout);\n this.activeFlowCancellations.delete(cancel);\n if (this.popup === popup) this.popup = null;\n };\n\n const settle = (fn: () => void) => {\n if (settled) return;\n settled = true;\n cleanup();\n fn();\n };\n\n // Poll to detect popup close\n const pollClosed = setInterval(() => {\n if (popup.closed) {\n settle(() => reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n }));\n }\n }, 500);\n\n // Timeout\n const timeout = setTimeout(() => {\n settle(() => {\n popup.close();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n });\n }, DEFAULT_TIMEOUT);\n\n // Sol MAJOR-3: correlation IDs for versioned popups.\n const outgoingRequestId = (message as { requestId?: unknown }).requestId;\n const outgoingProtocolVersion = (message as { protocolVersion?: unknown }).protocolVersion;\n const isVersionedRequest =\n typeof outgoingRequestId === 'string' &&\n typeof outgoingProtocolVersion === 'number' &&\n outgoingProtocolVersion >= 1;\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n if (event.source !== popup) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n // Widget is ready, send our request\n popup.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n // Sol MAJOR-4: correlate close messages to the current versioned\n // request. A same-origin, same-source stale close from a\n // previous /widget/sign window MUST NOT tear down the active\n // request. Unversioned close messages are only accepted on\n // unversioned flows (legacy signMessage callers).\n if (isVersionedRequest) {\n const closeRequestId = (data as { requestId?: unknown }).requestId;\n const closeVersion = (data as { protocolVersion?: unknown }).protocolVersion;\n if (closeRequestId !== outgoingRequestId) return;\n if (closeVersion !== outgoingProtocolVersion) return;\n }\n settle(() => {\n popup.close();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n });\n return;\n }\n\n if (data.type === 'openkey:auth:use-external-wallet') {\n settle(() => {\n popup.close();\n this.handleExternalWalletConnect<T>(resolve, reject);\n });\n return;\n }\n\n // Handle responses\n if (this.isResponseForAction(action, data)) {\n // Sol MAJOR-3: drop cross-correlated responses on versioned flows.\n if (isVersionedRequest) {\n const respRequestId = (data as { requestId?: unknown }).requestId;\n const respVersion = (data as { protocolVersion?: unknown }).protocolVersion;\n if (respRequestId !== outgoingRequestId) return;\n if (respVersion !== outgoingProtocolVersion) return;\n }\n settle(() => {\n popup.close();\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || 'MANAGED' } as T);\n } else if (data.type === 'openkey:link-wallet:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else if (data.type === 'openkey:sign-out:response') {\n resolve({ requestId: data.requestId, revoked: data.revoked } as T);\n } else if (data.type === 'openkey:externalSign:approve') {\n // Sol MAJOR-3 (continuation): hand the preview payload back\n // to authorizeTinyCloudExternal so it can invoke the wallet.\n resolve({\n authorizationContextToken: (data as any).authorizationContextToken,\n previewApprovalToken: (data as any).previewApprovalToken,\n signedMessage: (data as any).signedMessage,\n selectedActionIds: (data as any).selectedActionIds,\n address: (data as any).address,\n } as T);\n } else {\n resolve({\n signature: (data as any).signature,\n address: (data as any).address,\n signedMessage: (data as any).signedMessage,\n selectedActionKeys: (data as any).selectedActionKeys,\n permissions: (data as any).permissions,\n } as T);\n }\n } else {\n reject(data.error);\n }\n });\n }\n };\n\n window.addEventListener('message', handleMessage);\n cancel = () => {\n settle(() => {\n popup.close();\n reject({ code: 'USER_CANCELLED', message: 'Superseded by sign-out' });\n });\n };\n this.activeFlowCancellations.add(cancel);\n }\n\n private cancelActiveFlows() {\n for (const cancel of [...this.activeFlowCancellations]) cancel();\n }\n\n /**\n * Disconnect and close any open flows\n */\n disconnect() {\n this.cancelActiveFlows();\n this.popup?.close();\n this.popup = null;\n }\n}\n\n/**\n * EIP-1193 compatible provider that wraps an OpenKey instance.\n * Transparently routes signing to either OpenKey (managed keys) or the user's\n * wallet (external keys).\n */\n\nexport class OpenKeyProvider implements EIP1193Provider {\n private openkey: OpenKey;\n private address: string;\n private keyId: string;\n private keyType: 'MANAGED' | 'EXTERNAL';\n\n constructor(openkey: OpenKey, authResult: AuthResult) {\n this.openkey = openkey;\n this.address = authResult.address;\n this.keyId = authResult.keyId;\n this.keyType = authResult.keyType;\n }\n\n async request({ method, params }: { method: string; params?: any[] }): Promise<any> {\n switch (method) {\n case 'eth_accounts':\n case 'eth_requestAccounts':\n return [this.address];\n\n case 'eth_chainId':\n return '0x1'; // mainnet\n\n case 'personal_sign': {\n if (this.keyType === 'EXTERNAL') {\n try {\n const provider = await this.openkey.findWalletProvider(this.address);\n return await provider.request({ method, params });\n } catch (e: any) {\n showToast(e?.message || 'External wallet signing failed', 'error');\n throw e;\n }\n }\n // Managed key: route through OpenKey\n const message = this.hexToString(params![0] as string);\n const result = await this.openkey.signMessage({ message, keyId: this.keyId });\n return result.signature;\n }\n\n case 'eth_signTypedData_v4': {\n if (this.keyType === 'EXTERNAL') {\n try {\n const provider = await this.openkey.findWalletProvider(this.address);\n return await provider.request({ method, params });\n } catch (e: any) {\n showToast(e?.message || 'External wallet signing failed', 'error');\n throw e;\n }\n }\n const data = JSON.parse(params![1] as string);\n const result = await this.openkey.signTypedData(data);\n return result.signature;\n }\n\n default:\n throw new Error(`Unsupported method: ${method}`);\n }\n }\n\n private hexToString(hex: string): string {\n const bytes = new Uint8Array(\n (hex.startsWith('0x') ? hex.slice(2) : hex).match(/.{1,2}/g)!.map(b => parseInt(b, 16))\n );\n return new TextDecoder().decode(bytes);\n }\n}\n\n// Default export for convenience\nexport default OpenKey;\n\n// Create singleton instance\nlet defaultInstance: OpenKey | null = null;\n\nexport function getOpenKey(config?: OpenKeyConfig): OpenKey {\n if (!defaultInstance || config) {\n defaultInstance = new OpenKey(config);\n }\n return defaultInstance;\n}\n","/**\n * PKCE (Proof Key for Code Exchange) utilities.\n *\n * Uses a pure-JS base64url encoder (no `btoa()`) so it works across\n * Node.js, browsers, and React Native (including Hermes < 0.73).\n *\n * The default SHA-256 implementation uses `crypto.subtle.digest`.\n * For runtimes without Web Crypto, pass a custom `SHA256Fn`.\n */\n\n/**\n * A function that computes the SHA-256 hash of the input string.\n * Return the raw 32-byte digest as a Uint8Array.\n */\nexport type SHA256Fn = (input: string) => Promise<Uint8Array>;\n\n// Standard base64 alphabet lookup table\nconst BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\n/**\n * Pure JS base64url encoding from a Uint8Array.\n * Does NOT use `btoa()` — safe for all runtimes.\n */\nexport function base64UrlEncode(buffer: Uint8Array): string {\n let base64 = '';\n const len = buffer.length;\n\n for (let i = 0; i < len; i += 3) {\n const b0 = buffer[i]!;\n const b1 = i + 1 < len ? buffer[i + 1]! : 0;\n const b2 = i + 2 < len ? buffer[i + 2]! : 0;\n\n const triplet = (b0 << 16) | (b1 << 8) | b2;\n\n base64 += BASE64_CHARS[(triplet >> 18) & 0x3f];\n base64 += BASE64_CHARS[(triplet >> 12) & 0x3f];\n base64 += i + 1 < len ? BASE64_CHARS[(triplet >> 6) & 0x3f] : '';\n base64 += i + 2 < len ? BASE64_CHARS[triplet & 0x3f] : '';\n }\n\n // Convert base64 to base64url: replace + with -, / with _, strip = padding\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\n\n/**\n * Pure JS base64url decoding into a Uint8Array.\n * Accepts unpadded base64url (RFC 4648 §5); ignores `=` padding.\n */\nexport function base64UrlDecode(input: string): Uint8Array {\n const base64 = input.replace(/-/g, '+').replace(/_/g, '/');\n const clean = base64.replace(/=+$/, '');\n const bytes = new Uint8Array(Math.floor((clean.length * 6) / 8));\n\n let accumulator = 0;\n let bits = 0;\n let out = 0;\n for (const char of clean) {\n const value = BASE64_CHARS.indexOf(char);\n if (value < 0) {\n throw new Error(`Invalid base64url character: ${char}`);\n }\n accumulator = (accumulator << 6) | value;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n bytes[out++] = (accumulator >> bits) & 0xff;\n }\n }\n return bytes;\n}\n\n/**\n * Generate a cryptographically random code verifier for PKCE.\n * Returns a 43-character base64url string (32 random bytes).\n */\nexport function generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\n/**\n * Default SHA-256 implementation using Web Crypto API.\n * Available in Node.js 18+, modern browsers, and Hermes 0.73+.\n */\nasync function defaultSha256(input: string): Promise<Uint8Array> {\n const encoder = new TextEncoder();\n const data = encoder.encode(input);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(digest);\n}\n/** SHA-256 using Web Crypto, exported for hashing proof credentials. */\nexport const sha256: SHA256Fn = defaultSha256;\n\n/**\n * Generate the PKCE code challenge from a code verifier.\n *\n * @param verifier - The code verifier string\n * @param sha256 - Optional custom SHA-256 function (for expo-crypto, react-native-quick-crypto, etc.)\n * @returns Base64url-encoded SHA-256 hash of the verifier\n */\nexport async function generateCodeChallenge(\n verifier: string,\n sha256Fn?: SHA256Fn,\n): Promise<string> {\n const hashFn = sha256Fn ?? defaultSha256;\n const digest = await hashFn(verifier);\n return base64UrlEncode(digest);\n}\n\n/**\n * Generate a cryptographically random state parameter.\n * Returns a 22-character base64url string (16 random bytes).\n */\nexport function generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n","/**\n * Unified error codes across all OpenKey SDKs.\n */\nexport type OpenKeyErrorCode =\n | 'USER_CANCELLED'\n | 'POPUP_BLOCKED'\n | 'TIMEOUT'\n | 'NO_KEY'\n | 'UNAUTHORIZED'\n | 'ACCESS_DENIED'\n | 'STATE_MISMATCH'\n | 'NETWORK_ERROR'\n | 'SERVER'\n | 'UNKNOWN';\n\n/**\n * Error class for all OpenKey SDK errors.\n */\nexport class OpenKeyError extends Error {\n constructor(\n public code: OpenKeyErrorCode,\n message: string,\n ) {\n super(message);\n this.name = 'OpenKeyError';\n }\n}\n","import type { AuthTokens } from './types';\nimport { OpenKeyError } from './errors';\n\n// ======= Authorization URL =======\n\nexport interface BuildAuthorizationUrlOptions {\n /** OpenKey host URL (e.g. https://openkey.so) */\n host: string;\n /** OAuth client ID */\n clientId: string;\n /** Redirect URI for callback */\n redirectUri: string;\n /** PKCE code challenge (base64url-encoded) */\n codeChallenge: string;\n /** State parameter for CSRF protection */\n state: string;\n /** OAuth scopes to request */\n scopes: string[];\n /** Authorization endpoint path (default: /api/auth/oauth2/authorize) */\n authorizePath?: string;\n}\n\n/**\n * Build the OAuth 2.1 authorization URL with PKCE parameters.\n */\nexport function buildAuthorizationUrl(options: BuildAuthorizationUrlOptions): string {\n const {\n host,\n clientId,\n redirectUri,\n codeChallenge,\n state,\n scopes,\n authorizePath = '/api/auth/oauth2/authorize',\n } = options;\n\n const url = new URL(authorizePath, host);\n url.searchParams.set('client_id', clientId);\n url.searchParams.set('redirect_uri', redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', codeChallenge);\n url.searchParams.set('code_challenge_method', 'S256');\n\n return url.toString();\n}\n\n// ======= Token Exchange =======\n\nexport interface ExchangeCodeOptions {\n /** OpenKey host URL */\n host: string;\n /** Authorization code from callback */\n code: string;\n /** Redirect URI (must match authorization request) */\n redirectUri: string;\n /** OAuth client ID */\n clientId: string;\n /** PKCE code verifier */\n codeVerifier: string;\n /** RFC 9700 resource indicator (optional) */\n resource?: string;\n /** Token endpoint path (default: /api/auth/oauth2/token) */\n tokenPath?: string;\n}\n\n/**\n * Exchange an authorization code for tokens.\n */\nexport async function exchangeAuthorizationCode(\n options: ExchangeCodeOptions,\n): Promise<AuthTokens> {\n const {\n host,\n code,\n redirectUri,\n clientId,\n codeVerifier,\n resource,\n tokenPath = '/api/auth/oauth2/token',\n } = options;\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n });\n\n if (resource) {\n body.set('resource', resource);\n }\n\n let response: Response;\n try {\n response = await fetch(`${host}${tokenPath}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n } catch (error) {\n throw new OpenKeyError(\n 'NETWORK_ERROR',\n error instanceof Error ? error.message : 'Network request failed',\n );\n }\n\n if (!response.ok) {\n let detail = '';\n try {\n detail = await response.text();\n } catch {\n // ignore\n }\n throw new OpenKeyError(\n 'UNKNOWN',\n `Token exchange failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`,\n );\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n return mapTokenResponse(data);\n}\n\n// ======= Token Refresh =======\n\nexport interface RefreshTokenOptions {\n /** OpenKey host URL */\n host: string;\n /** Refresh token */\n refreshToken: string;\n /** OAuth client ID */\n clientId: string;\n /** RFC 9700 resource indicator (optional) */\n resource?: string;\n /** Token endpoint path (default: /api/auth/oauth2/token) */\n tokenPath?: string;\n}\n\n/**\n * Refresh an access token using a refresh token.\n */\nexport async function refreshAccessToken(\n options: RefreshTokenOptions,\n): Promise<AuthTokens> {\n const {\n host,\n refreshToken,\n clientId,\n resource,\n tokenPath = '/api/auth/oauth2/token',\n } = options;\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n });\n\n if (resource) {\n body.set('resource', resource);\n }\n\n let response: Response;\n try {\n response = await fetch(`${host}${tokenPath}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n } catch (error) {\n throw new OpenKeyError(\n 'NETWORK_ERROR',\n error instanceof Error ? error.message : 'Network request failed',\n );\n }\n\n if (!response.ok) {\n let detail = '';\n try {\n detail = await response.text();\n } catch {\n // ignore\n }\n throw new OpenKeyError(\n 'UNKNOWN',\n `Token refresh failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`,\n );\n }\n\n const data = (await response.json()) as Record<string, unknown>;\n return mapTokenResponse(data);\n}\n\n// ======= Helpers =======\n\n/**\n * Map a snake_case token response to the camelCase AuthTokens interface.\n */\nexport function mapTokenResponse(data: Record<string, unknown>): AuthTokens {\n return {\n accessToken: data.access_token as string,\n idToken: data.id_token as string,\n refreshToken: data.refresh_token as string | undefined,\n expiresIn: data.expires_in as number,\n };\n}\n\n/**\n * Parse an OAuth callback URL and extract the code and state parameters.\n * Returns null if the URL doesn't contain the expected parameters.\n */\nexport function parseOAuthCallback(url: string): { code: string; state: string } | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n\n const code = parsed.searchParams.get('code');\n const state = parsed.searchParams.get('state');\n\n if (!code || !state) {\n return null;\n }\n\n return { code, state };\n}\n","/**\n * Native TinyCloud delegation flow (TC-774 task S1).\n *\n * Platform-agnostic protocol helpers for native sign-in that returns a\n * TinyCloud delegation: RFC 8414 discovery, PKCE/PAR, an Ed25519 session key\n * (did:key), the OpenKey-Session-Proof compact JWS, callback parsing, token\n * response validation, and typed clients for the token, renew and revoke\n * endpoints. Both `@openkey/sdk-capacitor` and `@openkey/sdk-react-native`\n * build on this module.\n * Wire shapes follow `docs/native-tinycloud-delegation.md` (the O1 spec,\n * normative for O2–O5 and TC-774):\n * - PAR: `POST {metadata.pushed_authorization_request_endpoint}` (form)\n * - Token: `POST {metadata.token_endpoint}` (form)\n * - Renew: `POST {metadata.tinycloud_delegation_renew_endpoint}` (form)\n * - Revoke: `POST {metadata.tinycloud_delegation_revocation_endpoint}` (form)\n * - Proof header: `OpenKey-Session-Proof`; JWS `kid` is the session JWK's\n * RFC 7638 thumbprint, compared to the stored `sessionJkt`.\n */\n\nimport { ed25519 } from '@noble/curves/ed25519';\nimport { base64UrlEncode, base64UrlDecode, sha256, type SHA256Fn } from './pkce';\n\n// ======= Errors =======\n\n/** Error codes for the native delegation flow (spec \"SDK mapping\"). */\nexport type OpenKeyNativeErrorCode =\n | 'USER_CANCELLED'\n | 'ACCESS_DENIED'\n | 'STATE_MISMATCH'\n | 'CONSENT_REQUIRED'\n | 'INVALID_GRANT'\n | 'RENEWAL_CONFLICT'\n | 'RENEWAL_TOO_SOON'\n | 'SPACE_UNAVAILABLE'\n | 'INVALID_REQUEST'\n | 'TEMPORARILY_UNAVAILABLE'\n | 'NETWORK'\n | 'STORAGE'\n | 'SERVER'\n | 'NOT_SIGNED_IN'\n | 'UNAVAILABLE';\n\n/** Error class for the native delegation flow. */\nexport class OpenKeyNativeError extends Error {\n constructor(\n public code: OpenKeyNativeErrorCode,\n message: string,\n /** HTTP status when the failure came from the authorization server. */\n public status?: number,\n /** The server's `error` field when it returned a JSON OAuth error. */\n public serverError?: string,\n /** `Retry-After` in whole seconds, when the server sent it (429/503). */\n public retryAfterSeconds?: number,\n ) {\n super(message);\n this.name = 'OpenKeyNativeError';\n }\n\n /**\n * Set when a successful renew (or exchange) response was rejected after\n * the server already rotated the refresh token - delegation validation\n * or `hosting: \"failed\"`. The old token is dead; callers MUST persist\n * this value before reporting the error, or the session is lost.\n */\n public rotatedRefreshToken?: string;\n}\n\n// ======= Injectable fetch =======\n\n/**\n * Minimal `fetch` parameter surface so a stub can satisfy the type without\n * implementing the full `RequestInit`/`Response` contract.\n */\nexport interface NativeFetchInit {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface NativeFetchResponse {\n ok: boolean;\n status: number;\n /** Header lookup for `Retry-After`; case-insensitive like `Headers.get`. */\n headers?: { get(name: string): string | null };\n json(): Promise<unknown>;\n}\n\nexport type NativeFetch = (\n url: string,\n init?: NativeFetchInit,\n) => Promise<NativeFetchResponse>;\n\n/** Sleep function, injectable for tests. */\nexport type SleepFn = (ms: number) => Promise<void>;\n\n// Promise.withResolvers is newer than the ES2022 lib target and older\n// Hermes; keep the executor form.\nconst defaultSleep: SleepFn = (ms) =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/** Parse a `Retry-After` header as whole seconds; `undefined` if absent/invalid. */\nexport function parseRetryAfterSeconds(\n value: string | null | undefined,\n): number | undefined {\n if (value === null || value === undefined) return undefined;\n const seconds = Number.parseInt(value, 10);\n if (!Number.isFinite(seconds) || seconds < 0 || String(seconds) !== value.trim()) {\n return undefined;\n }\n return seconds;\n}\n\n// ======= Wire types =======\n\n/** `session_key` JWK inside `authorization_details` (public half only). */\nexport interface NativeSessionJwk {\n kty: 'OKP';\n crv: 'Ed25519';\n x: string;\n kid?: string;\n}\n\n/** One requested (or granted) TinyCloud capability entry. */\nexport interface NativeDelegationPermission {\n service: string;\n /** TinyCloud space; always `\"applications\"` for the current flow. */\n space: string;\n path?: string;\n actions: string[];\n}\n\n/** The `tinycloud_delegation` authorization-details element (PAR and renew). */\nexport interface TinyCloudDelegationRequest {\n type: 'tinycloud_delegation';\n session_key: NativeSessionJwk;\n permissions: NativeDelegationPermission[];\n ttl_seconds?: number;\n siwe_nonce?: string;\n}\n\n/**\n * The `tinycloud_delegation` object returned inside token and renew\n * responses. Field names mirror the server payload; `expiresAt` is accepted\n * as an ISO string or epoch seconds.\n */\nexport interface TinyCloudDelegation {\n version?: number;\n grantId?: string;\n address?: string;\n chainId?: number;\n ownerDid?: string;\n spaceId?: string;\n /** Verification method id — the session `did:key` with `#` fragment. */\n verificationMethod: string;\n siwe?: string;\n signature?: string;\n delegationHeader?: { Authorization: string };\n delegationCid?: string;\n issuedAt?: string;\n /** ISO timestamp or epoch seconds; must be in the future. */\n expiresAt: string | number;\n renewableUntil?: string | number;\n permissions: NativeDelegationPermission[];\n tinycloudHost: string;\n hosting?: 'existing' | 'created' | 'failed';\n}\n\n/** Validated RFC 8414 metadata for an OpenKey authorization server. */\nexport interface OpenKeyServerMetadata {\n /** The issuer URI exactly as configured (e.g. `https://api.openkey.so/api/auth`). */\n issuer: string;\n /** Origin of the issuer; endpoints are required to live on it. */\n origin: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n pushedAuthorizationRequestEndpoint: string;\n tinycloudDelegationRenewEndpoint: string;\n tinycloudDelegationRevocationEndpoint: string;\n}\n\n/** An Ed25519 session keypair plus its did:key and JWK serializations. */\nexport interface NativeSessionKeypair {\n /** `did:key:z…` (no fragment). */\n did: string;\n /**\n * Full verification-method id, `did:key:z…#z…`. This is the value the\n * server returns as `verificationMethod`. (The proof JWS `kid` is the\n * JWK's RFC 7638 thumbprint, not this value.)\n */\n keyId: string;\n /** Public JWK: `{kty: 'OKP', crv: 'Ed25519', x}` — the wire shape. */\n publicJwk: NativeSessionJwk;\n /** Private JWK: the public JWK plus `d`. */\n privateJwk: NativeSessionJwk & { d: string };\n}\n\n// ======= Constants =======\n\n/** Header carrying the session-proof compact JWS. */\nexport const SESSION_PROOF_HEADER = 'OpenKey-Session-Proof';\nexport const SESSION_PROOF_TYP = 'openkey-session-proof+jwt';\nexport const DELEGATION_SCOPE = 'tinycloud:delegation';\n/**\n * The permission entry every request must carry; the server refuses requests\n * without `tinycloud.capabilities/read`.\n */\nexport const CAPABILITIES_READ_PERMISSION: NativeDelegationPermission = {\n service: 'tinycloud.capabilities',\n space: 'applications',\n path: '',\n actions: ['tinycloud.capabilities/read'],\n};\n/** Scope set sent on PAR when no `extraScopes` are given. */\nexport const DEFAULT_DELEGATION_SCOPES = [\n 'openid',\n 'offline_access',\n DELEGATION_SCOPE,\n];\n\n// ======= Random helpers =======\n\n/**\n * Generate a nonce for `siwe_nonce` / `jti`. Alphanumeric output satisfies\n * the SIWE nonce grammar `[A-Za-z0-9]{8,64}`.\n */\nexport function generateNonce(length = 32): string {\n const alphabet =\n 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n const bytes = new Uint8Array(length);\n crypto.getRandomValues(bytes);\n let nonce = '';\n for (const byte of bytes) nonce += alphabet[byte % alphabet.length]!;\n return nonce;\n}\n\n// ======= Ed25519 session keys =======\n\nconst BASE58_ALPHABET =\n '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\nfunction base58btcEncode(bytes: Uint8Array): string {\n const digits = [0];\n for (const byte of bytes) {\n let carry = byte;\n for (let index = 0; index < digits.length; index += 1) {\n carry += digits[index]! << 8;\n digits[index] = carry % 58;\n carry = Math.floor(carry / 58);\n }\n while (carry > 0) {\n digits.push(carry % 58);\n carry = Math.floor(carry / 58);\n }\n }\n let output = '';\n for (const byte of bytes) {\n if (byte !== 0) break;\n output += BASE58_ALPHABET[0]!;\n }\n for (let index = digits.length - 1; index >= 0; index -= 1) {\n output += BASE58_ALPHABET[digits[index]!]!;\n }\n return output;\n}\n\n/** `did:key` id for a raw Ed25519 public key: multicodec 0xed01 + base58btc. */\nexport function sessionDidForPublicKey(publicKey: Uint8Array): string {\n if (publicKey.length !== 32) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Ed25519 public key must be 32 bytes',\n );\n }\n const identifier = `z${base58btcEncode(\n new Uint8Array([0xed, 0x01, ...publicKey]),\n )}`;\n return `did:key:${identifier}`;\n}\n\n/** Generate a fresh Ed25519 session keypair. */\nexport function generateSessionKeypair(): NativeSessionKeypair {\n const secretKey = ed25519.utils.randomSecretKey();\n const publicKey = ed25519.getPublicKey(secretKey);\n const did = sessionDidForPublicKey(publicKey);\n const keyId = `${did}#${did.slice('did:key:'.length)}`;\n const publicJwk: NativeSessionJwk = {\n kty: 'OKP',\n crv: 'Ed25519',\n x: base64UrlEncode(publicKey),\n };\n return {\n did,\n keyId,\n publicJwk,\n privateJwk: { ...publicJwk, d: base64UrlEncode(secretKey) },\n };\n}\n\n/**\n * RFC 7638 JWK thumbprint of a session public JWK — the value the server\n * stores as `sessionJkt` and requires as the proof JWS `kid`.\n */\nexport async function sessionJktForPublicJwk(\n jwk: Pick<NativeSessionJwk, 'crv' | 'kty' | 'x'>,\n sha256Fn?: SHA256Fn,\n): Promise<string> {\n const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x });\n return base64UrlEncode(await (sha256Fn ?? sha256)(canonical));\n}\n\n/**\n * Rebuild a `NativeSessionKeypair` from a persisted private JWK (d + x).\n * Verifies that `x` is the public key belonging to `d`.\n */\nexport function sessionKeypairFromJwk(\n privateJwk: NativeSessionJwk & { d: string },\n): NativeSessionKeypair {\n const secretKey = base64UrlDecode(privateJwk.d);\n const publicKey = ed25519.getPublicKey(secretKey);\n const x = base64UrlEncode(publicKey);\n if (x !== privateJwk.x) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'session key JWK: x does not match d',\n );\n }\n const did = sessionDidForPublicKey(publicKey);\n const keyId = `${did}#${did.slice('did:key:'.length)}`;\n const publicJwk: NativeSessionJwk = { kty: 'OKP', crv: 'Ed25519', x };\n if (privateJwk.kid !== undefined) publicJwk.kid = privateJwk.kid;\n return {\n did,\n keyId,\n publicJwk,\n privateJwk,\n };\n}\n\n// ======= RFC 8414 discovery =======\n\n/**\n * Build the metadata document URL for an issuer per RFC 8414 §3.1: the\n * `/.well-known/oauth-authorization-server` prefix is inserted between the\n * host and the issuer's path component.\n */\nexport function discoveryUrlForIssuer(issuer: string): string {\n const issuerUrl = new URL(issuer);\n const path = issuerUrl.pathname.replace(/\\/+$/, '');\n return `${issuerUrl.origin}/.well-known/oauth-authorization-server${path}`;\n}\n\nconst REQUIRED_ENDPOINTS = [\n 'authorization_endpoint',\n 'token_endpoint',\n 'pushed_authorization_request_endpoint',\n 'tinycloud_delegation_renew_endpoint',\n 'tinycloud_delegation_revocation_endpoint',\n] as const;\n\n/**\n * Fetch and validate the authorization server metadata.\n *\n * Validation (plan §1.1): HTTPS issuer, `metadata.issuer === issuer`, and\n * every endpoint on the issuer's origin.\n */\nexport async function discoverOpenKeyServer(\n issuer: string,\n fetchFn: NativeFetch = fetch as unknown as NativeFetch,\n): Promise<OpenKeyServerMetadata> {\n let issuerUrl: URL;\n try {\n issuerUrl = new URL(issuer);\n } catch {\n throw new OpenKeyNativeError('SERVER', `issuer is not a URL: ${issuer}`);\n }\n if (issuerUrl.protocol !== 'https:') {\n throw new OpenKeyNativeError(\n 'SERVER',\n `issuer must use https: ${issuer}`,\n );\n }\n\n const metadataUrl = discoveryUrlForIssuer(issuer);\n let response: NativeFetchResponse;\n try {\n response = await fetchFn(metadataUrl, { method: 'GET' });\n } catch (error) {\n throw new OpenKeyNativeError(\n 'NETWORK',\n error instanceof Error ? error.message : 'Discovery request failed',\n );\n }\n if (!response.ok) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `Discovery failed: HTTP ${response.status}`,\n response.status,\n );\n }\n\n let metadata: Record<string, unknown>;\n try {\n metadata = (await response.json()) as Record<string, unknown>;\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Discovery response is not JSON',\n response.status,\n );\n }\n\n if (metadata.issuer !== issuer) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `metadata issuer \"${String(metadata.issuer)}\" does not match \"${issuer}\"`,\n );\n }\n\n const endpointErrors: string[] = [];\n const endpoints: Record<(typeof REQUIRED_ENDPOINTS)[number], string> =\n {} as never;\n for (const field of REQUIRED_ENDPOINTS) {\n const value = metadata[field];\n if (typeof value !== 'string' || value.length === 0) {\n endpointErrors.push(`missing ${field}`);\n continue;\n }\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n endpointErrors.push(`${field} is not a URL`);\n continue;\n }\n if (url.origin !== issuerUrl.origin || url.protocol !== 'https:') {\n endpointErrors.push(`${field} is not on the issuer origin`);\n continue;\n }\n endpoints[field] = value;\n }\n if (endpointErrors.length > 0) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `Invalid server metadata: ${endpointErrors.join('; ')}`,\n );\n }\n\n return {\n issuer,\n origin: issuerUrl.origin,\n authorizationEndpoint: endpoints.authorization_endpoint,\n tokenEndpoint: endpoints.token_endpoint,\n pushedAuthorizationRequestEndpoint:\n endpoints.pushed_authorization_request_endpoint,\n tinycloudDelegationRenewEndpoint:\n endpoints.tinycloud_delegation_renew_endpoint,\n tinycloudDelegationRevocationEndpoint:\n endpoints.tinycloud_delegation_revocation_endpoint,\n };\n}\n\n// ======= authorization_details =======\n\n/**\n * The exact permission array that goes on the wire: the caller's list with\n * `CAPABILITIES_READ_PERMISSION` prepended when missing. Subset validation\n * MUST run against this normalized array, not the raw caller list -\n * otherwise a server grant that includes `tinycloud.capabilities/read`\n * fails validation and a rotated refresh token is discarded.\n */\nexport function normalizeDelegationPermissions(\n permissions: NativeDelegationPermission[],\n): NativeDelegationPermission[] {\n const hasCapabilitiesRead = permissions.some(\n (permission) =>\n permission.service === CAPABILITIES_READ_PERMISSION.service &&\n permission.actions.includes(CAPABILITIES_READ_PERMISSION.actions[0]!),\n );\n return hasCapabilitiesRead\n ? permissions\n : [CAPABILITIES_READ_PERMISSION, ...permissions];\n}\n\n/**\n * Build the `authorization_details` array for a `tinycloud_delegation`\n * request. Used for both PAR and renew. The mandatory\n * `tinycloud.capabilities/read` entry (`CAPABILITIES_READ_PERMISSION`) is\n * prepended when the caller's permissions do not already include it.\n */\nexport function buildAuthorizationDetails(options: {\n sessionKey: NativeSessionKeypair;\n permissions: NativeDelegationPermission[];\n ttlSeconds?: number;\n siweNonce?: string;\n}): TinyCloudDelegationRequest[] {\n const detail: TinyCloudDelegationRequest = {\n type: 'tinycloud_delegation',\n session_key: options.sessionKey.publicJwk,\n permissions: normalizeDelegationPermissions(options.permissions),\n };\n if (options.ttlSeconds !== undefined) detail.ttl_seconds = options.ttlSeconds;\n if (options.siweNonce !== undefined) detail.siwe_nonce = options.siweNonce;\n return [detail];\n}\n\n// ======= PAR =======\n\nexport interface BuildParRequestOptions {\n clientId: string;\n redirectUri: string;\n state: string;\n codeChallenge: string;\n sessionKey: NativeSessionKeypair;\n permissions: NativeDelegationPermission[];\n ttlSeconds?: number;\n siweNonce?: string;\n loginHint?: string;\n loginMethod?: 'email' | 'passkey' | 'google';\n /** Extra scopes appended to `openid offline_access tinycloud:delegation`. */\n extraScopes?: string[];\n}\n\n/**\n * Build the form-encoded PAR body (spec: \"Pushed authorization request\").\n * The caller POSTs it to `metadata.pushedAuthorizationRequestEndpoint` —\n * the client cannot choose the host.\n */\nexport function buildParRequest(options: BuildParRequestOptions): {\n body: string;\n contentType: string;\n} {\n const scopes = options.extraScopes\n ? [...DEFAULT_DELEGATION_SCOPES, ...options.extraScopes]\n : DEFAULT_DELEGATION_SCOPES;\n\n const form = new URLSearchParams();\n form.set('client_id', options.clientId);\n form.set('response_type', 'code');\n form.set('redirect_uri', options.redirectUri);\n form.set('state', options.state);\n form.set('code_challenge', options.codeChallenge);\n form.set('code_challenge_method', 'S256');\n form.set('scope', scopes.join(' '));\n form.set(\n 'authorization_details',\n JSON.stringify(buildAuthorizationDetails(options)),\n );\n if (options.loginMethod !== undefined && !['email', 'passkey', 'google'].includes(options.loginMethod)) {\n throw new OpenKeyNativeError('INVALID_REQUEST', 'Invalid login_method');\n }\n if (options.loginHint !== undefined) {\n const hint = options.loginHint.trim();\n if (hint.length > 254 || /[\\s\\p{C}]/u.test(hint) || (hint.match(/@/g) ?? []).length !== 1 ||\n hint.startsWith('@') || hint.endsWith('@') || options.loginMethod === 'passkey' || options.loginMethod === 'google') {\n throw new OpenKeyNativeError('INVALID_REQUEST', 'Invalid login_hint');\n }\n form.set('login_hint', hint.toLowerCase());\n }\n if (options.loginMethod !== undefined) form.set('login_method', options.loginMethod);\n\n return {\n body: form.toString(),\n contentType: 'application/x-www-form-urlencoded',\n };\n}\n\nexport interface PushedAuthorizationResponse {\n requestUri: string;\n expiresIn: number;\n}\n\n/**\n * POST the PAR body and validate the `201` response.\n */\nexport async function sendParRequest(\n metadata: OpenKeyServerMetadata,\n options: BuildParRequestOptions,\n fetchFn: NativeFetch = fetch as unknown as NativeFetch,\n): Promise<PushedAuthorizationResponse> {\n const { body, contentType } = buildParRequest(options);\n const response = await postForm(\n metadata.pushedAuthorizationRequestEndpoint,\n body,\n contentType,\n fetchFn,\n );\n if (!response.ok) await throwEndpointError(response, 'PAR');\n const data = await readJson(response, 'PAR');\n const requestUri = data.request_uri;\n const expiresIn = data.expires_in;\n if (typeof requestUri !== 'string' || requestUri.length === 0) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'PAR response missing request_uri',\n response.status,\n );\n }\n return {\n requestUri,\n expiresIn: typeof expiresIn === 'number' ? expiresIn : 90,\n };\n}\n\n// ======= Authorize URL =======\n\n/**\n * Build the authorization URL the app opens in ASWebAuthenticationSession /\n * a Custom Tab: `authorization_endpoint?client_id&request_uri`.\n */\nexport function buildNativeAuthorizeUrl(options: {\n authorizationEndpoint: string;\n clientId: string;\n requestUri: string;\n}): string {\n const url = new URL(options.authorizationEndpoint);\n url.searchParams.set('client_id', options.clientId);\n url.searchParams.set('request_uri', options.requestUri);\n return url.toString();\n}\n\n// ======= Callback =======\n\nexport interface NativeCallbackResult {\n code: string;\n state: string;\n /** RFC 9207 issuer from the callback; verified equal to `issuer`. */\n iss: string;\n}\n\n/**\n * Parse the redirect callback URL.\n *\n * `iss` must equal the issuer (RFC 9207; the provider sends it on every\n * redirect, so a missing `iss` also fails) and `state` must match.\n * `error=access_denied` maps to `ACCESS_DENIED`; a state mismatch to\n * `STATE_MISMATCH`; a missing or wrong `iss` to `STATE_MISMATCH` too.\n */\nexport function parseNativeCallback(options: {\n url: string;\n expectedState: string;\n issuer: string;\n}): NativeCallbackResult {\n let parsed: URL;\n try {\n parsed = new URL(options.url);\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n // Never echo the callback URL - it may carry code/state.\n 'Callback URL is not parseable',\n );\n }\n\n let params = parsed.searchParams;\n // Some redirect handlers deliver the response in the fragment.\n if (!params.has('code') && !params.has('error') && parsed.hash.length > 1) {\n const fragment = parsed.hash.startsWith('#')\n ? parsed.hash.slice(1)\n : parsed.hash;\n params = new URLSearchParams(fragment);\n }\n\n const iss = params.get('iss');\n if (!iss || iss !== options.issuer) {\n throw new OpenKeyNativeError(\n 'STATE_MISMATCH',\n // Fixed message: iss is attacker-controlled and the URL carries\n // the auth code.\n 'Callback iss does not match the issuer',\n );\n }\n\n const state = params.get('state');\n if (!state || state !== options.expectedState) {\n throw new OpenKeyNativeError(\n 'STATE_MISMATCH',\n 'Callback state does not match the pending request',\n );\n }\n\n const error = params.get('error');\n if (error) {\n const description = params.get('error_description') ?? undefined;\n throw mapOAuthError(error, description);\n }\n\n const code = params.get('code');\n if (!code) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Callback has neither code nor error',\n );\n }\n\n return { code, state, iss };\n}\n\n/** Map an OAuth `error` value (callback or JSON error body) to a typed error. */\nfunction mapOAuthError(\n error: string,\n description: string | undefined,\n status?: number,\n retryAfterSeconds?: number,\n): OpenKeyNativeError {\n const message = description ? `${error}: ${description}` : error;\n switch (error) {\n case 'access_denied':\n return new OpenKeyNativeError('ACCESS_DENIED', message, status, error);\n case 'consent_required':\n return new OpenKeyNativeError('CONSENT_REQUIRED', message, status, error);\n case 'invalid_grant':\n case 'invalid_session_proof':\n // Terminal, never retried (spec SDK mapping).\n return new OpenKeyNativeError('INVALID_GRANT', message, status, error);\n case 'unauthorized':\n return new OpenKeyNativeError('NOT_SIGNED_IN', message, status, error);\n case 'renewal_conflict':\n return new OpenKeyNativeError('RENEWAL_CONFLICT', message, status, error);\n case 'space_unavailable':\n return new OpenKeyNativeError('SPACE_UNAVAILABLE', message, status, error);\n case 'renewal_too_soon':\n return new OpenKeyNativeError(\n 'RENEWAL_TOO_SOON',\n message,\n status,\n error,\n retryAfterSeconds,\n );\n case 'temporarily_unavailable':\n return new OpenKeyNativeError(\n 'TEMPORARILY_UNAVAILABLE',\n message,\n status,\n error,\n retryAfterSeconds,\n );\n default:\n return new OpenKeyNativeError('SERVER', message, status, error);\n }\n}\n\n// ======= Session proof JWS =======\n\nexport interface SessionProofOptions {\n sessionKey: NativeSessionKeypair;\n /** HTTP method of the request this proof authenticates (`htm`). */\n htm: string;\n /** Exact endpoint URL from metadata (`htu`). */\n htu: string;\n clientId: string;\n /** The credential being presented; hashed into `cred_hash`. */\n credential: string;\n /** For runtimes without Web Crypto. */\n sha256Fn?: SHA256Fn;\n}\n\n/**\n * Sign an `OpenKey-Session-Proof` compact JWS.\n *\n * Header: `{typ: \"openkey-session-proof+jwt\", alg: \"EdDSA\", kid}` where `kid`\n * is the RFC 7638 JWK thumbprint of the session public JWK — the value the\n * server stores as `sessionJkt` at PAR.\n * Payload: exactly `{jti, iat, htm, htu, client_id, cred_hash}` where\n * `cred_hash` is `b64url(sha256(credential))`.\n */\nexport async function signSessionProof(\n options: SessionProofOptions,\n): Promise<string> {\n const hash = options.sha256Fn ?? sha256;\n const header = {\n typ: SESSION_PROOF_TYP,\n alg: 'EdDSA',\n kid: await sessionJktForPublicJwk(options.sessionKey.publicJwk, hash),\n };\n const payload = {\n jti: generateNonce(),\n iat: Math.floor(Date.now() / 1000),\n htm: options.htm,\n htu: options.htu,\n client_id: options.clientId,\n cred_hash: base64UrlEncode(await hash(options.credential)),\n };\n\n const encoder = new TextEncoder();\n const signingInput = `${base64UrlEncode(\n encoder.encode(JSON.stringify(header)),\n )}.${base64UrlEncode(encoder.encode(JSON.stringify(payload)))}`;\n\n const secretKey = base64UrlDecode(options.sessionKey.privateJwk.d);\n const signature = ed25519.sign(encoder.encode(signingInput), secretKey);\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\n// ======= Token response validation =======\n\nfunction parseExpiresAt(value: string | number, field: string): number {\n if (typeof value === 'number') {\n // Seconds vs milliseconds heuristic: seconds values past ~2286 exceed 1e11.\n // Non-finite and non-positive values are malformed - reject (NaN <= now is\n // false and would otherwise pass the future check).\n if (!Number.isFinite(value) || value <= 0) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.${field} is not a valid epoch or ISO time`,\n );\n }\n return value > 1e11 ? value : value * 1000;\n }\n if (typeof value === 'string') {\n const parsed = Date.parse(value);\n if (!Number.isNaN(parsed)) return parsed;\n }\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.${field} is not a valid epoch or ISO time`,\n );\n}\n\n/** True when `grantedPath` is covered by `requestedPath` (prefix grants end in `/`). */\nfunction pathCoveredBy(\n grantedPath: string | undefined,\n requestedPath: string | undefined,\n): boolean {\n if (requestedPath === undefined || requestedPath === '') return true;\n if (grantedPath === undefined || grantedPath === '') return false;\n if (requestedPath.endsWith('/')) return grantedPath.startsWith(requestedPath);\n return grantedPath === requestedPath;\n}\n\n/** True when every granted permission is covered by a requested permission. */\nexport function isPermissionSubset(\n granted: NativeDelegationPermission[],\n requested: NativeDelegationPermission[],\n): boolean {\n return granted.every((grant) =>\n requested.some(\n (request) =>\n request.service === grant.service &&\n request.space === grant.space &&\n pathCoveredBy(grant.path, request.path) &&\n grant.actions.every((action) => request.actions.includes(action)),\n ),\n );\n}\n\nfunction isPermission(value: unknown): value is NativeDelegationPermission {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const permission = value as Record<string, unknown>;\n return (\n typeof permission.service === 'string' &&\n typeof permission.space === 'string' &&\n (permission.path === undefined ||\n typeof permission.path === 'string') &&\n Array.isArray(permission.actions) &&\n permission.actions.every((action) => typeof action === 'string')\n );\n}\n\nexport interface ValidateDelegationOptions {\n sessionKey: NativeSessionKeypair;\n /** The permission set sent in `authorization_details`, for the subset check. */\n requestedPermissions: NativeDelegationPermission[];\n /**\n * The TinyCloud node the client expects (spec: `tinycloudHost` must be\n * \"the host it expects\"). Required: validation fails closed without it.\n * Exact-match against `delegation.tinycloudHost`.\n */\n expectedTinycloudHost: string;\n}\n\n/**\n * Validate the `tinycloud_delegation` object from a token or renew response:\n * `verificationMethod` equals the session DID, `expiresAt` is in the future,\n * permissions are a subset of what was requested, and `tinycloudHost` is an\n * https URL. Throws `OpenKeyNativeError` (code `SERVER`) on any violation.\n */\nexport function validateTinyCloudDelegation(\n value: unknown,\n options: ValidateDelegationOptions,\n): TinyCloudDelegation {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'response missing tinycloud_delegation',\n );\n }\n const delegation = value as TinyCloudDelegation;\n\n if (delegation.verificationMethod !== options.sessionKey.keyId) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `verificationMethod \"${String(\n delegation.verificationMethod,\n )}\" does not match session key \"${options.sessionKey.keyId}\"`,\n );\n }\n\n if (delegation.expiresAt === undefined || delegation.expiresAt === null) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.expiresAt is missing',\n );\n }\n const expiresAt = parseExpiresAt(delegation.expiresAt, 'expiresAt');\n if (expiresAt <= Date.now()) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation is already expired',\n );\n }\n\n if (!Array.isArray(delegation.permissions)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.permissions is missing',\n );\n }\n if (!delegation.permissions.every(isPermission)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.permissions has an invalid entry',\n );\n }\n if (\n !isPermissionSubset(delegation.permissions, options.requestedPermissions)\n ) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'granted permissions are not a subset of the requested permissions',\n );\n }\n\n if (\n typeof delegation.tinycloudHost !== 'string' ||\n delegation.tinycloudHost.length === 0\n ) {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.tinycloudHost is missing',\n );\n }\n let host: URL;\n try {\n host = new URL(delegation.tinycloudHost);\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'tinycloud_delegation.tinycloudHost is not a URL',\n );\n }\n if (host.protocol !== 'https:' && host.hostname !== 'localhost') {\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.tinycloudHost must be https: ${delegation.tinycloudHost}`,\n );\n }\n if (\n typeof options.expectedTinycloudHost !== 'string' ||\n options.expectedTinycloudHost.length === 0\n ) {\n // Fail closed: the host check must not silently vanish.\n throw new OpenKeyNativeError(\n 'SERVER',\n 'expectedTinycloudHost is required to validate tinycloud_delegation',\n );\n }\n if (delegation.tinycloudHost !== options.expectedTinycloudHost) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `tinycloud_delegation.tinycloudHost \"${delegation.tinycloudHost}\" is not the expected host \"${options.expectedTinycloudHost}\"`,\n );\n }\n\n return delegation;\n}\n\n// ======= Endpoint clients =======\n\nasync function postForm(\n url: string,\n body: string,\n contentType: string,\n fetchFn: NativeFetch,\n headers?: Record<string, string>,\n): Promise<NativeFetchResponse> {\n try {\n return await fetchFn(url, {\n method: 'POST',\n headers: { 'Content-Type': contentType, ...headers },\n body,\n });\n } catch (error) {\n throw new OpenKeyNativeError(\n 'NETWORK',\n error instanceof Error ? error.message : 'Network request failed',\n );\n }\n}\n\nasync function readJson(\n response: NativeFetchResponse,\n what: string,\n): Promise<Record<string, unknown>> {\n let data: unknown;\n try {\n data = await response.json();\n } catch {\n throw new OpenKeyNativeError(\n 'SERVER',\n `${what} response is not JSON (HTTP ${response.status})`,\n response.status,\n );\n }\n if (!data || typeof data !== 'object' || Array.isArray(data)) {\n throw new OpenKeyNativeError(\n 'SERVER',\n `${what} response is not a JSON object (HTTP ${response.status})`,\n response.status,\n );\n }\n return data as Record<string, unknown>;\n}\n\n/** On a non-ok response, map the JSON `error` field, else a status error. */\nasync function throwEndpointError(\n response: NativeFetchResponse,\n what: string,\n /**\n * Extra `error` values this endpoint maps to INVALID_GRANT (spec SDK\n * table: provider code-exchange failures are terminal). Scoped to the\n * token endpoint - PAR emits `invalid_request`/`invalid_client` for\n * different causes, which stay SERVER.\n */\n invalidGrantErrors?: readonly string[],\n): Promise<never> {\n const retryAfterSeconds = parseRetryAfterSeconds(\n response.headers?.get('Retry-After'),\n );\n try {\n const data = (await response.json()) as Record<string, unknown>;\n if (typeof data.error === 'string' && data.error.length > 0) {\n const description =\n typeof data.error_description === 'string'\n ? data.error_description\n : undefined;\n if (invalidGrantErrors?.includes(data.error)) {\n throw new OpenKeyNativeError(\n 'INVALID_GRANT',\n description ? `${data.error}: ${description}` : data.error,\n response.status,\n data.error,\n retryAfterSeconds,\n );\n }\n throw mapOAuthError(\n data.error,\n description,\n response.status,\n retryAfterSeconds,\n );\n }\n } catch (error) {\n if (error instanceof OpenKeyNativeError) throw error;\n // fall through to status error\n }\n throw new OpenKeyNativeError(\n 'SERVER',\n `${what} failed: HTTP ${response.status}`,\n response.status,\n undefined,\n retryAfterSeconds,\n );\n}\n\n// ----- Token exchange -----\n\nexport interface ExchangeNativeCodeOptions {\n metadata: OpenKeyServerMetadata;\n code: string;\n redirectUri: string;\n clientId: string;\n codeVerifier: string;\n sessionKey: NativeSessionKeypair;\n requestedPermissions: NativeDelegationPermission[];\n /** Expected TinyCloud node, exact-matched against `tinycloudHost`. Required - fails closed. */\n expectedTinycloudHost: string;\n fetchFn?: NativeFetch;\n sha256Fn?: SHA256Fn;\n /** Persist the issued refresh token before delegation validation or returning it. */\n onRefreshToken?: (refreshToken: string) => Promise<void>;\n}\n\nexport interface NativeTokenResult {\n accessToken: string;\n refreshToken: string;\n /** `expires_in`: access-token lifetime (300 s per spec). */\n expiresIn?: number;\n /** `expires_at`: access-token expiry in Unix seconds, when present. */\n accessTokenExpiresAt?: number;\n /** Raw `authorization_details` echo from the server, if present. */\n authorizationDetails?: unknown;\n /** Validated `tinycloud_delegation` payload. */\n delegation: TinyCloudDelegation;\n}\n\n/**\n * Exchange an authorization code for tokens plus the TinyCloud delegation\n * (spec: \"Code exchange\"). Sends `OpenKey-Session-Proof` with `cred_hash =\n * b64url(sha256(code))` and validates the delegation in the response.\n *\n * Never retried: any failed exchange spends the code — including a 503 —\n * so errors surface to the caller, which must start a fresh sign-in.\n * A successful response with `hosting: \"failed\"` throws `SPACE_UNAVAILABLE`.\n */\nexport async function exchangeDelegationCode(\n options: ExchangeNativeCodeOptions,\n): Promise<NativeTokenResult> {\n const fetchFn =\n options.fetchFn ?? (fetch as unknown as NativeFetch);\n const htu = options.metadata.tokenEndpoint;\n const proof = await signSessionProof({\n sessionKey: options.sessionKey,\n htm: 'POST',\n htu,\n clientId: options.clientId,\n credential: options.code,\n sha256Fn: options.sha256Fn,\n });\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code: options.code,\n redirect_uri: options.redirectUri,\n client_id: options.clientId,\n code_verifier: options.codeVerifier,\n });\n\n const response = await postForm(\n htu,\n body.toString(),\n 'application/x-www-form-urlencoded',\n fetchFn,\n { [SESSION_PROOF_HEADER]: proof },\n );\n if (!response.ok) {\n await throwEndpointError(response, 'Token exchange', [\n 'invalid_request',\n 'invalid_client',\n 'invalid_verification',\n ]);\n }\n\n const data = await readJson(response, 'Token exchange');\n const issuedRefreshToken = typeof data.refresh_token === 'string' ? data.refresh_token : undefined;\n if (issuedRefreshToken) await options.onRefreshToken?.(issuedRefreshToken);\n if (\n typeof data.access_token !== 'string' ||\n typeof data.refresh_token !== 'string'\n ) {\n const error = new OpenKeyNativeError(\n 'SERVER',\n 'Token response missing access_token or refresh_token',\n response.status,\n );\n error.rotatedRefreshToken = issuedRefreshToken;\n throw error;\n }\n\n let delegation: TinyCloudDelegation;\n try {\n delegation = validateTinyCloudDelegation(\n data.tinycloud_delegation,\n {\n sessionKey: options.sessionKey,\n // Same normalized array sent on PAR - includes capabilities/read.\n requestedPermissions: normalizeDelegationPermissions(\n options.requestedPermissions,\n ),\n expectedTinycloudHost: options.expectedTinycloudHost,\n },\n );\n // hosting: \"failed\" means the delegation can't reach the space:\n // terminal for this session (spec SDK mapping -> SPACE_UNAVAILABLE).\n if (delegation.hosting === 'failed') {\n throw new OpenKeyNativeError(\n 'SPACE_UNAVAILABLE',\n 'Delegation was issued but hosting the applications space failed',\n response.status,\n );\n }\n } catch (error) {\n // The code was exchanged: tokens are live server-side. Surface the\n // refresh token so the caller can persist it rather than lose the\n // session (it is valid even though this delegation was rejected).\n if (error instanceof OpenKeyNativeError) {\n error.rotatedRefreshToken = data.refresh_token;\n }\n throw error;\n }\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresIn:\n typeof data.expires_in === 'number' ? data.expires_in : undefined,\n accessTokenExpiresAt:\n typeof data.expires_at === 'number' ? data.expires_at : undefined,\n authorizationDetails: data.authorization_details,\n delegation,\n };\n}\n\n// ----- Renew -----\n\nexport interface RenewDelegationOptions {\n metadata: OpenKeyServerMetadata;\n clientId: string;\n refreshToken: string;\n sessionKey: NativeSessionKeypair;\n /** Permissions granted on the last delegation; used for the subset check. */\n requestedPermissions: NativeDelegationPermission[];\n /** Optional `siwe_nonce` form field (PAR rules: `[A-Za-z0-9]{8,64}`). */\n siweNonce?: string;\n /**\n * Optional permission subset sent as the `authorization_details` form\n * field (PAR shape, same session key, no `ttl_seconds`).\n */\n permissionsSubset?: NativeDelegationPermission[];\n /** Expected TinyCloud node, exact-matched against `tinycloudHost`. Required - fails closed. */\n expectedTinycloudHost: string;\n fetchFn?: NativeFetch;\n sha256Fn?: SHA256Fn;\n /** Injectable sleep for the spec-mandated `Retry-After` waits. */\n sleepFn?: SleepFn;\n}\n\nexport interface RenewDelegationResult {\n refreshToken: string;\n expiresIn?: number;\n delegation: TinyCloudDelegation;\n}\n\n/**\n * Renew a delegation (spec: \"Renewal\"): `POST\n * {metadata.tinycloudDelegationRenewEndpoint}` as form-encoded\n * `client_id=…&refresh_token=…[&siwe_nonce=…][&authorization_details=…]`\n * with `OpenKey-Session-Proof` where `cred_hash =\n * b64url(sha256(refresh_token))`.\n *\n * Per the spec's SDK mapping: on 429 `renewal_too_soon` waits\n * `Retry-After` seconds and retries once with the same token (no generic\n * backoff); on 503 `temporarily_unavailable` waits `Retry-After` and\n * retries once with a fresh proof. `invalid_session_proof`,\n * `invalid_grant`, `consent_required` and `access_denied` are terminal and\n * never retried. `hosting: \"failed\"` throws `SPACE_UNAVAILABLE`.\n *\n * The server rotates the refresh token on every success. If validation of\n * the returned delegation fails afterwards, the thrown error carries\n * `rotatedRefreshToken`: the old token is dead, so the caller MUST persist\n * it before reporting the error or the session is lost.\n */\nexport async function renewDelegation(\n options: RenewDelegationOptions,\n): Promise<RenewDelegationResult> {\n const fetchFn =\n options.fetchFn ?? (fetch as unknown as NativeFetch);\n const sleep = options.sleepFn ?? defaultSleep;\n const htu = options.metadata.tinycloudDelegationRenewEndpoint;\n\n // One normalized set: the exact array sent on the wire (including the\n // prepended capabilities/read) is what the grant is subset-checked\n // against - otherwise a 2xx renew that already rotated the refresh\n // token is rejected and the new token is lost.\n const requestedPermissions = normalizeDelegationPermissions(\n options.permissionsSubset ?? options.requestedPermissions,\n );\n // ttl_seconds; siwe_nonce travels as its own form field.\n const authorizationDetails = options.permissionsSubset\n ? buildAuthorizationDetails({\n sessionKey: options.sessionKey,\n permissions: options.permissionsSubset,\n })\n : undefined;\n\n const body = new URLSearchParams({\n client_id: options.clientId,\n refresh_token: options.refreshToken,\n });\n if (options.siweNonce !== undefined) {\n body.set('siwe_nonce', options.siweNonce);\n }\n if (authorizationDetails !== undefined) {\n body.set('authorization_details', JSON.stringify(authorizationDetails));\n }\n\n let retriedTooSoon = false;\n let retriedUnavailable = false;\n let response: NativeFetchResponse;\n for (;;) {\n // Fresh proof per attempt: `iat` must be within ±60 s.\n response = await postForm(\n htu,\n body.toString(),\n 'application/x-www-form-urlencoded',\n fetchFn,\n {\n [SESSION_PROOF_HEADER]: await signSessionProof({\n sessionKey: options.sessionKey,\n htm: 'POST',\n htu,\n clientId: options.clientId,\n credential: options.refreshToken,\n sha256Fn: options.sha256Fn,\n }),\n },\n );\n if (response.ok) break;\n\n let thrown: unknown;\n try {\n await throwEndpointError(response, 'Renew');\n } catch (caught) {\n thrown = caught;\n }\n if (!(thrown instanceof OpenKeyNativeError)) throw thrown;\n\n if (thrown.code === 'RENEWAL_TOO_SOON' && !retriedTooSoon) {\n retriedTooSoon = true;\n // Wait Retry-After seconds; the spec guarantees ≥ 1.\n await sleep(Math.max(thrown.retryAfterSeconds ?? 1, 1) * 1000);\n continue;\n }\n if (thrown.code === 'TEMPORARILY_UNAVAILABLE' && !retriedUnavailable) {\n retriedUnavailable = true;\n await sleep(Math.max(thrown.retryAfterSeconds ?? 2, 1) * 1000);\n continue;\n }\n throw thrown;\n }\n\n const data = await readJson(response, 'Renew');\n if (typeof data.refresh_token !== 'string') {\n throw new OpenKeyNativeError(\n 'SERVER',\n 'Renew response missing refresh_token',\n response.status,\n );\n }\n\n let delegation: TinyCloudDelegation;\n try {\n delegation = validateTinyCloudDelegation(data.tinycloud_delegation, {\n sessionKey: options.sessionKey,\n requestedPermissions,\n expectedTinycloudHost: options.expectedTinycloudHost,\n });\n if (delegation.hosting === 'failed') {\n throw new OpenKeyNativeError(\n 'SPACE_UNAVAILABLE',\n 'Renewed delegation could not reach the applications space',\n response.status,\n );\n }\n } catch (error) {\n // The refresh token was already rotated: never discard it. Surface it\n // on the error so the caller can persist it before reporting.\n if (error instanceof OpenKeyNativeError) {\n error.rotatedRefreshToken = data.refresh_token;\n }\n throw error;\n }\n\n return {\n refreshToken: data.refresh_token,\n expiresIn:\n typeof data.expires_in === 'number' ? data.expires_in : undefined,\n delegation,\n };\n}\n\n// ----- Revoke -----\n\nexport interface RevokeDelegationOptions {\n metadata: OpenKeyServerMetadata;\n clientId: string;\n refreshToken: string;\n sessionKey: NativeSessionKeypair;\n fetchFn?: NativeFetch;\n sha256Fn?: SHA256Fn;\n /** Injectable sleep for the spec-mandated `Retry-After` wait. */\n sleepFn?: SleepFn;\n}\n\n/**\n * Revoke a grant (spec: \"Native revocation\"): `POST\n * {metadata.tinycloudDelegationRevocationEndpoint}` as form-encoded\n * `client_id=…&refresh_token=…` with `OpenKey-Session-Proof` where\n * `cred_hash = b64url(sha256(refresh_token))`.\n *\n * Per the spec's SDK mapping: on 503 `temporarily_unavailable` waits\n * `Retry-After` and retries once with the same token and a fresh proof.\n * `401 invalid_session_proof` (bad proof or unknown token) is terminal —\n * it maps to `INVALID_GRANT` and is never retried.\n *\n * Callers (SDK `signOut`): wipe the local session key and tokens even when\n * this rejects with a terminal error — per spec, a terminal revoke failure\n * means the server-side grant is already unusable. Core owns no storage,\n * so the wipe lives in the SDK layer.\n */\nexport async function revokeDelegation(\n options: RevokeDelegationOptions,\n): Promise<void> {\n const fetchFn =\n options.fetchFn ?? (fetch as unknown as NativeFetch);\n const sleep = options.sleepFn ?? defaultSleep;\n const htu = options.metadata.tinycloudDelegationRevocationEndpoint;\n const body = new URLSearchParams({\n client_id: options.clientId,\n refresh_token: options.refreshToken,\n }).toString();\n\n let retriedUnavailable = false;\n for (;;) {\n const response = await postForm(\n htu,\n body,\n 'application/x-www-form-urlencoded',\n fetchFn,\n {\n [SESSION_PROOF_HEADER]: await signSessionProof({\n sessionKey: options.sessionKey,\n htm: 'POST',\n htu,\n clientId: options.clientId,\n credential: options.refreshToken,\n sha256Fn: options.sha256Fn,\n }),\n },\n );\n if (response.ok) return;\n\n let thrown: unknown;\n try {\n await throwEndpointError(response, 'Revoke');\n } catch (caught) {\n thrown = caught;\n }\n if (!(thrown instanceof OpenKeyNativeError)) throw thrown;\n if (\n thrown.code === 'TEMPORARILY_UNAVAILABLE' &&\n !retriedUnavailable\n ) {\n retriedUnavailable = true;\n await sleep(Math.max(thrown.retryAfterSeconds ?? 2, 1) * 1000);\n continue;\n }\n throw thrown;\n }\n}\n\n/**\n * True when the delegation should be renewed immediately — i.e. less than\n * the spec's renewal lead time remains on `expiresAt`. The lead is\n * `min(10 min, lifetime/4)` where `lifetime` comes from `issuedAt`; when\n * `issuedAt` is absent the full 10-minute lead is used. Call this after\n * sign-in (and on boot restore) to decide whether to renew now.\n */\nexport function delegationNeedsRenewalNow(\n delegation: Pick<TinyCloudDelegation, 'expiresAt' | 'issuedAt'>,\n now: number = Date.now(),\n): boolean {\n const expiresAt = parseExpiresAt(delegation.expiresAt, 'expiresAt');\n const issuedAt =\n typeof delegation.issuedAt === 'string'\n ? Date.parse(delegation.issuedAt)\n : Number.NaN;\n const lifetimeMs =\n Number.isFinite(issuedAt) && issuedAt < expiresAt\n ? expiresAt - issuedAt\n : Number.POSITIVE_INFINITY;\n const leadMs = Math.min(10 * 60 * 1000, lifetimeMs / 4);\n return expiresAt - now < leadMs;\n}\n","// OpenKey Nostr signer - browser client for third-party apps.\n//\n// Structurally separate from the Ethereum/EIP-1193 flows in index.ts:\n// - Nostr identities are secp256k1 Schnorr (BIP-340/NIP-01), never routed\n// through the ECDSA sign/signTypedData paths above.\n// - The OpenKey session and any signing grants never leave the\n// OpenKey-origin iframe. Unlike `openkey:auth:response`, none of the\n// postMessage payloads below ever carry a sessionToken.\n// - Every iframe created here uses a canonical, non-wildcard `host` origin\n// for both `postMessage` targeting and incoming-message verification\n// (`event.origin === host && event.source === iframe.contentWindow`).\n\n/** NIP-01 unsigned event template. */\nexport interface UnsignedNostrEvent {\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n}\n\n/** A fully signed NIP-01 event, as returned by OpenKey custody signing. */\nexport interface SignedNostrEvent extends UnsignedNostrEvent {\n id: string;\n sig: string;\n}\n\n/** Public descriptor for the user's OpenKey-custodied Nostr identity. Never includes the secret key. */\nexport interface NostrIdentity {\n keyId: string;\n pubkey: string;\n npub: string;\n}\n\nexport interface NostrError {\n code: 'USER_CANCELLED' | 'TIMEOUT' | 'INTERACTION_REQUIRED' | 'UNAUTHORIZED' | 'UNKNOWN';\n message: string;\n}\n\ntype NostrFrameMessage =\n | { type: 'openkey:ready'; protocolVersion: number }\n | { type: 'openkey:close'; requestId: string; protocolVersion: number }\n | { type: 'openkey:resize'; requestId: string; protocolVersion: number; height: number }\n | { type: 'openkey:nostr:show'; requestId: string; protocolVersion: number }\n | { type: 'openkey:nostr:connect:response'; requestId: string; protocolVersion: number; success: true; keyId: string; pubkey: string; npub: string }\n | { type: 'openkey:nostr:connect:response'; requestId: string; protocolVersion: number; success: false; error: NostrError }\n | { type: 'openkey:nostr:sign:response'; requestId: string; protocolVersion: number; success: true; event: SignedNostrEvent }\n | { type: 'openkey:nostr:sign:response'; requestId: string; protocolVersion: number; success: false; error: NostrError };\n\nconst NOSTR_FRAME_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes, matches the Ethereum widget flow\nconst NOSTR_PROTOCOL_VERSION = 1;\n\nfunction newRequestId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') {\n throw new Error('Secure request-id generation is unavailable.');\n }\n const bytes = new Uint32Array(4);\n crypto.getRandomValues(bytes);\n return `nostr-${Array.from(bytes, value => value.toString(16)).join('')}`;\n}\n\n/**\n * One ephemeral iframe per request. It is mounted off-screen at ~0 size and\n * only expanded into a visible, backdrop-covered modal when the OpenKey\n * widget page explicitly asks for it (`openkey:nostr:show`) - e.g. first-time\n * identity consent, a relay-scoped connect grant, or the first kind:9 message\n * to a new client origin. A silent signing request already covered by an\n * existing grant never sends `show`, so the user sees nothing at all.\n *\n * The hidden/visible states are pure CSS toggles on a container that always\n * holds the same `<iframe>` element - the iframe itself is never removed or\n * reparented, because doing so would reload it (and drop its in-flight\n * request state) in most browsers.\n */\nclass NostrFrame {\n private root: HTMLDivElement;\n private shadow: ShadowRoot;\n private iframe: HTMLIFrameElement;\n private outer: HTMLDivElement;\n private host: string;\n private messageHandler: (event: MessageEvent) => void = () => {};\n private revealed = false;\n private requestedHeight = 400;\n private viewportHandler = () => {\n this.syncViewport();\n this.applyFrameHeight();\n };\n\n constructor(host: string, url: string) {\n this.host = host;\n this.root = document.createElement('div');\n this.shadow = this.root.attachShadow({ mode: 'closed' });\n\n const style = document.createElement('style');\n style.textContent = `\n :host{all:initial}\n .nf-outer{position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}\n .nf-outer.nf-visible{position:fixed;top:var(--nf-viewport-top,0px);left:var(--nf-viewport-left,0px);width:var(--nf-viewport-width,100vw);height:var(--nf-viewport-height,100dvh);box-sizing:border-box;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));overflow:auto;overscroll-behavior:contain;opacity:1;pointer-events:auto;background:rgba(0,0,0,0.42);z-index:999999;display:flex;align-items:center;justify-content:center}\n .nf-card{width:1px;height:1px;overflow:hidden}\n .nf-visible .nf-card{background:#fafafa;width:min(400px,100%);height:auto;max-height:100%;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);overflow:auto;flex:0 1 auto}\n iframe{border:none;width:1px;height:1px;display:block;transition:height 200ms ease}\n .nf-visible iframe{width:100%;height:400px;max-height:100%}\n @media (prefers-reduced-motion:reduce){iframe{transition:none}}\n `;\n\n this.outer = document.createElement('div');\n this.outer.className = 'nf-outer';\n this.outer.setAttribute('role', 'dialog');\n this.outer.setAttribute('aria-modal', 'true');\n this.outer.setAttribute('aria-label', 'OpenKey approval');\n this.outer.setAttribute('aria-hidden', 'true');\n\n const card = document.createElement('div');\n card.className = 'nf-card';\n\n this.iframe = document.createElement('iframe');\n this.iframe.src = url;\n this.iframe.title = 'OpenKey approval';\n this.iframe.setAttribute('allow', 'clipboard-write *; publickey-credentials-get *; publickey-credentials-create *');\n this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox');\n\n card.appendChild(this.iframe);\n this.outer.appendChild(card);\n this.shadow.appendChild(style);\n this.shadow.appendChild(this.outer);\n document.body.appendChild(this.root);\n\n this.syncViewport();\n window.addEventListener('resize', this.viewportHandler);\n window.visualViewport?.addEventListener('resize', this.viewportHandler);\n window.visualViewport?.addEventListener('scroll', this.viewportHandler);\n }\n\n onMessage(handler: (data: NostrFrameMessage) => void) {\n this.messageHandler = (event: MessageEvent) => {\n // Canonical, non-wildcard origin only; exact window-identity match.\n if (event.origin !== this.host) return;\n if (event.source !== this.iframe.contentWindow) return;\n handler(event.data as NostrFrameMessage);\n };\n window.addEventListener('message', this.messageHandler);\n }\n\n reveal() {\n if (this.revealed) return;\n this.revealed = true;\n this.syncViewport();\n this.outer.classList.add('nf-visible');\n this.outer.setAttribute('aria-hidden', 'false');\n this.applyFrameHeight();\n }\n\n resize(height: number) {\n if (!Number.isFinite(height) || height <= 0) return;\n this.requestedHeight = height;\n if (this.revealed) this.applyFrameHeight();\n }\n\n private syncViewport() {\n const viewport = window.visualViewport;\n this.outer.style.setProperty('--nf-viewport-top', `${viewport?.offsetTop ?? 0}px`);\n this.outer.style.setProperty('--nf-viewport-left', `${viewport?.offsetLeft ?? 0}px`);\n this.outer.style.setProperty('--nf-viewport-width', `${viewport?.width ?? window.innerWidth}px`);\n this.outer.style.setProperty('--nf-viewport-height', `${viewport?.height ?? window.innerHeight}px`);\n }\n\n private applyFrameHeight() {\n if (!this.revealed) return;\n const viewportHeight = window.visualViewport?.height ?? window.innerHeight;\n const max = Math.max(1, Math.floor(viewportHeight - 32));\n this.iframe.style.height = `${Math.min(this.requestedHeight, max)}px`;\n }\n\n postMessage(message: object) {\n this.iframe.contentWindow?.postMessage(message, this.host);\n }\n\n destroy() {\n window.removeEventListener('message', this.messageHandler);\n window.removeEventListener('resize', this.viewportHandler);\n window.visualViewport?.removeEventListener('resize', this.viewportHandler);\n window.visualViewport?.removeEventListener('scroll', this.viewportHandler);\n this.root.remove();\n }\n}\n\n/**\n * Browser client for the OpenKey Nostr custody + signing flow. Get an\n * instance from `OpenKey#nostr` rather than constructing this directly.\n */\nexport class OpenKeyNostr {\n constructor(\n private host: string,\n private passkeysSupported: boolean = true,\n ) {}\n\n /**\n * Get-or-create the user's OpenKey-custodied Nostr identity. Always shows\n * a visible consent card (identity creation, or - when `relayUrl` is\n * given - a relay-scoped kind:22242/NIP-42 grant for that exact relay).\n * Never resolves with the secret key; only `keyId`/`pubkey`/`npub`.\n */\n async connect(opts?: { relayUrl?: string }): Promise<NostrIdentity> {\n const url = this.widgetUrl();\n return this.run<NostrIdentity>(\n url,\n { type: 'openkey:nostr:connect:request', relayUrl: opts?.relayUrl },\n (data, resolve, reject) => {\n if (data.type !== 'openkey:nostr:connect:response') return false;\n if (data.success) resolve({ keyId: data.keyId, pubkey: data.pubkey, npub: data.npub });\n else reject(data.error);\n return true;\n },\n );\n }\n\n /**\n * Sign a NIP-01 event template with the user's OpenKey Nostr key. Silent\n * (no visible UI) when an active grant already covers this client origin\n * + key + kind (+ exact relay, for kind 22242). Otherwise shows a visible\n * consent card before signing - required for every first kind:9 message to\n * a new origin, per NIP-29 channel-message policy.\n */\n async signEvent(keyId: string, event: UnsignedNostrEvent): Promise<SignedNostrEvent> {\n const url = this.widgetUrl();\n return this.run<SignedNostrEvent>(\n url,\n { type: 'openkey:nostr:sign:request', keyId, event },\n (data, resolve, reject) => {\n if (data.type !== 'openkey:nostr:sign:response') return false;\n if (data.success) resolve(data.event);\n else reject(data.error);\n return true;\n },\n );\n }\n\n private widgetUrl(): string {\n const origin = encodeURIComponent(window.location.origin);\n const passkeysFlag = this.passkeysSupported ? '' : '&passkeys=false';\n return `${this.host}/widget/embed/nostr/approve?origin=${origin}${passkeysFlag}`;\n }\n\n private run<T>(\n url: string,\n request: object,\n onData: (data: NostrFrameMessage, resolve: (v: T) => void, reject: (e: NostrError) => void) => boolean,\n ): Promise<T> {\n return new Promise((resolve, reject) => {\n let settled = false;\n const frame = new NostrFrame(this.host, url);\n const requestId = newRequestId();\n\n const settle = (fn: () => void) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeout);\n fn();\n frame.destroy();\n };\n\n frame.onMessage((data) => {\n if (!data || typeof (data as { type?: unknown }).type !== 'string') return;\n\n if (data.type === 'openkey:ready') {\n if (data.protocolVersion !== NOSTR_PROTOCOL_VERSION) return;\n frame.postMessage({ ...request, requestId, protocolVersion: NOSTR_PROTOCOL_VERSION });\n return;\n }\n if (\n data.requestId !== requestId ||\n data.protocolVersion !== NOSTR_PROTOCOL_VERSION\n ) return;\n if (data.type === 'openkey:resize') {\n frame.resize(data.height);\n return;\n }\n if (data.type === 'openkey:nostr:show') {\n frame.reveal();\n return;\n }\n if (data.type === 'openkey:close') {\n settle(() => reject({ code: 'USER_CANCELLED', message: 'Request closed' }));\n return;\n }\n\n onData(data, (v) => settle(() => resolve(v)), (e) => settle(() => reject(e)));\n });\n\n const timeout = setTimeout(() => {\n settle(() => reject({ code: 'TIMEOUT', message: 'Nostr request timed out' }));\n }, NOSTR_FRAME_TIMEOUT_MS);\n });\n }\n}\n","// Detects when a popup cannot work, so flows fail fast instead of waiting on\n// a window that will never answer (embedded app WebViews cannot open popups).\n\nexport interface PopupError {\n code: 'POPUP_BLOCKED';\n message: string;\n /** Set when the popup is unavailable because the page runs in an embedded app WebView. */\n reason?: 'embedded-webview';\n}\n\ntype PopupEnvironment = {\n Capacitor?: { isNativePlatform?: () => boolean };\n ReactNativeWebView?: unknown;\n navigator?: { userAgent?: string };\n};\n\n/** True inside a known embedded app WebView, where `window.open` cannot show a popup. */\nexport function isEmbeddedAppWebView(env?: PopupEnvironment): boolean {\n const target = env ?? (typeof window === 'undefined' ? undefined : (window as unknown as PopupEnvironment));\n if (!target) return false;\n\n if (target.Capacitor?.isNativePlatform?.() === true) return true;\n if (target.ReactNativeWebView) return true;\n\n const userAgent = target.navigator?.userAgent ?? '';\n // Known limitation: a WKWebView in iPadOS desktop content mode can report a\n // Macintosh UA that includes `Safari/`, which this heuristic cannot tell apart\n // from real Safari. Capacitor and React Native are still caught by the\n // capability checks above, and `isUsablePopup` still fails fast when\n // `window.open` returns null or a closed window.\n // Android WebView adds a `wv` token to the platform comment.\n if (/\\bAndroid\\b[^)]*;\\s*wv\\b/.test(userAgent)) return true;\n // iOS WKWebView sends an Apple WebKit UA without the `Safari/` token that\n // Safari and the iOS browsers built on WebKit (Chrome, Firefox, Edge) add.\n if (/\\b(iPhone|iPad|iPod)\\b/.test(userAgent) && /AppleWebKit/.test(userAgent) && !/Safari\\//.test(userAgent)) {\n return true;\n }\n return false;\n}\n\n/** `window.open` returns null when blocked, and can return an already-closed window. */\nexport function isUsablePopup(popup: Window | null | undefined): popup is Window {\n return !!popup && popup.closed !== true;\n}\n\nexport function popupUnavailableError(embeddedWebView: boolean): PopupError {\n if (embeddedWebView) {\n return {\n code: 'POPUP_BLOCKED',\n message: 'Popups are not available in this embedded app WebView. Make sure the OpenKey host is allowed by frame-src, or use native sign-in.',\n reason: 'embedded-webview',\n };\n }\n return { code: 'POPUP_BLOCKED', message: 'Popup was blocked. Please allow popups for this site.' };\n}\n"],"mappings":"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","names":["BASE64_CHARS","base64UrlEncode","buffer","base64","len","b0","b1","b2","triplet","generateCodeVerifier","array","base64UrlEncode","defaultSha256","input","data","digest","generateCodeChallenge","verifier","sha256Fn","digest","defaultSha256","base64UrlEncode","generateState","array","buildAuthorizationUrl","options","host","clientId","redirectUri","codeChallenge","state","scopes","authorizePath","url","url"]}
|