Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql

feat: add storage namespace isolation for multi-app support

Derive unique namespace from clientId hash to prevent storage collisions
when multiple apps use quickslice-client-js on the same domain.

Changes:
- Storage keys prefixed with 8-char SHA-256 hash of clientId
- IndexedDB database name includes namespace
- Lock keys include namespace
- Breaking: existing users will need to re-login once

Bumps version to 0.3.0

+1724 -405
+1157
dev-docs/plans/2025-12-19-storage-namespace.md
··· 1 + # Storage Namespace Isolation Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Prevent storage collisions when multiple apps on the same domain use quickslice-client-js by deriving a unique namespace from clientId. 6 + 7 + **Architecture:** Compute an 8-character SHA-256 hash of clientId at client init. Thread this namespace through all storage operations: localStorage/sessionStorage keys, IndexedDB database name, and lock keys. No migration — existing users re-login once. 8 + 9 + **Tech Stack:** TypeScript, Web Crypto API (SHA-256), localStorage, sessionStorage, IndexedDB 10 + 11 + --- 12 + 13 + ## Task 1: Add namespace hash utility 14 + 15 + **Files:** 16 + - Modify: `src/utils/crypto.ts:1-36` 17 + 18 + **Step 1: Add namespace hash function** 19 + 20 + Add after the existing `sha256Base64Url` function: 21 + 22 + ```typescript 23 + /** 24 + * Generate an 8-character namespace hash from clientId 25 + */ 26 + export async function generateNamespaceHash(clientId: string): Promise<string> { 27 + const encoder = new TextEncoder(); 28 + const hash = await crypto.subtle.digest('SHA-256', encoder.encode(clientId)); 29 + const hashArray = Array.from(new Uint8Array(hash)); 30 + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); 31 + return hashHex.substring(0, 8); 32 + } 33 + ``` 34 + 35 + **Step 2: Verify build passes** 36 + 37 + Run: `npm run build` 38 + Expected: Build succeeds with no errors 39 + 40 + **Step 3: Commit** 41 + 42 + ```bash 43 + git add src/utils/crypto.ts 44 + git commit -m "feat: add namespace hash utility for storage isolation" 45 + ``` 46 + 47 + --- 48 + 49 + ## Task 2: Make storage keys dynamic 50 + 51 + **Files:** 52 + - Modify: `src/storage/keys.ts:1-16` 53 + 54 + **Step 1: Replace static keys with factory function** 55 + 56 + Replace the entire file contents: 57 + 58 + ```typescript 59 + /** 60 + * Storage key factory - generates namespaced keys 61 + */ 62 + export interface StorageKeys { 63 + accessToken: string; 64 + refreshToken: string; 65 + tokenExpiresAt: string; 66 + clientId: string; 67 + userDid: string; 68 + codeVerifier: string; 69 + oauthState: string; 70 + redirectUri: string; 71 + } 72 + 73 + export function createStorageKeys(namespace: string): StorageKeys { 74 + return { 75 + accessToken: `quickslice_${namespace}_access_token`, 76 + refreshToken: `quickslice_${namespace}_refresh_token`, 77 + tokenExpiresAt: `quickslice_${namespace}_token_expires_at`, 78 + clientId: `quickslice_${namespace}_client_id`, 79 + userDid: `quickslice_${namespace}_user_did`, 80 + codeVerifier: `quickslice_${namespace}_code_verifier`, 81 + oauthState: `quickslice_${namespace}_oauth_state`, 82 + redirectUri: `quickslice_${namespace}_redirect_uri`, 83 + }; 84 + } 85 + 86 + export type StorageKey = string; 87 + ``` 88 + 89 + **Step 2: Verify build fails** 90 + 91 + Run: `npm run build` 92 + Expected: Build fails — other files still import `STORAGE_KEYS` 93 + 94 + **Step 3: Commit (WIP)** 95 + 96 + ```bash 97 + git add src/storage/keys.ts 98 + git commit -m "wip: make storage keys dynamic with namespace" 99 + ``` 100 + 101 + --- 102 + 103 + ## Task 3: Update storage module to accept keys 104 + 105 + **Files:** 106 + - Modify: `src/storage/storage.ts:1-37` 107 + 108 + **Step 1: Rewrite storage module** 109 + 110 + Replace the entire file contents: 111 + 112 + ```typescript 113 + import { StorageKeys } from './keys'; 114 + 115 + /** 116 + * Create a namespaced storage interface 117 + */ 118 + export function createStorage(keys: StorageKeys) { 119 + return { 120 + get(key: keyof StorageKeys): string | null { 121 + const storageKey = keys[key]; 122 + // OAuth flow state stays in sessionStorage (per-tab) 123 + if (key === 'codeVerifier' || key === 'oauthState') { 124 + return sessionStorage.getItem(storageKey); 125 + } 126 + // Tokens go in localStorage (shared across tabs) 127 + return localStorage.getItem(storageKey); 128 + }, 129 + 130 + set(key: keyof StorageKeys, value: string): void { 131 + const storageKey = keys[key]; 132 + if (key === 'codeVerifier' || key === 'oauthState') { 133 + sessionStorage.setItem(storageKey, value); 134 + } else { 135 + localStorage.setItem(storageKey, value); 136 + } 137 + }, 138 + 139 + remove(key: keyof StorageKeys): void { 140 + const storageKey = keys[key]; 141 + sessionStorage.removeItem(storageKey); 142 + localStorage.removeItem(storageKey); 143 + }, 144 + 145 + clear(): void { 146 + (Object.keys(keys) as Array<keyof StorageKeys>).forEach((key) => { 147 + const storageKey = keys[key]; 148 + sessionStorage.removeItem(storageKey); 149 + localStorage.removeItem(storageKey); 150 + }); 151 + }, 152 + }; 153 + } 154 + 155 + export type Storage = ReturnType<typeof createStorage>; 156 + ``` 157 + 158 + **Step 2: Verify build still fails** 159 + 160 + Run: `npm run build` 161 + Expected: Build fails — consumers still use old imports 162 + 163 + **Step 3: Commit (WIP)** 164 + 165 + ```bash 166 + git add src/storage/storage.ts 167 + git commit -m "wip: storage module accepts namespaced keys" 168 + ``` 169 + 170 + --- 171 + 172 + ## Task 4: Update lock module with namespace 173 + 174 + **Files:** 175 + - Modify: `src/storage/lock.ts:1-57` 176 + 177 + **Step 1: Rewrite lock module to accept namespace** 178 + 179 + Replace the entire file contents: 180 + 181 + ```typescript 182 + const LOCK_TIMEOUT = 5000; // 5 seconds 183 + 184 + function sleep(ms: number): Promise<void> { 185 + return new Promise((resolve) => setTimeout(resolve, ms)); 186 + } 187 + 188 + function getLockKey(namespace: string, key: string): string { 189 + return `quickslice_${namespace}_lock_${key}`; 190 + } 191 + 192 + /** 193 + * Acquire a lock using localStorage for multi-tab coordination 194 + */ 195 + export async function acquireLock( 196 + namespace: string, 197 + key: string, 198 + timeout = LOCK_TIMEOUT 199 + ): Promise<string | null> { 200 + const lockKey = getLockKey(namespace, key); 201 + const lockValue = `${Date.now()}_${Math.random()}`; 202 + const deadline = Date.now() + timeout; 203 + 204 + while (Date.now() < deadline) { 205 + const existing = localStorage.getItem(lockKey); 206 + 207 + if (existing) { 208 + // Check if lock is stale (older than timeout) 209 + const [timestamp] = existing.split('_'); 210 + if (Date.now() - parseInt(timestamp) > LOCK_TIMEOUT) { 211 + // Lock is stale, remove it 212 + localStorage.removeItem(lockKey); 213 + } else { 214 + // Lock is held, wait and retry 215 + await sleep(50); 216 + continue; 217 + } 218 + } 219 + 220 + // Try to acquire 221 + localStorage.setItem(lockKey, lockValue); 222 + 223 + // Verify we got it (handle race condition) 224 + await sleep(10); 225 + if (localStorage.getItem(lockKey) === lockValue) { 226 + return lockValue; // Lock acquired 227 + } 228 + } 229 + 230 + return null; // Failed to acquire 231 + } 232 + 233 + /** 234 + * Release a lock 235 + */ 236 + export function releaseLock(namespace: string, key: string, lockValue: string): void { 237 + const lockKey = getLockKey(namespace, key); 238 + // Only release if we still hold it 239 + if (localStorage.getItem(lockKey) === lockValue) { 240 + localStorage.removeItem(lockKey); 241 + } 242 + } 243 + ``` 244 + 245 + **Step 2: Verify build still fails** 246 + 247 + Run: `npm run build` 248 + Expected: Build fails — tokens.ts uses old lock signature 249 + 250 + **Step 3: Commit (WIP)** 251 + 252 + ```bash 253 + git add src/storage/lock.ts 254 + git commit -m "wip: lock module uses namespace prefix" 255 + ``` 256 + 257 + --- 258 + 259 + ## Task 5: Update DPoP module with namespaced database 260 + 261 + **Files:** 262 + - Modify: `src/auth/dpop.ts:1-147` 263 + 264 + **Step 1: Change database name to use namespace** 265 + 266 + Replace lines 1-36 with: 267 + 268 + ```typescript 269 + import { generateRandomString } from '../utils/base64url'; 270 + import { sha256Base64Url } from '../utils/crypto'; 271 + 272 + const DB_VERSION = 1; 273 + const KEY_STORE = 'dpop-keys'; 274 + const KEY_ID = 'dpop-key'; 275 + 276 + interface DPoPKeyData { 277 + id: string; 278 + privateKey: CryptoKey; 279 + publicJwk: JsonWebKey; 280 + createdAt: number; 281 + } 282 + 283 + // Cache database connections per namespace 284 + const dbPromises = new Map<string, Promise<IDBDatabase>>(); 285 + 286 + function getDbName(namespace: string): string { 287 + return `quickslice-oauth-${namespace}`; 288 + } 289 + 290 + function openDatabase(namespace: string): Promise<IDBDatabase> { 291 + const existing = dbPromises.get(namespace); 292 + if (existing) return existing; 293 + 294 + const promise = new Promise<IDBDatabase>((resolve, reject) => { 295 + const request = indexedDB.open(getDbName(namespace), DB_VERSION); 296 + 297 + request.onerror = () => reject(request.error); 298 + request.onsuccess = () => resolve(request.result); 299 + 300 + request.onupgradeneeded = (event) => { 301 + const db = (event.target as IDBOpenDBRequest).result; 302 + if (!db.objectStoreNames.contains(KEY_STORE)) { 303 + db.createObjectStore(KEY_STORE, { keyPath: 'id' }); 304 + } 305 + }; 306 + }); 307 + 308 + dbPromises.set(namespace, promise); 309 + return promise; 310 + } 311 + ``` 312 + 313 + **Step 2: Update getDPoPKey to accept namespace** 314 + 315 + Replace the getDPoPKey function (lines 38-48) with: 316 + 317 + ```typescript 318 + async function getDPoPKey(namespace: string): Promise<DPoPKeyData | null> { 319 + const db = await openDatabase(namespace); 320 + return new Promise((resolve, reject) => { 321 + const tx = db.transaction(KEY_STORE, 'readonly'); 322 + const store = tx.objectStore(KEY_STORE); 323 + const request = store.get(KEY_ID); 324 + 325 + request.onerror = () => reject(request.error); 326 + request.onsuccess = () => resolve(request.result || null); 327 + }); 328 + } 329 + ``` 330 + 331 + **Step 3: Update storeDPoPKey to accept namespace** 332 + 333 + Replace the storeDPoPKey function (lines 50-68) with: 334 + 335 + ```typescript 336 + async function storeDPoPKey( 337 + namespace: string, 338 + privateKey: CryptoKey, 339 + publicJwk: JsonWebKey 340 + ): Promise<void> { 341 + const db = await openDatabase(namespace); 342 + return new Promise((resolve, reject) => { 343 + const tx = db.transaction(KEY_STORE, 'readwrite'); 344 + const store = tx.objectStore(KEY_STORE); 345 + const request = store.put({ 346 + id: KEY_ID, 347 + privateKey, 348 + publicJwk, 349 + createdAt: Date.now(), 350 + }); 351 + 352 + request.onerror = () => reject(request.error); 353 + request.onsuccess = () => resolve(); 354 + }); 355 + } 356 + ``` 357 + 358 + **Step 4: Update getOrCreateDPoPKey to accept namespace** 359 + 360 + Replace the getOrCreateDPoPKey function (lines 70-96) with: 361 + 362 + ```typescript 363 + export async function getOrCreateDPoPKey(namespace: string): Promise<DPoPKeyData> { 364 + const keyData = await getDPoPKey(namespace); 365 + 366 + if (keyData) { 367 + return keyData; 368 + } 369 + 370 + // Generate new P-256 key pair 371 + const keyPair = await crypto.subtle.generateKey( 372 + { name: 'ECDSA', namedCurve: 'P-256' }, 373 + false, // NOT extractable - critical for security 374 + ['sign'] 375 + ); 376 + 377 + // Export public key as JWK 378 + const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey); 379 + 380 + // Store in IndexedDB 381 + await storeDPoPKey(namespace, keyPair.privateKey, publicJwk); 382 + 383 + return { 384 + id: KEY_ID, 385 + privateKey: keyPair.privateKey, 386 + publicJwk, 387 + createdAt: Date.now(), 388 + }; 389 + } 390 + ``` 391 + 392 + **Step 5: Update createDPoPProof to accept namespace** 393 + 394 + Replace the createDPoPProof function (lines 98-131) with: 395 + 396 + ```typescript 397 + /** 398 + * Create a DPoP proof JWT 399 + */ 400 + export async function createDPoPProof( 401 + namespace: string, 402 + method: string, 403 + url: string, 404 + accessToken: string | null = null 405 + ): Promise<string> { 406 + const keyData = await getOrCreateDPoPKey(namespace); 407 + 408 + // Strip WebCrypto-specific fields from JWK for interoperability 409 + const { kty, crv, x, y } = keyData.publicJwk; 410 + const minimalJwk = { kty, crv, x, y }; 411 + 412 + const header = { 413 + alg: 'ES256', 414 + typ: 'dpop+jwt', 415 + jwk: minimalJwk, 416 + }; 417 + 418 + const payload: Record<string, unknown> = { 419 + jti: generateRandomString(16), 420 + htm: method, 421 + htu: url, 422 + iat: Math.floor(Date.now() / 1000), 423 + }; 424 + 425 + // Add access token hash if provided (for resource requests) 426 + if (accessToken) { 427 + payload.ath = await sha256Base64Url(accessToken); 428 + } 429 + 430 + return await signJwt(header, payload, keyData.privateKey); 431 + } 432 + ``` 433 + 434 + **Step 6: Update clearDPoPKeys to accept namespace** 435 + 436 + Replace the clearDPoPKeys function (lines 133-146) with: 437 + 438 + ```typescript 439 + /** 440 + * Clear DPoP keys from IndexedDB 441 + */ 442 + export async function clearDPoPKeys(namespace: string): Promise<void> { 443 + const db = await openDatabase(namespace); 444 + return new Promise((resolve, reject) => { 445 + const tx = db.transaction(KEY_STORE, 'readwrite'); 446 + const store = tx.objectStore(KEY_STORE); 447 + const request = store.clear(); 448 + 449 + request.onerror = () => reject(request.error); 450 + request.onsuccess = () => resolve(); 451 + }); 452 + } 453 + ``` 454 + 455 + **Step 7: Add signJwt import** 456 + 457 + Add to the imports at line 2: 458 + 459 + ```typescript 460 + import { sha256Base64Url, signJwt } from '../utils/crypto'; 461 + ``` 462 + 463 + **Step 8: Verify build still fails** 464 + 465 + Run: `npm run build` 466 + Expected: Build fails — consumers don't pass namespace 467 + 468 + **Step 9: Commit (WIP)** 469 + 470 + ```bash 471 + git add src/auth/dpop.ts 472 + git commit -m "wip: dpop module uses namespaced IndexedDB database" 473 + ``` 474 + 475 + --- 476 + 477 + ## Task 6: Update tokens module 478 + 479 + **Files:** 480 + - Modify: `src/auth/tokens.ts:1-137` 481 + 482 + **Step 1: Rewrite tokens module to use storage instance** 483 + 484 + Replace the entire file contents: 485 + 486 + ```typescript 487 + import { Storage } from '../storage/storage'; 488 + import { acquireLock, releaseLock } from '../storage/lock'; 489 + import { createDPoPProof } from './dpop'; 490 + 491 + const TOKEN_REFRESH_BUFFER_MS = 60000; // 60 seconds before expiry 492 + 493 + function sleep(ms: number): Promise<void> { 494 + return new Promise((resolve) => setTimeout(resolve, ms)); 495 + } 496 + 497 + /** 498 + * Refresh tokens using the refresh token 499 + */ 500 + async function refreshTokens( 501 + storage: Storage, 502 + namespace: string, 503 + tokenUrl: string 504 + ): Promise<string> { 505 + const refreshToken = storage.get('refreshToken'); 506 + const clientId = storage.get('clientId'); 507 + 508 + if (!refreshToken || !clientId) { 509 + throw new Error('No refresh token available'); 510 + } 511 + 512 + const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl); 513 + 514 + const response = await fetch(tokenUrl, { 515 + method: 'POST', 516 + headers: { 517 + 'Content-Type': 'application/x-www-form-urlencoded', 518 + DPoP: dpopProof, 519 + }, 520 + body: new URLSearchParams({ 521 + grant_type: 'refresh_token', 522 + refresh_token: refreshToken, 523 + client_id: clientId, 524 + }), 525 + }); 526 + 527 + if (!response.ok) { 528 + const errorData = await response.json().catch(() => ({})); 529 + throw new Error( 530 + `Token refresh failed: ${errorData.error_description || response.statusText}` 531 + ); 532 + } 533 + 534 + const tokens = await response.json(); 535 + 536 + // Store new tokens (rotation - new refresh token each time) 537 + storage.set('accessToken', tokens.access_token); 538 + if (tokens.refresh_token) { 539 + storage.set('refreshToken', tokens.refresh_token); 540 + } 541 + 542 + const expiresAt = Date.now() + tokens.expires_in * 1000; 543 + storage.set('tokenExpiresAt', expiresAt.toString()); 544 + 545 + return tokens.access_token; 546 + } 547 + 548 + /** 549 + * Get a valid access token, refreshing if necessary. 550 + * Uses multi-tab locking to prevent duplicate refresh requests. 551 + */ 552 + export async function getValidAccessToken( 553 + storage: Storage, 554 + namespace: string, 555 + tokenUrl: string 556 + ): Promise<string> { 557 + const accessToken = storage.get('accessToken'); 558 + const expiresAt = parseInt(storage.get('tokenExpiresAt') || '0'); 559 + 560 + // Check if token is still valid (with buffer) 561 + if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) { 562 + return accessToken; 563 + } 564 + 565 + // Need to refresh - acquire lock first 566 + const lockKey = 'token_refresh'; 567 + const lockValue = await acquireLock(namespace, lockKey); 568 + 569 + if (!lockValue) { 570 + // Failed to acquire lock, another tab is refreshing 571 + // Wait a bit and check cache again 572 + await sleep(100); 573 + const freshToken = storage.get('accessToken'); 574 + const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0'); 575 + if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 576 + return freshToken; 577 + } 578 + throw new Error('Failed to refresh token'); 579 + } 580 + 581 + try { 582 + // Double-check after acquiring lock 583 + const freshToken = storage.get('accessToken'); 584 + const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0'); 585 + if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 586 + return freshToken; 587 + } 588 + 589 + // Actually refresh 590 + return await refreshTokens(storage, namespace, tokenUrl); 591 + } finally { 592 + releaseLock(namespace, lockKey, lockValue); 593 + } 594 + } 595 + 596 + /** 597 + * Store tokens from OAuth response 598 + */ 599 + export function storeTokens( 600 + storage: Storage, 601 + tokens: { 602 + access_token: string; 603 + refresh_token?: string; 604 + expires_in: number; 605 + sub?: string; 606 + } 607 + ): void { 608 + storage.set('accessToken', tokens.access_token); 609 + if (tokens.refresh_token) { 610 + storage.set('refreshToken', tokens.refresh_token); 611 + } 612 + 613 + const expiresAt = Date.now() + tokens.expires_in * 1000; 614 + storage.set('tokenExpiresAt', expiresAt.toString()); 615 + 616 + if (tokens.sub) { 617 + storage.set('userDid', tokens.sub); 618 + } 619 + } 620 + 621 + /** 622 + * Check if we have a valid session 623 + */ 624 + export function hasValidSession(storage: Storage): boolean { 625 + const accessToken = storage.get('accessToken'); 626 + const refreshToken = storage.get('refreshToken'); 627 + return !!(accessToken || refreshToken); 628 + } 629 + ``` 630 + 631 + **Step 2: Verify build still fails** 632 + 633 + Run: `npm run build` 634 + Expected: Build fails — oauth.ts and client.ts use old signatures 635 + 636 + **Step 3: Commit (WIP)** 637 + 638 + ```bash 639 + git add src/auth/tokens.ts 640 + git commit -m "wip: tokens module accepts storage and namespace" 641 + ``` 642 + 643 + --- 644 + 645 + ## Task 7: Update oauth module 646 + 647 + **Files:** 648 + - Modify: `src/auth/oauth.ts:1-141` 649 + 650 + **Step 1: Rewrite oauth module** 651 + 652 + Replace the entire file contents: 653 + 654 + ```typescript 655 + import { Storage } from '../storage/storage'; 656 + import { createDPoPProof, clearDPoPKeys } from './dpop'; 657 + import { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce'; 658 + import { storeTokens } from './tokens'; 659 + 660 + export interface LoginOptions { 661 + handle?: string; 662 + redirectUri?: string; 663 + scope?: string; 664 + } 665 + 666 + /** 667 + * Initiate OAuth login flow with PKCE 668 + */ 669 + export async function initiateLogin( 670 + storage: Storage, 671 + authorizeUrl: string, 672 + clientId: string, 673 + options: LoginOptions = {} 674 + ): Promise<void> { 675 + const codeVerifier = generateCodeVerifier(); 676 + const codeChallenge = await generateCodeChallenge(codeVerifier); 677 + const state = generateState(); 678 + 679 + // Build redirect URI (use provided or derive from current page) 680 + const redirectUri = options.redirectUri || (window.location.origin + window.location.pathname); 681 + 682 + // Store for callback 683 + storage.set('codeVerifier', codeVerifier); 684 + storage.set('oauthState', state); 685 + storage.set('clientId', clientId); 686 + storage.set('redirectUri', redirectUri); 687 + 688 + // Build authorization URL 689 + const params = new URLSearchParams({ 690 + client_id: clientId, 691 + redirect_uri: redirectUri, 692 + response_type: 'code', 693 + code_challenge: codeChallenge, 694 + code_challenge_method: 'S256', 695 + state: state, 696 + }); 697 + 698 + if (options.handle) { 699 + params.set('login_hint', options.handle); 700 + } 701 + 702 + if (options.scope) { 703 + params.set('scope', options.scope); 704 + } 705 + 706 + window.location.href = `${authorizeUrl}?${params.toString()}`; 707 + } 708 + 709 + /** 710 + * Handle OAuth callback - exchange code for tokens 711 + * Returns true if callback was handled, false if not a callback 712 + */ 713 + export async function handleOAuthCallback( 714 + storage: Storage, 715 + namespace: string, 716 + tokenUrl: string 717 + ): Promise<boolean> { 718 + const params = new URLSearchParams(window.location.search); 719 + const code = params.get('code'); 720 + const state = params.get('state'); 721 + const error = params.get('error'); 722 + 723 + if (error) { 724 + throw new Error( 725 + `OAuth error: ${error} - ${params.get('error_description') || ''}` 726 + ); 727 + } 728 + 729 + if (!code || !state) { 730 + return false; // Not a callback 731 + } 732 + 733 + // Verify state 734 + const storedState = storage.get('oauthState'); 735 + if (state !== storedState) { 736 + throw new Error('OAuth state mismatch - possible CSRF attack'); 737 + } 738 + 739 + // Get stored values 740 + const codeVerifier = storage.get('codeVerifier'); 741 + const clientId = storage.get('clientId'); 742 + const redirectUri = storage.get('redirectUri'); 743 + 744 + if (!codeVerifier || !clientId || !redirectUri) { 745 + throw new Error('Missing OAuth session data'); 746 + } 747 + 748 + // Exchange code for tokens with DPoP 749 + const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl); 750 + 751 + const tokenResponse = await fetch(tokenUrl, { 752 + method: 'POST', 753 + headers: { 754 + 'Content-Type': 'application/x-www-form-urlencoded', 755 + DPoP: dpopProof, 756 + }, 757 + body: new URLSearchParams({ 758 + grant_type: 'authorization_code', 759 + code: code, 760 + redirect_uri: redirectUri, 761 + client_id: clientId, 762 + code_verifier: codeVerifier, 763 + }), 764 + }); 765 + 766 + if (!tokenResponse.ok) { 767 + const errorData = await tokenResponse.json().catch(() => ({})); 768 + throw new Error( 769 + `Token exchange failed: ${errorData.error_description || tokenResponse.statusText}` 770 + ); 771 + } 772 + 773 + const tokens = await tokenResponse.json(); 774 + 775 + // Store tokens 776 + storeTokens(storage, tokens); 777 + 778 + // Clean up OAuth state 779 + storage.remove('codeVerifier'); 780 + storage.remove('oauthState'); 781 + storage.remove('redirectUri'); 782 + 783 + // Clear URL params 784 + window.history.replaceState({}, document.title, window.location.pathname); 785 + 786 + return true; 787 + } 788 + 789 + /** 790 + * Logout - clear all stored data 791 + */ 792 + export async function logout( 793 + storage: Storage, 794 + namespace: string, 795 + options: { reload?: boolean } = {} 796 + ): Promise<void> { 797 + storage.clear(); 798 + await clearDPoPKeys(namespace); 799 + 800 + if (options.reload !== false) { 801 + window.location.reload(); 802 + } 803 + } 804 + ``` 805 + 806 + **Step 2: Verify build still fails** 807 + 808 + Run: `npm run build` 809 + Expected: Build fails — client.ts uses old signatures 810 + 811 + **Step 3: Commit (WIP)** 812 + 813 + ```bash 814 + git add src/auth/oauth.ts 815 + git commit -m "wip: oauth module accepts storage and namespace" 816 + ``` 817 + 818 + --- 819 + 820 + ## Task 8: Update graphql module 821 + 822 + **Files:** 823 + - Modify: `src/graphql.ts:1-53` 824 + 825 + **Step 1: Rewrite graphql module** 826 + 827 + Replace the entire file contents: 828 + 829 + ```typescript 830 + import { createDPoPProof } from './auth/dpop'; 831 + import { getValidAccessToken } from './auth/tokens'; 832 + import { Storage } from './storage/storage'; 833 + 834 + export interface GraphQLResponse<T = unknown> { 835 + data?: T; 836 + errors?: Array<{ message: string; path?: string[] }>; 837 + } 838 + 839 + /** 840 + * Execute a GraphQL query or mutation 841 + */ 842 + export async function graphqlRequest<T = unknown>( 843 + storage: Storage, 844 + namespace: string, 845 + graphqlUrl: string, 846 + tokenUrl: string, 847 + query: string, 848 + variables: Record<string, unknown> = {}, 849 + requireAuth = false 850 + ): Promise<T> { 851 + const headers: Record<string, string> = { 852 + 'Content-Type': 'application/json', 853 + }; 854 + 855 + if (requireAuth) { 856 + const token = await getValidAccessToken(storage, namespace, tokenUrl); 857 + if (!token) { 858 + throw new Error('Not authenticated'); 859 + } 860 + 861 + // Create DPoP proof bound to this request 862 + const dpopProof = await createDPoPProof(namespace, 'POST', graphqlUrl, token); 863 + 864 + headers['Authorization'] = `DPoP ${token}`; 865 + headers['DPoP'] = dpopProof; 866 + } 867 + 868 + const response = await fetch(graphqlUrl, { 869 + method: 'POST', 870 + headers, 871 + body: JSON.stringify({ query, variables }), 872 + }); 873 + 874 + if (!response.ok) { 875 + throw new Error(`GraphQL request failed: ${response.statusText}`); 876 + } 877 + 878 + const result: GraphQLResponse<T> = await response.json(); 879 + 880 + if (result.errors && result.errors.length > 0) { 881 + throw new Error(`GraphQL error: ${result.errors[0].message}`); 882 + } 883 + 884 + return result.data as T; 885 + } 886 + ``` 887 + 888 + **Step 2: Verify build still fails** 889 + 890 + Run: `npm run build` 891 + Expected: Build fails — client.ts uses old import 892 + 893 + **Step 3: Commit (WIP)** 894 + 895 + ```bash 896 + git add src/graphql.ts 897 + git commit -m "wip: graphql module accepts storage and namespace" 898 + ``` 899 + 900 + --- 901 + 902 + ## Task 9: Update client module (main integration) 903 + 904 + **Files:** 905 + - Modify: `src/client.ts:1-155` 906 + 907 + **Step 1: Rewrite client module** 908 + 909 + Replace the entire file contents: 910 + 911 + ```typescript 912 + import { createStorageKeys } from './storage/keys'; 913 + import { createStorage, Storage } from './storage/storage'; 914 + import { getOrCreateDPoPKey } from './auth/dpop'; 915 + import { initiateLogin, handleOAuthCallback, logout as doLogout, LoginOptions } from './auth/oauth'; 916 + import { getValidAccessToken, hasValidSession } from './auth/tokens'; 917 + import { graphqlRequest } from './graphql'; 918 + import { generateNamespaceHash } from './utils/crypto'; 919 + 920 + export interface QuicksliceClientOptions { 921 + server: string; 922 + clientId: string; 923 + redirectUri?: string; 924 + scope?: string; 925 + } 926 + 927 + export interface User { 928 + did: string; 929 + } 930 + 931 + export class QuicksliceClient { 932 + private server: string; 933 + private clientId: string; 934 + private redirectUri?: string; 935 + private scope?: string; 936 + private graphqlUrl: string; 937 + private authorizeUrl: string; 938 + private tokenUrl: string; 939 + private initialized = false; 940 + private namespace: string = ''; 941 + private storage: Storage | null = null; 942 + 943 + constructor(options: QuicksliceClientOptions) { 944 + this.server = options.server.replace(/\/$/, ''); // Remove trailing slash 945 + this.clientId = options.clientId; 946 + this.redirectUri = options.redirectUri; 947 + this.scope = options.scope; 948 + 949 + this.graphqlUrl = `${this.server}/graphql`; 950 + this.authorizeUrl = `${this.server}/oauth/authorize`; 951 + this.tokenUrl = `${this.server}/oauth/token`; 952 + } 953 + 954 + /** 955 + * Initialize the client - must be called before other methods 956 + */ 957 + async init(): Promise<void> { 958 + if (this.initialized) return; 959 + 960 + // Generate namespace from clientId 961 + this.namespace = await generateNamespaceHash(this.clientId); 962 + 963 + // Create namespaced storage 964 + const keys = createStorageKeys(this.namespace); 965 + this.storage = createStorage(keys); 966 + 967 + // Ensure DPoP key exists 968 + await getOrCreateDPoPKey(this.namespace); 969 + 970 + this.initialized = true; 971 + } 972 + 973 + private getStorage(): Storage { 974 + if (!this.storage) { 975 + throw new Error('Client not initialized. Call init() first.'); 976 + } 977 + return this.storage; 978 + } 979 + 980 + /** 981 + * Start OAuth login flow 982 + */ 983 + async loginWithRedirect(options: LoginOptions = {}): Promise<void> { 984 + await this.init(); 985 + await initiateLogin(this.getStorage(), this.authorizeUrl, this.clientId, { 986 + ...options, 987 + redirectUri: options.redirectUri || this.redirectUri, 988 + scope: options.scope || this.scope, 989 + }); 990 + } 991 + 992 + /** 993 + * Handle OAuth callback after redirect 994 + * Returns true if callback was handled 995 + */ 996 + async handleRedirectCallback(): Promise<boolean> { 997 + await this.init(); 998 + return await handleOAuthCallback(this.getStorage(), this.namespace, this.tokenUrl); 999 + } 1000 + 1001 + /** 1002 + * Logout and clear all stored data 1003 + */ 1004 + async logout(options: { reload?: boolean } = {}): Promise<void> { 1005 + await this.init(); 1006 + await doLogout(this.getStorage(), this.namespace, options); 1007 + } 1008 + 1009 + /** 1010 + * Check if user is authenticated 1011 + */ 1012 + async isAuthenticated(): Promise<boolean> { 1013 + await this.init(); 1014 + return hasValidSession(this.getStorage()); 1015 + } 1016 + 1017 + /** 1018 + * Get current user's DID (from stored token data) 1019 + * For richer profile info, use client.query() with your own schema 1020 + */ 1021 + async getUser(): Promise<User | null> { 1022 + await this.init(); 1023 + if (!hasValidSession(this.getStorage())) { 1024 + return null; 1025 + } 1026 + 1027 + const did = this.getStorage().get('userDid'); 1028 + if (!did) { 1029 + return null; 1030 + } 1031 + 1032 + return { did }; 1033 + } 1034 + 1035 + /** 1036 + * Get access token (auto-refreshes if needed) 1037 + */ 1038 + async getAccessToken(): Promise<string> { 1039 + await this.init(); 1040 + return await getValidAccessToken(this.getStorage(), this.namespace, this.tokenUrl); 1041 + } 1042 + 1043 + /** 1044 + * Execute a GraphQL query (authenticated) 1045 + */ 1046 + async query<T = unknown>( 1047 + query: string, 1048 + variables: Record<string, unknown> = {} 1049 + ): Promise<T> { 1050 + await this.init(); 1051 + return await graphqlRequest<T>( 1052 + this.getStorage(), 1053 + this.namespace, 1054 + this.graphqlUrl, 1055 + this.tokenUrl, 1056 + query, 1057 + variables, 1058 + true 1059 + ); 1060 + } 1061 + 1062 + /** 1063 + * Execute a GraphQL mutation (authenticated) 1064 + */ 1065 + async mutate<T = unknown>( 1066 + mutation: string, 1067 + variables: Record<string, unknown> = {} 1068 + ): Promise<T> { 1069 + return this.query<T>(mutation, variables); 1070 + } 1071 + 1072 + /** 1073 + * Execute a public GraphQL query (no auth) 1074 + */ 1075 + async publicQuery<T = unknown>( 1076 + query: string, 1077 + variables: Record<string, unknown> = {} 1078 + ): Promise<T> { 1079 + await this.init(); 1080 + return await graphqlRequest<T>( 1081 + this.getStorage(), 1082 + this.namespace, 1083 + this.graphqlUrl, 1084 + this.tokenUrl, 1085 + query, 1086 + variables, 1087 + false 1088 + ); 1089 + } 1090 + } 1091 + ``` 1092 + 1093 + **Step 2: Verify build passes** 1094 + 1095 + Run: `npm run build` 1096 + Expected: Build succeeds 1097 + 1098 + **Step 3: Commit** 1099 + 1100 + ```bash 1101 + git add src/client.ts 1102 + git commit -m "feat: integrate namespace throughout client for storage isolation" 1103 + ``` 1104 + 1105 + --- 1106 + 1107 + ## Task 10: Squash WIP commits and finalize 1108 + 1109 + **Step 1: Interactive rebase to squash WIP commits** 1110 + 1111 + Run: `git log --oneline -10` to see recent commits 1112 + 1113 + **Step 2: Squash into a single feature commit** 1114 + 1115 + ```bash 1116 + git rebase -i HEAD~9 1117 + ``` 1118 + 1119 + Mark all but the first commit as "squash", use message: 1120 + 1121 + ``` 1122 + feat: add storage namespace isolation for multi-app support 1123 + 1124 + Derive unique namespace from clientId hash to prevent storage collisions 1125 + when multiple apps use quickslice-client-js on the same domain. 1126 + 1127 + Changes: 1128 + - Storage keys prefixed with 8-char SHA-256 hash of clientId 1129 + - IndexedDB database name includes namespace 1130 + - Lock keys include namespace 1131 + - Breaking: existing users will need to re-login once 1132 + ``` 1133 + 1134 + **Step 3: Verify final build** 1135 + 1136 + Run: `npm run build` 1137 + Expected: Build succeeds 1138 + 1139 + --- 1140 + 1141 + ## Summary 1142 + 1143 + | Task | Description | 1144 + |------|-------------| 1145 + | 1 | Add namespace hash utility | 1146 + | 2 | Make storage keys dynamic | 1147 + | 3 | Update storage module | 1148 + | 4 | Update lock module | 1149 + | 5 | Update DPoP module | 1150 + | 6 | Update tokens module | 1151 + | 7 | Update oauth module | 1152 + | 8 | Update graphql module | 1153 + | 9 | Update client module (integration) | 1154 + | 10 | Squash commits and finalize | 1155 + 1156 + **Total files modified:** 9 1157 + **Breaking change:** Users will appear logged out and need to re-login once after update.
+3 -3
quickslice-client-js/dist/auth/dpop.d.ts
··· 4 4 publicJwk: JsonWebKey; 5 5 createdAt: number; 6 6 } 7 - export declare function getOrCreateDPoPKey(): Promise<DPoPKeyData>; 7 + export declare function getOrCreateDPoPKey(namespace: string): Promise<DPoPKeyData>; 8 8 /** 9 9 * Create a DPoP proof JWT 10 10 */ 11 - export declare function createDPoPProof(method: string, url: string, accessToken?: string | null): Promise<string>; 11 + export declare function createDPoPProof(namespace: string, method: string, url: string, accessToken?: string | null): Promise<string>; 12 12 /** 13 13 * Clear DPoP keys from IndexedDB 14 14 */ 15 - export declare function clearDPoPKeys(): Promise<void>; 15 + export declare function clearDPoPKeys(namespace: string): Promise<void>; 16 16 export {};
+4 -3
quickslice-client-js/dist/auth/oauth.d.ts
··· 1 + import { Storage } from '../storage/storage'; 1 2 export interface LoginOptions { 2 3 handle?: string; 3 4 redirectUri?: string; ··· 6 7 /** 7 8 * Initiate OAuth login flow with PKCE 8 9 */ 9 - export declare function initiateLogin(authorizeUrl: string, clientId: string, options?: LoginOptions): Promise<void>; 10 + export declare function initiateLogin(storage: Storage, authorizeUrl: string, clientId: string, options?: LoginOptions): Promise<void>; 10 11 /** 11 12 * Handle OAuth callback - exchange code for tokens 12 13 * Returns true if callback was handled, false if not a callback 13 14 */ 14 - export declare function handleOAuthCallback(tokenUrl: string): Promise<boolean>; 15 + export declare function handleOAuthCallback(storage: Storage, namespace: string, tokenUrl: string): Promise<boolean>; 15 16 /** 16 17 * Logout - clear all stored data 17 18 */ 18 - export declare function logout(options?: { 19 + export declare function logout(storage: Storage, namespace: string, options?: { 19 20 reload?: boolean; 20 21 }): Promise<void>;
+4 -3
quickslice-client-js/dist/auth/tokens.d.ts
··· 1 + import { Storage } from '../storage/storage'; 1 2 /** 2 3 * Get a valid access token, refreshing if necessary. 3 4 * Uses multi-tab locking to prevent duplicate refresh requests. 4 5 */ 5 - export declare function getValidAccessToken(tokenUrl: string): Promise<string>; 6 + export declare function getValidAccessToken(storage: Storage, namespace: string, tokenUrl: string): Promise<string>; 6 7 /** 7 8 * Store tokens from OAuth response 8 9 */ 9 - export declare function storeTokens(tokens: { 10 + export declare function storeTokens(storage: Storage, tokens: { 10 11 access_token: string; 11 12 refresh_token?: string; 12 13 expires_in: number; ··· 15 16 /** 16 17 * Check if we have a valid session 17 18 */ 18 - export declare function hasValidSession(): boolean; 19 + export declare function hasValidSession(storage: Storage): boolean;
+4 -1
quickslice-client-js/dist/client.d.ts
··· 17 17 private authorizeUrl; 18 18 private tokenUrl; 19 19 private initialized; 20 + private namespace; 21 + private storage; 20 22 constructor(options: QuicksliceClientOptions); 21 23 /** 22 24 * Initialize the client - must be called before other methods 23 25 */ 24 26 init(): Promise<void>; 27 + private getStorage; 25 28 /** 26 29 * Start OAuth login flow 27 30 */ ··· 45 48 * Get current user's DID (from stored token data) 46 49 * For richer profile info, use client.query() with your own schema 47 50 */ 48 - getUser(): User | null; 51 + getUser(): Promise<User | null>; 49 52 /** 50 53 * Get access token (auto-refreshes if needed) 51 54 */
+2 -1
quickslice-client-js/dist/graphql.d.ts
··· 1 + import { Storage } from './storage/storage'; 1 2 export interface GraphQLResponse<T = unknown> { 2 3 data?: T; 3 4 errors?: Array<{ ··· 8 9 /** 9 10 * Execute a GraphQL query or mutation 10 11 */ 11 - export declare function graphqlRequest<T = unknown>(graphqlUrl: string, tokenUrl: string, query: string, variables?: Record<string, unknown>, requireAuth?: boolean): Promise<T>; 12 + export declare function graphqlRequest<T = unknown>(storage: Storage, namespace: string, graphqlUrl: string, tokenUrl: string, query: string, variables?: Record<string, unknown>, requireAuth?: boolean): Promise<T>;
+151 -117
quickslice-client-js/dist/quickslice-client.esm.js
··· 1 1 // src/storage/keys.ts 2 - var STORAGE_KEYS = { 3 - accessToken: "quickslice_access_token", 4 - refreshToken: "quickslice_refresh_token", 5 - tokenExpiresAt: "quickslice_token_expires_at", 6 - clientId: "quickslice_client_id", 7 - userDid: "quickslice_user_did", 8 - codeVerifier: "quickslice_code_verifier", 9 - oauthState: "quickslice_oauth_state", 10 - redirectUri: "quickslice_redirect_uri" 11 - }; 2 + function createStorageKeys(namespace) { 3 + return { 4 + accessToken: `quickslice_${namespace}_access_token`, 5 + refreshToken: `quickslice_${namespace}_refresh_token`, 6 + tokenExpiresAt: `quickslice_${namespace}_token_expires_at`, 7 + clientId: `quickslice_${namespace}_client_id`, 8 + userDid: `quickslice_${namespace}_user_did`, 9 + codeVerifier: `quickslice_${namespace}_code_verifier`, 10 + oauthState: `quickslice_${namespace}_oauth_state`, 11 + redirectUri: `quickslice_${namespace}_redirect_uri` 12 + }; 13 + } 12 14 13 15 // src/storage/storage.ts 14 - var storage = { 15 - get(key) { 16 - if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) { 17 - return sessionStorage.getItem(key); 16 + function createStorage(keys) { 17 + return { 18 + get(key) { 19 + const storageKey = keys[key]; 20 + if (key === "codeVerifier" || key === "oauthState") { 21 + return sessionStorage.getItem(storageKey); 22 + } 23 + return localStorage.getItem(storageKey); 24 + }, 25 + set(key, value) { 26 + const storageKey = keys[key]; 27 + if (key === "codeVerifier" || key === "oauthState") { 28 + sessionStorage.setItem(storageKey, value); 29 + } else { 30 + localStorage.setItem(storageKey, value); 31 + } 32 + }, 33 + remove(key) { 34 + const storageKey = keys[key]; 35 + sessionStorage.removeItem(storageKey); 36 + localStorage.removeItem(storageKey); 37 + }, 38 + clear() { 39 + Object.keys(keys).forEach((key) => { 40 + const storageKey = keys[key]; 41 + sessionStorage.removeItem(storageKey); 42 + localStorage.removeItem(storageKey); 43 + }); 18 44 } 19 - return localStorage.getItem(key); 20 - }, 21 - set(key, value) { 22 - if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) { 23 - sessionStorage.setItem(key, value); 24 - } else { 25 - localStorage.setItem(key, value); 26 - } 27 - }, 28 - remove(key) { 29 - sessionStorage.removeItem(key); 30 - localStorage.removeItem(key); 31 - }, 32 - clear() { 33 - Object.values(STORAGE_KEYS).forEach((key) => { 34 - sessionStorage.removeItem(key); 35 - localStorage.removeItem(key); 36 - }); 37 - } 38 - }; 45 + }; 46 + } 39 47 40 48 // src/utils/base64url.ts 41 49 function base64UrlEncode(buffer) { ··· 58 66 const hash = await crypto.subtle.digest("SHA-256", encoder.encode(data)); 59 67 return base64UrlEncode(hash); 60 68 } 69 + async function generateNamespaceHash(clientId) { 70 + const encoder = new TextEncoder(); 71 + const hash = await crypto.subtle.digest("SHA-256", encoder.encode(clientId)); 72 + const hashArray = Array.from(new Uint8Array(hash)); 73 + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); 74 + return hashHex.substring(0, 8); 75 + } 61 76 async function signJwt(header, payload, privateKey) { 62 77 const encoder = new TextEncoder(); 63 78 const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header))); ··· 73 88 } 74 89 75 90 // src/auth/dpop.ts 76 - var DB_NAME = "quickslice-oauth"; 77 91 var DB_VERSION = 1; 78 92 var KEY_STORE = "dpop-keys"; 79 93 var KEY_ID = "dpop-key"; 80 - var dbPromise = null; 81 - function openDatabase() { 82 - if (dbPromise) return dbPromise; 83 - dbPromise = new Promise((resolve, reject) => { 84 - const request = indexedDB.open(DB_NAME, DB_VERSION); 94 + var dbPromises = /* @__PURE__ */ new Map(); 95 + function getDbName(namespace) { 96 + return `quickslice-oauth-${namespace}`; 97 + } 98 + function openDatabase(namespace) { 99 + const existing = dbPromises.get(namespace); 100 + if (existing) return existing; 101 + const promise = new Promise((resolve, reject) => { 102 + const request = indexedDB.open(getDbName(namespace), DB_VERSION); 85 103 request.onerror = () => reject(request.error); 86 104 request.onsuccess = () => resolve(request.result); 87 105 request.onupgradeneeded = (event) => { ··· 91 109 } 92 110 }; 93 111 }); 94 - return dbPromise; 112 + dbPromises.set(namespace, promise); 113 + return promise; 95 114 } 96 - async function getDPoPKey() { 97 - const db = await openDatabase(); 115 + async function getDPoPKey(namespace) { 116 + const db = await openDatabase(namespace); 98 117 return new Promise((resolve, reject) => { 99 118 const tx = db.transaction(KEY_STORE, "readonly"); 100 119 const store = tx.objectStore(KEY_STORE); ··· 103 122 request.onsuccess = () => resolve(request.result || null); 104 123 }); 105 124 } 106 - async function storeDPoPKey(privateKey, publicJwk) { 107 - const db = await openDatabase(); 125 + async function storeDPoPKey(namespace, privateKey, publicJwk) { 126 + const db = await openDatabase(namespace); 108 127 return new Promise((resolve, reject) => { 109 128 const tx = db.transaction(KEY_STORE, "readwrite"); 110 129 const store = tx.objectStore(KEY_STORE); ··· 118 137 request.onsuccess = () => resolve(); 119 138 }); 120 139 } 121 - async function getOrCreateDPoPKey() { 122 - const keyData = await getDPoPKey(); 140 + async function getOrCreateDPoPKey(namespace) { 141 + const keyData = await getDPoPKey(namespace); 123 142 if (keyData) { 124 143 return keyData; 125 144 } ··· 130 149 ["sign"] 131 150 ); 132 151 const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); 133 - await storeDPoPKey(keyPair.privateKey, publicJwk); 152 + await storeDPoPKey(namespace, keyPair.privateKey, publicJwk); 134 153 return { 135 154 id: KEY_ID, 136 155 privateKey: keyPair.privateKey, ··· 138 157 createdAt: Date.now() 139 158 }; 140 159 } 141 - async function createDPoPProof(method, url, accessToken = null) { 142 - const keyData = await getOrCreateDPoPKey(); 160 + async function createDPoPProof(namespace, method, url, accessToken = null) { 161 + const keyData = await getOrCreateDPoPKey(namespace); 143 162 const { kty, crv, x, y } = keyData.publicJwk; 144 163 const minimalJwk = { kty, crv, x, y }; 145 164 const header = { ··· 158 177 } 159 178 return await signJwt(header, payload, keyData.privateKey); 160 179 } 161 - async function clearDPoPKeys() { 162 - const db = await openDatabase(); 180 + async function clearDPoPKeys(namespace) { 181 + const db = await openDatabase(namespace); 163 182 return new Promise((resolve, reject) => { 164 183 const tx = db.transaction(KEY_STORE, "readwrite"); 165 184 const store = tx.objectStore(KEY_STORE); ··· 185 204 186 205 // src/storage/lock.ts 187 206 var LOCK_TIMEOUT = 5e3; 188 - var LOCK_PREFIX = "quickslice_lock_"; 189 207 function sleep(ms) { 190 208 return new Promise((resolve) => setTimeout(resolve, ms)); 191 209 } 192 - async function acquireLock(key, timeout = LOCK_TIMEOUT) { 193 - const lockKey = LOCK_PREFIX + key; 210 + function getLockKey(namespace, key) { 211 + return `quickslice_${namespace}_lock_${key}`; 212 + } 213 + async function acquireLock(namespace, key, timeout = LOCK_TIMEOUT) { 214 + const lockKey = getLockKey(namespace, key); 194 215 const lockValue = `${Date.now()}_${Math.random()}`; 195 216 const deadline = Date.now() + timeout; 196 217 while (Date.now() < deadline) { ··· 212 233 } 213 234 return null; 214 235 } 215 - function releaseLock(key, lockValue) { 216 - const lockKey = LOCK_PREFIX + key; 236 + function releaseLock(namespace, key, lockValue) { 237 + const lockKey = getLockKey(namespace, key); 217 238 if (localStorage.getItem(lockKey) === lockValue) { 218 239 localStorage.removeItem(lockKey); 219 240 } ··· 224 245 function sleep2(ms) { 225 246 return new Promise((resolve) => setTimeout(resolve, ms)); 226 247 } 227 - async function refreshTokens(tokenUrl) { 228 - const refreshToken = storage.get(STORAGE_KEYS.refreshToken); 229 - const clientId = storage.get(STORAGE_KEYS.clientId); 248 + async function refreshTokens(storage, namespace, tokenUrl) { 249 + const refreshToken = storage.get("refreshToken"); 250 + const clientId = storage.get("clientId"); 230 251 if (!refreshToken || !clientId) { 231 252 throw new Error("No refresh token available"); 232 253 } 233 - const dpopProof = await createDPoPProof("POST", tokenUrl); 254 + const dpopProof = await createDPoPProof(namespace, "POST", tokenUrl); 234 255 const response = await fetch(tokenUrl, { 235 256 method: "POST", 236 257 headers: { ··· 250 271 ); 251 272 } 252 273 const tokens = await response.json(); 253 - storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 274 + storage.set("accessToken", tokens.access_token); 254 275 if (tokens.refresh_token) { 255 - storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 276 + storage.set("refreshToken", tokens.refresh_token); 256 277 } 257 278 const expiresAt = Date.now() + tokens.expires_in * 1e3; 258 - storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString()); 279 + storage.set("tokenExpiresAt", expiresAt.toString()); 259 280 return tokens.access_token; 260 281 } 261 - async function getValidAccessToken(tokenUrl) { 262 - const accessToken = storage.get(STORAGE_KEYS.accessToken); 263 - const expiresAt = parseInt(storage.get(STORAGE_KEYS.tokenExpiresAt) || "0"); 282 + async function getValidAccessToken(storage, namespace, tokenUrl) { 283 + const accessToken = storage.get("accessToken"); 284 + const expiresAt = parseInt(storage.get("tokenExpiresAt") || "0"); 264 285 if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) { 265 286 return accessToken; 266 287 } 267 - const clientId = storage.get(STORAGE_KEYS.clientId); 268 - const lockKey = `token_refresh_${clientId}`; 269 - const lockValue = await acquireLock(lockKey); 288 + const lockKey = "token_refresh"; 289 + const lockValue = await acquireLock(namespace, lockKey); 270 290 if (!lockValue) { 271 291 await sleep2(100); 272 - const freshToken = storage.get(STORAGE_KEYS.accessToken); 273 - const freshExpiry = parseInt( 274 - storage.get(STORAGE_KEYS.tokenExpiresAt) || "0" 275 - ); 292 + const freshToken = storage.get("accessToken"); 293 + const freshExpiry = parseInt(storage.get("tokenExpiresAt") || "0"); 276 294 if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 277 295 return freshToken; 278 296 } 279 297 throw new Error("Failed to refresh token"); 280 298 } 281 299 try { 282 - const freshToken = storage.get(STORAGE_KEYS.accessToken); 283 - const freshExpiry = parseInt( 284 - storage.get(STORAGE_KEYS.tokenExpiresAt) || "0" 285 - ); 300 + const freshToken = storage.get("accessToken"); 301 + const freshExpiry = parseInt(storage.get("tokenExpiresAt") || "0"); 286 302 if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 287 303 return freshToken; 288 304 } 289 - return await refreshTokens(tokenUrl); 305 + return await refreshTokens(storage, namespace, tokenUrl); 290 306 } finally { 291 - releaseLock(lockKey, lockValue); 307 + releaseLock(namespace, lockKey, lockValue); 292 308 } 293 309 } 294 - function storeTokens(tokens) { 295 - storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 310 + function storeTokens(storage, tokens) { 311 + storage.set("accessToken", tokens.access_token); 296 312 if (tokens.refresh_token) { 297 - storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 313 + storage.set("refreshToken", tokens.refresh_token); 298 314 } 299 315 const expiresAt = Date.now() + tokens.expires_in * 1e3; 300 - storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString()); 316 + storage.set("tokenExpiresAt", expiresAt.toString()); 301 317 if (tokens.sub) { 302 - storage.set(STORAGE_KEYS.userDid, tokens.sub); 318 + storage.set("userDid", tokens.sub); 303 319 } 304 320 } 305 - function hasValidSession() { 306 - const accessToken = storage.get(STORAGE_KEYS.accessToken); 307 - const refreshToken = storage.get(STORAGE_KEYS.refreshToken); 321 + function hasValidSession(storage) { 322 + const accessToken = storage.get("accessToken"); 323 + const refreshToken = storage.get("refreshToken"); 308 324 return !!(accessToken || refreshToken); 309 325 } 310 326 311 327 // src/auth/oauth.ts 312 - async function initiateLogin(authorizeUrl, clientId, options = {}) { 328 + async function initiateLogin(storage, authorizeUrl, clientId, options = {}) { 313 329 const codeVerifier = generateCodeVerifier(); 314 330 const codeChallenge = await generateCodeChallenge(codeVerifier); 315 331 const state = generateState(); 316 332 const redirectUri = options.redirectUri || window.location.origin + window.location.pathname; 317 - storage.set(STORAGE_KEYS.codeVerifier, codeVerifier); 318 - storage.set(STORAGE_KEYS.oauthState, state); 319 - storage.set(STORAGE_KEYS.clientId, clientId); 320 - storage.set(STORAGE_KEYS.redirectUri, redirectUri); 333 + storage.set("codeVerifier", codeVerifier); 334 + storage.set("oauthState", state); 335 + storage.set("clientId", clientId); 336 + storage.set("redirectUri", redirectUri); 321 337 const params = new URLSearchParams({ 322 338 client_id: clientId, 323 339 redirect_uri: redirectUri, ··· 334 350 } 335 351 window.location.href = `${authorizeUrl}?${params.toString()}`; 336 352 } 337 - async function handleOAuthCallback(tokenUrl) { 353 + async function handleOAuthCallback(storage, namespace, tokenUrl) { 338 354 const params = new URLSearchParams(window.location.search); 339 355 const code = params.get("code"); 340 356 const state = params.get("state"); ··· 347 363 if (!code || !state) { 348 364 return false; 349 365 } 350 - const storedState = storage.get(STORAGE_KEYS.oauthState); 366 + const storedState = storage.get("oauthState"); 351 367 if (state !== storedState) { 352 368 throw new Error("OAuth state mismatch - possible CSRF attack"); 353 369 } 354 - const codeVerifier = storage.get(STORAGE_KEYS.codeVerifier); 355 - const clientId = storage.get(STORAGE_KEYS.clientId); 356 - const redirectUri = storage.get(STORAGE_KEYS.redirectUri); 370 + const codeVerifier = storage.get("codeVerifier"); 371 + const clientId = storage.get("clientId"); 372 + const redirectUri = storage.get("redirectUri"); 357 373 if (!codeVerifier || !clientId || !redirectUri) { 358 374 throw new Error("Missing OAuth session data"); 359 375 } 360 - const dpopProof = await createDPoPProof("POST", tokenUrl); 376 + const dpopProof = await createDPoPProof(namespace, "POST", tokenUrl); 361 377 const tokenResponse = await fetch(tokenUrl, { 362 378 method: "POST", 363 379 headers: { ··· 379 395 ); 380 396 } 381 397 const tokens = await tokenResponse.json(); 382 - storeTokens(tokens); 383 - storage.remove(STORAGE_KEYS.codeVerifier); 384 - storage.remove(STORAGE_KEYS.oauthState); 385 - storage.remove(STORAGE_KEYS.redirectUri); 398 + storeTokens(storage, tokens); 399 + storage.remove("codeVerifier"); 400 + storage.remove("oauthState"); 401 + storage.remove("redirectUri"); 386 402 window.history.replaceState({}, document.title, window.location.pathname); 387 403 return true; 388 404 } 389 - async function logout(options = {}) { 405 + async function logout(storage, namespace, options = {}) { 390 406 storage.clear(); 391 - await clearDPoPKeys(); 407 + await clearDPoPKeys(namespace); 392 408 if (options.reload !== false) { 393 409 window.location.reload(); 394 410 } 395 411 } 396 412 397 413 // src/graphql.ts 398 - async function graphqlRequest(graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false) { 414 + async function graphqlRequest(storage, namespace, graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false) { 399 415 const headers = { 400 416 "Content-Type": "application/json" 401 417 }; 402 418 if (requireAuth) { 403 - const token = await getValidAccessToken(tokenUrl); 419 + const token = await getValidAccessToken(storage, namespace, tokenUrl); 404 420 if (!token) { 405 421 throw new Error("Not authenticated"); 406 422 } 407 - const dpopProof = await createDPoPProof("POST", graphqlUrl, token); 423 + const dpopProof = await createDPoPProof(namespace, "POST", graphqlUrl, token); 408 424 headers["Authorization"] = `DPoP ${token}`; 409 425 headers["DPoP"] = dpopProof; 410 426 } ··· 427 443 var QuicksliceClient = class { 428 444 constructor(options) { 429 445 this.initialized = false; 446 + this.namespace = ""; 447 + this.storage = null; 430 448 this.server = options.server.replace(/\/$/, ""); 431 449 this.clientId = options.clientId; 432 450 this.redirectUri = options.redirectUri; ··· 440 458 */ 441 459 async init() { 442 460 if (this.initialized) return; 443 - await getOrCreateDPoPKey(); 461 + this.namespace = await generateNamespaceHash(this.clientId); 462 + const keys = createStorageKeys(this.namespace); 463 + this.storage = createStorage(keys); 464 + await getOrCreateDPoPKey(this.namespace); 444 465 this.initialized = true; 445 466 } 467 + getStorage() { 468 + if (!this.storage) { 469 + throw new Error("Client not initialized. Call init() first."); 470 + } 471 + return this.storage; 472 + } 446 473 /** 447 474 * Start OAuth login flow 448 475 */ 449 476 async loginWithRedirect(options = {}) { 450 477 await this.init(); 451 - await initiateLogin(this.authorizeUrl, this.clientId, { 478 + await initiateLogin(this.getStorage(), this.authorizeUrl, this.clientId, { 452 479 ...options, 453 480 redirectUri: options.redirectUri || this.redirectUri, 454 481 scope: options.scope || this.scope ··· 460 487 */ 461 488 async handleRedirectCallback() { 462 489 await this.init(); 463 - return await handleOAuthCallback(this.tokenUrl); 490 + return await handleOAuthCallback(this.getStorage(), this.namespace, this.tokenUrl); 464 491 } 465 492 /** 466 493 * Logout and clear all stored data 467 494 */ 468 495 async logout(options = {}) { 469 - await logout(options); 496 + await this.init(); 497 + await logout(this.getStorage(), this.namespace, options); 470 498 } 471 499 /** 472 500 * Check if user is authenticated 473 501 */ 474 502 async isAuthenticated() { 475 - return hasValidSession(); 503 + await this.init(); 504 + return hasValidSession(this.getStorage()); 476 505 } 477 506 /** 478 507 * Get current user's DID (from stored token data) 479 508 * For richer profile info, use client.query() with your own schema 480 509 */ 481 - getUser() { 482 - if (!hasValidSession()) { 510 + async getUser() { 511 + await this.init(); 512 + if (!hasValidSession(this.getStorage())) { 483 513 return null; 484 514 } 485 - const did = storage.get(STORAGE_KEYS.userDid); 515 + const did = this.getStorage().get("userDid"); 486 516 if (!did) { 487 517 return null; 488 518 } ··· 493 523 */ 494 524 async getAccessToken() { 495 525 await this.init(); 496 - return await getValidAccessToken(this.tokenUrl); 526 + return await getValidAccessToken(this.getStorage(), this.namespace, this.tokenUrl); 497 527 } 498 528 /** 499 529 * Execute a GraphQL query (authenticated) ··· 501 531 async query(query, variables = {}) { 502 532 await this.init(); 503 533 return await graphqlRequest( 534 + this.getStorage(), 535 + this.namespace, 504 536 this.graphqlUrl, 505 537 this.tokenUrl, 506 538 query, ··· 520 552 async publicQuery(query, variables = {}) { 521 553 await this.init(); 522 554 return await graphqlRequest( 555 + this.getStorage(), 556 + this.namespace, 523 557 this.graphqlUrl, 524 558 this.tokenUrl, 525 559 query,
+2 -2
quickslice-client-js/dist/quickslice-client.esm.js.map
··· 1 1 { 2 2 "version": 3, 3 3 "sources": ["../src/storage/keys.ts", "../src/storage/storage.ts", "../src/utils/base64url.ts", "../src/utils/crypto.ts", "../src/auth/dpop.ts", "../src/auth/pkce.ts", "../src/storage/lock.ts", "../src/auth/tokens.ts", "../src/auth/oauth.ts", "../src/graphql.ts", "../src/client.ts", "../src/errors.ts", "../src/index.ts"], 4 - "sourcesContent": ["/**\n * Storage key constants\n */\nexport const STORAGE_KEYS = {\n accessToken: 'quickslice_access_token',\n refreshToken: 'quickslice_refresh_token',\n tokenExpiresAt: 'quickslice_token_expires_at',\n clientId: 'quickslice_client_id',\n userDid: 'quickslice_user_did',\n codeVerifier: 'quickslice_code_verifier',\n oauthState: 'quickslice_oauth_state',\n redirectUri: 'quickslice_redirect_uri',\n} as const;\n\nexport type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];\n", "import { STORAGE_KEYS, StorageKey } from './keys';\n\n/**\n * Hybrid storage utility - sessionStorage for OAuth flow state,\n * localStorage for tokens (shared across tabs)\n */\nexport const storage = {\n get(key: StorageKey): string | null {\n // OAuth flow state stays in sessionStorage (per-tab)\n if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) {\n return sessionStorage.getItem(key);\n }\n // Tokens go in localStorage (shared across tabs)\n return localStorage.getItem(key);\n },\n\n set(key: StorageKey, value: string): void {\n if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) {\n sessionStorage.setItem(key, value);\n } else {\n localStorage.setItem(key, value);\n }\n },\n\n remove(key: StorageKey): void {\n sessionStorage.removeItem(key);\n localStorage.removeItem(key);\n },\n\n clear(): void {\n Object.values(STORAGE_KEYS).forEach((key) => {\n sessionStorage.removeItem(key);\n localStorage.removeItem(key);\n });\n },\n};\n", "/**\n * Base64 URL encode a buffer (Uint8Array or ArrayBuffer)\n */\nexport function base64UrlEncode(buffer: ArrayBuffer | Uint8Array): string {\n const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Generate a random base64url string\n */\nexport function generateRandomString(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n", "import { base64UrlEncode } from './base64url';\n\n/**\n * SHA-256 hash, returned as base64url string\n */\nexport async function sha256Base64Url(data: string): Promise<string> {\n const encoder = new TextEncoder();\n const hash = await crypto.subtle.digest('SHA-256', encoder.encode(data));\n return base64UrlEncode(hash);\n}\n\n/**\n * Sign a JWT with an ECDSA P-256 private key\n */\nexport async function signJwt(\n header: Record<string, unknown>,\n payload: Record<string, unknown>,\n privateKey: CryptoKey\n): Promise<string> {\n const encoder = new TextEncoder();\n\n const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header)));\n const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));\n\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const signature = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n privateKey,\n encoder.encode(signingInput)\n );\n\n const signatureB64 = base64UrlEncode(signature);\n\n return `${signingInput}.${signatureB64}`;\n}\n", "import { generateRandomString } from '../utils/base64url';\nimport { sha256Base64Url, signJwt } from '../utils/crypto';\n\nconst DB_NAME = 'quickslice-oauth';\nconst DB_VERSION = 1;\nconst KEY_STORE = 'dpop-keys';\nconst KEY_ID = 'dpop-key';\n\ninterface DPoPKeyData {\n id: string;\n privateKey: CryptoKey;\n publicJwk: JsonWebKey;\n createdAt: number;\n}\n\nlet dbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openDatabase(): Promise<IDBDatabase> {\n if (dbPromise) return dbPromise;\n\n dbPromise = new Promise((resolve, reject) => {\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n if (!db.objectStoreNames.contains(KEY_STORE)) {\n db.createObjectStore(KEY_STORE, { keyPath: 'id' });\n }\n };\n });\n\n return dbPromise;\n}\n\nasync function getDPoPKey(): Promise<DPoPKeyData | null> {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readonly');\n const store = tx.objectStore(KEY_STORE);\n const request = store.get(KEY_ID);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result || null);\n });\n}\n\nasync function storeDPoPKey(\n privateKey: CryptoKey,\n publicJwk: JsonWebKey\n): Promise<void> {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.put({\n id: KEY_ID,\n privateKey,\n publicJwk,\n createdAt: Date.now(),\n });\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n\nexport async function getOrCreateDPoPKey(): Promise<DPoPKeyData> {\n const keyData = await getDPoPKey();\n\n if (keyData) {\n return keyData;\n }\n\n // Generate new P-256 key pair\n const keyPair = await crypto.subtle.generateKey(\n { name: 'ECDSA', namedCurve: 'P-256' },\n false, // NOT extractable - critical for security\n ['sign']\n );\n\n // Export public key as JWK\n const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);\n\n // Store in IndexedDB\n await storeDPoPKey(keyPair.privateKey, publicJwk);\n\n return {\n id: KEY_ID,\n privateKey: keyPair.privateKey,\n publicJwk,\n createdAt: Date.now(),\n };\n}\n\n/**\n * Create a DPoP proof JWT\n */\nexport async function createDPoPProof(\n method: string,\n url: string,\n accessToken: string | null = null\n): Promise<string> {\n const keyData = await getOrCreateDPoPKey();\n\n // Strip WebCrypto-specific fields from JWK for interoperability\n const { kty, crv, x, y } = keyData.publicJwk;\n const minimalJwk = { kty, crv, x, y };\n\n const header = {\n alg: 'ES256',\n typ: 'dpop+jwt',\n jwk: minimalJwk,\n };\n\n const payload: Record<string, unknown> = {\n jti: generateRandomString(16),\n htm: method,\n htu: url,\n iat: Math.floor(Date.now() / 1000),\n };\n\n // Add access token hash if provided (for resource requests)\n if (accessToken) {\n payload.ath = await sha256Base64Url(accessToken);\n }\n\n return await signJwt(header, payload, keyData.privateKey);\n}\n\n/**\n * Clear DPoP keys from IndexedDB\n */\nexport async function clearDPoPKeys(): Promise<void> {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.clear();\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n", "import { base64UrlEncode, generateRandomString } from '../utils/base64url';\n\n/**\n * Generate a PKCE code verifier (32 random bytes, base64url encoded)\n */\nexport function generateCodeVerifier(): string {\n return generateRandomString(32);\n}\n\n/**\n * Generate a PKCE code challenge from a verifier (SHA-256, base64url encoded)\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(hash);\n}\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nexport function generateState(): string {\n return generateRandomString(16);\n}\n", "const LOCK_TIMEOUT = 5000; // 5 seconds\nconst LOCK_PREFIX = 'quickslice_lock_';\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Acquire a lock using localStorage for multi-tab coordination\n */\nexport async function acquireLock(\n key: string,\n timeout = LOCK_TIMEOUT\n): Promise<string | null> {\n const lockKey = LOCK_PREFIX + key;\n const lockValue = `${Date.now()}_${Math.random()}`;\n const deadline = Date.now() + timeout;\n\n while (Date.now() < deadline) {\n const existing = localStorage.getItem(lockKey);\n\n if (existing) {\n // Check if lock is stale (older than timeout)\n const [timestamp] = existing.split('_');\n if (Date.now() - parseInt(timestamp) > LOCK_TIMEOUT) {\n // Lock is stale, remove it\n localStorage.removeItem(lockKey);\n } else {\n // Lock is held, wait and retry\n await sleep(50);\n continue;\n }\n }\n\n // Try to acquire\n localStorage.setItem(lockKey, lockValue);\n\n // Verify we got it (handle race condition)\n await sleep(10);\n if (localStorage.getItem(lockKey) === lockValue) {\n return lockValue; // Lock acquired\n }\n }\n\n return null; // Failed to acquire\n}\n\n/**\n * Release a lock\n */\nexport function releaseLock(key: string, lockValue: string): void {\n const lockKey = LOCK_PREFIX + key;\n // Only release if we still hold it\n if (localStorage.getItem(lockKey) === lockValue) {\n localStorage.removeItem(lockKey);\n }\n}\n", "import { storage } from '../storage/storage';\nimport { STORAGE_KEYS } from '../storage/keys';\nimport { acquireLock, releaseLock } from '../storage/lock';\nimport { createDPoPProof } from './dpop';\n\nconst TOKEN_REFRESH_BUFFER_MS = 60000; // 60 seconds before expiry\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Refresh tokens using the refresh token\n */\nasync function refreshTokens(tokenUrl: string): Promise<string> {\n const refreshToken = storage.get(STORAGE_KEYS.refreshToken);\n const clientId = storage.get(STORAGE_KEYS.clientId);\n\n if (!refreshToken || !clientId) {\n throw new Error('No refresh token available');\n }\n\n const dpopProof = await createDPoPProof('POST', tokenUrl);\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(\n `Token refresh failed: ${errorData.error_description || response.statusText}`\n );\n }\n\n const tokens = await response.json();\n\n // Store new tokens (rotation - new refresh token each time)\n storage.set(STORAGE_KEYS.accessToken, tokens.access_token);\n if (tokens.refresh_token) {\n storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString());\n\n return tokens.access_token;\n}\n\n/**\n * Get a valid access token, refreshing if necessary.\n * Uses multi-tab locking to prevent duplicate refresh requests.\n */\nexport async function getValidAccessToken(tokenUrl: string): Promise<string> {\n const accessToken = storage.get(STORAGE_KEYS.accessToken);\n const expiresAt = parseInt(storage.get(STORAGE_KEYS.tokenExpiresAt) || '0');\n\n // Check if token is still valid (with buffer)\n if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) {\n return accessToken;\n }\n\n // Need to refresh - acquire lock first\n const clientId = storage.get(STORAGE_KEYS.clientId);\n const lockKey = `token_refresh_${clientId}`;\n const lockValue = await acquireLock(lockKey);\n\n if (!lockValue) {\n // Failed to acquire lock, another tab is refreshing\n // Wait a bit and check cache again\n await sleep(100);\n const freshToken = storage.get(STORAGE_KEYS.accessToken);\n const freshExpiry = parseInt(\n storage.get(STORAGE_KEYS.tokenExpiresAt) || '0'\n );\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n throw new Error('Failed to refresh token');\n }\n\n try {\n // Double-check after acquiring lock\n const freshToken = storage.get(STORAGE_KEYS.accessToken);\n const freshExpiry = parseInt(\n storage.get(STORAGE_KEYS.tokenExpiresAt) || '0'\n );\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n\n // Actually refresh\n return await refreshTokens(tokenUrl);\n } finally {\n releaseLock(lockKey, lockValue);\n }\n}\n\n/**\n * Store tokens from OAuth response\n */\nexport function storeTokens(tokens: {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n sub?: string;\n}): void {\n storage.set(STORAGE_KEYS.accessToken, tokens.access_token);\n if (tokens.refresh_token) {\n storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString());\n\n if (tokens.sub) {\n storage.set(STORAGE_KEYS.userDid, tokens.sub);\n }\n}\n\n/**\n * Check if we have a valid session\n */\nexport function hasValidSession(): boolean {\n const accessToken = storage.get(STORAGE_KEYS.accessToken);\n const refreshToken = storage.get(STORAGE_KEYS.refreshToken);\n return !!(accessToken || refreshToken);\n}\n", "import { storage } from '../storage/storage';\nimport { STORAGE_KEYS } from '../storage/keys';\nimport { createDPoPProof, clearDPoPKeys } from './dpop';\nimport { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { storeTokens } from './tokens';\n\nexport interface LoginOptions {\n handle?: string;\n redirectUri?: string;\n scope?: string;\n}\n\n/**\n * Initiate OAuth login flow with PKCE\n */\nexport async function initiateLogin(\n authorizeUrl: string,\n clientId: string,\n options: LoginOptions = {}\n): Promise<void> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n // Build redirect URI (use provided or derive from current page)\n const redirectUri = options.redirectUri || (window.location.origin + window.location.pathname);\n\n // Store for callback\n storage.set(STORAGE_KEYS.codeVerifier, codeVerifier);\n storage.set(STORAGE_KEYS.oauthState, state);\n storage.set(STORAGE_KEYS.clientId, clientId);\n storage.set(STORAGE_KEYS.redirectUri, redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state: state,\n });\n\n if (options.handle) {\n params.set('login_hint', options.handle);\n }\n\n if (options.scope) {\n params.set('scope', options.scope);\n }\n\n window.location.href = `${authorizeUrl}?${params.toString()}`;\n}\n\n/**\n * Handle OAuth callback - exchange code for tokens\n * Returns true if callback was handled, false if not a callback\n */\nexport async function handleOAuthCallback(tokenUrl: string): Promise<boolean> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n throw new Error(\n `OAuth error: ${error} - ${params.get('error_description') || ''}`\n );\n }\n\n if (!code || !state) {\n return false; // Not a callback\n }\n\n // Verify state\n const storedState = storage.get(STORAGE_KEYS.oauthState);\n if (state !== storedState) {\n throw new Error('OAuth state mismatch - possible CSRF attack');\n }\n\n // Get stored values\n const codeVerifier = storage.get(STORAGE_KEYS.codeVerifier);\n const clientId = storage.get(STORAGE_KEYS.clientId);\n const redirectUri = storage.get(STORAGE_KEYS.redirectUri);\n\n if (!codeVerifier || !clientId || !redirectUri) {\n throw new Error('Missing OAuth session data');\n }\n\n // Exchange code for tokens with DPoP\n const dpopProof = await createDPoPProof('POST', tokenUrl);\n\n const tokenResponse = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const errorData = await tokenResponse.json().catch(() => ({}));\n throw new Error(\n `Token exchange failed: ${errorData.error_description || tokenResponse.statusText}`\n );\n }\n\n const tokens = await tokenResponse.json();\n\n // Store tokens\n storeTokens(tokens);\n\n // Clean up OAuth state\n storage.remove(STORAGE_KEYS.codeVerifier);\n storage.remove(STORAGE_KEYS.oauthState);\n storage.remove(STORAGE_KEYS.redirectUri);\n\n // Clear URL params\n window.history.replaceState({}, document.title, window.location.pathname);\n\n return true;\n}\n\n/**\n * Logout - clear all stored data\n */\nexport async function logout(options: { reload?: boolean } = {}): Promise<void> {\n storage.clear();\n await clearDPoPKeys();\n\n if (options.reload !== false) {\n window.location.reload();\n }\n}\n", "import { createDPoPProof } from './auth/dpop';\nimport { getValidAccessToken } from './auth/tokens';\n\nexport interface GraphQLResponse<T = unknown> {\n data?: T;\n errors?: Array<{ message: string; path?: string[] }>;\n}\n\n/**\n * Execute a GraphQL query or mutation\n */\nexport async function graphqlRequest<T = unknown>(\n graphqlUrl: string,\n tokenUrl: string,\n query: string,\n variables: Record<string, unknown> = {},\n requireAuth = false\n): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n if (requireAuth) {\n const token = await getValidAccessToken(tokenUrl);\n if (!token) {\n throw new Error('Not authenticated');\n }\n\n // Create DPoP proof bound to this request\n const dpopProof = await createDPoPProof('POST', graphqlUrl, token);\n\n headers['Authorization'] = `DPoP ${token}`;\n headers['DPoP'] = dpopProof;\n }\n\n const response = await fetch(graphqlUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`GraphQL request failed: ${response.statusText}`);\n }\n\n const result: GraphQLResponse<T> = await response.json();\n\n if (result.errors && result.errors.length > 0) {\n throw new Error(`GraphQL error: ${result.errors[0].message}`);\n }\n\n return result.data as T;\n}\n", "import { storage } from './storage/storage';\nimport { STORAGE_KEYS } from './storage/keys';\nimport { getOrCreateDPoPKey } from './auth/dpop';\nimport { initiateLogin, handleOAuthCallback, logout as doLogout, LoginOptions } from './auth/oauth';\nimport { getValidAccessToken, hasValidSession } from './auth/tokens';\nimport { graphqlRequest } from './graphql';\n\nexport interface QuicksliceClientOptions {\n server: string;\n clientId: string;\n redirectUri?: string;\n scope?: string;\n}\n\nexport interface User {\n did: string;\n}\n\nexport class QuicksliceClient {\n private server: string;\n private clientId: string;\n private redirectUri?: string;\n private scope?: string;\n private graphqlUrl: string;\n private authorizeUrl: string;\n private tokenUrl: string;\n private initialized = false;\n\n constructor(options: QuicksliceClientOptions) {\n this.server = options.server.replace(/\\/$/, ''); // Remove trailing slash\n this.clientId = options.clientId;\n this.redirectUri = options.redirectUri;\n this.scope = options.scope;\n\n this.graphqlUrl = `${this.server}/graphql`;\n this.authorizeUrl = `${this.server}/oauth/authorize`;\n this.tokenUrl = `${this.server}/oauth/token`;\n }\n\n /**\n * Initialize the client - must be called before other methods\n */\n async init(): Promise<void> {\n if (this.initialized) return;\n\n // Ensure DPoP key exists\n await getOrCreateDPoPKey();\n\n this.initialized = true;\n }\n\n /**\n * Start OAuth login flow\n */\n async loginWithRedirect(options: LoginOptions = {}): Promise<void> {\n await this.init();\n await initiateLogin(this.authorizeUrl, this.clientId, {\n ...options,\n redirectUri: options.redirectUri || this.redirectUri,\n scope: options.scope || this.scope,\n });\n }\n\n /**\n * Handle OAuth callback after redirect\n * Returns true if callback was handled\n */\n async handleRedirectCallback(): Promise<boolean> {\n await this.init();\n return await handleOAuthCallback(this.tokenUrl);\n }\n\n /**\n * Logout and clear all stored data\n */\n async logout(options: { reload?: boolean } = {}): Promise<void> {\n await doLogout(options);\n }\n\n /**\n * Check if user is authenticated\n */\n async isAuthenticated(): Promise<boolean> {\n return hasValidSession();\n }\n\n /**\n * Get current user's DID (from stored token data)\n * For richer profile info, use client.query() with your own schema\n */\n getUser(): User | null {\n if (!hasValidSession()) {\n return null;\n }\n\n const did = storage.get(STORAGE_KEYS.userDid);\n if (!did) {\n return null;\n }\n\n return { did };\n }\n\n /**\n * Get access token (auto-refreshes if needed)\n */\n async getAccessToken(): Promise<string> {\n await this.init();\n return await getValidAccessToken(this.tokenUrl);\n }\n\n /**\n * Execute a GraphQL query (authenticated)\n */\n async query<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n true\n );\n }\n\n /**\n * Execute a GraphQL mutation (authenticated)\n */\n async mutate<T = unknown>(\n mutation: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n return this.query<T>(mutation, variables);\n }\n\n /**\n * Execute a public GraphQL query (no auth)\n */\n async publicQuery<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n false\n );\n }\n}\n", "/**\n * Base error class for Quickslice client errors\n */\nexport class QuicksliceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'QuicksliceError';\n }\n}\n\n/**\n * Thrown when authentication is required but user is not logged in\n */\nexport class LoginRequiredError extends QuicksliceError {\n constructor(message = 'Login required') {\n super(message);\n this.name = 'LoginRequiredError';\n }\n}\n\n/**\n * Thrown when network request fails\n */\nexport class NetworkError extends QuicksliceError {\n constructor(message: string) {\n super(message);\n this.name = 'NetworkError';\n }\n}\n\n/**\n * Thrown when OAuth flow fails\n */\nexport class OAuthError extends QuicksliceError {\n public code: string;\n public description?: string;\n\n constructor(code: string, description?: string) {\n super(`OAuth error: ${code}${description ? ` - ${description}` : ''}`);\n this.name = 'OAuthError';\n this.code = code;\n this.description = description;\n }\n}\n", "export { QuicksliceClient, QuicksliceClientOptions, User } from './client';\nexport {\n QuicksliceError,\n LoginRequiredError,\n NetworkError,\n OAuthError,\n} from './errors';\n\nimport { QuicksliceClient, QuicksliceClientOptions } from './client';\n\n/**\n * Create and initialize a Quickslice client\n */\nexport async function createQuicksliceClient(\n options: QuicksliceClientOptions\n): Promise<QuicksliceClient> {\n const client = new QuicksliceClient(options);\n await client.init();\n return client;\n}\n"], 5 - "mappings": "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", 4 + "sourcesContent": ["/**\n * Storage key factory - generates namespaced keys\n */\nexport interface StorageKeys {\n accessToken: string;\n refreshToken: string;\n tokenExpiresAt: string;\n clientId: string;\n userDid: string;\n codeVerifier: string;\n oauthState: string;\n redirectUri: string;\n}\n\nexport function createStorageKeys(namespace: string): StorageKeys {\n return {\n accessToken: `quickslice_${namespace}_access_token`,\n refreshToken: `quickslice_${namespace}_refresh_token`,\n tokenExpiresAt: `quickslice_${namespace}_token_expires_at`,\n clientId: `quickslice_${namespace}_client_id`,\n userDid: `quickslice_${namespace}_user_did`,\n codeVerifier: `quickslice_${namespace}_code_verifier`,\n oauthState: `quickslice_${namespace}_oauth_state`,\n redirectUri: `quickslice_${namespace}_redirect_uri`,\n };\n}\n\nexport type StorageKey = string;\n", "import { StorageKeys } from './keys';\n\n/**\n * Create a namespaced storage interface\n */\nexport function createStorage(keys: StorageKeys) {\n return {\n get(key: keyof StorageKeys): string | null {\n const storageKey = keys[key];\n // OAuth flow state stays in sessionStorage (per-tab)\n if (key === 'codeVerifier' || key === 'oauthState') {\n return sessionStorage.getItem(storageKey);\n }\n // Tokens go in localStorage (shared across tabs)\n return localStorage.getItem(storageKey);\n },\n\n set(key: keyof StorageKeys, value: string): void {\n const storageKey = keys[key];\n if (key === 'codeVerifier' || key === 'oauthState') {\n sessionStorage.setItem(storageKey, value);\n } else {\n localStorage.setItem(storageKey, value);\n }\n },\n\n remove(key: keyof StorageKeys): void {\n const storageKey = keys[key];\n sessionStorage.removeItem(storageKey);\n localStorage.removeItem(storageKey);\n },\n\n clear(): void {\n (Object.keys(keys) as Array<keyof StorageKeys>).forEach((key) => {\n const storageKey = keys[key];\n sessionStorage.removeItem(storageKey);\n localStorage.removeItem(storageKey);\n });\n },\n };\n}\n\nexport type Storage = ReturnType<typeof createStorage>;\n", "/**\n * Base64 URL encode a buffer (Uint8Array or ArrayBuffer)\n */\nexport function base64UrlEncode(buffer: ArrayBuffer | Uint8Array): string {\n const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Generate a random base64url string\n */\nexport function generateRandomString(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n", "import { base64UrlEncode } from './base64url';\n\n/**\n * SHA-256 hash, returned as base64url string\n */\nexport async function sha256Base64Url(data: string): Promise<string> {\n const encoder = new TextEncoder();\n const hash = await crypto.subtle.digest('SHA-256', encoder.encode(data));\n return base64UrlEncode(hash);\n}\n\n/**\n * Generate an 8-character namespace hash from clientId\n */\nexport async function generateNamespaceHash(clientId: string): Promise<string> {\n const encoder = new TextEncoder();\n const hash = await crypto.subtle.digest('SHA-256', encoder.encode(clientId));\n const hashArray = Array.from(new Uint8Array(hash));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n return hashHex.substring(0, 8);\n}\n\n/**\n * Sign a JWT with an ECDSA P-256 private key\n */\nexport async function signJwt(\n header: Record<string, unknown>,\n payload: Record<string, unknown>,\n privateKey: CryptoKey\n): Promise<string> {\n const encoder = new TextEncoder();\n\n const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header)));\n const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));\n\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const signature = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n privateKey,\n encoder.encode(signingInput)\n );\n\n const signatureB64 = base64UrlEncode(signature);\n\n return `${signingInput}.${signatureB64}`;\n}\n", "import { generateRandomString } from '../utils/base64url';\nimport { sha256Base64Url, signJwt } from '../utils/crypto';\n\nconst DB_VERSION = 1;\nconst KEY_STORE = 'dpop-keys';\nconst KEY_ID = 'dpop-key';\n\ninterface DPoPKeyData {\n id: string;\n privateKey: CryptoKey;\n publicJwk: JsonWebKey;\n createdAt: number;\n}\n\n// Cache database connections per namespace\nconst dbPromises = new Map<string, Promise<IDBDatabase>>();\n\nfunction getDbName(namespace: string): string {\n return `quickslice-oauth-${namespace}`;\n}\n\nfunction openDatabase(namespace: string): Promise<IDBDatabase> {\n const existing = dbPromises.get(namespace);\n if (existing) return existing;\n\n const promise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(getDbName(namespace), DB_VERSION);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n if (!db.objectStoreNames.contains(KEY_STORE)) {\n db.createObjectStore(KEY_STORE, { keyPath: 'id' });\n }\n };\n });\n\n dbPromises.set(namespace, promise);\n return promise;\n}\n\nasync function getDPoPKey(namespace: string): Promise<DPoPKeyData | null> {\n const db = await openDatabase(namespace);\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readonly');\n const store = tx.objectStore(KEY_STORE);\n const request = store.get(KEY_ID);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result || null);\n });\n}\n\nasync function storeDPoPKey(\n namespace: string,\n privateKey: CryptoKey,\n publicJwk: JsonWebKey\n): Promise<void> {\n const db = await openDatabase(namespace);\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.put({\n id: KEY_ID,\n privateKey,\n publicJwk,\n createdAt: Date.now(),\n });\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n\nexport async function getOrCreateDPoPKey(namespace: string): Promise<DPoPKeyData> {\n const keyData = await getDPoPKey(namespace);\n\n if (keyData) {\n return keyData;\n }\n\n // Generate new P-256 key pair\n const keyPair = await crypto.subtle.generateKey(\n { name: 'ECDSA', namedCurve: 'P-256' },\n false, // NOT extractable - critical for security\n ['sign']\n );\n\n // Export public key as JWK\n const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);\n\n // Store in IndexedDB\n await storeDPoPKey(namespace, keyPair.privateKey, publicJwk);\n\n return {\n id: KEY_ID,\n privateKey: keyPair.privateKey,\n publicJwk,\n createdAt: Date.now(),\n };\n}\n\n/**\n * Create a DPoP proof JWT\n */\nexport async function createDPoPProof(\n namespace: string,\n method: string,\n url: string,\n accessToken: string | null = null\n): Promise<string> {\n const keyData = await getOrCreateDPoPKey(namespace);\n\n // Strip WebCrypto-specific fields from JWK for interoperability\n const { kty, crv, x, y } = keyData.publicJwk;\n const minimalJwk = { kty, crv, x, y };\n\n const header = {\n alg: 'ES256',\n typ: 'dpop+jwt',\n jwk: minimalJwk,\n };\n\n const payload: Record<string, unknown> = {\n jti: generateRandomString(16),\n htm: method,\n htu: url,\n iat: Math.floor(Date.now() / 1000),\n };\n\n // Add access token hash if provided (for resource requests)\n if (accessToken) {\n payload.ath = await sha256Base64Url(accessToken);\n }\n\n return await signJwt(header, payload, keyData.privateKey);\n}\n\n/**\n * Clear DPoP keys from IndexedDB\n */\nexport async function clearDPoPKeys(namespace: string): Promise<void> {\n const db = await openDatabase(namespace);\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.clear();\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n", "import { base64UrlEncode, generateRandomString } from '../utils/base64url';\n\n/**\n * Generate a PKCE code verifier (32 random bytes, base64url encoded)\n */\nexport function generateCodeVerifier(): string {\n return generateRandomString(32);\n}\n\n/**\n * Generate a PKCE code challenge from a verifier (SHA-256, base64url encoded)\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(hash);\n}\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nexport function generateState(): string {\n return generateRandomString(16);\n}\n", "const LOCK_TIMEOUT = 5000; // 5 seconds\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getLockKey(namespace: string, key: string): string {\n return `quickslice_${namespace}_lock_${key}`;\n}\n\n/**\n * Acquire a lock using localStorage for multi-tab coordination\n */\nexport async function acquireLock(\n namespace: string,\n key: string,\n timeout = LOCK_TIMEOUT\n): Promise<string | null> {\n const lockKey = getLockKey(namespace, key);\n const lockValue = `${Date.now()}_${Math.random()}`;\n const deadline = Date.now() + timeout;\n\n while (Date.now() < deadline) {\n const existing = localStorage.getItem(lockKey);\n\n if (existing) {\n // Check if lock is stale (older than timeout)\n const [timestamp] = existing.split('_');\n if (Date.now() - parseInt(timestamp) > LOCK_TIMEOUT) {\n // Lock is stale, remove it\n localStorage.removeItem(lockKey);\n } else {\n // Lock is held, wait and retry\n await sleep(50);\n continue;\n }\n }\n\n // Try to acquire\n localStorage.setItem(lockKey, lockValue);\n\n // Verify we got it (handle race condition)\n await sleep(10);\n if (localStorage.getItem(lockKey) === lockValue) {\n return lockValue; // Lock acquired\n }\n }\n\n return null; // Failed to acquire\n}\n\n/**\n * Release a lock\n */\nexport function releaseLock(namespace: string, key: string, lockValue: string): void {\n const lockKey = getLockKey(namespace, key);\n // Only release if we still hold it\n if (localStorage.getItem(lockKey) === lockValue) {\n localStorage.removeItem(lockKey);\n }\n}\n", "import { Storage } from '../storage/storage';\nimport { acquireLock, releaseLock } from '../storage/lock';\nimport { createDPoPProof } from './dpop';\n\nconst TOKEN_REFRESH_BUFFER_MS = 60000; // 60 seconds before expiry\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Refresh tokens using the refresh token\n */\nasync function refreshTokens(\n storage: Storage,\n namespace: string,\n tokenUrl: string\n): Promise<string> {\n const refreshToken = storage.get('refreshToken');\n const clientId = storage.get('clientId');\n\n if (!refreshToken || !clientId) {\n throw new Error('No refresh token available');\n }\n\n const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl);\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(\n `Token refresh failed: ${errorData.error_description || response.statusText}`\n );\n }\n\n const tokens = await response.json();\n\n // Store new tokens (rotation - new refresh token each time)\n storage.set('accessToken', tokens.access_token);\n if (tokens.refresh_token) {\n storage.set('refreshToken', tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set('tokenExpiresAt', expiresAt.toString());\n\n return tokens.access_token;\n}\n\n/**\n * Get a valid access token, refreshing if necessary.\n * Uses multi-tab locking to prevent duplicate refresh requests.\n */\nexport async function getValidAccessToken(\n storage: Storage,\n namespace: string,\n tokenUrl: string\n): Promise<string> {\n const accessToken = storage.get('accessToken');\n const expiresAt = parseInt(storage.get('tokenExpiresAt') || '0');\n\n // Check if token is still valid (with buffer)\n if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) {\n return accessToken;\n }\n\n // Need to refresh - acquire lock first\n const lockKey = 'token_refresh';\n const lockValue = await acquireLock(namespace, lockKey);\n\n if (!lockValue) {\n // Failed to acquire lock, another tab is refreshing\n // Wait a bit and check cache again\n await sleep(100);\n const freshToken = storage.get('accessToken');\n const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0');\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n throw new Error('Failed to refresh token');\n }\n\n try {\n // Double-check after acquiring lock\n const freshToken = storage.get('accessToken');\n const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0');\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n\n // Actually refresh\n return await refreshTokens(storage, namespace, tokenUrl);\n } finally {\n releaseLock(namespace, lockKey, lockValue);\n }\n}\n\n/**\n * Store tokens from OAuth response\n */\nexport function storeTokens(\n storage: Storage,\n tokens: {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n sub?: string;\n }\n): void {\n storage.set('accessToken', tokens.access_token);\n if (tokens.refresh_token) {\n storage.set('refreshToken', tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set('tokenExpiresAt', expiresAt.toString());\n\n if (tokens.sub) {\n storage.set('userDid', tokens.sub);\n }\n}\n\n/**\n * Check if we have a valid session\n */\nexport function hasValidSession(storage: Storage): boolean {\n const accessToken = storage.get('accessToken');\n const refreshToken = storage.get('refreshToken');\n return !!(accessToken || refreshToken);\n}\n", "import { Storage } from '../storage/storage';\nimport { createDPoPProof, clearDPoPKeys } from './dpop';\nimport { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { storeTokens } from './tokens';\n\nexport interface LoginOptions {\n handle?: string;\n redirectUri?: string;\n scope?: string;\n}\n\n/**\n * Initiate OAuth login flow with PKCE\n */\nexport async function initiateLogin(\n storage: Storage,\n authorizeUrl: string,\n clientId: string,\n options: LoginOptions = {}\n): Promise<void> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n // Build redirect URI (use provided or derive from current page)\n const redirectUri = options.redirectUri || (window.location.origin + window.location.pathname);\n\n // Store for callback\n storage.set('codeVerifier', codeVerifier);\n storage.set('oauthState', state);\n storage.set('clientId', clientId);\n storage.set('redirectUri', redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state: state,\n });\n\n if (options.handle) {\n params.set('login_hint', options.handle);\n }\n\n if (options.scope) {\n params.set('scope', options.scope);\n }\n\n window.location.href = `${authorizeUrl}?${params.toString()}`;\n}\n\n/**\n * Handle OAuth callback - exchange code for tokens\n * Returns true if callback was handled, false if not a callback\n */\nexport async function handleOAuthCallback(\n storage: Storage,\n namespace: string,\n tokenUrl: string\n): Promise<boolean> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n throw new Error(\n `OAuth error: ${error} - ${params.get('error_description') || ''}`\n );\n }\n\n if (!code || !state) {\n return false; // Not a callback\n }\n\n // Verify state\n const storedState = storage.get('oauthState');\n if (state !== storedState) {\n throw new Error('OAuth state mismatch - possible CSRF attack');\n }\n\n // Get stored values\n const codeVerifier = storage.get('codeVerifier');\n const clientId = storage.get('clientId');\n const redirectUri = storage.get('redirectUri');\n\n if (!codeVerifier || !clientId || !redirectUri) {\n throw new Error('Missing OAuth session data');\n }\n\n // Exchange code for tokens with DPoP\n const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl);\n\n const tokenResponse = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const errorData = await tokenResponse.json().catch(() => ({}));\n throw new Error(\n `Token exchange failed: ${errorData.error_description || tokenResponse.statusText}`\n );\n }\n\n const tokens = await tokenResponse.json();\n\n // Store tokens\n storeTokens(storage, tokens);\n\n // Clean up OAuth state\n storage.remove('codeVerifier');\n storage.remove('oauthState');\n storage.remove('redirectUri');\n\n // Clear URL params\n window.history.replaceState({}, document.title, window.location.pathname);\n\n return true;\n}\n\n/**\n * Logout - clear all stored data\n */\nexport async function logout(\n storage: Storage,\n namespace: string,\n options: { reload?: boolean } = {}\n): Promise<void> {\n storage.clear();\n await clearDPoPKeys(namespace);\n\n if (options.reload !== false) {\n window.location.reload();\n }\n}\n", "import { createDPoPProof } from './auth/dpop';\nimport { getValidAccessToken } from './auth/tokens';\nimport { Storage } from './storage/storage';\n\nexport interface GraphQLResponse<T = unknown> {\n data?: T;\n errors?: Array<{ message: string; path?: string[] }>;\n}\n\n/**\n * Execute a GraphQL query or mutation\n */\nexport async function graphqlRequest<T = unknown>(\n storage: Storage,\n namespace: string,\n graphqlUrl: string,\n tokenUrl: string,\n query: string,\n variables: Record<string, unknown> = {},\n requireAuth = false\n): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n if (requireAuth) {\n const token = await getValidAccessToken(storage, namespace, tokenUrl);\n if (!token) {\n throw new Error('Not authenticated');\n }\n\n // Create DPoP proof bound to this request\n const dpopProof = await createDPoPProof(namespace, 'POST', graphqlUrl, token);\n\n headers['Authorization'] = `DPoP ${token}`;\n headers['DPoP'] = dpopProof;\n }\n\n const response = await fetch(graphqlUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`GraphQL request failed: ${response.statusText}`);\n }\n\n const result: GraphQLResponse<T> = await response.json();\n\n if (result.errors && result.errors.length > 0) {\n throw new Error(`GraphQL error: ${result.errors[0].message}`);\n }\n\n return result.data as T;\n}\n", "import { createStorageKeys } from './storage/keys';\nimport { createStorage, Storage } from './storage/storage';\nimport { getOrCreateDPoPKey } from './auth/dpop';\nimport { initiateLogin, handleOAuthCallback, logout as doLogout, LoginOptions } from './auth/oauth';\nimport { getValidAccessToken, hasValidSession } from './auth/tokens';\nimport { graphqlRequest } from './graphql';\nimport { generateNamespaceHash } from './utils/crypto';\n\nexport interface QuicksliceClientOptions {\n server: string;\n clientId: string;\n redirectUri?: string;\n scope?: string;\n}\n\nexport interface User {\n did: string;\n}\n\nexport class QuicksliceClient {\n private server: string;\n private clientId: string;\n private redirectUri?: string;\n private scope?: string;\n private graphqlUrl: string;\n private authorizeUrl: string;\n private tokenUrl: string;\n private initialized = false;\n private namespace: string = '';\n private storage: Storage | null = null;\n\n constructor(options: QuicksliceClientOptions) {\n this.server = options.server.replace(/\\/$/, ''); // Remove trailing slash\n this.clientId = options.clientId;\n this.redirectUri = options.redirectUri;\n this.scope = options.scope;\n\n this.graphqlUrl = `${this.server}/graphql`;\n this.authorizeUrl = `${this.server}/oauth/authorize`;\n this.tokenUrl = `${this.server}/oauth/token`;\n }\n\n /**\n * Initialize the client - must be called before other methods\n */\n async init(): Promise<void> {\n if (this.initialized) return;\n\n // Generate namespace from clientId\n this.namespace = await generateNamespaceHash(this.clientId);\n\n // Create namespaced storage\n const keys = createStorageKeys(this.namespace);\n this.storage = createStorage(keys);\n\n // Ensure DPoP key exists\n await getOrCreateDPoPKey(this.namespace);\n\n this.initialized = true;\n }\n\n private getStorage(): Storage {\n if (!this.storage) {\n throw new Error('Client not initialized. Call init() first.');\n }\n return this.storage;\n }\n\n /**\n * Start OAuth login flow\n */\n async loginWithRedirect(options: LoginOptions = {}): Promise<void> {\n await this.init();\n await initiateLogin(this.getStorage(), this.authorizeUrl, this.clientId, {\n ...options,\n redirectUri: options.redirectUri || this.redirectUri,\n scope: options.scope || this.scope,\n });\n }\n\n /**\n * Handle OAuth callback after redirect\n * Returns true if callback was handled\n */\n async handleRedirectCallback(): Promise<boolean> {\n await this.init();\n return await handleOAuthCallback(this.getStorage(), this.namespace, this.tokenUrl);\n }\n\n /**\n * Logout and clear all stored data\n */\n async logout(options: { reload?: boolean } = {}): Promise<void> {\n await this.init();\n await doLogout(this.getStorage(), this.namespace, options);\n }\n\n /**\n * Check if user is authenticated\n */\n async isAuthenticated(): Promise<boolean> {\n await this.init();\n return hasValidSession(this.getStorage());\n }\n\n /**\n * Get current user's DID (from stored token data)\n * For richer profile info, use client.query() with your own schema\n */\n async getUser(): Promise<User | null> {\n await this.init();\n if (!hasValidSession(this.getStorage())) {\n return null;\n }\n\n const did = this.getStorage().get('userDid');\n if (!did) {\n return null;\n }\n\n return { did };\n }\n\n /**\n * Get access token (auto-refreshes if needed)\n */\n async getAccessToken(): Promise<string> {\n await this.init();\n return await getValidAccessToken(this.getStorage(), this.namespace, this.tokenUrl);\n }\n\n /**\n * Execute a GraphQL query (authenticated)\n */\n async query<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.getStorage(),\n this.namespace,\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n true\n );\n }\n\n /**\n * Execute a GraphQL mutation (authenticated)\n */\n async mutate<T = unknown>(\n mutation: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n return this.query<T>(mutation, variables);\n }\n\n /**\n * Execute a public GraphQL query (no auth)\n */\n async publicQuery<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.getStorage(),\n this.namespace,\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n false\n );\n }\n}\n", "/**\n * Base error class for Quickslice client errors\n */\nexport class QuicksliceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'QuicksliceError';\n }\n}\n\n/**\n * Thrown when authentication is required but user is not logged in\n */\nexport class LoginRequiredError extends QuicksliceError {\n constructor(message = 'Login required') {\n super(message);\n this.name = 'LoginRequiredError';\n }\n}\n\n/**\n * Thrown when network request fails\n */\nexport class NetworkError extends QuicksliceError {\n constructor(message: string) {\n super(message);\n this.name = 'NetworkError';\n }\n}\n\n/**\n * Thrown when OAuth flow fails\n */\nexport class OAuthError extends QuicksliceError {\n public code: string;\n public description?: string;\n\n constructor(code: string, description?: string) {\n super(`OAuth error: ${code}${description ? ` - ${description}` : ''}`);\n this.name = 'OAuthError';\n this.code = code;\n this.description = description;\n }\n}\n", "export { QuicksliceClient, QuicksliceClientOptions, User } from './client';\nexport {\n QuicksliceError,\n LoginRequiredError,\n NetworkError,\n OAuthError,\n} from './errors';\n\nimport { QuicksliceClient, QuicksliceClientOptions } from './client';\n\n/**\n * Create and initialize a Quickslice client\n */\nexport async function createQuicksliceClient(\n options: QuicksliceClientOptions\n): Promise<QuicksliceClient> {\n const client = new QuicksliceClient(options);\n await client.init();\n return client;\n}\n"], 5 + "mappings": "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", 6 6 "names": ["sleep"] 7 7 }
+151 -117
quickslice-client-js/dist/quickslice-client.js
··· 30 30 }); 31 31 32 32 // src/storage/keys.ts 33 - var STORAGE_KEYS = { 34 - accessToken: "quickslice_access_token", 35 - refreshToken: "quickslice_refresh_token", 36 - tokenExpiresAt: "quickslice_token_expires_at", 37 - clientId: "quickslice_client_id", 38 - userDid: "quickslice_user_did", 39 - codeVerifier: "quickslice_code_verifier", 40 - oauthState: "quickslice_oauth_state", 41 - redirectUri: "quickslice_redirect_uri" 42 - }; 33 + function createStorageKeys(namespace) { 34 + return { 35 + accessToken: `quickslice_${namespace}_access_token`, 36 + refreshToken: `quickslice_${namespace}_refresh_token`, 37 + tokenExpiresAt: `quickslice_${namespace}_token_expires_at`, 38 + clientId: `quickslice_${namespace}_client_id`, 39 + userDid: `quickslice_${namespace}_user_did`, 40 + codeVerifier: `quickslice_${namespace}_code_verifier`, 41 + oauthState: `quickslice_${namespace}_oauth_state`, 42 + redirectUri: `quickslice_${namespace}_redirect_uri` 43 + }; 44 + } 43 45 44 46 // src/storage/storage.ts 45 - var storage = { 46 - get(key) { 47 - if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) { 48 - return sessionStorage.getItem(key); 47 + function createStorage(keys) { 48 + return { 49 + get(key) { 50 + const storageKey = keys[key]; 51 + if (key === "codeVerifier" || key === "oauthState") { 52 + return sessionStorage.getItem(storageKey); 53 + } 54 + return localStorage.getItem(storageKey); 55 + }, 56 + set(key, value) { 57 + const storageKey = keys[key]; 58 + if (key === "codeVerifier" || key === "oauthState") { 59 + sessionStorage.setItem(storageKey, value); 60 + } else { 61 + localStorage.setItem(storageKey, value); 62 + } 63 + }, 64 + remove(key) { 65 + const storageKey = keys[key]; 66 + sessionStorage.removeItem(storageKey); 67 + localStorage.removeItem(storageKey); 68 + }, 69 + clear() { 70 + Object.keys(keys).forEach((key) => { 71 + const storageKey = keys[key]; 72 + sessionStorage.removeItem(storageKey); 73 + localStorage.removeItem(storageKey); 74 + }); 49 75 } 50 - return localStorage.getItem(key); 51 - }, 52 - set(key, value) { 53 - if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) { 54 - sessionStorage.setItem(key, value); 55 - } else { 56 - localStorage.setItem(key, value); 57 - } 58 - }, 59 - remove(key) { 60 - sessionStorage.removeItem(key); 61 - localStorage.removeItem(key); 62 - }, 63 - clear() { 64 - Object.values(STORAGE_KEYS).forEach((key) => { 65 - sessionStorage.removeItem(key); 66 - localStorage.removeItem(key); 67 - }); 68 - } 69 - }; 76 + }; 77 + } 70 78 71 79 // src/utils/base64url.ts 72 80 function base64UrlEncode(buffer) { ··· 89 97 const hash = await crypto.subtle.digest("SHA-256", encoder.encode(data)); 90 98 return base64UrlEncode(hash); 91 99 } 100 + async function generateNamespaceHash(clientId) { 101 + const encoder = new TextEncoder(); 102 + const hash = await crypto.subtle.digest("SHA-256", encoder.encode(clientId)); 103 + const hashArray = Array.from(new Uint8Array(hash)); 104 + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); 105 + return hashHex.substring(0, 8); 106 + } 92 107 async function signJwt(header, payload, privateKey) { 93 108 const encoder = new TextEncoder(); 94 109 const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header))); ··· 104 119 } 105 120 106 121 // src/auth/dpop.ts 107 - var DB_NAME = "quickslice-oauth"; 108 122 var DB_VERSION = 1; 109 123 var KEY_STORE = "dpop-keys"; 110 124 var KEY_ID = "dpop-key"; 111 - var dbPromise = null; 112 - function openDatabase() { 113 - if (dbPromise) return dbPromise; 114 - dbPromise = new Promise((resolve, reject) => { 115 - const request = indexedDB.open(DB_NAME, DB_VERSION); 125 + var dbPromises = /* @__PURE__ */ new Map(); 126 + function getDbName(namespace) { 127 + return `quickslice-oauth-${namespace}`; 128 + } 129 + function openDatabase(namespace) { 130 + const existing = dbPromises.get(namespace); 131 + if (existing) return existing; 132 + const promise = new Promise((resolve, reject) => { 133 + const request = indexedDB.open(getDbName(namespace), DB_VERSION); 116 134 request.onerror = () => reject(request.error); 117 135 request.onsuccess = () => resolve(request.result); 118 136 request.onupgradeneeded = (event) => { ··· 122 140 } 123 141 }; 124 142 }); 125 - return dbPromise; 143 + dbPromises.set(namespace, promise); 144 + return promise; 126 145 } 127 - async function getDPoPKey() { 128 - const db = await openDatabase(); 146 + async function getDPoPKey(namespace) { 147 + const db = await openDatabase(namespace); 129 148 return new Promise((resolve, reject) => { 130 149 const tx = db.transaction(KEY_STORE, "readonly"); 131 150 const store = tx.objectStore(KEY_STORE); ··· 134 153 request.onsuccess = () => resolve(request.result || null); 135 154 }); 136 155 } 137 - async function storeDPoPKey(privateKey, publicJwk) { 138 - const db = await openDatabase(); 156 + async function storeDPoPKey(namespace, privateKey, publicJwk) { 157 + const db = await openDatabase(namespace); 139 158 return new Promise((resolve, reject) => { 140 159 const tx = db.transaction(KEY_STORE, "readwrite"); 141 160 const store = tx.objectStore(KEY_STORE); ··· 149 168 request.onsuccess = () => resolve(); 150 169 }); 151 170 } 152 - async function getOrCreateDPoPKey() { 153 - const keyData = await getDPoPKey(); 171 + async function getOrCreateDPoPKey(namespace) { 172 + const keyData = await getDPoPKey(namespace); 154 173 if (keyData) { 155 174 return keyData; 156 175 } ··· 161 180 ["sign"] 162 181 ); 163 182 const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); 164 - await storeDPoPKey(keyPair.privateKey, publicJwk); 183 + await storeDPoPKey(namespace, keyPair.privateKey, publicJwk); 165 184 return { 166 185 id: KEY_ID, 167 186 privateKey: keyPair.privateKey, ··· 169 188 createdAt: Date.now() 170 189 }; 171 190 } 172 - async function createDPoPProof(method, url, accessToken = null) { 173 - const keyData = await getOrCreateDPoPKey(); 191 + async function createDPoPProof(namespace, method, url, accessToken = null) { 192 + const keyData = await getOrCreateDPoPKey(namespace); 174 193 const { kty, crv, x, y } = keyData.publicJwk; 175 194 const minimalJwk = { kty, crv, x, y }; 176 195 const header = { ··· 189 208 } 190 209 return await signJwt(header, payload, keyData.privateKey); 191 210 } 192 - async function clearDPoPKeys() { 193 - const db = await openDatabase(); 211 + async function clearDPoPKeys(namespace) { 212 + const db = await openDatabase(namespace); 194 213 return new Promise((resolve, reject) => { 195 214 const tx = db.transaction(KEY_STORE, "readwrite"); 196 215 const store = tx.objectStore(KEY_STORE); ··· 216 235 217 236 // src/storage/lock.ts 218 237 var LOCK_TIMEOUT = 5e3; 219 - var LOCK_PREFIX = "quickslice_lock_"; 220 238 function sleep(ms) { 221 239 return new Promise((resolve) => setTimeout(resolve, ms)); 222 240 } 223 - async function acquireLock(key, timeout = LOCK_TIMEOUT) { 224 - const lockKey = LOCK_PREFIX + key; 241 + function getLockKey(namespace, key) { 242 + return `quickslice_${namespace}_lock_${key}`; 243 + } 244 + async function acquireLock(namespace, key, timeout = LOCK_TIMEOUT) { 245 + const lockKey = getLockKey(namespace, key); 225 246 const lockValue = `${Date.now()}_${Math.random()}`; 226 247 const deadline = Date.now() + timeout; 227 248 while (Date.now() < deadline) { ··· 243 264 } 244 265 return null; 245 266 } 246 - function releaseLock(key, lockValue) { 247 - const lockKey = LOCK_PREFIX + key; 267 + function releaseLock(namespace, key, lockValue) { 268 + const lockKey = getLockKey(namespace, key); 248 269 if (localStorage.getItem(lockKey) === lockValue) { 249 270 localStorage.removeItem(lockKey); 250 271 } ··· 255 276 function sleep2(ms) { 256 277 return new Promise((resolve) => setTimeout(resolve, ms)); 257 278 } 258 - async function refreshTokens(tokenUrl) { 259 - const refreshToken = storage.get(STORAGE_KEYS.refreshToken); 260 - const clientId = storage.get(STORAGE_KEYS.clientId); 279 + async function refreshTokens(storage, namespace, tokenUrl) { 280 + const refreshToken = storage.get("refreshToken"); 281 + const clientId = storage.get("clientId"); 261 282 if (!refreshToken || !clientId) { 262 283 throw new Error("No refresh token available"); 263 284 } 264 - const dpopProof = await createDPoPProof("POST", tokenUrl); 285 + const dpopProof = await createDPoPProof(namespace, "POST", tokenUrl); 265 286 const response = await fetch(tokenUrl, { 266 287 method: "POST", 267 288 headers: { ··· 281 302 ); 282 303 } 283 304 const tokens = await response.json(); 284 - storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 305 + storage.set("accessToken", tokens.access_token); 285 306 if (tokens.refresh_token) { 286 - storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 307 + storage.set("refreshToken", tokens.refresh_token); 287 308 } 288 309 const expiresAt = Date.now() + tokens.expires_in * 1e3; 289 - storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString()); 310 + storage.set("tokenExpiresAt", expiresAt.toString()); 290 311 return tokens.access_token; 291 312 } 292 - async function getValidAccessToken(tokenUrl) { 293 - const accessToken = storage.get(STORAGE_KEYS.accessToken); 294 - const expiresAt = parseInt(storage.get(STORAGE_KEYS.tokenExpiresAt) || "0"); 313 + async function getValidAccessToken(storage, namespace, tokenUrl) { 314 + const accessToken = storage.get("accessToken"); 315 + const expiresAt = parseInt(storage.get("tokenExpiresAt") || "0"); 295 316 if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) { 296 317 return accessToken; 297 318 } 298 - const clientId = storage.get(STORAGE_KEYS.clientId); 299 - const lockKey = `token_refresh_${clientId}`; 300 - const lockValue = await acquireLock(lockKey); 319 + const lockKey = "token_refresh"; 320 + const lockValue = await acquireLock(namespace, lockKey); 301 321 if (!lockValue) { 302 322 await sleep2(100); 303 - const freshToken = storage.get(STORAGE_KEYS.accessToken); 304 - const freshExpiry = parseInt( 305 - storage.get(STORAGE_KEYS.tokenExpiresAt) || "0" 306 - ); 323 + const freshToken = storage.get("accessToken"); 324 + const freshExpiry = parseInt(storage.get("tokenExpiresAt") || "0"); 307 325 if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 308 326 return freshToken; 309 327 } 310 328 throw new Error("Failed to refresh token"); 311 329 } 312 330 try { 313 - const freshToken = storage.get(STORAGE_KEYS.accessToken); 314 - const freshExpiry = parseInt( 315 - storage.get(STORAGE_KEYS.tokenExpiresAt) || "0" 316 - ); 331 + const freshToken = storage.get("accessToken"); 332 + const freshExpiry = parseInt(storage.get("tokenExpiresAt") || "0"); 317 333 if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 318 334 return freshToken; 319 335 } 320 - return await refreshTokens(tokenUrl); 336 + return await refreshTokens(storage, namespace, tokenUrl); 321 337 } finally { 322 - releaseLock(lockKey, lockValue); 338 + releaseLock(namespace, lockKey, lockValue); 323 339 } 324 340 } 325 - function storeTokens(tokens) { 326 - storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 341 + function storeTokens(storage, tokens) { 342 + storage.set("accessToken", tokens.access_token); 327 343 if (tokens.refresh_token) { 328 - storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 344 + storage.set("refreshToken", tokens.refresh_token); 329 345 } 330 346 const expiresAt = Date.now() + tokens.expires_in * 1e3; 331 - storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString()); 347 + storage.set("tokenExpiresAt", expiresAt.toString()); 332 348 if (tokens.sub) { 333 - storage.set(STORAGE_KEYS.userDid, tokens.sub); 349 + storage.set("userDid", tokens.sub); 334 350 } 335 351 } 336 - function hasValidSession() { 337 - const accessToken = storage.get(STORAGE_KEYS.accessToken); 338 - const refreshToken = storage.get(STORAGE_KEYS.refreshToken); 352 + function hasValidSession(storage) { 353 + const accessToken = storage.get("accessToken"); 354 + const refreshToken = storage.get("refreshToken"); 339 355 return !!(accessToken || refreshToken); 340 356 } 341 357 342 358 // src/auth/oauth.ts 343 - async function initiateLogin(authorizeUrl, clientId, options = {}) { 359 + async function initiateLogin(storage, authorizeUrl, clientId, options = {}) { 344 360 const codeVerifier = generateCodeVerifier(); 345 361 const codeChallenge = await generateCodeChallenge(codeVerifier); 346 362 const state = generateState(); 347 363 const redirectUri = options.redirectUri || window.location.origin + window.location.pathname; 348 - storage.set(STORAGE_KEYS.codeVerifier, codeVerifier); 349 - storage.set(STORAGE_KEYS.oauthState, state); 350 - storage.set(STORAGE_KEYS.clientId, clientId); 351 - storage.set(STORAGE_KEYS.redirectUri, redirectUri); 364 + storage.set("codeVerifier", codeVerifier); 365 + storage.set("oauthState", state); 366 + storage.set("clientId", clientId); 367 + storage.set("redirectUri", redirectUri); 352 368 const params = new URLSearchParams({ 353 369 client_id: clientId, 354 370 redirect_uri: redirectUri, ··· 365 381 } 366 382 window.location.href = `${authorizeUrl}?${params.toString()}`; 367 383 } 368 - async function handleOAuthCallback(tokenUrl) { 384 + async function handleOAuthCallback(storage, namespace, tokenUrl) { 369 385 const params = new URLSearchParams(window.location.search); 370 386 const code = params.get("code"); 371 387 const state = params.get("state"); ··· 378 394 if (!code || !state) { 379 395 return false; 380 396 } 381 - const storedState = storage.get(STORAGE_KEYS.oauthState); 397 + const storedState = storage.get("oauthState"); 382 398 if (state !== storedState) { 383 399 throw new Error("OAuth state mismatch - possible CSRF attack"); 384 400 } 385 - const codeVerifier = storage.get(STORAGE_KEYS.codeVerifier); 386 - const clientId = storage.get(STORAGE_KEYS.clientId); 387 - const redirectUri = storage.get(STORAGE_KEYS.redirectUri); 401 + const codeVerifier = storage.get("codeVerifier"); 402 + const clientId = storage.get("clientId"); 403 + const redirectUri = storage.get("redirectUri"); 388 404 if (!codeVerifier || !clientId || !redirectUri) { 389 405 throw new Error("Missing OAuth session data"); 390 406 } 391 - const dpopProof = await createDPoPProof("POST", tokenUrl); 407 + const dpopProof = await createDPoPProof(namespace, "POST", tokenUrl); 392 408 const tokenResponse = await fetch(tokenUrl, { 393 409 method: "POST", 394 410 headers: { ··· 410 426 ); 411 427 } 412 428 const tokens = await tokenResponse.json(); 413 - storeTokens(tokens); 414 - storage.remove(STORAGE_KEYS.codeVerifier); 415 - storage.remove(STORAGE_KEYS.oauthState); 416 - storage.remove(STORAGE_KEYS.redirectUri); 429 + storeTokens(storage, tokens); 430 + storage.remove("codeVerifier"); 431 + storage.remove("oauthState"); 432 + storage.remove("redirectUri"); 417 433 window.history.replaceState({}, document.title, window.location.pathname); 418 434 return true; 419 435 } 420 - async function logout(options = {}) { 436 + async function logout(storage, namespace, options = {}) { 421 437 storage.clear(); 422 - await clearDPoPKeys(); 438 + await clearDPoPKeys(namespace); 423 439 if (options.reload !== false) { 424 440 window.location.reload(); 425 441 } 426 442 } 427 443 428 444 // src/graphql.ts 429 - async function graphqlRequest(graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false) { 445 + async function graphqlRequest(storage, namespace, graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false) { 430 446 const headers = { 431 447 "Content-Type": "application/json" 432 448 }; 433 449 if (requireAuth) { 434 - const token = await getValidAccessToken(tokenUrl); 450 + const token = await getValidAccessToken(storage, namespace, tokenUrl); 435 451 if (!token) { 436 452 throw new Error("Not authenticated"); 437 453 } 438 - const dpopProof = await createDPoPProof("POST", graphqlUrl, token); 454 + const dpopProof = await createDPoPProof(namespace, "POST", graphqlUrl, token); 439 455 headers["Authorization"] = `DPoP ${token}`; 440 456 headers["DPoP"] = dpopProof; 441 457 } ··· 458 474 var QuicksliceClient = class { 459 475 constructor(options) { 460 476 this.initialized = false; 477 + this.namespace = ""; 478 + this.storage = null; 461 479 this.server = options.server.replace(/\/$/, ""); 462 480 this.clientId = options.clientId; 463 481 this.redirectUri = options.redirectUri; ··· 471 489 */ 472 490 async init() { 473 491 if (this.initialized) return; 474 - await getOrCreateDPoPKey(); 492 + this.namespace = await generateNamespaceHash(this.clientId); 493 + const keys = createStorageKeys(this.namespace); 494 + this.storage = createStorage(keys); 495 + await getOrCreateDPoPKey(this.namespace); 475 496 this.initialized = true; 476 497 } 498 + getStorage() { 499 + if (!this.storage) { 500 + throw new Error("Client not initialized. Call init() first."); 501 + } 502 + return this.storage; 503 + } 477 504 /** 478 505 * Start OAuth login flow 479 506 */ 480 507 async loginWithRedirect(options = {}) { 481 508 await this.init(); 482 - await initiateLogin(this.authorizeUrl, this.clientId, { 509 + await initiateLogin(this.getStorage(), this.authorizeUrl, this.clientId, { 483 510 ...options, 484 511 redirectUri: options.redirectUri || this.redirectUri, 485 512 scope: options.scope || this.scope ··· 491 518 */ 492 519 async handleRedirectCallback() { 493 520 await this.init(); 494 - return await handleOAuthCallback(this.tokenUrl); 521 + return await handleOAuthCallback(this.getStorage(), this.namespace, this.tokenUrl); 495 522 } 496 523 /** 497 524 * Logout and clear all stored data 498 525 */ 499 526 async logout(options = {}) { 500 - await logout(options); 527 + await this.init(); 528 + await logout(this.getStorage(), this.namespace, options); 501 529 } 502 530 /** 503 531 * Check if user is authenticated 504 532 */ 505 533 async isAuthenticated() { 506 - return hasValidSession(); 534 + await this.init(); 535 + return hasValidSession(this.getStorage()); 507 536 } 508 537 /** 509 538 * Get current user's DID (from stored token data) 510 539 * For richer profile info, use client.query() with your own schema 511 540 */ 512 - getUser() { 513 - if (!hasValidSession()) { 541 + async getUser() { 542 + await this.init(); 543 + if (!hasValidSession(this.getStorage())) { 514 544 return null; 515 545 } 516 - const did = storage.get(STORAGE_KEYS.userDid); 546 + const did = this.getStorage().get("userDid"); 517 547 if (!did) { 518 548 return null; 519 549 } ··· 524 554 */ 525 555 async getAccessToken() { 526 556 await this.init(); 527 - return await getValidAccessToken(this.tokenUrl); 557 + return await getValidAccessToken(this.getStorage(), this.namespace, this.tokenUrl); 528 558 } 529 559 /** 530 560 * Execute a GraphQL query (authenticated) ··· 532 562 async query(query, variables = {}) { 533 563 await this.init(); 534 564 return await graphqlRequest( 565 + this.getStorage(), 566 + this.namespace, 535 567 this.graphqlUrl, 536 568 this.tokenUrl, 537 569 query, ··· 551 583 async publicQuery(query, variables = {}) { 552 584 await this.init(); 553 585 return await graphqlRequest( 586 + this.getStorage(), 587 + this.namespace, 554 588 this.graphqlUrl, 555 589 this.tokenUrl, 556 590 query,
+2 -2
quickslice-client-js/dist/quickslice-client.js.map
··· 1 1 { 2 2 "version": 3, 3 3 "sources": ["../src/index.ts", "../src/storage/keys.ts", "../src/storage/storage.ts", "../src/utils/base64url.ts", "../src/utils/crypto.ts", "../src/auth/dpop.ts", "../src/auth/pkce.ts", "../src/storage/lock.ts", "../src/auth/tokens.ts", "../src/auth/oauth.ts", "../src/graphql.ts", "../src/client.ts", "../src/errors.ts"], 4 - "sourcesContent": ["export { QuicksliceClient, QuicksliceClientOptions, User } from './client';\nexport {\n QuicksliceError,\n LoginRequiredError,\n NetworkError,\n OAuthError,\n} from './errors';\n\nimport { QuicksliceClient, QuicksliceClientOptions } from './client';\n\n/**\n * Create and initialize a Quickslice client\n */\nexport async function createQuicksliceClient(\n options: QuicksliceClientOptions\n): Promise<QuicksliceClient> {\n const client = new QuicksliceClient(options);\n await client.init();\n return client;\n}\n", "/**\n * Storage key constants\n */\nexport const STORAGE_KEYS = {\n accessToken: 'quickslice_access_token',\n refreshToken: 'quickslice_refresh_token',\n tokenExpiresAt: 'quickslice_token_expires_at',\n clientId: 'quickslice_client_id',\n userDid: 'quickslice_user_did',\n codeVerifier: 'quickslice_code_verifier',\n oauthState: 'quickslice_oauth_state',\n redirectUri: 'quickslice_redirect_uri',\n} as const;\n\nexport type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];\n", "import { STORAGE_KEYS, StorageKey } from './keys';\n\n/**\n * Hybrid storage utility - sessionStorage for OAuth flow state,\n * localStorage for tokens (shared across tabs)\n */\nexport const storage = {\n get(key: StorageKey): string | null {\n // OAuth flow state stays in sessionStorage (per-tab)\n if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) {\n return sessionStorage.getItem(key);\n }\n // Tokens go in localStorage (shared across tabs)\n return localStorage.getItem(key);\n },\n\n set(key: StorageKey, value: string): void {\n if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) {\n sessionStorage.setItem(key, value);\n } else {\n localStorage.setItem(key, value);\n }\n },\n\n remove(key: StorageKey): void {\n sessionStorage.removeItem(key);\n localStorage.removeItem(key);\n },\n\n clear(): void {\n Object.values(STORAGE_KEYS).forEach((key) => {\n sessionStorage.removeItem(key);\n localStorage.removeItem(key);\n });\n },\n};\n", "/**\n * Base64 URL encode a buffer (Uint8Array or ArrayBuffer)\n */\nexport function base64UrlEncode(buffer: ArrayBuffer | Uint8Array): string {\n const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Generate a random base64url string\n */\nexport function generateRandomString(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n", "import { base64UrlEncode } from './base64url';\n\n/**\n * SHA-256 hash, returned as base64url string\n */\nexport async function sha256Base64Url(data: string): Promise<string> {\n const encoder = new TextEncoder();\n const hash = await crypto.subtle.digest('SHA-256', encoder.encode(data));\n return base64UrlEncode(hash);\n}\n\n/**\n * Sign a JWT with an ECDSA P-256 private key\n */\nexport async function signJwt(\n header: Record<string, unknown>,\n payload: Record<string, unknown>,\n privateKey: CryptoKey\n): Promise<string> {\n const encoder = new TextEncoder();\n\n const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header)));\n const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));\n\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const signature = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n privateKey,\n encoder.encode(signingInput)\n );\n\n const signatureB64 = base64UrlEncode(signature);\n\n return `${signingInput}.${signatureB64}`;\n}\n", "import { generateRandomString } from '../utils/base64url';\nimport { sha256Base64Url, signJwt } from '../utils/crypto';\n\nconst DB_NAME = 'quickslice-oauth';\nconst DB_VERSION = 1;\nconst KEY_STORE = 'dpop-keys';\nconst KEY_ID = 'dpop-key';\n\ninterface DPoPKeyData {\n id: string;\n privateKey: CryptoKey;\n publicJwk: JsonWebKey;\n createdAt: number;\n}\n\nlet dbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openDatabase(): Promise<IDBDatabase> {\n if (dbPromise) return dbPromise;\n\n dbPromise = new Promise((resolve, reject) => {\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n if (!db.objectStoreNames.contains(KEY_STORE)) {\n db.createObjectStore(KEY_STORE, { keyPath: 'id' });\n }\n };\n });\n\n return dbPromise;\n}\n\nasync function getDPoPKey(): Promise<DPoPKeyData | null> {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readonly');\n const store = tx.objectStore(KEY_STORE);\n const request = store.get(KEY_ID);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result || null);\n });\n}\n\nasync function storeDPoPKey(\n privateKey: CryptoKey,\n publicJwk: JsonWebKey\n): Promise<void> {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.put({\n id: KEY_ID,\n privateKey,\n publicJwk,\n createdAt: Date.now(),\n });\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n\nexport async function getOrCreateDPoPKey(): Promise<DPoPKeyData> {\n const keyData = await getDPoPKey();\n\n if (keyData) {\n return keyData;\n }\n\n // Generate new P-256 key pair\n const keyPair = await crypto.subtle.generateKey(\n { name: 'ECDSA', namedCurve: 'P-256' },\n false, // NOT extractable - critical for security\n ['sign']\n );\n\n // Export public key as JWK\n const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);\n\n // Store in IndexedDB\n await storeDPoPKey(keyPair.privateKey, publicJwk);\n\n return {\n id: KEY_ID,\n privateKey: keyPair.privateKey,\n publicJwk,\n createdAt: Date.now(),\n };\n}\n\n/**\n * Create a DPoP proof JWT\n */\nexport async function createDPoPProof(\n method: string,\n url: string,\n accessToken: string | null = null\n): Promise<string> {\n const keyData = await getOrCreateDPoPKey();\n\n // Strip WebCrypto-specific fields from JWK for interoperability\n const { kty, crv, x, y } = keyData.publicJwk;\n const minimalJwk = { kty, crv, x, y };\n\n const header = {\n alg: 'ES256',\n typ: 'dpop+jwt',\n jwk: minimalJwk,\n };\n\n const payload: Record<string, unknown> = {\n jti: generateRandomString(16),\n htm: method,\n htu: url,\n iat: Math.floor(Date.now() / 1000),\n };\n\n // Add access token hash if provided (for resource requests)\n if (accessToken) {\n payload.ath = await sha256Base64Url(accessToken);\n }\n\n return await signJwt(header, payload, keyData.privateKey);\n}\n\n/**\n * Clear DPoP keys from IndexedDB\n */\nexport async function clearDPoPKeys(): Promise<void> {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.clear();\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n", "import { base64UrlEncode, generateRandomString } from '../utils/base64url';\n\n/**\n * Generate a PKCE code verifier (32 random bytes, base64url encoded)\n */\nexport function generateCodeVerifier(): string {\n return generateRandomString(32);\n}\n\n/**\n * Generate a PKCE code challenge from a verifier (SHA-256, base64url encoded)\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(hash);\n}\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nexport function generateState(): string {\n return generateRandomString(16);\n}\n", "const LOCK_TIMEOUT = 5000; // 5 seconds\nconst LOCK_PREFIX = 'quickslice_lock_';\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Acquire a lock using localStorage for multi-tab coordination\n */\nexport async function acquireLock(\n key: string,\n timeout = LOCK_TIMEOUT\n): Promise<string | null> {\n const lockKey = LOCK_PREFIX + key;\n const lockValue = `${Date.now()}_${Math.random()}`;\n const deadline = Date.now() + timeout;\n\n while (Date.now() < deadline) {\n const existing = localStorage.getItem(lockKey);\n\n if (existing) {\n // Check if lock is stale (older than timeout)\n const [timestamp] = existing.split('_');\n if (Date.now() - parseInt(timestamp) > LOCK_TIMEOUT) {\n // Lock is stale, remove it\n localStorage.removeItem(lockKey);\n } else {\n // Lock is held, wait and retry\n await sleep(50);\n continue;\n }\n }\n\n // Try to acquire\n localStorage.setItem(lockKey, lockValue);\n\n // Verify we got it (handle race condition)\n await sleep(10);\n if (localStorage.getItem(lockKey) === lockValue) {\n return lockValue; // Lock acquired\n }\n }\n\n return null; // Failed to acquire\n}\n\n/**\n * Release a lock\n */\nexport function releaseLock(key: string, lockValue: string): void {\n const lockKey = LOCK_PREFIX + key;\n // Only release if we still hold it\n if (localStorage.getItem(lockKey) === lockValue) {\n localStorage.removeItem(lockKey);\n }\n}\n", "import { storage } from '../storage/storage';\nimport { STORAGE_KEYS } from '../storage/keys';\nimport { acquireLock, releaseLock } from '../storage/lock';\nimport { createDPoPProof } from './dpop';\n\nconst TOKEN_REFRESH_BUFFER_MS = 60000; // 60 seconds before expiry\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Refresh tokens using the refresh token\n */\nasync function refreshTokens(tokenUrl: string): Promise<string> {\n const refreshToken = storage.get(STORAGE_KEYS.refreshToken);\n const clientId = storage.get(STORAGE_KEYS.clientId);\n\n if (!refreshToken || !clientId) {\n throw new Error('No refresh token available');\n }\n\n const dpopProof = await createDPoPProof('POST', tokenUrl);\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(\n `Token refresh failed: ${errorData.error_description || response.statusText}`\n );\n }\n\n const tokens = await response.json();\n\n // Store new tokens (rotation - new refresh token each time)\n storage.set(STORAGE_KEYS.accessToken, tokens.access_token);\n if (tokens.refresh_token) {\n storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString());\n\n return tokens.access_token;\n}\n\n/**\n * Get a valid access token, refreshing if necessary.\n * Uses multi-tab locking to prevent duplicate refresh requests.\n */\nexport async function getValidAccessToken(tokenUrl: string): Promise<string> {\n const accessToken = storage.get(STORAGE_KEYS.accessToken);\n const expiresAt = parseInt(storage.get(STORAGE_KEYS.tokenExpiresAt) || '0');\n\n // Check if token is still valid (with buffer)\n if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) {\n return accessToken;\n }\n\n // Need to refresh - acquire lock first\n const clientId = storage.get(STORAGE_KEYS.clientId);\n const lockKey = `token_refresh_${clientId}`;\n const lockValue = await acquireLock(lockKey);\n\n if (!lockValue) {\n // Failed to acquire lock, another tab is refreshing\n // Wait a bit and check cache again\n await sleep(100);\n const freshToken = storage.get(STORAGE_KEYS.accessToken);\n const freshExpiry = parseInt(\n storage.get(STORAGE_KEYS.tokenExpiresAt) || '0'\n );\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n throw new Error('Failed to refresh token');\n }\n\n try {\n // Double-check after acquiring lock\n const freshToken = storage.get(STORAGE_KEYS.accessToken);\n const freshExpiry = parseInt(\n storage.get(STORAGE_KEYS.tokenExpiresAt) || '0'\n );\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n\n // Actually refresh\n return await refreshTokens(tokenUrl);\n } finally {\n releaseLock(lockKey, lockValue);\n }\n}\n\n/**\n * Store tokens from OAuth response\n */\nexport function storeTokens(tokens: {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n sub?: string;\n}): void {\n storage.set(STORAGE_KEYS.accessToken, tokens.access_token);\n if (tokens.refresh_token) {\n storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString());\n\n if (tokens.sub) {\n storage.set(STORAGE_KEYS.userDid, tokens.sub);\n }\n}\n\n/**\n * Check if we have a valid session\n */\nexport function hasValidSession(): boolean {\n const accessToken = storage.get(STORAGE_KEYS.accessToken);\n const refreshToken = storage.get(STORAGE_KEYS.refreshToken);\n return !!(accessToken || refreshToken);\n}\n", "import { storage } from '../storage/storage';\nimport { STORAGE_KEYS } from '../storage/keys';\nimport { createDPoPProof, clearDPoPKeys } from './dpop';\nimport { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { storeTokens } from './tokens';\n\nexport interface LoginOptions {\n handle?: string;\n redirectUri?: string;\n scope?: string;\n}\n\n/**\n * Initiate OAuth login flow with PKCE\n */\nexport async function initiateLogin(\n authorizeUrl: string,\n clientId: string,\n options: LoginOptions = {}\n): Promise<void> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n // Build redirect URI (use provided or derive from current page)\n const redirectUri = options.redirectUri || (window.location.origin + window.location.pathname);\n\n // Store for callback\n storage.set(STORAGE_KEYS.codeVerifier, codeVerifier);\n storage.set(STORAGE_KEYS.oauthState, state);\n storage.set(STORAGE_KEYS.clientId, clientId);\n storage.set(STORAGE_KEYS.redirectUri, redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state: state,\n });\n\n if (options.handle) {\n params.set('login_hint', options.handle);\n }\n\n if (options.scope) {\n params.set('scope', options.scope);\n }\n\n window.location.href = `${authorizeUrl}?${params.toString()}`;\n}\n\n/**\n * Handle OAuth callback - exchange code for tokens\n * Returns true if callback was handled, false if not a callback\n */\nexport async function handleOAuthCallback(tokenUrl: string): Promise<boolean> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n throw new Error(\n `OAuth error: ${error} - ${params.get('error_description') || ''}`\n );\n }\n\n if (!code || !state) {\n return false; // Not a callback\n }\n\n // Verify state\n const storedState = storage.get(STORAGE_KEYS.oauthState);\n if (state !== storedState) {\n throw new Error('OAuth state mismatch - possible CSRF attack');\n }\n\n // Get stored values\n const codeVerifier = storage.get(STORAGE_KEYS.codeVerifier);\n const clientId = storage.get(STORAGE_KEYS.clientId);\n const redirectUri = storage.get(STORAGE_KEYS.redirectUri);\n\n if (!codeVerifier || !clientId || !redirectUri) {\n throw new Error('Missing OAuth session data');\n }\n\n // Exchange code for tokens with DPoP\n const dpopProof = await createDPoPProof('POST', tokenUrl);\n\n const tokenResponse = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const errorData = await tokenResponse.json().catch(() => ({}));\n throw new Error(\n `Token exchange failed: ${errorData.error_description || tokenResponse.statusText}`\n );\n }\n\n const tokens = await tokenResponse.json();\n\n // Store tokens\n storeTokens(tokens);\n\n // Clean up OAuth state\n storage.remove(STORAGE_KEYS.codeVerifier);\n storage.remove(STORAGE_KEYS.oauthState);\n storage.remove(STORAGE_KEYS.redirectUri);\n\n // Clear URL params\n window.history.replaceState({}, document.title, window.location.pathname);\n\n return true;\n}\n\n/**\n * Logout - clear all stored data\n */\nexport async function logout(options: { reload?: boolean } = {}): Promise<void> {\n storage.clear();\n await clearDPoPKeys();\n\n if (options.reload !== false) {\n window.location.reload();\n }\n}\n", "import { createDPoPProof } from './auth/dpop';\nimport { getValidAccessToken } from './auth/tokens';\n\nexport interface GraphQLResponse<T = unknown> {\n data?: T;\n errors?: Array<{ message: string; path?: string[] }>;\n}\n\n/**\n * Execute a GraphQL query or mutation\n */\nexport async function graphqlRequest<T = unknown>(\n graphqlUrl: string,\n tokenUrl: string,\n query: string,\n variables: Record<string, unknown> = {},\n requireAuth = false\n): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n if (requireAuth) {\n const token = await getValidAccessToken(tokenUrl);\n if (!token) {\n throw new Error('Not authenticated');\n }\n\n // Create DPoP proof bound to this request\n const dpopProof = await createDPoPProof('POST', graphqlUrl, token);\n\n headers['Authorization'] = `DPoP ${token}`;\n headers['DPoP'] = dpopProof;\n }\n\n const response = await fetch(graphqlUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`GraphQL request failed: ${response.statusText}`);\n }\n\n const result: GraphQLResponse<T> = await response.json();\n\n if (result.errors && result.errors.length > 0) {\n throw new Error(`GraphQL error: ${result.errors[0].message}`);\n }\n\n return result.data as T;\n}\n", "import { storage } from './storage/storage';\nimport { STORAGE_KEYS } from './storage/keys';\nimport { getOrCreateDPoPKey } from './auth/dpop';\nimport { initiateLogin, handleOAuthCallback, logout as doLogout, LoginOptions } from './auth/oauth';\nimport { getValidAccessToken, hasValidSession } from './auth/tokens';\nimport { graphqlRequest } from './graphql';\n\nexport interface QuicksliceClientOptions {\n server: string;\n clientId: string;\n redirectUri?: string;\n scope?: string;\n}\n\nexport interface User {\n did: string;\n}\n\nexport class QuicksliceClient {\n private server: string;\n private clientId: string;\n private redirectUri?: string;\n private scope?: string;\n private graphqlUrl: string;\n private authorizeUrl: string;\n private tokenUrl: string;\n private initialized = false;\n\n constructor(options: QuicksliceClientOptions) {\n this.server = options.server.replace(/\\/$/, ''); // Remove trailing slash\n this.clientId = options.clientId;\n this.redirectUri = options.redirectUri;\n this.scope = options.scope;\n\n this.graphqlUrl = `${this.server}/graphql`;\n this.authorizeUrl = `${this.server}/oauth/authorize`;\n this.tokenUrl = `${this.server}/oauth/token`;\n }\n\n /**\n * Initialize the client - must be called before other methods\n */\n async init(): Promise<void> {\n if (this.initialized) return;\n\n // Ensure DPoP key exists\n await getOrCreateDPoPKey();\n\n this.initialized = true;\n }\n\n /**\n * Start OAuth login flow\n */\n async loginWithRedirect(options: LoginOptions = {}): Promise<void> {\n await this.init();\n await initiateLogin(this.authorizeUrl, this.clientId, {\n ...options,\n redirectUri: options.redirectUri || this.redirectUri,\n scope: options.scope || this.scope,\n });\n }\n\n /**\n * Handle OAuth callback after redirect\n * Returns true if callback was handled\n */\n async handleRedirectCallback(): Promise<boolean> {\n await this.init();\n return await handleOAuthCallback(this.tokenUrl);\n }\n\n /**\n * Logout and clear all stored data\n */\n async logout(options: { reload?: boolean } = {}): Promise<void> {\n await doLogout(options);\n }\n\n /**\n * Check if user is authenticated\n */\n async isAuthenticated(): Promise<boolean> {\n return hasValidSession();\n }\n\n /**\n * Get current user's DID (from stored token data)\n * For richer profile info, use client.query() with your own schema\n */\n getUser(): User | null {\n if (!hasValidSession()) {\n return null;\n }\n\n const did = storage.get(STORAGE_KEYS.userDid);\n if (!did) {\n return null;\n }\n\n return { did };\n }\n\n /**\n * Get access token (auto-refreshes if needed)\n */\n async getAccessToken(): Promise<string> {\n await this.init();\n return await getValidAccessToken(this.tokenUrl);\n }\n\n /**\n * Execute a GraphQL query (authenticated)\n */\n async query<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n true\n );\n }\n\n /**\n * Execute a GraphQL mutation (authenticated)\n */\n async mutate<T = unknown>(\n mutation: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n return this.query<T>(mutation, variables);\n }\n\n /**\n * Execute a public GraphQL query (no auth)\n */\n async publicQuery<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n false\n );\n }\n}\n", "/**\n * Base error class for Quickslice client errors\n */\nexport class QuicksliceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'QuicksliceError';\n }\n}\n\n/**\n * Thrown when authentication is required but user is not logged in\n */\nexport class LoginRequiredError extends QuicksliceError {\n constructor(message = 'Login required') {\n super(message);\n this.name = 'LoginRequiredError';\n }\n}\n\n/**\n * Thrown when network request fails\n */\nexport class NetworkError extends QuicksliceError {\n constructor(message: string) {\n super(message);\n this.name = 'NetworkError';\n }\n}\n\n/**\n * Thrown when OAuth flow fails\n */\nexport class OAuthError extends QuicksliceError {\n public code: string;\n public description?: string;\n\n constructor(code: string, description?: string) {\n super(`OAuth error: ${code}${description ? ` - ${description}` : ''}`);\n this.name = 'OAuthError';\n this.code = code;\n this.description = description;\n }\n}\n"], 5 - "mappings": "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", 4 + "sourcesContent": ["export { QuicksliceClient, QuicksliceClientOptions, User } from './client';\nexport {\n QuicksliceError,\n LoginRequiredError,\n NetworkError,\n OAuthError,\n} from './errors';\n\nimport { QuicksliceClient, QuicksliceClientOptions } from './client';\n\n/**\n * Create and initialize a Quickslice client\n */\nexport async function createQuicksliceClient(\n options: QuicksliceClientOptions\n): Promise<QuicksliceClient> {\n const client = new QuicksliceClient(options);\n await client.init();\n return client;\n}\n", "/**\n * Storage key factory - generates namespaced keys\n */\nexport interface StorageKeys {\n accessToken: string;\n refreshToken: string;\n tokenExpiresAt: string;\n clientId: string;\n userDid: string;\n codeVerifier: string;\n oauthState: string;\n redirectUri: string;\n}\n\nexport function createStorageKeys(namespace: string): StorageKeys {\n return {\n accessToken: `quickslice_${namespace}_access_token`,\n refreshToken: `quickslice_${namespace}_refresh_token`,\n tokenExpiresAt: `quickslice_${namespace}_token_expires_at`,\n clientId: `quickslice_${namespace}_client_id`,\n userDid: `quickslice_${namespace}_user_did`,\n codeVerifier: `quickslice_${namespace}_code_verifier`,\n oauthState: `quickslice_${namespace}_oauth_state`,\n redirectUri: `quickslice_${namespace}_redirect_uri`,\n };\n}\n\nexport type StorageKey = string;\n", "import { StorageKeys } from './keys';\n\n/**\n * Create a namespaced storage interface\n */\nexport function createStorage(keys: StorageKeys) {\n return {\n get(key: keyof StorageKeys): string | null {\n const storageKey = keys[key];\n // OAuth flow state stays in sessionStorage (per-tab)\n if (key === 'codeVerifier' || key === 'oauthState') {\n return sessionStorage.getItem(storageKey);\n }\n // Tokens go in localStorage (shared across tabs)\n return localStorage.getItem(storageKey);\n },\n\n set(key: keyof StorageKeys, value: string): void {\n const storageKey = keys[key];\n if (key === 'codeVerifier' || key === 'oauthState') {\n sessionStorage.setItem(storageKey, value);\n } else {\n localStorage.setItem(storageKey, value);\n }\n },\n\n remove(key: keyof StorageKeys): void {\n const storageKey = keys[key];\n sessionStorage.removeItem(storageKey);\n localStorage.removeItem(storageKey);\n },\n\n clear(): void {\n (Object.keys(keys) as Array<keyof StorageKeys>).forEach((key) => {\n const storageKey = keys[key];\n sessionStorage.removeItem(storageKey);\n localStorage.removeItem(storageKey);\n });\n },\n };\n}\n\nexport type Storage = ReturnType<typeof createStorage>;\n", "/**\n * Base64 URL encode a buffer (Uint8Array or ArrayBuffer)\n */\nexport function base64UrlEncode(buffer: ArrayBuffer | Uint8Array): string {\n const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Generate a random base64url string\n */\nexport function generateRandomString(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n", "import { base64UrlEncode } from './base64url';\n\n/**\n * SHA-256 hash, returned as base64url string\n */\nexport async function sha256Base64Url(data: string): Promise<string> {\n const encoder = new TextEncoder();\n const hash = await crypto.subtle.digest('SHA-256', encoder.encode(data));\n return base64UrlEncode(hash);\n}\n\n/**\n * Generate an 8-character namespace hash from clientId\n */\nexport async function generateNamespaceHash(clientId: string): Promise<string> {\n const encoder = new TextEncoder();\n const hash = await crypto.subtle.digest('SHA-256', encoder.encode(clientId));\n const hashArray = Array.from(new Uint8Array(hash));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n return hashHex.substring(0, 8);\n}\n\n/**\n * Sign a JWT with an ECDSA P-256 private key\n */\nexport async function signJwt(\n header: Record<string, unknown>,\n payload: Record<string, unknown>,\n privateKey: CryptoKey\n): Promise<string> {\n const encoder = new TextEncoder();\n\n const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header)));\n const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));\n\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const signature = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n privateKey,\n encoder.encode(signingInput)\n );\n\n const signatureB64 = base64UrlEncode(signature);\n\n return `${signingInput}.${signatureB64}`;\n}\n", "import { generateRandomString } from '../utils/base64url';\nimport { sha256Base64Url, signJwt } from '../utils/crypto';\n\nconst DB_VERSION = 1;\nconst KEY_STORE = 'dpop-keys';\nconst KEY_ID = 'dpop-key';\n\ninterface DPoPKeyData {\n id: string;\n privateKey: CryptoKey;\n publicJwk: JsonWebKey;\n createdAt: number;\n}\n\n// Cache database connections per namespace\nconst dbPromises = new Map<string, Promise<IDBDatabase>>();\n\nfunction getDbName(namespace: string): string {\n return `quickslice-oauth-${namespace}`;\n}\n\nfunction openDatabase(namespace: string): Promise<IDBDatabase> {\n const existing = dbPromises.get(namespace);\n if (existing) return existing;\n\n const promise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(getDbName(namespace), DB_VERSION);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n if (!db.objectStoreNames.contains(KEY_STORE)) {\n db.createObjectStore(KEY_STORE, { keyPath: 'id' });\n }\n };\n });\n\n dbPromises.set(namespace, promise);\n return promise;\n}\n\nasync function getDPoPKey(namespace: string): Promise<DPoPKeyData | null> {\n const db = await openDatabase(namespace);\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readonly');\n const store = tx.objectStore(KEY_STORE);\n const request = store.get(KEY_ID);\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve(request.result || null);\n });\n}\n\nasync function storeDPoPKey(\n namespace: string,\n privateKey: CryptoKey,\n publicJwk: JsonWebKey\n): Promise<void> {\n const db = await openDatabase(namespace);\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.put({\n id: KEY_ID,\n privateKey,\n publicJwk,\n createdAt: Date.now(),\n });\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n\nexport async function getOrCreateDPoPKey(namespace: string): Promise<DPoPKeyData> {\n const keyData = await getDPoPKey(namespace);\n\n if (keyData) {\n return keyData;\n }\n\n // Generate new P-256 key pair\n const keyPair = await crypto.subtle.generateKey(\n { name: 'ECDSA', namedCurve: 'P-256' },\n false, // NOT extractable - critical for security\n ['sign']\n );\n\n // Export public key as JWK\n const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);\n\n // Store in IndexedDB\n await storeDPoPKey(namespace, keyPair.privateKey, publicJwk);\n\n return {\n id: KEY_ID,\n privateKey: keyPair.privateKey,\n publicJwk,\n createdAt: Date.now(),\n };\n}\n\n/**\n * Create a DPoP proof JWT\n */\nexport async function createDPoPProof(\n namespace: string,\n method: string,\n url: string,\n accessToken: string | null = null\n): Promise<string> {\n const keyData = await getOrCreateDPoPKey(namespace);\n\n // Strip WebCrypto-specific fields from JWK for interoperability\n const { kty, crv, x, y } = keyData.publicJwk;\n const minimalJwk = { kty, crv, x, y };\n\n const header = {\n alg: 'ES256',\n typ: 'dpop+jwt',\n jwk: minimalJwk,\n };\n\n const payload: Record<string, unknown> = {\n jti: generateRandomString(16),\n htm: method,\n htu: url,\n iat: Math.floor(Date.now() / 1000),\n };\n\n // Add access token hash if provided (for resource requests)\n if (accessToken) {\n payload.ath = await sha256Base64Url(accessToken);\n }\n\n return await signJwt(header, payload, keyData.privateKey);\n}\n\n/**\n * Clear DPoP keys from IndexedDB\n */\nexport async function clearDPoPKeys(namespace: string): Promise<void> {\n const db = await openDatabase(namespace);\n return new Promise((resolve, reject) => {\n const tx = db.transaction(KEY_STORE, 'readwrite');\n const store = tx.objectStore(KEY_STORE);\n const request = store.clear();\n\n request.onerror = () => reject(request.error);\n request.onsuccess = () => resolve();\n });\n}\n", "import { base64UrlEncode, generateRandomString } from '../utils/base64url';\n\n/**\n * Generate a PKCE code verifier (32 random bytes, base64url encoded)\n */\nexport function generateCodeVerifier(): string {\n return generateRandomString(32);\n}\n\n/**\n * Generate a PKCE code challenge from a verifier (SHA-256, base64url encoded)\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(hash);\n}\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nexport function generateState(): string {\n return generateRandomString(16);\n}\n", "const LOCK_TIMEOUT = 5000; // 5 seconds\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getLockKey(namespace: string, key: string): string {\n return `quickslice_${namespace}_lock_${key}`;\n}\n\n/**\n * Acquire a lock using localStorage for multi-tab coordination\n */\nexport async function acquireLock(\n namespace: string,\n key: string,\n timeout = LOCK_TIMEOUT\n): Promise<string | null> {\n const lockKey = getLockKey(namespace, key);\n const lockValue = `${Date.now()}_${Math.random()}`;\n const deadline = Date.now() + timeout;\n\n while (Date.now() < deadline) {\n const existing = localStorage.getItem(lockKey);\n\n if (existing) {\n // Check if lock is stale (older than timeout)\n const [timestamp] = existing.split('_');\n if (Date.now() - parseInt(timestamp) > LOCK_TIMEOUT) {\n // Lock is stale, remove it\n localStorage.removeItem(lockKey);\n } else {\n // Lock is held, wait and retry\n await sleep(50);\n continue;\n }\n }\n\n // Try to acquire\n localStorage.setItem(lockKey, lockValue);\n\n // Verify we got it (handle race condition)\n await sleep(10);\n if (localStorage.getItem(lockKey) === lockValue) {\n return lockValue; // Lock acquired\n }\n }\n\n return null; // Failed to acquire\n}\n\n/**\n * Release a lock\n */\nexport function releaseLock(namespace: string, key: string, lockValue: string): void {\n const lockKey = getLockKey(namespace, key);\n // Only release if we still hold it\n if (localStorage.getItem(lockKey) === lockValue) {\n localStorage.removeItem(lockKey);\n }\n}\n", "import { Storage } from '../storage/storage';\nimport { acquireLock, releaseLock } from '../storage/lock';\nimport { createDPoPProof } from './dpop';\n\nconst TOKEN_REFRESH_BUFFER_MS = 60000; // 60 seconds before expiry\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Refresh tokens using the refresh token\n */\nasync function refreshTokens(\n storage: Storage,\n namespace: string,\n tokenUrl: string\n): Promise<string> {\n const refreshToken = storage.get('refreshToken');\n const clientId = storage.get('clientId');\n\n if (!refreshToken || !clientId) {\n throw new Error('No refresh token available');\n }\n\n const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl);\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(\n `Token refresh failed: ${errorData.error_description || response.statusText}`\n );\n }\n\n const tokens = await response.json();\n\n // Store new tokens (rotation - new refresh token each time)\n storage.set('accessToken', tokens.access_token);\n if (tokens.refresh_token) {\n storage.set('refreshToken', tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set('tokenExpiresAt', expiresAt.toString());\n\n return tokens.access_token;\n}\n\n/**\n * Get a valid access token, refreshing if necessary.\n * Uses multi-tab locking to prevent duplicate refresh requests.\n */\nexport async function getValidAccessToken(\n storage: Storage,\n namespace: string,\n tokenUrl: string\n): Promise<string> {\n const accessToken = storage.get('accessToken');\n const expiresAt = parseInt(storage.get('tokenExpiresAt') || '0');\n\n // Check if token is still valid (with buffer)\n if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) {\n return accessToken;\n }\n\n // Need to refresh - acquire lock first\n const lockKey = 'token_refresh';\n const lockValue = await acquireLock(namespace, lockKey);\n\n if (!lockValue) {\n // Failed to acquire lock, another tab is refreshing\n // Wait a bit and check cache again\n await sleep(100);\n const freshToken = storage.get('accessToken');\n const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0');\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n throw new Error('Failed to refresh token');\n }\n\n try {\n // Double-check after acquiring lock\n const freshToken = storage.get('accessToken');\n const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0');\n if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) {\n return freshToken;\n }\n\n // Actually refresh\n return await refreshTokens(storage, namespace, tokenUrl);\n } finally {\n releaseLock(namespace, lockKey, lockValue);\n }\n}\n\n/**\n * Store tokens from OAuth response\n */\nexport function storeTokens(\n storage: Storage,\n tokens: {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n sub?: string;\n }\n): void {\n storage.set('accessToken', tokens.access_token);\n if (tokens.refresh_token) {\n storage.set('refreshToken', tokens.refresh_token);\n }\n\n const expiresAt = Date.now() + tokens.expires_in * 1000;\n storage.set('tokenExpiresAt', expiresAt.toString());\n\n if (tokens.sub) {\n storage.set('userDid', tokens.sub);\n }\n}\n\n/**\n * Check if we have a valid session\n */\nexport function hasValidSession(storage: Storage): boolean {\n const accessToken = storage.get('accessToken');\n const refreshToken = storage.get('refreshToken');\n return !!(accessToken || refreshToken);\n}\n", "import { Storage } from '../storage/storage';\nimport { createDPoPProof, clearDPoPKeys } from './dpop';\nimport { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { storeTokens } from './tokens';\n\nexport interface LoginOptions {\n handle?: string;\n redirectUri?: string;\n scope?: string;\n}\n\n/**\n * Initiate OAuth login flow with PKCE\n */\nexport async function initiateLogin(\n storage: Storage,\n authorizeUrl: string,\n clientId: string,\n options: LoginOptions = {}\n): Promise<void> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n // Build redirect URI (use provided or derive from current page)\n const redirectUri = options.redirectUri || (window.location.origin + window.location.pathname);\n\n // Store for callback\n storage.set('codeVerifier', codeVerifier);\n storage.set('oauthState', state);\n storage.set('clientId', clientId);\n storage.set('redirectUri', redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state: state,\n });\n\n if (options.handle) {\n params.set('login_hint', options.handle);\n }\n\n if (options.scope) {\n params.set('scope', options.scope);\n }\n\n window.location.href = `${authorizeUrl}?${params.toString()}`;\n}\n\n/**\n * Handle OAuth callback - exchange code for tokens\n * Returns true if callback was handled, false if not a callback\n */\nexport async function handleOAuthCallback(\n storage: Storage,\n namespace: string,\n tokenUrl: string\n): Promise<boolean> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n throw new Error(\n `OAuth error: ${error} - ${params.get('error_description') || ''}`\n );\n }\n\n if (!code || !state) {\n return false; // Not a callback\n }\n\n // Verify state\n const storedState = storage.get('oauthState');\n if (state !== storedState) {\n throw new Error('OAuth state mismatch - possible CSRF attack');\n }\n\n // Get stored values\n const codeVerifier = storage.get('codeVerifier');\n const clientId = storage.get('clientId');\n const redirectUri = storage.get('redirectUri');\n\n if (!codeVerifier || !clientId || !redirectUri) {\n throw new Error('Missing OAuth session data');\n }\n\n // Exchange code for tokens with DPoP\n const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl);\n\n const tokenResponse = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n DPoP: dpopProof,\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const errorData = await tokenResponse.json().catch(() => ({}));\n throw new Error(\n `Token exchange failed: ${errorData.error_description || tokenResponse.statusText}`\n );\n }\n\n const tokens = await tokenResponse.json();\n\n // Store tokens\n storeTokens(storage, tokens);\n\n // Clean up OAuth state\n storage.remove('codeVerifier');\n storage.remove('oauthState');\n storage.remove('redirectUri');\n\n // Clear URL params\n window.history.replaceState({}, document.title, window.location.pathname);\n\n return true;\n}\n\n/**\n * Logout - clear all stored data\n */\nexport async function logout(\n storage: Storage,\n namespace: string,\n options: { reload?: boolean } = {}\n): Promise<void> {\n storage.clear();\n await clearDPoPKeys(namespace);\n\n if (options.reload !== false) {\n window.location.reload();\n }\n}\n", "import { createDPoPProof } from './auth/dpop';\nimport { getValidAccessToken } from './auth/tokens';\nimport { Storage } from './storage/storage';\n\nexport interface GraphQLResponse<T = unknown> {\n data?: T;\n errors?: Array<{ message: string; path?: string[] }>;\n}\n\n/**\n * Execute a GraphQL query or mutation\n */\nexport async function graphqlRequest<T = unknown>(\n storage: Storage,\n namespace: string,\n graphqlUrl: string,\n tokenUrl: string,\n query: string,\n variables: Record<string, unknown> = {},\n requireAuth = false\n): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n if (requireAuth) {\n const token = await getValidAccessToken(storage, namespace, tokenUrl);\n if (!token) {\n throw new Error('Not authenticated');\n }\n\n // Create DPoP proof bound to this request\n const dpopProof = await createDPoPProof(namespace, 'POST', graphqlUrl, token);\n\n headers['Authorization'] = `DPoP ${token}`;\n headers['DPoP'] = dpopProof;\n }\n\n const response = await fetch(graphqlUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`GraphQL request failed: ${response.statusText}`);\n }\n\n const result: GraphQLResponse<T> = await response.json();\n\n if (result.errors && result.errors.length > 0) {\n throw new Error(`GraphQL error: ${result.errors[0].message}`);\n }\n\n return result.data as T;\n}\n", "import { createStorageKeys } from './storage/keys';\nimport { createStorage, Storage } from './storage/storage';\nimport { getOrCreateDPoPKey } from './auth/dpop';\nimport { initiateLogin, handleOAuthCallback, logout as doLogout, LoginOptions } from './auth/oauth';\nimport { getValidAccessToken, hasValidSession } from './auth/tokens';\nimport { graphqlRequest } from './graphql';\nimport { generateNamespaceHash } from './utils/crypto';\n\nexport interface QuicksliceClientOptions {\n server: string;\n clientId: string;\n redirectUri?: string;\n scope?: string;\n}\n\nexport interface User {\n did: string;\n}\n\nexport class QuicksliceClient {\n private server: string;\n private clientId: string;\n private redirectUri?: string;\n private scope?: string;\n private graphqlUrl: string;\n private authorizeUrl: string;\n private tokenUrl: string;\n private initialized = false;\n private namespace: string = '';\n private storage: Storage | null = null;\n\n constructor(options: QuicksliceClientOptions) {\n this.server = options.server.replace(/\\/$/, ''); // Remove trailing slash\n this.clientId = options.clientId;\n this.redirectUri = options.redirectUri;\n this.scope = options.scope;\n\n this.graphqlUrl = `${this.server}/graphql`;\n this.authorizeUrl = `${this.server}/oauth/authorize`;\n this.tokenUrl = `${this.server}/oauth/token`;\n }\n\n /**\n * Initialize the client - must be called before other methods\n */\n async init(): Promise<void> {\n if (this.initialized) return;\n\n // Generate namespace from clientId\n this.namespace = await generateNamespaceHash(this.clientId);\n\n // Create namespaced storage\n const keys = createStorageKeys(this.namespace);\n this.storage = createStorage(keys);\n\n // Ensure DPoP key exists\n await getOrCreateDPoPKey(this.namespace);\n\n this.initialized = true;\n }\n\n private getStorage(): Storage {\n if (!this.storage) {\n throw new Error('Client not initialized. Call init() first.');\n }\n return this.storage;\n }\n\n /**\n * Start OAuth login flow\n */\n async loginWithRedirect(options: LoginOptions = {}): Promise<void> {\n await this.init();\n await initiateLogin(this.getStorage(), this.authorizeUrl, this.clientId, {\n ...options,\n redirectUri: options.redirectUri || this.redirectUri,\n scope: options.scope || this.scope,\n });\n }\n\n /**\n * Handle OAuth callback after redirect\n * Returns true if callback was handled\n */\n async handleRedirectCallback(): Promise<boolean> {\n await this.init();\n return await handleOAuthCallback(this.getStorage(), this.namespace, this.tokenUrl);\n }\n\n /**\n * Logout and clear all stored data\n */\n async logout(options: { reload?: boolean } = {}): Promise<void> {\n await this.init();\n await doLogout(this.getStorage(), this.namespace, options);\n }\n\n /**\n * Check if user is authenticated\n */\n async isAuthenticated(): Promise<boolean> {\n await this.init();\n return hasValidSession(this.getStorage());\n }\n\n /**\n * Get current user's DID (from stored token data)\n * For richer profile info, use client.query() with your own schema\n */\n async getUser(): Promise<User | null> {\n await this.init();\n if (!hasValidSession(this.getStorage())) {\n return null;\n }\n\n const did = this.getStorage().get('userDid');\n if (!did) {\n return null;\n }\n\n return { did };\n }\n\n /**\n * Get access token (auto-refreshes if needed)\n */\n async getAccessToken(): Promise<string> {\n await this.init();\n return await getValidAccessToken(this.getStorage(), this.namespace, this.tokenUrl);\n }\n\n /**\n * Execute a GraphQL query (authenticated)\n */\n async query<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.getStorage(),\n this.namespace,\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n true\n );\n }\n\n /**\n * Execute a GraphQL mutation (authenticated)\n */\n async mutate<T = unknown>(\n mutation: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n return this.query<T>(mutation, variables);\n }\n\n /**\n * Execute a public GraphQL query (no auth)\n */\n async publicQuery<T = unknown>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n await this.init();\n return await graphqlRequest<T>(\n this.getStorage(),\n this.namespace,\n this.graphqlUrl,\n this.tokenUrl,\n query,\n variables,\n false\n );\n }\n}\n", "/**\n * Base error class for Quickslice client errors\n */\nexport class QuicksliceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'QuicksliceError';\n }\n}\n\n/**\n * Thrown when authentication is required but user is not logged in\n */\nexport class LoginRequiredError extends QuicksliceError {\n constructor(message = 'Login required') {\n super(message);\n this.name = 'LoginRequiredError';\n }\n}\n\n/**\n * Thrown when network request fails\n */\nexport class NetworkError extends QuicksliceError {\n constructor(message: string) {\n super(message);\n this.name = 'NetworkError';\n }\n}\n\n/**\n * Thrown when OAuth flow fails\n */\nexport class OAuthError extends QuicksliceError {\n public code: string;\n public description?: string;\n\n constructor(code: string, description?: string) {\n super(`OAuth error: ${code}${description ? ` - ${description}` : ''}`);\n this.name = 'OAuthError';\n this.code = code;\n this.description = description;\n }\n}\n"], 5 + "mappings": "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", 6 6 "names": ["sleep"] 7 7 }
+1 -1
quickslice-client-js/dist/quickslice-client.min.js
··· 1 - "use strict";var QuicksliceClient=(()=>{var x=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var F=Object.prototype.hasOwnProperty;var H=(t,e)=>{for(var r in e)x(t,r,{get:e[r],enumerable:!0})},W=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of M(e))!F.call(t,a)&&a!==r&&x(t,a,{get:()=>e[a],enumerable:!(n=Y(e,a))||n.enumerable});return t};var X=t=>W(x({},"__esModule",{value:!0}),t);var se={};H(se,{LoginRequiredError:()=>S,NetworkError:()=>T,OAuthError:()=>_,QuicksliceClient:()=>m,QuicksliceError:()=>h,createQuicksliceClient:()=>ie});var o={accessToken:"quickslice_access_token",refreshToken:"quickslice_refresh_token",tokenExpiresAt:"quickslice_token_expires_at",clientId:"quickslice_client_id",userDid:"quickslice_user_did",codeVerifier:"quickslice_code_verifier",oauthState:"quickslice_oauth_state",redirectUri:"quickslice_redirect_uri"};var s={get(t){return t===o.codeVerifier||t===o.oauthState?sessionStorage.getItem(t):localStorage.getItem(t)},set(t,e){t===o.codeVerifier||t===o.oauthState?sessionStorage.setItem(t,e):localStorage.setItem(t,e)},remove(t){sessionStorage.removeItem(t),localStorage.removeItem(t)},clear(){Object.values(o).forEach(t=>{sessionStorage.removeItem(t),localStorage.removeItem(t)})}};function d(t){let e=t instanceof Uint8Array?t:new Uint8Array(t),r="";for(let n=0;n<e.length;n++)r+=String.fromCharCode(e[n]);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function k(t){let e=new Uint8Array(t);return crypto.getRandomValues(e),d(e)}async function A(t){let e=new TextEncoder,r=await crypto.subtle.digest("SHA-256",e.encode(t));return d(r)}async function K(t,e,r){let n=new TextEncoder,a=d(n.encode(JSON.stringify(t))),i=d(n.encode(JSON.stringify(e))),c=`${a}.${i}`,l=await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},r,n.encode(c)),u=d(l);return`${c}.${u}`}var Z="quickslice-oauth",ee=1,g="dpop-keys",E="dpop-key",y=null;function b(){return y||(y=new Promise((t,e)=>{let r=indexedDB.open(Z,ee);r.onerror=()=>e(r.error),r.onsuccess=()=>t(r.result),r.onupgradeneeded=n=>{let a=n.target.result;a.objectStoreNames.contains(g)||a.createObjectStore(g,{keyPath:"id"})}}),y)}async function te(){let t=await b();return new Promise((e,r)=>{let i=t.transaction(g,"readonly").objectStore(g).get(E);i.onerror=()=>r(i.error),i.onsuccess=()=>e(i.result||null)})}async function re(t,e){let r=await b();return new Promise((n,a)=>{let l=r.transaction(g,"readwrite").objectStore(g).put({id:E,privateKey:t,publicJwk:e,createdAt:Date.now()});l.onerror=()=>a(l.error),l.onsuccess=()=>n()})}async function D(){let t=await te();if(t)return t;let e=await crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},!1,["sign"]),r=await crypto.subtle.exportKey("jwk",e.publicKey);return await re(e.privateKey,r),{id:E,privateKey:e.privateKey,publicJwk:r,createdAt:Date.now()}}async function f(t,e,r=null){let n=await D(),{kty:a,crv:i,x:c,y:l}=n.publicJwk,w={alg:"ES256",typ:"dpop+jwt",jwk:{kty:a,crv:i,x:c,y:l}},p={jti:k(16),htm:t,htu:e,iat:Math.floor(Date.now()/1e3)};return r&&(p.ath=await A(r)),await K(w,p,n.privateKey)}async function I(){let t=await b();return new Promise((e,r)=>{let i=t.transaction(g,"readwrite").objectStore(g).clear();i.onerror=()=>r(i.error),i.onsuccess=()=>e()})}function R(){return k(32)}async function C(t){let r=new TextEncoder().encode(t),n=await crypto.subtle.digest("SHA-256",r);return d(n)}function q(){return k(16)}var $="quickslice_lock_";function L(t){return new Promise(e=>setTimeout(e,t))}async function V(t,e=5e3){let r=$+t,n=`${Date.now()}_${Math.random()}`,a=Date.now()+e;for(;Date.now()<a;){let i=localStorage.getItem(r);if(i){let[c]=i.split("_");if(Date.now()-parseInt(c)>5e3)localStorage.removeItem(r);else{await L(50);continue}}if(localStorage.setItem(r,n),await L(10),localStorage.getItem(r)===n)return n}return null}function j(t,e){let r=$+t;localStorage.getItem(r)===e&&localStorage.removeItem(r)}var v=6e4;function oe(t){return new Promise(e=>setTimeout(e,t))}async function ne(t){let e=s.get(o.refreshToken),r=s.get(o.clientId);if(!e||!r)throw new Error("No refresh token available");let n=await f("POST",t),a=await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",DPoP:n},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:e,client_id:r})});if(!a.ok){let l=await a.json().catch(()=>({}));throw new Error(`Token refresh failed: ${l.error_description||a.statusText}`)}let i=await a.json();s.set(o.accessToken,i.access_token),i.refresh_token&&s.set(o.refreshToken,i.refresh_token);let c=Date.now()+i.expires_in*1e3;return s.set(o.tokenExpiresAt,c.toString()),i.access_token}async function P(t){let e=s.get(o.accessToken),r=parseInt(s.get(o.tokenExpiresAt)||"0");if(e&&Date.now()<r-v)return e;let a=`token_refresh_${s.get(o.clientId)}`,i=await V(a);if(!i){await oe(100);let c=s.get(o.accessToken),l=parseInt(s.get(o.tokenExpiresAt)||"0");if(c&&Date.now()<l-v)return c;throw new Error("Failed to refresh token")}try{let c=s.get(o.accessToken),l=parseInt(s.get(o.tokenExpiresAt)||"0");return c&&Date.now()<l-v?c:await ne(t)}finally{j(a,i)}}function B(t){s.set(o.accessToken,t.access_token),t.refresh_token&&s.set(o.refreshToken,t.refresh_token);let e=Date.now()+t.expires_in*1e3;s.set(o.tokenExpiresAt,e.toString()),t.sub&&s.set(o.userDid,t.sub)}function O(){let t=s.get(o.accessToken),e=s.get(o.refreshToken);return!!(t||e)}async function Q(t,e,r={}){let n=R(),a=await C(n),i=q(),c=r.redirectUri||window.location.origin+window.location.pathname;s.set(o.codeVerifier,n),s.set(o.oauthState,i),s.set(o.clientId,e),s.set(o.redirectUri,c);let l=new URLSearchParams({client_id:e,redirect_uri:c,response_type:"code",code_challenge:a,code_challenge_method:"S256",state:i});r.handle&&l.set("login_hint",r.handle),r.scope&&l.set("scope",r.scope),window.location.href=`${t}?${l.toString()}`}async function J(t){let e=new URLSearchParams(window.location.search),r=e.get("code"),n=e.get("state"),a=e.get("error");if(a)throw new Error(`OAuth error: ${a} - ${e.get("error_description")||""}`);if(!r||!n)return!1;let i=s.get(o.oauthState);if(n!==i)throw new Error("OAuth state mismatch - possible CSRF attack");let c=s.get(o.codeVerifier),l=s.get(o.clientId),u=s.get(o.redirectUri);if(!c||!l||!u)throw new Error("Missing OAuth session data");let w=await f("POST",t),p=await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",DPoP:w},body:new URLSearchParams({grant_type:"authorization_code",code:r,redirect_uri:u,client_id:l,code_verifier:c})});if(!p.ok){let z=await p.json().catch(()=>({}));throw new Error(`Token exchange failed: ${z.error_description||p.statusText}`)}let N=await p.json();return B(N),s.remove(o.codeVerifier),s.remove(o.oauthState),s.remove(o.redirectUri),window.history.replaceState({},document.title,window.location.pathname),!0}async function G(t={}){s.clear(),await I(),t.reload!==!1&&window.location.reload()}async function U(t,e,r,n={},a=!1){let i={"Content-Type":"application/json"};if(a){let u=await P(e);if(!u)throw new Error("Not authenticated");let w=await f("POST",t,u);i.Authorization=`DPoP ${u}`,i.DPoP=w}let c=await fetch(t,{method:"POST",headers:i,body:JSON.stringify({query:r,variables:n})});if(!c.ok)throw new Error(`GraphQL request failed: ${c.statusText}`);let l=await c.json();if(l.errors&&l.errors.length>0)throw new Error(`GraphQL error: ${l.errors[0].message}`);return l.data}var m=class{constructor(e){this.initialized=!1;this.server=e.server.replace(/\/$/,""),this.clientId=e.clientId,this.redirectUri=e.redirectUri,this.scope=e.scope,this.graphqlUrl=`${this.server}/graphql`,this.authorizeUrl=`${this.server}/oauth/authorize`,this.tokenUrl=`${this.server}/oauth/token`}async init(){this.initialized||(await D(),this.initialized=!0)}async loginWithRedirect(e={}){await this.init(),await Q(this.authorizeUrl,this.clientId,{...e,redirectUri:e.redirectUri||this.redirectUri,scope:e.scope||this.scope})}async handleRedirectCallback(){return await this.init(),await J(this.tokenUrl)}async logout(e={}){await G(e)}async isAuthenticated(){return O()}getUser(){if(!O())return null;let e=s.get(o.userDid);return e?{did:e}:null}async getAccessToken(){return await this.init(),await P(this.tokenUrl)}async query(e,r={}){return await this.init(),await U(this.graphqlUrl,this.tokenUrl,e,r,!0)}async mutate(e,r={}){return this.query(e,r)}async publicQuery(e,r={}){return await this.init(),await U(this.graphqlUrl,this.tokenUrl,e,r,!1)}};var h=class extends Error{constructor(e){super(e),this.name="QuicksliceError"}},S=class extends h{constructor(e="Login required"){super(e),this.name="LoginRequiredError"}},T=class extends h{constructor(e){super(e),this.name="NetworkError"}},_=class extends h{constructor(e,r){super(`OAuth error: ${e}${r?` - ${r}`:""}`),this.name="OAuthError",this.code=e,this.description=r}};async function ie(t){let e=new m(t);return await e.init(),e}return X(se);})(); 1 + "use strict";var QuicksliceClient=(()=>{var _=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var X=Object.prototype.hasOwnProperty;var Z=(t,e)=>{for(var r in e)_(t,r,{get:e[r],enumerable:!0})},ee=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Y(e))!X.call(t,i)&&i!==r&&_(t,i,{get:()=>e[i],enumerable:!(o=W(e,i))||o.enumerable});return t};var te=t=>ee(_({},"__esModule",{value:!0}),t);var ge={};Z(ge,{LoginRequiredError:()=>S,NetworkError:()=>P,OAuthError:()=>x,QuicksliceClient:()=>m,QuicksliceError:()=>h,createQuicksliceClient:()=>ce});function K(t){return{accessToken:`quickslice_${t}_access_token`,refreshToken:`quickslice_${t}_refresh_token`,tokenExpiresAt:`quickslice_${t}_token_expires_at`,clientId:`quickslice_${t}_client_id`,userDid:`quickslice_${t}_user_did`,codeVerifier:`quickslice_${t}_code_verifier`,oauthState:`quickslice_${t}_oauth_state`,redirectUri:`quickslice_${t}_redirect_uri`}}function A(t){return{get(e){let r=t[e];return e==="codeVerifier"||e==="oauthState"?sessionStorage.getItem(r):localStorage.getItem(r)},set(e,r){let o=t[e];e==="codeVerifier"||e==="oauthState"?sessionStorage.setItem(o,r):localStorage.setItem(o,r)},remove(e){let r=t[e];sessionStorage.removeItem(r),localStorage.removeItem(r)},clear(){Object.keys(t).forEach(e=>{let r=t[e];sessionStorage.removeItem(r),localStorage.removeItem(r)})}}}function u(t){let e=t instanceof Uint8Array?t:new Uint8Array(t),r="";for(let o=0;o<e.length;o++)r+=String.fromCharCode(e[o]);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function w(t){let e=new Uint8Array(t);return crypto.getRandomValues(e),u(e)}async function I(t){let e=new TextEncoder,r=await crypto.subtle.digest("SHA-256",e.encode(t));return u(r)}async function O(t){let e=new TextEncoder,r=await crypto.subtle.digest("SHA-256",e.encode(t));return Array.from(new Uint8Array(r)).map(a=>a.toString(16).padStart(2,"0")).join("").substring(0,8)}async function C(t,e,r){let o=new TextEncoder,i=u(o.encode(JSON.stringify(t))),a=u(o.encode(JSON.stringify(e))),n=`${i}.${a}`,s=await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},r,o.encode(n)),c=u(s);return`${n}.${c}`}var re=1,d="dpop-keys",T="dpop-key",$=new Map;function oe(t){return`quickslice-oauth-${t}`}function b(t){let e=$.get(t);if(e)return e;let r=new Promise((o,i)=>{let a=indexedDB.open(oe(t),re);a.onerror=()=>i(a.error),a.onsuccess=()=>o(a.result),a.onupgradeneeded=n=>{let s=n.target.result;s.objectStoreNames.contains(d)||s.createObjectStore(d,{keyPath:"id"})}});return $.set(t,r),r}async function ne(t){let e=await b(t);return new Promise((r,o)=>{let n=e.transaction(d,"readonly").objectStore(d).get(T);n.onerror=()=>o(n.error),n.onsuccess=()=>r(n.result||null)})}async function ie(t,e,r){let o=await b(t);return new Promise((i,a)=>{let c=o.transaction(d,"readwrite").objectStore(d).put({id:T,privateKey:e,publicJwk:r,createdAt:Date.now()});c.onerror=()=>a(c.error),c.onsuccess=()=>i()})}async function D(t){let e=await ne(t);if(e)return e;let r=await crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},!1,["sign"]),o=await crypto.subtle.exportKey("jwk",r.publicKey);return await ie(t,r.privateKey,o),{id:T,privateKey:r.privateKey,publicJwk:o,createdAt:Date.now()}}async function f(t,e,r,o=null){let i=await D(t),{kty:a,crv:n,x:s,y:c}=i.publicJwk,l={alg:"ES256",typ:"dpop+jwt",jwk:{kty:a,crv:n,x:s,y:c}},p={jti:w(16),htm:e,htu:r,iat:Math.floor(Date.now()/1e3)};return o&&(p.ath=await I(o)),await C(l,p,i.privateKey)}async function R(t){let e=await b(t);return new Promise((r,o)=>{let n=e.transaction(d,"readwrite").objectStore(d).clear();n.onerror=()=>o(n.error),n.onsuccess=()=>r()})}function q(){return w(32)}async function L(t){let r=new TextEncoder().encode(t),o=await crypto.subtle.digest("SHA-256",r);return u(o)}function V(){return w(16)}function j(t){return new Promise(e=>setTimeout(e,t))}function B(t,e){return`quickslice_${t}_lock_${e}`}async function Q(t,e,r=5e3){let o=B(t,e),i=`${Date.now()}_${Math.random()}`,a=Date.now()+r;for(;Date.now()<a;){let n=localStorage.getItem(o);if(n){let[s]=n.split("_");if(Date.now()-parseInt(s)>5e3)localStorage.removeItem(o);else{await j(50);continue}}if(localStorage.setItem(o,i),await j(10),localStorage.getItem(o)===i)return i}return null}function J(t,e,r){let o=B(t,e);localStorage.getItem(o)===r&&localStorage.removeItem(o)}var v=6e4;function se(t){return new Promise(e=>setTimeout(e,t))}async function ae(t,e,r){let o=t.get("refreshToken"),i=t.get("clientId");if(!o||!i)throw new Error("No refresh token available");let a=await f(e,"POST",r),n=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",DPoP:a},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:o,client_id:i})});if(!n.ok){let g=await n.json().catch(()=>({}));throw new Error(`Token refresh failed: ${g.error_description||n.statusText}`)}let s=await n.json();t.set("accessToken",s.access_token),s.refresh_token&&t.set("refreshToken",s.refresh_token);let c=Date.now()+s.expires_in*1e3;return t.set("tokenExpiresAt",c.toString()),s.access_token}async function k(t,e,r){let o=t.get("accessToken"),i=parseInt(t.get("tokenExpiresAt")||"0");if(o&&Date.now()<i-v)return o;let a="token_refresh",n=await Q(e,a);if(!n){await se(100);let s=t.get("accessToken"),c=parseInt(t.get("tokenExpiresAt")||"0");if(s&&Date.now()<c-v)return s;throw new Error("Failed to refresh token")}try{let s=t.get("accessToken"),c=parseInt(t.get("tokenExpiresAt")||"0");return s&&Date.now()<c-v?s:await ae(t,e,r)}finally{J(e,a,n)}}function N(t,e){t.set("accessToken",e.access_token),e.refresh_token&&t.set("refreshToken",e.refresh_token);let r=Date.now()+e.expires_in*1e3;t.set("tokenExpiresAt",r.toString()),e.sub&&t.set("userDid",e.sub)}function U(t){let e=t.get("accessToken"),r=t.get("refreshToken");return!!(e||r)}async function z(t,e,r,o={}){let i=q(),a=await L(i),n=V(),s=o.redirectUri||window.location.origin+window.location.pathname;t.set("codeVerifier",i),t.set("oauthState",n),t.set("clientId",r),t.set("redirectUri",s);let c=new URLSearchParams({client_id:r,redirect_uri:s,response_type:"code",code_challenge:a,code_challenge_method:"S256",state:n});o.handle&&c.set("login_hint",o.handle),o.scope&&c.set("scope",o.scope),window.location.href=`${e}?${c.toString()}`}async function H(t,e,r){let o=new URLSearchParams(window.location.search),i=o.get("code"),a=o.get("state"),n=o.get("error");if(n)throw new Error(`OAuth error: ${n} - ${o.get("error_description")||""}`);if(!i||!a)return!1;let s=t.get("oauthState");if(a!==s)throw new Error("OAuth state mismatch - possible CSRF attack");let c=t.get("codeVerifier"),g=t.get("clientId"),l=t.get("redirectUri");if(!c||!g||!l)throw new Error("Missing OAuth session data");let p=await f(e,"POST",r),y=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",DPoP:p},body:new URLSearchParams({grant_type:"authorization_code",code:i,redirect_uri:l,client_id:g,code_verifier:c})});if(!y.ok){let G=await y.json().catch(()=>({}));throw new Error(`Token exchange failed: ${G.error_description||y.statusText}`)}let F=await y.json();return N(t,F),t.remove("codeVerifier"),t.remove("oauthState"),t.remove("redirectUri"),window.history.replaceState({},document.title,window.location.pathname),!0}async function M(t,e,r={}){t.clear(),await R(e),r.reload!==!1&&window.location.reload()}async function E(t,e,r,o,i,a={},n=!1){let s={"Content-Type":"application/json"};if(n){let l=await k(t,e,o);if(!l)throw new Error("Not authenticated");let p=await f(e,"POST",r,l);s.Authorization=`DPoP ${l}`,s.DPoP=p}let c=await fetch(r,{method:"POST",headers:s,body:JSON.stringify({query:i,variables:a})});if(!c.ok)throw new Error(`GraphQL request failed: ${c.statusText}`);let g=await c.json();if(g.errors&&g.errors.length>0)throw new Error(`GraphQL error: ${g.errors[0].message}`);return g.data}var m=class{constructor(e){this.initialized=!1;this.namespace="";this.storage=null;this.server=e.server.replace(/\/$/,""),this.clientId=e.clientId,this.redirectUri=e.redirectUri,this.scope=e.scope,this.graphqlUrl=`${this.server}/graphql`,this.authorizeUrl=`${this.server}/oauth/authorize`,this.tokenUrl=`${this.server}/oauth/token`}async init(){if(this.initialized)return;this.namespace=await O(this.clientId);let e=K(this.namespace);this.storage=A(e),await D(this.namespace),this.initialized=!0}getStorage(){if(!this.storage)throw new Error("Client not initialized. Call init() first.");return this.storage}async loginWithRedirect(e={}){await this.init(),await z(this.getStorage(),this.authorizeUrl,this.clientId,{...e,redirectUri:e.redirectUri||this.redirectUri,scope:e.scope||this.scope})}async handleRedirectCallback(){return await this.init(),await H(this.getStorage(),this.namespace,this.tokenUrl)}async logout(e={}){await this.init(),await M(this.getStorage(),this.namespace,e)}async isAuthenticated(){return await this.init(),U(this.getStorage())}async getUser(){if(await this.init(),!U(this.getStorage()))return null;let e=this.getStorage().get("userDid");return e?{did:e}:null}async getAccessToken(){return await this.init(),await k(this.getStorage(),this.namespace,this.tokenUrl)}async query(e,r={}){return await this.init(),await E(this.getStorage(),this.namespace,this.graphqlUrl,this.tokenUrl,e,r,!0)}async mutate(e,r={}){return this.query(e,r)}async publicQuery(e,r={}){return await this.init(),await E(this.getStorage(),this.namespace,this.graphqlUrl,this.tokenUrl,e,r,!1)}};var h=class extends Error{constructor(e){super(e),this.name="QuicksliceError"}},S=class extends h{constructor(e="Login required"){super(e),this.name="LoginRequiredError"}},P=class extends h{constructor(e){super(e),this.name="NetworkError"}},x=class extends h{constructor(e,r){super(`OAuth error: ${e}${r?` - ${r}`:""}`),this.name="OAuthError",this.code=e,this.description=r}};async function ce(t){let e=new m(t);return await e.init(),e}return te(ge);})();
+13 -12
quickslice-client-js/dist/storage/keys.d.ts
··· 1 1 /** 2 - * Storage key constants 2 + * Storage key factory - generates namespaced keys 3 3 */ 4 - export declare const STORAGE_KEYS: { 5 - readonly accessToken: "quickslice_access_token"; 6 - readonly refreshToken: "quickslice_refresh_token"; 7 - readonly tokenExpiresAt: "quickslice_token_expires_at"; 8 - readonly clientId: "quickslice_client_id"; 9 - readonly userDid: "quickslice_user_did"; 10 - readonly codeVerifier: "quickslice_code_verifier"; 11 - readonly oauthState: "quickslice_oauth_state"; 12 - readonly redirectUri: "quickslice_redirect_uri"; 13 - }; 14 - export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]; 4 + export interface StorageKeys { 5 + accessToken: string; 6 + refreshToken: string; 7 + tokenExpiresAt: string; 8 + clientId: string; 9 + userDid: string; 10 + codeVerifier: string; 11 + oauthState: string; 12 + redirectUri: string; 13 + } 14 + export declare function createStorageKeys(namespace: string): StorageKeys; 15 + export type StorageKey = string;
+2 -2
quickslice-client-js/dist/storage/lock.d.ts
··· 1 1 /** 2 2 * Acquire a lock using localStorage for multi-tab coordination 3 3 */ 4 - export declare function acquireLock(key: string, timeout?: number): Promise<string | null>; 4 + export declare function acquireLock(namespace: string, key: string, timeout?: number): Promise<string | null>; 5 5 /** 6 6 * Release a lock 7 7 */ 8 - export declare function releaseLock(key: string, lockValue: string): void; 8 + export declare function releaseLock(namespace: string, key: string, lockValue: string): void;
+7 -7
quickslice-client-js/dist/storage/storage.d.ts
··· 1 - import { StorageKey } from './keys'; 1 + import { StorageKeys } from './keys'; 2 2 /** 3 - * Hybrid storage utility - sessionStorage for OAuth flow state, 4 - * localStorage for tokens (shared across tabs) 3 + * Create a namespaced storage interface 5 4 */ 6 - export declare const storage: { 7 - get(key: StorageKey): string | null; 8 - set(key: StorageKey, value: string): void; 9 - remove(key: StorageKey): void; 5 + export declare function createStorage(keys: StorageKeys): { 6 + get(key: keyof StorageKeys): string | null; 7 + set(key: keyof StorageKeys, value: string): void; 8 + remove(key: keyof StorageKeys): void; 10 9 clear(): void; 11 10 }; 11 + export type Storage = ReturnType<typeof createStorage>;
+4
quickslice-client-js/dist/utils/crypto.d.ts
··· 3 3 */ 4 4 export declare function sha256Base64Url(data: string): Promise<string>; 5 5 /** 6 + * Generate an 8-character namespace hash from clientId 7 + */ 8 + export declare function generateNamespaceHash(clientId: string): Promise<string>; 9 + /** 6 10 * Sign a JWT with an ECDSA P-256 private key 7 11 */ 8 12 export declare function signJwt(header: Record<string, unknown>, payload: Record<string, unknown>, privateKey: CryptoKey): Promise<string>;
+2 -2
quickslice-client-js/package-lock.json
··· 1 1 { 2 2 "name": "quickslice-client-js", 3 - "version": "0.1.0", 3 + "version": "0.3.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "quickslice-client-js", 9 - "version": "0.1.0", 9 + "version": "0.3.0", 10 10 "license": "MIT", 11 11 "devDependencies": { 12 12 "esbuild": "^0.24.0",
+1 -1
quickslice-client-js/package.json
··· 1 1 { 2 2 "name": "quickslice-client-js", 3 - "version": "0.2.0", 3 + "version": "0.3.0", 4 4 "description": "Quickslice client SDK for browser SPAs", 5 5 "type": "module", 6 6 "main": "dist/quickslice-client.js",
+24 -16
quickslice-client-js/src/auth/dpop.ts
··· 1 1 import { generateRandomString } from '../utils/base64url'; 2 2 import { sha256Base64Url, signJwt } from '../utils/crypto'; 3 3 4 - const DB_NAME = 'quickslice-oauth'; 5 4 const DB_VERSION = 1; 6 5 const KEY_STORE = 'dpop-keys'; 7 6 const KEY_ID = 'dpop-key'; ··· 13 12 createdAt: number; 14 13 } 15 14 16 - let dbPromise: Promise<IDBDatabase> | null = null; 15 + // Cache database connections per namespace 16 + const dbPromises = new Map<string, Promise<IDBDatabase>>(); 17 17 18 - function openDatabase(): Promise<IDBDatabase> { 19 - if (dbPromise) return dbPromise; 18 + function getDbName(namespace: string): string { 19 + return `quickslice-oauth-${namespace}`; 20 + } 20 21 21 - dbPromise = new Promise((resolve, reject) => { 22 - const request = indexedDB.open(DB_NAME, DB_VERSION); 22 + function openDatabase(namespace: string): Promise<IDBDatabase> { 23 + const existing = dbPromises.get(namespace); 24 + if (existing) return existing; 25 + 26 + const promise = new Promise<IDBDatabase>((resolve, reject) => { 27 + const request = indexedDB.open(getDbName(namespace), DB_VERSION); 23 28 24 29 request.onerror = () => reject(request.error); 25 30 request.onsuccess = () => resolve(request.result); ··· 32 37 }; 33 38 }); 34 39 35 - return dbPromise; 40 + dbPromises.set(namespace, promise); 41 + return promise; 36 42 } 37 43 38 - async function getDPoPKey(): Promise<DPoPKeyData | null> { 39 - const db = await openDatabase(); 44 + async function getDPoPKey(namespace: string): Promise<DPoPKeyData | null> { 45 + const db = await openDatabase(namespace); 40 46 return new Promise((resolve, reject) => { 41 47 const tx = db.transaction(KEY_STORE, 'readonly'); 42 48 const store = tx.objectStore(KEY_STORE); ··· 48 54 } 49 55 50 56 async function storeDPoPKey( 57 + namespace: string, 51 58 privateKey: CryptoKey, 52 59 publicJwk: JsonWebKey 53 60 ): Promise<void> { 54 - const db = await openDatabase(); 61 + const db = await openDatabase(namespace); 55 62 return new Promise((resolve, reject) => { 56 63 const tx = db.transaction(KEY_STORE, 'readwrite'); 57 64 const store = tx.objectStore(KEY_STORE); ··· 67 74 }); 68 75 } 69 76 70 - export async function getOrCreateDPoPKey(): Promise<DPoPKeyData> { 71 - const keyData = await getDPoPKey(); 77 + export async function getOrCreateDPoPKey(namespace: string): Promise<DPoPKeyData> { 78 + const keyData = await getDPoPKey(namespace); 72 79 73 80 if (keyData) { 74 81 return keyData; ··· 85 92 const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey); 86 93 87 94 // Store in IndexedDB 88 - await storeDPoPKey(keyPair.privateKey, publicJwk); 95 + await storeDPoPKey(namespace, keyPair.privateKey, publicJwk); 89 96 90 97 return { 91 98 id: KEY_ID, ··· 99 106 * Create a DPoP proof JWT 100 107 */ 101 108 export async function createDPoPProof( 109 + namespace: string, 102 110 method: string, 103 111 url: string, 104 112 accessToken: string | null = null 105 113 ): Promise<string> { 106 - const keyData = await getOrCreateDPoPKey(); 114 + const keyData = await getOrCreateDPoPKey(namespace); 107 115 108 116 // Strip WebCrypto-specific fields from JWK for interoperability 109 117 const { kty, crv, x, y } = keyData.publicJwk; ··· 133 141 /** 134 142 * Clear DPoP keys from IndexedDB 135 143 */ 136 - export async function clearDPoPKeys(): Promise<void> { 137 - const db = await openDatabase(); 144 + export async function clearDPoPKeys(namespace: string): Promise<void> { 145 + const db = await openDatabase(namespace); 138 146 return new Promise((resolve, reject) => { 139 147 const tx = db.transaction(KEY_STORE, 'readwrite'); 140 148 const store = tx.objectStore(KEY_STORE);
+26 -18
quickslice-client-js/src/auth/oauth.ts
··· 1 - import { storage } from '../storage/storage'; 2 - import { STORAGE_KEYS } from '../storage/keys'; 1 + import { Storage } from '../storage/storage'; 3 2 import { createDPoPProof, clearDPoPKeys } from './dpop'; 4 3 import { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce'; 5 4 import { storeTokens } from './tokens'; ··· 14 13 * Initiate OAuth login flow with PKCE 15 14 */ 16 15 export async function initiateLogin( 16 + storage: Storage, 17 17 authorizeUrl: string, 18 18 clientId: string, 19 19 options: LoginOptions = {} ··· 26 26 const redirectUri = options.redirectUri || (window.location.origin + window.location.pathname); 27 27 28 28 // Store for callback 29 - storage.set(STORAGE_KEYS.codeVerifier, codeVerifier); 30 - storage.set(STORAGE_KEYS.oauthState, state); 31 - storage.set(STORAGE_KEYS.clientId, clientId); 32 - storage.set(STORAGE_KEYS.redirectUri, redirectUri); 29 + storage.set('codeVerifier', codeVerifier); 30 + storage.set('oauthState', state); 31 + storage.set('clientId', clientId); 32 + storage.set('redirectUri', redirectUri); 33 33 34 34 // Build authorization URL 35 35 const params = new URLSearchParams({ ··· 56 56 * Handle OAuth callback - exchange code for tokens 57 57 * Returns true if callback was handled, false if not a callback 58 58 */ 59 - export async function handleOAuthCallback(tokenUrl: string): Promise<boolean> { 59 + export async function handleOAuthCallback( 60 + storage: Storage, 61 + namespace: string, 62 + tokenUrl: string 63 + ): Promise<boolean> { 60 64 const params = new URLSearchParams(window.location.search); 61 65 const code = params.get('code'); 62 66 const state = params.get('state'); ··· 73 77 } 74 78 75 79 // Verify state 76 - const storedState = storage.get(STORAGE_KEYS.oauthState); 80 + const storedState = storage.get('oauthState'); 77 81 if (state !== storedState) { 78 82 throw new Error('OAuth state mismatch - possible CSRF attack'); 79 83 } 80 84 81 85 // Get stored values 82 - const codeVerifier = storage.get(STORAGE_KEYS.codeVerifier); 83 - const clientId = storage.get(STORAGE_KEYS.clientId); 84 - const redirectUri = storage.get(STORAGE_KEYS.redirectUri); 86 + const codeVerifier = storage.get('codeVerifier'); 87 + const clientId = storage.get('clientId'); 88 + const redirectUri = storage.get('redirectUri'); 85 89 86 90 if (!codeVerifier || !clientId || !redirectUri) { 87 91 throw new Error('Missing OAuth session data'); 88 92 } 89 93 90 94 // Exchange code for tokens with DPoP 91 - const dpopProof = await createDPoPProof('POST', tokenUrl); 95 + const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl); 92 96 93 97 const tokenResponse = await fetch(tokenUrl, { 94 98 method: 'POST', ··· 115 119 const tokens = await tokenResponse.json(); 116 120 117 121 // Store tokens 118 - storeTokens(tokens); 122 + storeTokens(storage, tokens); 119 123 120 124 // Clean up OAuth state 121 - storage.remove(STORAGE_KEYS.codeVerifier); 122 - storage.remove(STORAGE_KEYS.oauthState); 123 - storage.remove(STORAGE_KEYS.redirectUri); 125 + storage.remove('codeVerifier'); 126 + storage.remove('oauthState'); 127 + storage.remove('redirectUri'); 124 128 125 129 // Clear URL params 126 130 window.history.replaceState({}, document.title, window.location.pathname); ··· 131 135 /** 132 136 * Logout - clear all stored data 133 137 */ 134 - export async function logout(options: { reload?: boolean } = {}): Promise<void> { 138 + export async function logout( 139 + storage: Storage, 140 + namespace: string, 141 + options: { reload?: boolean } = {} 142 + ): Promise<void> { 135 143 storage.clear(); 136 - await clearDPoPKeys(); 144 + await clearDPoPKeys(namespace); 137 145 138 146 if (options.reload !== false) { 139 147 window.location.reload();
+43 -38
quickslice-client-js/src/auth/tokens.ts
··· 1 - import { storage } from '../storage/storage'; 2 - import { STORAGE_KEYS } from '../storage/keys'; 1 + import { Storage } from '../storage/storage'; 3 2 import { acquireLock, releaseLock } from '../storage/lock'; 4 3 import { createDPoPProof } from './dpop'; 5 4 ··· 12 11 /** 13 12 * Refresh tokens using the refresh token 14 13 */ 15 - async function refreshTokens(tokenUrl: string): Promise<string> { 16 - const refreshToken = storage.get(STORAGE_KEYS.refreshToken); 17 - const clientId = storage.get(STORAGE_KEYS.clientId); 14 + async function refreshTokens( 15 + storage: Storage, 16 + namespace: string, 17 + tokenUrl: string 18 + ): Promise<string> { 19 + const refreshToken = storage.get('refreshToken'); 20 + const clientId = storage.get('clientId'); 18 21 19 22 if (!refreshToken || !clientId) { 20 23 throw new Error('No refresh token available'); 21 24 } 22 25 23 - const dpopProof = await createDPoPProof('POST', tokenUrl); 26 + const dpopProof = await createDPoPProof(namespace, 'POST', tokenUrl); 24 27 25 28 const response = await fetch(tokenUrl, { 26 29 method: 'POST', ··· 45 48 const tokens = await response.json(); 46 49 47 50 // Store new tokens (rotation - new refresh token each time) 48 - storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 51 + storage.set('accessToken', tokens.access_token); 49 52 if (tokens.refresh_token) { 50 - storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 53 + storage.set('refreshToken', tokens.refresh_token); 51 54 } 52 55 53 56 const expiresAt = Date.now() + tokens.expires_in * 1000; 54 - storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString()); 57 + storage.set('tokenExpiresAt', expiresAt.toString()); 55 58 56 59 return tokens.access_token; 57 60 } ··· 60 63 * Get a valid access token, refreshing if necessary. 61 64 * Uses multi-tab locking to prevent duplicate refresh requests. 62 65 */ 63 - export async function getValidAccessToken(tokenUrl: string): Promise<string> { 64 - const accessToken = storage.get(STORAGE_KEYS.accessToken); 65 - const expiresAt = parseInt(storage.get(STORAGE_KEYS.tokenExpiresAt) || '0'); 66 + export async function getValidAccessToken( 67 + storage: Storage, 68 + namespace: string, 69 + tokenUrl: string 70 + ): Promise<string> { 71 + const accessToken = storage.get('accessToken'); 72 + const expiresAt = parseInt(storage.get('tokenExpiresAt') || '0'); 66 73 67 74 // Check if token is still valid (with buffer) 68 75 if (accessToken && Date.now() < expiresAt - TOKEN_REFRESH_BUFFER_MS) { ··· 70 77 } 71 78 72 79 // Need to refresh - acquire lock first 73 - const clientId = storage.get(STORAGE_KEYS.clientId); 74 - const lockKey = `token_refresh_${clientId}`; 75 - const lockValue = await acquireLock(lockKey); 80 + const lockKey = 'token_refresh'; 81 + const lockValue = await acquireLock(namespace, lockKey); 76 82 77 83 if (!lockValue) { 78 84 // Failed to acquire lock, another tab is refreshing 79 85 // Wait a bit and check cache again 80 86 await sleep(100); 81 - const freshToken = storage.get(STORAGE_KEYS.accessToken); 82 - const freshExpiry = parseInt( 83 - storage.get(STORAGE_KEYS.tokenExpiresAt) || '0' 84 - ); 87 + const freshToken = storage.get('accessToken'); 88 + const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0'); 85 89 if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 86 90 return freshToken; 87 91 } ··· 90 94 91 95 try { 92 96 // Double-check after acquiring lock 93 - const freshToken = storage.get(STORAGE_KEYS.accessToken); 94 - const freshExpiry = parseInt( 95 - storage.get(STORAGE_KEYS.tokenExpiresAt) || '0' 96 - ); 97 + const freshToken = storage.get('accessToken'); 98 + const freshExpiry = parseInt(storage.get('tokenExpiresAt') || '0'); 97 99 if (freshToken && Date.now() < freshExpiry - TOKEN_REFRESH_BUFFER_MS) { 98 100 return freshToken; 99 101 } 100 102 101 103 // Actually refresh 102 - return await refreshTokens(tokenUrl); 104 + return await refreshTokens(storage, namespace, tokenUrl); 103 105 } finally { 104 - releaseLock(lockKey, lockValue); 106 + releaseLock(namespace, lockKey, lockValue); 105 107 } 106 108 } 107 109 108 110 /** 109 111 * Store tokens from OAuth response 110 112 */ 111 - export function storeTokens(tokens: { 112 - access_token: string; 113 - refresh_token?: string; 114 - expires_in: number; 115 - sub?: string; 116 - }): void { 117 - storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 113 + export function storeTokens( 114 + storage: Storage, 115 + tokens: { 116 + access_token: string; 117 + refresh_token?: string; 118 + expires_in: number; 119 + sub?: string; 120 + } 121 + ): void { 122 + storage.set('accessToken', tokens.access_token); 118 123 if (tokens.refresh_token) { 119 - storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 124 + storage.set('refreshToken', tokens.refresh_token); 120 125 } 121 126 122 127 const expiresAt = Date.now() + tokens.expires_in * 1000; 123 - storage.set(STORAGE_KEYS.tokenExpiresAt, expiresAt.toString()); 128 + storage.set('tokenExpiresAt', expiresAt.toString()); 124 129 125 130 if (tokens.sub) { 126 - storage.set(STORAGE_KEYS.userDid, tokens.sub); 131 + storage.set('userDid', tokens.sub); 127 132 } 128 133 } 129 134 130 135 /** 131 136 * Check if we have a valid session 132 137 */ 133 - export function hasValidSession(): boolean { 134 - const accessToken = storage.get(STORAGE_KEYS.accessToken); 135 - const refreshToken = storage.get(STORAGE_KEYS.refreshToken); 138 + export function hasValidSession(storage: Storage): boolean { 139 + const accessToken = storage.get('accessToken'); 140 + const refreshToken = storage.get('refreshToken'); 136 141 return !!(accessToken || refreshToken); 137 142 }
+35 -11
quickslice-client-js/src/client.ts
··· 1 - import { storage } from './storage/storage'; 2 - import { STORAGE_KEYS } from './storage/keys'; 1 + import { createStorageKeys } from './storage/keys'; 2 + import { createStorage, Storage } from './storage/storage'; 3 3 import { getOrCreateDPoPKey } from './auth/dpop'; 4 4 import { initiateLogin, handleOAuthCallback, logout as doLogout, LoginOptions } from './auth/oauth'; 5 5 import { getValidAccessToken, hasValidSession } from './auth/tokens'; 6 6 import { graphqlRequest } from './graphql'; 7 + import { generateNamespaceHash } from './utils/crypto'; 7 8 8 9 export interface QuicksliceClientOptions { 9 10 server: string; ··· 25 26 private authorizeUrl: string; 26 27 private tokenUrl: string; 27 28 private initialized = false; 29 + private namespace: string = ''; 30 + private storage: Storage | null = null; 28 31 29 32 constructor(options: QuicksliceClientOptions) { 30 33 this.server = options.server.replace(/\/$/, ''); // Remove trailing slash ··· 43 46 async init(): Promise<void> { 44 47 if (this.initialized) return; 45 48 49 + // Generate namespace from clientId 50 + this.namespace = await generateNamespaceHash(this.clientId); 51 + 52 + // Create namespaced storage 53 + const keys = createStorageKeys(this.namespace); 54 + this.storage = createStorage(keys); 55 + 46 56 // Ensure DPoP key exists 47 - await getOrCreateDPoPKey(); 57 + await getOrCreateDPoPKey(this.namespace); 48 58 49 59 this.initialized = true; 60 + } 61 + 62 + private getStorage(): Storage { 63 + if (!this.storage) { 64 + throw new Error('Client not initialized. Call init() first.'); 65 + } 66 + return this.storage; 50 67 } 51 68 52 69 /** ··· 54 71 */ 55 72 async loginWithRedirect(options: LoginOptions = {}): Promise<void> { 56 73 await this.init(); 57 - await initiateLogin(this.authorizeUrl, this.clientId, { 74 + await initiateLogin(this.getStorage(), this.authorizeUrl, this.clientId, { 58 75 ...options, 59 76 redirectUri: options.redirectUri || this.redirectUri, 60 77 scope: options.scope || this.scope, ··· 67 84 */ 68 85 async handleRedirectCallback(): Promise<boolean> { 69 86 await this.init(); 70 - return await handleOAuthCallback(this.tokenUrl); 87 + return await handleOAuthCallback(this.getStorage(), this.namespace, this.tokenUrl); 71 88 } 72 89 73 90 /** 74 91 * Logout and clear all stored data 75 92 */ 76 93 async logout(options: { reload?: boolean } = {}): Promise<void> { 77 - await doLogout(options); 94 + await this.init(); 95 + await doLogout(this.getStorage(), this.namespace, options); 78 96 } 79 97 80 98 /** 81 99 * Check if user is authenticated 82 100 */ 83 101 async isAuthenticated(): Promise<boolean> { 84 - return hasValidSession(); 102 + await this.init(); 103 + return hasValidSession(this.getStorage()); 85 104 } 86 105 87 106 /** 88 107 * Get current user's DID (from stored token data) 89 108 * For richer profile info, use client.query() with your own schema 90 109 */ 91 - getUser(): User | null { 92 - if (!hasValidSession()) { 110 + async getUser(): Promise<User | null> { 111 + await this.init(); 112 + if (!hasValidSession(this.getStorage())) { 93 113 return null; 94 114 } 95 115 96 - const did = storage.get(STORAGE_KEYS.userDid); 116 + const did = this.getStorage().get('userDid'); 97 117 if (!did) { 98 118 return null; 99 119 } ··· 106 126 */ 107 127 async getAccessToken(): Promise<string> { 108 128 await this.init(); 109 - return await getValidAccessToken(this.tokenUrl); 129 + return await getValidAccessToken(this.getStorage(), this.namespace, this.tokenUrl); 110 130 } 111 131 112 132 /** ··· 118 138 ): Promise<T> { 119 139 await this.init(); 120 140 return await graphqlRequest<T>( 141 + this.getStorage(), 142 + this.namespace, 121 143 this.graphqlUrl, 122 144 this.tokenUrl, 123 145 query, ··· 145 167 ): Promise<T> { 146 168 await this.init(); 147 169 return await graphqlRequest<T>( 170 + this.getStorage(), 171 + this.namespace, 148 172 this.graphqlUrl, 149 173 this.tokenUrl, 150 174 query,
+5 -2
quickslice-client-js/src/graphql.ts
··· 1 1 import { createDPoPProof } from './auth/dpop'; 2 2 import { getValidAccessToken } from './auth/tokens'; 3 + import { Storage } from './storage/storage'; 3 4 4 5 export interface GraphQLResponse<T = unknown> { 5 6 data?: T; ··· 10 11 * Execute a GraphQL query or mutation 11 12 */ 12 13 export async function graphqlRequest<T = unknown>( 14 + storage: Storage, 15 + namespace: string, 13 16 graphqlUrl: string, 14 17 tokenUrl: string, 15 18 query: string, ··· 21 24 }; 22 25 23 26 if (requireAuth) { 24 - const token = await getValidAccessToken(tokenUrl); 27 + const token = await getValidAccessToken(storage, namespace, tokenUrl); 25 28 if (!token) { 26 29 throw new Error('Not authenticated'); 27 30 } 28 31 29 32 // Create DPoP proof bound to this request 30 - const dpopProof = await createDPoPProof('POST', graphqlUrl, token); 33 + const dpopProof = await createDPoPProof(namespace, 'POST', graphqlUrl, token); 31 34 32 35 headers['Authorization'] = `DPoP ${token}`; 33 36 headers['DPoP'] = dpopProof;
+25 -12
quickslice-client-js/src/storage/keys.ts
··· 1 1 /** 2 - * Storage key constants 2 + * Storage key factory - generates namespaced keys 3 3 */ 4 - export const STORAGE_KEYS = { 5 - accessToken: 'quickslice_access_token', 6 - refreshToken: 'quickslice_refresh_token', 7 - tokenExpiresAt: 'quickslice_token_expires_at', 8 - clientId: 'quickslice_client_id', 9 - userDid: 'quickslice_user_did', 10 - codeVerifier: 'quickslice_code_verifier', 11 - oauthState: 'quickslice_oauth_state', 12 - redirectUri: 'quickslice_redirect_uri', 13 - } as const; 4 + export interface StorageKeys { 5 + accessToken: string; 6 + refreshToken: string; 7 + tokenExpiresAt: string; 8 + clientId: string; 9 + userDid: string; 10 + codeVerifier: string; 11 + oauthState: string; 12 + redirectUri: string; 13 + } 14 + 15 + export function createStorageKeys(namespace: string): StorageKeys { 16 + return { 17 + accessToken: `quickslice_${namespace}_access_token`, 18 + refreshToken: `quickslice_${namespace}_refresh_token`, 19 + tokenExpiresAt: `quickslice_${namespace}_token_expires_at`, 20 + clientId: `quickslice_${namespace}_client_id`, 21 + userDid: `quickslice_${namespace}_user_did`, 22 + codeVerifier: `quickslice_${namespace}_code_verifier`, 23 + oauthState: `quickslice_${namespace}_oauth_state`, 24 + redirectUri: `quickslice_${namespace}_redirect_uri`, 25 + }; 26 + } 14 27 15 - export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]; 28 + export type StorageKey = string;
+8 -4
quickslice-client-js/src/storage/lock.ts
··· 1 1 const LOCK_TIMEOUT = 5000; // 5 seconds 2 - const LOCK_PREFIX = 'quickslice_lock_'; 3 2 4 3 function sleep(ms: number): Promise<void> { 5 4 return new Promise((resolve) => setTimeout(resolve, ms)); 6 5 } 7 6 7 + function getLockKey(namespace: string, key: string): string { 8 + return `quickslice_${namespace}_lock_${key}`; 9 + } 10 + 8 11 /** 9 12 * Acquire a lock using localStorage for multi-tab coordination 10 13 */ 11 14 export async function acquireLock( 15 + namespace: string, 12 16 key: string, 13 17 timeout = LOCK_TIMEOUT 14 18 ): Promise<string | null> { 15 - const lockKey = LOCK_PREFIX + key; 19 + const lockKey = getLockKey(namespace, key); 16 20 const lockValue = `${Date.now()}_${Math.random()}`; 17 21 const deadline = Date.now() + timeout; 18 22 ··· 48 52 /** 49 53 * Release a lock 50 54 */ 51 - export function releaseLock(key: string, lockValue: string): void { 52 - const lockKey = LOCK_PREFIX + key; 55 + export function releaseLock(namespace: string, key: string, lockValue: string): void { 56 + const lockKey = getLockKey(namespace, key); 53 57 // Only release if we still hold it 54 58 if (localStorage.getItem(lockKey) === lockValue) { 55 59 localStorage.removeItem(lockKey);
+37 -30
quickslice-client-js/src/storage/storage.ts
··· 1 - import { STORAGE_KEYS, StorageKey } from './keys'; 1 + import { StorageKeys } from './keys'; 2 2 3 3 /** 4 - * Hybrid storage utility - sessionStorage for OAuth flow state, 5 - * localStorage for tokens (shared across tabs) 4 + * Create a namespaced storage interface 6 5 */ 7 - export const storage = { 8 - get(key: StorageKey): string | null { 9 - // OAuth flow state stays in sessionStorage (per-tab) 10 - if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) { 11 - return sessionStorage.getItem(key); 12 - } 13 - // Tokens go in localStorage (shared across tabs) 14 - return localStorage.getItem(key); 15 - }, 6 + export function createStorage(keys: StorageKeys) { 7 + return { 8 + get(key: keyof StorageKeys): string | null { 9 + const storageKey = keys[key]; 10 + // OAuth flow state stays in sessionStorage (per-tab) 11 + if (key === 'codeVerifier' || key === 'oauthState') { 12 + return sessionStorage.getItem(storageKey); 13 + } 14 + // Tokens go in localStorage (shared across tabs) 15 + return localStorage.getItem(storageKey); 16 + }, 17 + 18 + set(key: keyof StorageKeys, value: string): void { 19 + const storageKey = keys[key]; 20 + if (key === 'codeVerifier' || key === 'oauthState') { 21 + sessionStorage.setItem(storageKey, value); 22 + } else { 23 + localStorage.setItem(storageKey, value); 24 + } 25 + }, 16 26 17 - set(key: StorageKey, value: string): void { 18 - if (key === STORAGE_KEYS.codeVerifier || key === STORAGE_KEYS.oauthState) { 19 - sessionStorage.setItem(key, value); 20 - } else { 21 - localStorage.setItem(key, value); 22 - } 23 - }, 27 + remove(key: keyof StorageKeys): void { 28 + const storageKey = keys[key]; 29 + sessionStorage.removeItem(storageKey); 30 + localStorage.removeItem(storageKey); 31 + }, 24 32 25 - remove(key: StorageKey): void { 26 - sessionStorage.removeItem(key); 27 - localStorage.removeItem(key); 28 - }, 33 + clear(): void { 34 + (Object.keys(keys) as Array<keyof StorageKeys>).forEach((key) => { 35 + const storageKey = keys[key]; 36 + sessionStorage.removeItem(storageKey); 37 + localStorage.removeItem(storageKey); 38 + }); 39 + }, 40 + }; 41 + } 29 42 30 - clear(): void { 31 - Object.values(STORAGE_KEYS).forEach((key) => { 32 - sessionStorage.removeItem(key); 33 - localStorage.removeItem(key); 34 - }); 35 - }, 36 - }; 43 + export type Storage = ReturnType<typeof createStorage>;
+11
quickslice-client-js/src/utils/crypto.ts
··· 10 10 } 11 11 12 12 /** 13 + * Generate an 8-character namespace hash from clientId 14 + */ 15 + export async function generateNamespaceHash(clientId: string): Promise<string> { 16 + const encoder = new TextEncoder(); 17 + const hash = await crypto.subtle.digest('SHA-256', encoder.encode(clientId)); 18 + const hashArray = Array.from(new Uint8Array(hash)); 19 + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); 20 + return hashHex.substring(0, 8); 21 + } 22 + 23 + /** 13 24 * Sign a JWT with an ECDSA P-256 private key 14 25 */ 15 26 export async function signJwt(