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

Compare changes

Choose any two refs to compare.

+968 -133
+10
CHANGELOG.md
··· 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 7 8 + ## v0.20.2 9 + 10 + ### Fixed 11 + - Fix filtered queries (`where` clause) returning incorrect results on PostgreSQL due to parameter index collision (WHERE clause placeholders always started at `$1` instead of accounting for prior bind values) 12 + 13 + ## v0.20.1 14 + 15 + ### Fixed 16 + - Fix cursor-based pagination (`after`/`before`) returning 0 results on PostgreSQL due to incorrect SQL placeholders (literal `?` instead of numbered `$1`, `$2`, etc.) 17 + 8 18 ## v0.20.0 9 19 10 20 ### Added
+520
dev-docs/plans/2026-01-19-fix-pagination-placeholders.md
··· 1 + # Fix Pagination Cursor Placeholder Bug 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Fix cursor-based pagination (`after`/`before`) which returns 0 results on PostgreSQL due to incorrect SQL placeholders. 6 + 7 + **Architecture:** The `build_cursor_where_clause` function in `pagination.gleam` uses literal `?` placeholders, but PostgreSQL requires numbered placeholders (`$1`, `$2`, etc.). We need to pass the executor and a starting index so proper placeholders can be generated. 8 + 9 + **Tech Stack:** Gleam, PostgreSQL, SQLite 10 + 11 + --- 12 + 13 + ## Root Cause 14 + 15 + In `server/src/database/queries/pagination.gleam`, the `build_cursor_where_clause` and `build_progressive_clauses` functions build SQL with literal `?`: 16 + 17 + ```gleam 18 + let new_part = field_ref <> " = ?" // Line 261 19 + let comparison_part = field_ref <> " " <> comparison_op <> " ?" // Line 273 20 + ``` 21 + 22 + But PostgreSQL needs `$1, $2, $3`. The executor has a `placeholder(index)` function that returns the correct format for each dialect, but it's not being used. 23 + 24 + --- 25 + 26 + ### Task 1: Update `build_cursor_where_clause` Signature 27 + 28 + **Files:** 29 + - Modify: `server/src/database/queries/pagination.gleam:210-237` 30 + 31 + **Step 1: Update function signature to accept start_index** 32 + 33 + Change the function signature from: 34 + 35 + ```gleam 36 + pub fn build_cursor_where_clause( 37 + exec: Executor, 38 + decoded_cursor: DecodedCursor, 39 + sort_by: Option(List(#(String, String))), 40 + is_before: Bool, 41 + ) -> #(String, List(String)) { 42 + ``` 43 + 44 + To: 45 + 46 + ```gleam 47 + pub fn build_cursor_where_clause( 48 + exec: Executor, 49 + decoded_cursor: DecodedCursor, 50 + sort_by: Option(List(#(String, String))), 51 + is_before: Bool, 52 + start_index: Int, 53 + ) -> #(String, List(String)) { 54 + ``` 55 + 56 + **Step 2: Update the call to build_progressive_clauses** 57 + 58 + Change line 225-231 from: 59 + 60 + ```gleam 61 + let clauses = 62 + build_progressive_clauses( 63 + exec, 64 + sort_fields, 65 + decoded_cursor.field_values, 66 + decoded_cursor.cid, 67 + is_before, 68 + ) 69 + ``` 70 + 71 + To: 72 + 73 + ```gleam 74 + let clauses = 75 + build_progressive_clauses( 76 + exec, 77 + sort_fields, 78 + decoded_cursor.field_values, 79 + decoded_cursor.cid, 80 + is_before, 81 + start_index, 82 + ) 83 + ``` 84 + 85 + **Step 3: Run build to check for compilation errors** 86 + 87 + Run: `cd ~/code/quickslice/server && gleam build` 88 + Expected: Compilation errors about missing argument (we'll fix callers in Task 3) 89 + 90 + --- 91 + 92 + ### Task 2: Update `build_progressive_clauses` to Use Numbered Placeholders 93 + 94 + **Files:** 95 + - Modify: `server/src/database/queries/pagination.gleam:239-307` 96 + 97 + **Step 1: Update function signature** 98 + 99 + Change line 240-246 from: 100 + 101 + ```gleam 102 + fn build_progressive_clauses( 103 + exec: Executor, 104 + sort_fields: List(#(String, String)), 105 + field_values: List(String), 106 + cid: String, 107 + is_before: Bool, 108 + ) -> #(List(String), List(String)) { 109 + ``` 110 + 111 + To: 112 + 113 + ```gleam 114 + fn build_progressive_clauses( 115 + exec: Executor, 116 + sort_fields: List(#(String, String)), 117 + field_values: List(String), 118 + cid: String, 119 + is_before: Bool, 120 + start_index: Int, 121 + ) -> #(List(String), List(String)) { 122 + ``` 123 + 124 + **Step 2: Rewrite the function body to track placeholder indices** 125 + 126 + Replace the entire function body (lines 247-307) with: 127 + 128 + ```gleam 129 + fn build_progressive_clauses( 130 + exec: Executor, 131 + sort_fields: List(#(String, String)), 132 + field_values: List(String), 133 + cid: String, 134 + is_before: Bool, 135 + start_index: Int, 136 + ) -> #(List(String), List(String)) { 137 + // Build clauses with tracked parameter index 138 + let #(clauses, params, next_index) = 139 + list.index_fold(sort_fields, #([], [], start_index), fn(acc, field, i) { 140 + let #(acc_clauses, acc_params, param_index) = acc 141 + 142 + // Build equality parts for prior fields 143 + let #(equality_parts, equality_params, idx_after_eq) = case i { 144 + 0 -> #([], [], param_index) 145 + _ -> { 146 + list.index_fold( 147 + list.take(sort_fields, i), 148 + #([], [], param_index), 149 + fn(eq_acc, prior_field, j) { 150 + let #(eq_parts, eq_params, eq_idx) = eq_acc 151 + let value = list_at(field_values, j) |> result.unwrap("") 152 + let field_ref = build_cursor_field_reference(exec, prior_field.0) 153 + let placeholder = executor.placeholder(exec, eq_idx) 154 + let new_part = field_ref <> " = " <> placeholder 155 + #( 156 + list.append(eq_parts, [new_part]), 157 + list.append(eq_params, [value]), 158 + eq_idx + 1, 159 + ) 160 + }, 161 + ) 162 + } 163 + } 164 + 165 + let value = list_at(field_values, i) |> result.unwrap("") 166 + let comparison_op = get_comparison_operator(field.1, is_before) 167 + let field_ref = build_cursor_field_reference(exec, field.0) 168 + let placeholder = executor.placeholder(exec, idx_after_eq) 169 + 170 + let comparison_part = field_ref <> " " <> comparison_op <> " " <> placeholder 171 + let all_parts = list.append(equality_parts, [comparison_part]) 172 + let all_params = list.append(equality_params, [value]) 173 + 174 + let clause = "(" <> string.join(all_parts, " AND ") <> ")" 175 + 176 + #( 177 + list.append(acc_clauses, [clause]), 178 + list.append(acc_params, all_params), 179 + idx_after_eq + 1, 180 + ) 181 + }) 182 + 183 + // Build final clause with all fields equal and CID comparison 184 + let #(final_equality_parts, final_equality_params, idx_after_final_eq) = 185 + list.index_fold(sort_fields, #([], [], next_index), fn(acc, field, j) { 186 + let #(parts, params, idx) = acc 187 + let value = list_at(field_values, j) |> result.unwrap("") 188 + let field_ref = build_cursor_field_reference(exec, field.0) 189 + let placeholder = executor.placeholder(exec, idx) 190 + #( 191 + list.append(parts, [field_ref <> " = " <> placeholder]), 192 + list.append(params, [value]), 193 + idx + 1, 194 + ) 195 + }) 196 + 197 + let last_field = list.last(sort_fields) |> result.unwrap(#("", "desc")) 198 + let cid_comparison_op = get_comparison_operator(last_field.1, is_before) 199 + let cid_placeholder = executor.placeholder(exec, idx_after_final_eq) 200 + 201 + let final_parts = 202 + list.append(final_equality_parts, ["cid " <> cid_comparison_op <> " " <> cid_placeholder]) 203 + let final_params = list.append(final_equality_params, [cid]) 204 + 205 + let final_clause = "(" <> string.join(final_parts, " AND ") <> ")" 206 + let all_clauses = list.append(clauses, [final_clause]) 207 + let all_params = list.append(params, final_params) 208 + 209 + #(all_clauses, all_params) 210 + } 211 + ``` 212 + 213 + **Step 3: Run build to verify syntax** 214 + 215 + Run: `cd ~/code/quickslice/server && gleam build` 216 + Expected: Compilation errors about callers (we'll fix in Task 3) 217 + 218 + --- 219 + 220 + ### Task 3: Update All Callers in records.gleam 221 + 222 + **Files:** 223 + - Modify: `server/src/database/repositories/records.gleam` 224 + 225 + There are 5 places that call `build_cursor_where_clause`. Each needs to pass the current parameter count + 1 as the start_index. 226 + 227 + **Step 1: Update get_by_collection_paginated (line ~548)** 228 + 229 + Find lines 548-555 and change from: 230 + 231 + ```gleam 232 + let #(cursor_where, cursor_params) = 233 + pagination.build_cursor_where_clause( 234 + exec, 235 + decoded_cursor, 236 + sort_by, 237 + !is_forward, 238 + ) 239 + ``` 240 + 241 + To: 242 + 243 + ```gleam 244 + let #(cursor_where, cursor_params) = 245 + pagination.build_cursor_where_clause( 246 + exec, 247 + decoded_cursor, 248 + sort_by, 249 + !is_forward, 250 + list.length(bind_values) + 1, 251 + ) 252 + ``` 253 + 254 + **Step 2: Update get_by_collection_paginated_with_where (line ~701)** 255 + 256 + Find lines 701-708 and change from: 257 + 258 + ```gleam 259 + let #(cursor_where, cursor_params) = 260 + pagination.build_cursor_where_clause( 261 + exec, 262 + decoded_cursor, 263 + sort_by, 264 + !is_forward, 265 + ) 266 + ``` 267 + 268 + To: 269 + 270 + ```gleam 271 + let #(cursor_where, cursor_params) = 272 + pagination.build_cursor_where_clause( 273 + exec, 274 + decoded_cursor, 275 + sort_by, 276 + !is_forward, 277 + list.length(bind_values) + 1, 278 + ) 279 + ``` 280 + 281 + **Step 3: Update get_by_reference_field_paginated (line ~941)** 282 + 283 + Find lines 941-948 and change from: 284 + 285 + ```gleam 286 + let #(cursor_where, cursor_params) = 287 + pagination.build_cursor_where_clause( 288 + exec, 289 + decoded_cursor, 290 + sort_by, 291 + !is_forward, 292 + ) 293 + ``` 294 + 295 + To: 296 + 297 + ```gleam 298 + let #(cursor_where, cursor_params) = 299 + pagination.build_cursor_where_clause( 300 + exec, 301 + decoded_cursor, 302 + sort_by, 303 + !is_forward, 304 + list.length(with_where_values) + 1, 305 + ) 306 + ``` 307 + 308 + **Step 4: Update get_by_dids_and_collection_paginated (line ~1297)** 309 + 310 + Find lines 1297-1304 and change from: 311 + 312 + ```gleam 313 + let #(cursor_where, cursor_params) = 314 + pagination.build_cursor_where_clause( 315 + exec, 316 + decoded_cursor, 317 + sort_by, 318 + !is_forward, 319 + ) 320 + ``` 321 + 322 + To: 323 + 324 + ```gleam 325 + let #(cursor_where, cursor_params) = 326 + pagination.build_cursor_where_clause( 327 + exec, 328 + decoded_cursor, 329 + sort_by, 330 + !is_forward, 331 + list.length(with_where_values) + 1, 332 + ) 333 + ``` 334 + 335 + **Step 5: Build to verify all callers are updated** 336 + 337 + Run: `cd ~/code/quickslice/server && gleam build` 338 + Expected: BUILD SUCCESS 339 + 340 + **Step 6: Commit the fix** 341 + 342 + ```bash 343 + cd ~/code/quickslice/server 344 + git add src/database/queries/pagination.gleam src/database/repositories/records.gleam 345 + git commit -m "fix: use numbered placeholders in cursor WHERE clause for PostgreSQL 346 + 347 + The build_cursor_where_clause function was using literal '?' placeholders, 348 + which works for SQLite but fails on PostgreSQL (which needs \$1, \$2, etc.). 349 + 350 + Now accepts a start_index parameter and uses executor.placeholder() to 351 + generate the correct format for each database dialect. 352 + 353 + Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>" 354 + ``` 355 + 356 + --- 357 + 358 + ### Task 4: Update Unit Tests 359 + 360 + **Files:** 361 + - Modify: `server/test/pagination_test.gleam` 362 + 363 + The existing tests check for `?` placeholders. They pass because tests use SQLite. We need to update tests to pass the new start_index parameter. 364 + 365 + **Step 1: Update build_where_single_field_desc_test** 366 + 367 + Find the test around line 272 and change: 368 + 369 + ```gleam 370 + let #(sql, params) = 371 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 372 + ``` 373 + 374 + To: 375 + 376 + ```gleam 377 + let #(sql, params) = 378 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 379 + ``` 380 + 381 + **Step 2: Update build_where_single_field_asc_test** 382 + 383 + Find the test around line 298 and change: 384 + 385 + ```gleam 386 + let #(sql, params) = 387 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 388 + ``` 389 + 390 + To: 391 + 392 + ```gleam 393 + let #(sql, params) = 394 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 395 + ``` 396 + 397 + **Step 3: Update build_where_json_field_test** 398 + 399 + Find the test around line 324 and change: 400 + 401 + ```gleam 402 + let #(sql, params) = 403 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 404 + ``` 405 + 406 + To: 407 + 408 + ```gleam 409 + let #(sql, params) = 410 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 411 + ``` 412 + 413 + **Step 4: Update build_where_nested_json_field_test** 414 + 415 + Find the test around line 345 and change: 416 + 417 + ```gleam 418 + let #(sql, params) = 419 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 420 + ``` 421 + 422 + To: 423 + 424 + ```gleam 425 + let #(sql, params) = 426 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 427 + ``` 428 + 429 + **Step 5: Update build_where_multi_field_test** 430 + 431 + Find the test around line 366 and change: 432 + 433 + ```gleam 434 + let #(sql, params) = 435 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 436 + ``` 437 + 438 + To: 439 + 440 + ```gleam 441 + let #(sql, params) = 442 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 443 + ``` 444 + 445 + **Step 6: Update build_where_backward_test** 446 + 447 + Find the test around line 398 and change: 448 + 449 + ```gleam 450 + let #(sql, params) = 451 + pagination.build_cursor_where_clause(exec, decoded, sort_by, True) 452 + ``` 453 + 454 + To: 455 + 456 + ```gleam 457 + let #(sql, params) = 458 + pagination.build_cursor_where_clause(exec, decoded, sort_by, True, 1) 459 + ``` 460 + 461 + **Step 7: Run tests** 462 + 463 + Run: `cd ~/code/quickslice/server && gleam test` 464 + Expected: All tests pass (SQLite uses `?` regardless of index, so assertions still work) 465 + 466 + **Step 8: Commit test updates** 467 + 468 + ```bash 469 + cd ~/code/quickslice/server 470 + git add test/pagination_test.gleam 471 + git commit -m "test: update pagination tests with start_index parameter 472 + 473 + Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>" 474 + ``` 475 + 476 + --- 477 + 478 + ### Task 5: Manual Verification with MCP 479 + 480 + **Step 1: Test pagination via quickslice MCP** 481 + 482 + Run this GraphQL query: 483 + 484 + ```graphql 485 + query { 486 + gamesGamesgamesgamesgamesGame(first: 2) { 487 + pageInfo { 488 + hasNextPage 489 + endCursor 490 + } 491 + edges { 492 + node { name } 493 + } 494 + } 495 + } 496 + ``` 497 + 498 + **Step 2: Test pagination with cursor** 499 + 500 + Use the `endCursor` from step 1: 501 + 502 + ```graphql 503 + query { 504 + gamesGamesgamesgamesgamesGame(first: 2, after: "<endCursor>") { 505 + pageInfo { 506 + hasNextPage 507 + endCursor 508 + } 509 + edges { 510 + node { name } 511 + } 512 + } 513 + } 514 + ``` 515 + 516 + Expected: Returns the NEXT 2 records (not empty, not the same as page 1) 517 + 518 + **Step 3: Commit verification note (optional)** 519 + 520 + If all works, the fix is complete.
+46
docs/guides/moderation.md
··· 301 301 - `!warn` and `!hide` always apply their effects 302 302 303 303 Attempting to set a preference for a system label returns an error. 304 + 305 + ### Client-Side Filtering 306 + 307 + The server filters takedown labels automatically, but clients must apply user preferences for other labels. Here's the pattern: 308 + 309 + ```typescript 310 + // 1. Fetch preferences once and cache 311 + const prefs = await client.query(`{ 312 + viewerLabelPreferences { val visibility } 313 + }`) 314 + const prefMap = new Map(prefs.map(p => [p.val, p.visibility])) 315 + 316 + // 2. Check visibility for each record 317 + function getVisibility(record) { 318 + for (const label of record.labels ?? []) { 319 + const vis = prefMap.get(label.val) ?? 'WARN' 320 + if (vis === 'HIDE') return { show: false } 321 + if (vis === 'WARN') return { show: true, blur: true } 322 + } 323 + return { show: true, blur: false } 324 + } 325 + 326 + // 3. Apply in your UI 327 + function RecordCard({ record }) { 328 + const { show, blur } = getVisibility(record) 329 + 330 + if (!show) return null 331 + 332 + if (blur) { 333 + return ( 334 + <BlurOverlay onReveal={() => setRevealed(true)}> 335 + <Content record={record} /> 336 + </BlurOverlay> 337 + ) 338 + } 339 + 340 + return <Content record={record} /> 341 + } 342 + ``` 343 + 344 + Key points: 345 + 346 + - Cache preferences at session start or when user updates them 347 + - Default unknown labels to `WARN` for safety 348 + - Multiple labels on one record: apply the most restrictive 349 + - `IGNORE` and `SHOW` both display normally; `SHOW` is for explicit opt-in to adult content
+179
docs/guides/notifications.md
··· 1 + # Notifications 2 + 3 + Notifications show records that mention the authenticated user. When someone likes your post, follows you, or references your DID in any record, it appears in your notifications. 4 + 5 + ## How It Works 6 + 7 + The `notifications` query searches all records for your DID. It returns matches where: 8 + - The record's JSON contains your DID (as a URI or raw DID) 9 + - The record was authored by someone else (self-mentions excluded) 10 + 11 + The server identifies you from your access token. Authentication is required. 12 + 13 + ## Basic Query 14 + 15 + ```graphql 16 + query { 17 + notifications(first: 20) { 18 + edges { 19 + node { 20 + __typename 21 + ... on AppBskyFeedLike { 22 + uri 23 + did 24 + createdAt 25 + } 26 + ... on AppBskyGraphFollow { 27 + uri 28 + did 29 + createdAt 30 + } 31 + } 32 + cursor 33 + } 34 + pageInfo { 35 + hasNextPage 36 + endCursor 37 + } 38 + } 39 + } 40 + ``` 41 + 42 + The `node` is a union type containing all record types in your schema. Use inline fragments (`... on TypeName`) to access type-specific fields. 43 + 44 + ## Response Example 45 + 46 + When Alice likes your post and Bob follows you: 47 + 48 + ```json 49 + { 50 + "data": { 51 + "notifications": { 52 + "edges": [ 53 + { 54 + "node": { 55 + "__typename": "AppBskyGraphFollow", 56 + "uri": "at://did:plc:bob/app.bsky.graph.follow/3k2yab7", 57 + "did": "did:plc:bob", 58 + "createdAt": "2024-01-03T12:00:00Z" 59 + }, 60 + "cursor": "eyJ..." 61 + }, 62 + { 63 + "node": { 64 + "__typename": "AppBskyFeedLike", 65 + "uri": "at://did:plc:alice/app.bsky.feed.like/3k2xz9m", 66 + "did": "did:plc:alice", 67 + "createdAt": "2024-01-02T10:30:00Z" 68 + }, 69 + "cursor": "eyJ..." 70 + } 71 + ], 72 + "pageInfo": { 73 + "hasNextPage": false, 74 + "endCursor": "eyJ..." 75 + } 76 + } 77 + } 78 + } 79 + ``` 80 + 81 + Results are sorted newest-first by rkey (TID). 82 + 83 + ## Filtering by Collection 84 + 85 + Filter to specific record types using the `collections` argument: 86 + 87 + ```graphql 88 + query { 89 + notifications(collections: [APP_BSKY_FEED_LIKE], first: 20) { 90 + edges { 91 + node { 92 + ... on AppBskyFeedLike { 93 + uri 94 + did 95 + } 96 + } 97 + } 98 + } 99 + } 100 + ``` 101 + 102 + Collection names use the enum format: `app.bsky.feed.like` becomes `APP_BSKY_FEED_LIKE`. 103 + 104 + Filter to multiple types: 105 + 106 + ```graphql 107 + query { 108 + notifications( 109 + collections: [APP_BSKY_FEED_LIKE, APP_BSKY_GRAPH_FOLLOW] 110 + first: 20 111 + ) { 112 + # ... 113 + } 114 + } 115 + ``` 116 + 117 + ## Pagination 118 + 119 + Use cursor-based pagination to fetch more results: 120 + 121 + ```graphql 122 + query { 123 + notifications(first: 20, after: "eyJ...") { 124 + edges { 125 + node { __typename } 126 + cursor 127 + } 128 + pageInfo { 129 + hasNextPage 130 + endCursor 131 + } 132 + } 133 + } 134 + ``` 135 + 136 + Pass `pageInfo.endCursor` as the `after` argument to fetch the next page. 137 + 138 + ## Real-time Updates 139 + 140 + Subscribe to new notifications as they happen: 141 + 142 + ```graphql 143 + subscription { 144 + notificationCreated { 145 + __typename 146 + ... on AppBskyFeedLike { 147 + uri 148 + did 149 + createdAt 150 + } 151 + ... on AppBskyGraphFollow { 152 + uri 153 + did 154 + createdAt 155 + } 156 + } 157 + } 158 + ``` 159 + 160 + Filter to specific collections: 161 + 162 + ```graphql 163 + subscription { 164 + notificationCreated(collections: [APP_BSKY_FEED_LIKE]) { 165 + ... on AppBskyFeedLike { 166 + uri 167 + did 168 + } 169 + } 170 + } 171 + ``` 172 + 173 + See [Subscriptions](./subscriptions.md) for WebSocket connection details. 174 + 175 + ## Authentication Required 176 + 177 + Notifications require authentication. Without a valid access token, the query returns an error. 178 + 179 + Use the [Quickslice client SDK](./authentication.md#using-the-client-sdk) to handle authentication automatically.
+6 -3
quickslice-client-js/dist/client.d.ts
··· 8 8 export interface User { 9 9 did: string; 10 10 } 11 + export interface QueryOptions { 12 + signal?: AbortSignal; 13 + } 11 14 export declare class QuicksliceClient { 12 15 private server; 13 16 private clientId; ··· 56 59 /** 57 60 * Execute a GraphQL query (authenticated) 58 61 */ 59 - query<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T>; 62 + query<T = unknown>(query: string, variables?: Record<string, unknown>, options?: QueryOptions): Promise<T>; 60 63 /** 61 64 * Execute a GraphQL mutation (authenticated) 62 65 */ 63 - mutate<T = unknown>(mutation: string, variables?: Record<string, unknown>): Promise<T>; 66 + mutate<T = unknown>(mutation: string, variables?: Record<string, unknown>, options?: QueryOptions): Promise<T>; 64 67 /** 65 68 * Execute a public GraphQL query (no auth) 66 69 */ 67 - publicQuery<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T>; 70 + publicQuery<T = unknown>(query: string, variables?: Record<string, unknown>, options?: QueryOptions): Promise<T>; 68 71 }
+1 -1
quickslice-client-js/dist/graphql.d.ts
··· 9 9 /** 10 10 * Execute a GraphQL query or mutation 11 11 */ 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>; 12 + export declare function graphqlRequest<T = unknown>(storage: Storage, namespace: string, graphqlUrl: string, tokenUrl: string, query: string, variables?: Record<string, unknown>, requireAuth?: boolean, signal?: AbortSignal): Promise<T>;
+1 -1
quickslice-client-js/dist/index.d.ts
··· 1 - export { QuicksliceClient, QuicksliceClientOptions, User } from './client'; 1 + export { QuicksliceClient, QuicksliceClientOptions, QueryOptions, User } from './client'; 2 2 export { QuicksliceError, LoginRequiredError, NetworkError, OAuthError, } from './errors'; 3 3 import { QuicksliceClient, QuicksliceClientOptions } from './client'; 4 4 /**
+11 -8
quickslice-client-js/dist/quickslice-client.esm.js
··· 411 411 } 412 412 413 413 // src/graphql.ts 414 - async function graphqlRequest(storage, namespace, graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false) { 414 + async function graphqlRequest(storage, namespace, graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false, signal) { 415 415 const headers = { 416 416 "Content-Type": "application/json" 417 417 }; ··· 427 427 const response = await fetch(graphqlUrl, { 428 428 method: "POST", 429 429 headers, 430 - body: JSON.stringify({ query, variables }) 430 + body: JSON.stringify({ query, variables }), 431 + signal 431 432 }); 432 433 if (!response.ok) { 433 434 throw new Error(`GraphQL request failed: ${response.statusText}`); ··· 528 529 /** 529 530 * Execute a GraphQL query (authenticated) 530 531 */ 531 - async query(query, variables = {}) { 532 + async query(query, variables = {}, options = {}) { 532 533 await this.init(); 533 534 return await graphqlRequest( 534 535 this.getStorage(), ··· 537 538 this.tokenUrl, 538 539 query, 539 540 variables, 540 - true 541 + true, 542 + options.signal 541 543 ); 542 544 } 543 545 /** 544 546 * Execute a GraphQL mutation (authenticated) 545 547 */ 546 - async mutate(mutation, variables = {}) { 547 - return this.query(mutation, variables); 548 + async mutate(mutation, variables = {}, options = {}) { 549 + return this.query(mutation, variables, options); 548 550 } 549 551 /** 550 552 * Execute a public GraphQL query (no auth) 551 553 */ 552 - async publicQuery(query, variables = {}) { 554 + async publicQuery(query, variables = {}, options = {}) { 553 555 await this.init(); 554 556 return await graphqlRequest( 555 557 this.getStorage(), ··· 558 560 this.tokenUrl, 559 561 query, 560 562 variables, 561 - false 563 + false, 564 + options.signal 562 565 ); 563 566 } 564 567 };
+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 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": ";AAcO,SAAS,kBAAkB,WAAgC;AAChE,SAAO;AAAA,IACL,aAAa,cAAc,SAAS;AAAA,IACpC,cAAc,cAAc,SAAS;AAAA,IACrC,gBAAgB,cAAc,SAAS;AAAA,IACvC,UAAU,cAAc,SAAS;AAAA,IACjC,SAAS,cAAc,SAAS;AAAA,IAChC,cAAc,cAAc,SAAS;AAAA,IACrC,YAAY,cAAc,SAAS;AAAA,IACnC,aAAa,cAAc,SAAS;AAAA,EACtC;AACF;;;ACpBO,SAAS,cAAc,MAAmB;AAC/C,SAAO;AAAA,IACL,IAAI,KAAuC;AACzC,YAAM,aAAa,KAAK,GAAG;AAE3B,UAAI,QAAQ,kBAAkB,QAAQ,cAAc;AAClD,eAAO,eAAe,QAAQ,UAAU;AAAA,MAC1C;AAEA,aAAO,aAAa,QAAQ,UAAU;AAAA,IACxC;AAAA,IAEA,IAAI,KAAwB,OAAqB;AAC/C,YAAM,aAAa,KAAK,GAAG;AAC3B,UAAI,QAAQ,kBAAkB,QAAQ,cAAc;AAClD,uBAAe,QAAQ,YAAY,KAAK;AAAA,MAC1C,OAAO;AACL,qBAAa,QAAQ,YAAY,KAAK;AAAA,MACxC;AAAA,IACF;AAAA,IAEA,OAAO,KAA8B;AACnC,YAAM,aAAa,KAAK,GAAG;AAC3B,qBAAe,WAAW,UAAU;AACpC,mBAAa,WAAW,UAAU;AAAA,IACpC;AAAA,IAEA,QAAc;AACZ,MAAC,OAAO,KAAK,IAAI,EAA+B,QAAQ,CAAC,QAAQ;AAC/D,cAAM,aAAa,KAAK,GAAG;AAC3B,uBAAe,WAAW,UAAU;AACpC,qBAAa,WAAW,UAAU;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;ACrCO,SAAS,gBAAgB,QAA0C;AACxE,QAAM,QAAQ,kBAAkB,aAAa,SAAS,IAAI,WAAW,MAAM;AAC3E,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACxC;AACA,SAAO,KAAK,MAAM,EACf,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAKO,SAAS,qBAAqB,YAA4B;AAC/D,QAAM,QAAQ,IAAI,WAAW,UAAU;AACvC,SAAO,gBAAgB,KAAK;AAC5B,SAAO,gBAAgB,KAAK;AAC9B;;;ACjBA,eAAsB,gBAAgB,MAA+B;AACnE,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,QAAQ,OAAO,IAAI,CAAC;AACvE,SAAO,gBAAgB,IAAI;AAC7B;AAKA,eAAsB,sBAAsB,UAAmC;AAC7E,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,QAAQ,OAAO,QAAQ,CAAC;AAC3E,QAAM,YAAY,MAAM,KAAK,IAAI,WAAW,IAAI,CAAC;AACjD,QAAM,UAAU,UAAU,IAAI,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC3E,SAAO,QAAQ,UAAU,GAAG,CAAC;AAC/B;AAKA,eAAsB,QACpB,QACA,SACA,YACiB;AACjB,QAAM,UAAU,IAAI,YAAY;AAEhC,QAAM,YAAY,gBAAgB,QAAQ,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC;AACxE,QAAM,aAAa,gBAAgB,QAAQ,OAAO,KAAK,UAAU,OAAO,CAAC,CAAC;AAE1E,QAAM,eAAe,GAAG,SAAS,IAAI,UAAU;AAE/C,QAAM,YAAY,MAAM,OAAO,OAAO;AAAA,IACpC,EAAE,MAAM,SAAS,MAAM,UAAU;AAAA,IACjC;AAAA,IACA,QAAQ,OAAO,YAAY;AAAA,EAC7B;AAEA,QAAM,eAAe,gBAAgB,SAAS;AAE9C,SAAO,GAAG,YAAY,IAAI,YAAY;AACxC;;;AC3CA,IAAM,aAAa;AACnB,IAAM,YAAY;AAClB,IAAM,SAAS;AAUf,IAAM,aAAa,oBAAI,IAAkC;AAEzD,SAAS,UAAU,WAA2B;AAC5C,SAAO,oBAAoB,SAAS;AACtC;AAEA,SAAS,aAAa,WAAyC;AAC7D,QAAM,WAAW,WAAW,IAAI,SAAS;AACzC,MAAI,SAAU,QAAO;AAErB,QAAM,UAAU,IAAI,QAAqB,CAAC,SAAS,WAAW;AAC5D,UAAM,UAAU,UAAU,KAAK,UAAU,SAAS,GAAG,UAAU;AAE/D,YAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,YAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAEhD,YAAQ,kBAAkB,CAAC,UAAU;AACnC,YAAM,KAAM,MAAM,OAA4B;AAC9C,UAAI,CAAC,GAAG,iBAAiB,SAAS,SAAS,GAAG;AAC5C,WAAG,kBAAkB,WAAW,EAAE,SAAS,KAAK,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,EACF,CAAC;AAED,aAAW,IAAI,WAAW,OAAO;AACjC,SAAO;AACT;AAEA,eAAe,WAAW,WAAgD;AACxE,QAAM,KAAK,MAAM,aAAa,SAAS;AACvC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,UAAM,QAAQ,GAAG,YAAY,SAAS;AACtC,UAAM,UAAU,MAAM,IAAI,MAAM;AAEhC,YAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,YAAQ,YAAY,MAAM,QAAQ,QAAQ,UAAU,IAAI;AAAA,EAC1D,CAAC;AACH;AAEA,eAAe,aACb,WACA,YACA,WACe;AACf,QAAM,KAAK,MAAM,aAAa,SAAS;AACvC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,UAAM,QAAQ,GAAG,YAAY,SAAS;AACtC,UAAM,UAAU,MAAM,IAAI;AAAA,MACxB,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,MACA,WAAW,KAAK,IAAI;AAAA,IACtB,CAAC;AAED,YAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,YAAQ,YAAY,MAAM,QAAQ;AAAA,EACpC,CAAC;AACH;AAEA,eAAsB,mBAAmB,WAAyC;AAChF,QAAM,UAAU,MAAM,WAAW,SAAS;AAE1C,MAAI,SAAS;AACX,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM,OAAO,OAAO;AAAA,IAClC,EAAE,MAAM,SAAS,YAAY,QAAQ;AAAA,IACrC;AAAA;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAGA,QAAM,YAAY,MAAM,OAAO,OAAO,UAAU,OAAO,QAAQ,SAAS;AAGxE,QAAM,aAAa,WAAW,QAAQ,YAAY,SAAS;AAE3D,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,YAAY,QAAQ;AAAA,IACpB;AAAA,IACA,WAAW,KAAK,IAAI;AAAA,EACtB;AACF;AAKA,eAAsB,gBACpB,WACA,QACA,KACA,cAA6B,MACZ;AACjB,QAAM,UAAU,MAAM,mBAAmB,SAAS;AAGlD,QAAM,EAAE,KAAK,KAAK,GAAG,EAAE,IAAI,QAAQ;AACnC,QAAM,aAAa,EAAE,KAAK,KAAK,GAAG,EAAE;AAEpC,QAAM,SAAS;AAAA,IACb,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAEA,QAAM,UAAmC;AAAA,IACvC,KAAK,qBAAqB,EAAE;AAAA,IAC5B,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EACnC;AAGA,MAAI,aAAa;AACf,YAAQ,MAAM,MAAM,gBAAgB,WAAW;AAAA,EACjD;AAEA,SAAO,MAAM,QAAQ,QAAQ,SAAS,QAAQ,UAAU;AAC1D;AAKA,eAAsB,cAAc,WAAkC;AACpE,QAAM,KAAK,MAAM,aAAa,SAAS;AACvC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,UAAM,QAAQ,GAAG,YAAY,SAAS;AACtC,UAAM,UAAU,MAAM,MAAM;AAE5B,YAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,YAAQ,YAAY,MAAM,QAAQ;AAAA,EACpC,CAAC;AACH;;;ACpJO,SAAS,uBAA+B;AAC7C,SAAO,qBAAqB,EAAE;AAChC;AAKA,eAAsB,sBAAsB,UAAmC;AAC7E,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,QAAQ;AACpC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AACvD,SAAO,gBAAgB,IAAI;AAC7B;AAKO,SAAS,gBAAwB;AACtC,SAAO,qBAAqB,EAAE;AAChC;;;ACxBA,IAAM,eAAe;AAErB,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,SAAS,WAAW,WAAmB,KAAqB;AAC1D,SAAO,cAAc,SAAS,SAAS,GAAG;AAC5C;AAKA,eAAsB,YACpB,WACA,KACA,UAAU,cACc;AACxB,QAAM,UAAU,WAAW,WAAW,GAAG;AACzC,QAAM,YAAY,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;AAChD,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,UAAM,WAAW,aAAa,QAAQ,OAAO;AAE7C,QAAI,UAAU;AAEZ,YAAM,CAAC,SAAS,IAAI,SAAS,MAAM,GAAG;AACtC,UAAI,KAAK,IAAI,IAAI,SAAS,SAAS,IAAI,cAAc;AAEnD,qBAAa,WAAW,OAAO;AAAA,MACjC,OAAO;AAEL,cAAM,MAAM,EAAE;AACd;AAAA,MACF;AAAA,IACF;AAGA,iBAAa,QAAQ,SAAS,SAAS;AAGvC,UAAM,MAAM,EAAE;AACd,QAAI,aAAa,QAAQ,OAAO,MAAM,WAAW;AAC/C,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,YAAY,WAAmB,KAAa,WAAyB;AACnF,QAAM,UAAU,WAAW,WAAW,GAAG;AAEzC,MAAI,aAAa,QAAQ,OAAO,MAAM,WAAW;AAC/C,iBAAa,WAAW,OAAO;AAAA,EACjC;AACF;;;ACxDA,IAAM,0BAA0B;AAEhC,SAASA,OAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAKA,eAAe,cACb,SACA,WACA,UACiB;AACjB,QAAM,eAAe,QAAQ,IAAI,cAAc;AAC/C,QAAM,WAAW,QAAQ,IAAI,UAAU;AAEvC,MAAI,CAAC,gBAAgB,CAAC,UAAU;AAC9B,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAEA,QAAM,YAAY,MAAM,gBAAgB,WAAW,QAAQ,QAAQ;AAEnE,QAAM,WAAW,MAAM,MAAM,UAAU;AAAA,IACrC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,MAAM;AAAA,IACR;AAAA,IACA,MAAM,IAAI,gBAAgB;AAAA,MACxB,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,WAAW;AAAA,IACb,CAAC;AAAA,EACH,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACxD,UAAM,IAAI;AAAA,MACR,yBAAyB,UAAU,qBAAqB,SAAS,UAAU;AAAA,IAC7E;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,SAAS,KAAK;AAGnC,UAAQ,IAAI,eAAe,OAAO,YAAY;AAC9C,MAAI,OAAO,eAAe;AACxB,YAAQ,IAAI,gBAAgB,OAAO,aAAa;AAAA,EAClD;AAEA,QAAM,YAAY,KAAK,IAAI,IAAI,OAAO,aAAa;AACnD,UAAQ,IAAI,kBAAkB,UAAU,SAAS,CAAC;AAElD,SAAO,OAAO;AAChB;AAMA,eAAsB,oBACpB,SACA,WACA,UACiB;AACjB,QAAM,cAAc,QAAQ,IAAI,aAAa;AAC7C,QAAM,YAAY,SAAS,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AAG/D,MAAI,eAAe,KAAK,IAAI,IAAI,YAAY,yBAAyB;AACnE,WAAO;AAAA,EACT;AAGA,QAAM,UAAU;AAChB,QAAM,YAAY,MAAM,YAAY,WAAW,OAAO;AAEtD,MAAI,CAAC,WAAW;AAGd,UAAMA,OAAM,GAAG;AACf,UAAM,aAAa,QAAQ,IAAI,aAAa;AAC5C,UAAM,cAAc,SAAS,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AACjE,QAAI,cAAc,KAAK,IAAI,IAAI,cAAc,yBAAyB;AACpE,aAAO;AAAA,IACT;AACA,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAEA,MAAI;AAEF,UAAM,aAAa,QAAQ,IAAI,aAAa;AAC5C,UAAM,cAAc,SAAS,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AACjE,QAAI,cAAc,KAAK,IAAI,IAAI,cAAc,yBAAyB;AACpE,aAAO;AAAA,IACT;AAGA,WAAO,MAAM,cAAc,SAAS,WAAW,QAAQ;AAAA,EACzD,UAAE;AACA,gBAAY,WAAW,SAAS,SAAS;AAAA,EAC3C;AACF;AAKO,SAAS,YACd,SACA,QAMM;AACN,UAAQ,IAAI,eAAe,OAAO,YAAY;AAC9C,MAAI,OAAO,eAAe;AACxB,YAAQ,IAAI,gBAAgB,OAAO,aAAa;AAAA,EAClD;AAEA,QAAM,YAAY,KAAK,IAAI,IAAI,OAAO,aAAa;AACnD,UAAQ,IAAI,kBAAkB,UAAU,SAAS,CAAC;AAElD,MAAI,OAAO,KAAK;AACd,YAAQ,IAAI,WAAW,OAAO,GAAG;AAAA,EACnC;AACF;AAKO,SAAS,gBAAgB,SAA2B;AACzD,QAAM,cAAc,QAAQ,IAAI,aAAa;AAC7C,QAAM,eAAe,QAAQ,IAAI,cAAc;AAC/C,SAAO,CAAC,EAAE,eAAe;AAC3B;;;AC/HA,eAAsB,cACpB,SACA,cACA,UACA,UAAwB,CAAC,GACV;AACf,QAAM,eAAe,qBAAqB;AAC1C,QAAM,gBAAgB,MAAM,sBAAsB,YAAY;AAC9D,QAAM,QAAQ,cAAc;AAG5B,QAAM,cAAc,QAAQ,eAAgB,OAAO,SAAS,SAAS,OAAO,SAAS;AAGrF,UAAQ,IAAI,gBAAgB,YAAY;AACxC,UAAQ,IAAI,cAAc,KAAK;AAC/B,UAAQ,IAAI,YAAY,QAAQ;AAChC,UAAQ,IAAI,eAAe,WAAW;AAGtC,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,WAAW;AAAA,IACX,cAAc;AAAA,IACd,eAAe;AAAA,IACf,gBAAgB;AAAA,IAChB,uBAAuB;AAAA,IACvB;AAAA,EACF,CAAC;AAED,MAAI,QAAQ,QAAQ;AAClB,WAAO,IAAI,cAAc,QAAQ,MAAM;AAAA,EACzC;AAEA,MAAI,QAAQ,OAAO;AACjB,WAAO,IAAI,SAAS,QAAQ,KAAK;AAAA,EACnC;AAEA,SAAO,SAAS,OAAO,GAAG,YAAY,IAAI,OAAO,SAAS,CAAC;AAC7D;AAMA,eAAsB,oBACpB,SACA,WACA,UACkB;AAClB,QAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,QAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,QAAM,QAAQ,OAAO,IAAI,OAAO;AAChC,QAAM,QAAQ,OAAO,IAAI,OAAO;AAEhC,MAAI,OAAO;AACT,UAAM,IAAI;AAAA,MACR,gBAAgB,KAAK,MAAM,OAAO,IAAI,mBAAmB,KAAK,EAAE;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,CAAC,QAAQ,CAAC,OAAO;AACnB,WAAO;AAAA,EACT;AAGA,QAAM,cAAc,QAAQ,IAAI,YAAY;AAC5C,MAAI,UAAU,aAAa;AACzB,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AAGA,QAAM,eAAe,QAAQ,IAAI,cAAc;AAC/C,QAAM,WAAW,QAAQ,IAAI,UAAU;AACvC,QAAM,cAAc,QAAQ,IAAI,aAAa;AAE7C,MAAI,CAAC,gBAAgB,CAAC,YAAY,CAAC,aAAa;AAC9C,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAGA,QAAM,YAAY,MAAM,gBAAgB,WAAW,QAAQ,QAAQ;AAEnE,QAAM,gBAAgB,MAAM,MAAM,UAAU;AAAA,IAC1C,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,MAAM;AAAA,IACR;AAAA,IACA,MAAM,IAAI,gBAAgB;AAAA,MACxB,YAAY;AAAA,MACZ;AAAA,MACA,cAAc;AAAA,MACd,WAAW;AAAA,MACX,eAAe;AAAA,IACjB,CAAC;AAAA,EACH,CAAC;AAED,MAAI,CAAC,cAAc,IAAI;AACrB,UAAM,YAAY,MAAM,cAAc,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC7D,UAAM,IAAI;AAAA,MACR,0BAA0B,UAAU,qBAAqB,cAAc,UAAU;AAAA,IACnF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,cAAc,KAAK;AAGxC,cAAY,SAAS,MAAM;AAG3B,UAAQ,OAAO,cAAc;AAC7B,UAAQ,OAAO,YAAY;AAC3B,UAAQ,OAAO,aAAa;AAG5B,SAAO,QAAQ,aAAa,CAAC,GAAG,SAAS,OAAO,OAAO,SAAS,QAAQ;AAExE,SAAO;AACT;AAKA,eAAsB,OACpB,SACA,WACA,UAAgC,CAAC,GAClB;AACf,UAAQ,MAAM;AACd,QAAM,cAAc,SAAS;AAE7B,MAAI,QAAQ,WAAW,OAAO;AAC5B,WAAO,SAAS,OAAO;AAAA,EACzB;AACF;;;ACxIA,eAAsB,eACpB,SACA,WACA,YACA,UACA,OACA,YAAqC,CAAC,GACtC,cAAc,OACF;AACZ,QAAM,UAAkC;AAAA,IACtC,gBAAgB;AAAA,EAClB;AAEA,MAAI,aAAa;AACf,UAAM,QAAQ,MAAM,oBAAoB,SAAS,WAAW,QAAQ;AACpE,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,mBAAmB;AAAA,IACrC;AAGA,UAAM,YAAY,MAAM,gBAAgB,WAAW,QAAQ,YAAY,KAAK;AAE5E,YAAQ,eAAe,IAAI,QAAQ,KAAK;AACxC,YAAQ,MAAM,IAAI;AAAA,EACpB;AAEA,QAAM,WAAW,MAAM,MAAM,YAAY;AAAA,IACvC,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK,UAAU,EAAE,OAAO,UAAU,CAAC;AAAA,EAC3C,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,2BAA2B,SAAS,UAAU,EAAE;AAAA,EAClE;AAEA,QAAM,SAA6B,MAAM,SAAS,KAAK;AAEvD,MAAI,OAAO,UAAU,OAAO,OAAO,SAAS,GAAG;AAC7C,UAAM,IAAI,MAAM,kBAAkB,OAAO,OAAO,CAAC,EAAE,OAAO,EAAE;AAAA,EAC9D;AAEA,SAAO,OAAO;AAChB;;;ACpCO,IAAM,mBAAN,MAAuB;AAAA,EAY5B,YAAY,SAAkC;AAJ9C,SAAQ,cAAc;AACtB,SAAQ,YAAoB;AAC5B,SAAQ,UAA0B;AAGhC,SAAK,SAAS,QAAQ,OAAO,QAAQ,OAAO,EAAE;AAC9C,SAAK,WAAW,QAAQ;AACxB,SAAK,cAAc,QAAQ;AAC3B,SAAK,QAAQ,QAAQ;AAErB,SAAK,aAAa,GAAG,KAAK,MAAM;AAChC,SAAK,eAAe,GAAG,KAAK,MAAM;AAClC,SAAK,WAAW,GAAG,KAAK,MAAM;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,QAAI,KAAK,YAAa;AAGtB,SAAK,YAAY,MAAM,sBAAsB,KAAK,QAAQ;AAG1D,UAAM,OAAO,kBAAkB,KAAK,SAAS;AAC7C,SAAK,UAAU,cAAc,IAAI;AAGjC,UAAM,mBAAmB,KAAK,SAAS;AAEvC,SAAK,cAAc;AAAA,EACrB;AAAA,EAEQ,aAAsB;AAC5B,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC9D;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,kBAAkB,UAAwB,CAAC,GAAkB;AACjE,UAAM,KAAK,KAAK;AAChB,UAAM,cAAc,KAAK,WAAW,GAAG,KAAK,cAAc,KAAK,UAAU;AAAA,MACvE,GAAG;AAAA,MACH,aAAa,QAAQ,eAAe,KAAK;AAAA,MACzC,OAAO,QAAQ,SAAS,KAAK;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,yBAA2C;AAC/C,UAAM,KAAK,KAAK;AAChB,WAAO,MAAM,oBAAoB,KAAK,WAAW,GAAG,KAAK,WAAW,KAAK,QAAQ;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAO,UAAgC,CAAC,GAAkB;AAC9D,UAAM,KAAK,KAAK;AAChB,UAAM,OAAS,KAAK,WAAW,GAAG,KAAK,WAAW,OAAO;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,kBAAoC;AACxC,UAAM,KAAK,KAAK;AAChB,WAAO,gBAAgB,KAAK,WAAW,CAAC;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAgC;AACpC,UAAM,KAAK,KAAK;AAChB,QAAI,CAAC,gBAAgB,KAAK,WAAW,CAAC,GAAG;AACvC,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,KAAK,WAAW,EAAE,IAAI,SAAS;AAC3C,QAAI,CAAC,KAAK;AACR,aAAO;AAAA,IACT;AAEA,WAAO,EAAE,IAAI;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAkC;AACtC,UAAM,KAAK,KAAK;AAChB,WAAO,MAAM,oBAAoB,KAAK,WAAW,GAAG,KAAK,WAAW,KAAK,QAAQ;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MACJ,OACA,YAAqC,CAAC,GAC1B;AACZ,UAAM,KAAK,KAAK;AAChB,WAAO,MAAM;AAAA,MACX,KAAK,WAAW;AAAA,MAChB,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OACJ,UACA,YAAqC,CAAC,GAC1B;AACZ,WAAO,KAAK,MAAS,UAAU,SAAS;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YACJ,OACA,YAAqC,CAAC,GAC1B;AACZ,UAAM,KAAK,KAAK;AAChB,WAAO,MAAM;AAAA,MACX,KAAK,WAAW;AAAA,MAChB,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;;;AC/KO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACzC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,qBAAN,cAAiC,gBAAgB;AAAA,EACtD,YAAY,UAAU,kBAAkB;AACtC,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,eAAN,cAA2B,gBAAgB;AAAA,EAChD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,aAAN,cAAyB,gBAAgB;AAAA,EAI9C,YAAY,MAAc,aAAsB;AAC9C,UAAM,gBAAgB,IAAI,GAAG,cAAc,MAAM,WAAW,KAAK,EAAE,EAAE;AACrE,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,cAAc;AAAA,EACrB;AACF;;;AC9BA,eAAsB,uBACpB,SAC2B;AAC3B,QAAM,SAAS,IAAI,iBAAiB,OAAO;AAC3C,QAAM,OAAO,KAAK;AAClB,SAAO;AACT;", 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 signal?: AbortSignal\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 signal,\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 interface QueryOptions {\n signal?: AbortSignal;\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 options: QueryOptions = {}\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 options.signal\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 options: QueryOptions = {}\n ): Promise<T> {\n return this.query<T>(mutation, variables, options);\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 options: QueryOptions = {}\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 options.signal\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, QueryOptions, 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 }
+11 -8
quickslice-client-js/dist/quickslice-client.js
··· 442 442 } 443 443 444 444 // src/graphql.ts 445 - async function graphqlRequest(storage, namespace, graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false) { 445 + async function graphqlRequest(storage, namespace, graphqlUrl, tokenUrl, query, variables = {}, requireAuth = false, signal) { 446 446 const headers = { 447 447 "Content-Type": "application/json" 448 448 }; ··· 458 458 const response = await fetch(graphqlUrl, { 459 459 method: "POST", 460 460 headers, 461 - body: JSON.stringify({ query, variables }) 461 + body: JSON.stringify({ query, variables }), 462 + signal 462 463 }); 463 464 if (!response.ok) { 464 465 throw new Error(`GraphQL request failed: ${response.statusText}`); ··· 559 560 /** 560 561 * Execute a GraphQL query (authenticated) 561 562 */ 562 - async query(query, variables = {}) { 563 + async query(query, variables = {}, options = {}) { 563 564 await this.init(); 564 565 return await graphqlRequest( 565 566 this.getStorage(), ··· 568 569 this.tokenUrl, 569 570 query, 570 571 variables, 571 - true 572 + true, 573 + options.signal 572 574 ); 573 575 } 574 576 /** 575 577 * Execute a GraphQL mutation (authenticated) 576 578 */ 577 - async mutate(mutation, variables = {}) { 578 - return this.query(mutation, variables); 579 + async mutate(mutation, variables = {}, options = {}) { 580 + return this.query(mutation, variables, options); 579 581 } 580 582 /** 581 583 * Execute a public GraphQL query (no auth) 582 584 */ 583 - async publicQuery(query, variables = {}) { 585 + async publicQuery(query, variables = {}, options = {}) { 584 586 await this.init(); 585 587 return await graphqlRequest( 586 588 this.getStorage(), ··· 589 591 this.tokenUrl, 590 592 query, 591 593 variables, 592 - false 594 + false, 595 + options.signal 593 596 ); 594 597 } 595 598 };
+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 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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", 4 + "sourcesContent": ["export { QuicksliceClient, QuicksliceClientOptions, QueryOptions, 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 signal?: AbortSignal\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 signal,\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 interface QueryOptions {\n signal?: AbortSignal;\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 options: QueryOptions = {}\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 options.signal\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 options: QueryOptions = {}\n ): Promise<T> {\n return this.query<T>(mutation, variables, options);\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 options: QueryOptions = {}\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 options.signal\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 _=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);})(); 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:()=>w,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 d(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 y(t){let e=new Uint8Array(t);return crypto.getRandomValues(e),d(e)}async function O(t){let e=new TextEncoder,r=await crypto.subtle.digest("SHA-256",e.encode(t));return d(r)}async function I(t){let e=new TextEncoder,r=await crypto.subtle.digest("SHA-256",e.encode(t));return Array.from(new Uint8Array(r)).map(s=>s.toString(16).padStart(2,"0")).join("").substring(0,8)}async function C(t,e,r){let o=new TextEncoder,i=d(o.encode(JSON.stringify(t))),s=d(o.encode(JSON.stringify(e))),n=`${i}.${s}`,a=await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},r,o.encode(n)),c=d(a);return`${n}.${c}`}var re=1,p="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 s=indexedDB.open(oe(t),re);s.onerror=()=>i(s.error),s.onsuccess=()=>o(s.result),s.onupgradeneeded=n=>{let a=n.target.result;a.objectStoreNames.contains(p)||a.createObjectStore(p,{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(p,"readonly").objectStore(p).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,s)=>{let c=o.transaction(p,"readwrite").objectStore(p).put({id:T,privateKey:e,publicJwk:r,createdAt:Date.now()});c.onerror=()=>s(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 m(t,e,r,o=null){let i=await D(t),{kty:s,crv:n,x:a,y:c}=i.publicJwk,l={alg:"ES256",typ:"dpop+jwt",jwk:{kty:s,crv:n,x:a,y:c}},u={jti:y(16),htm:e,htu:r,iat:Math.floor(Date.now()/1e3)};return o&&(u.ath=await O(o)),await C(l,u,i.privateKey)}async function R(t){let e=await b(t);return new Promise((r,o)=>{let n=e.transaction(p,"readwrite").objectStore(p).clear();n.onerror=()=>o(n.error),n.onsuccess=()=>r()})}function q(){return y(32)}async function L(t){let r=new TextEncoder().encode(t),o=await crypto.subtle.digest("SHA-256",r);return d(o)}function V(){return y(16)}function Q(t){return new Promise(e=>setTimeout(e,t))}function j(t,e){return`quickslice_${t}_lock_${e}`}async function B(t,e,r=5e3){let o=j(t,e),i=`${Date.now()}_${Math.random()}`,s=Date.now()+r;for(;Date.now()<s;){let n=localStorage.getItem(o);if(n){let[a]=n.split("_");if(Date.now()-parseInt(a)>5e3)localStorage.removeItem(o);else{await Q(50);continue}}if(localStorage.setItem(o,i),await Q(10),localStorage.getItem(o)===i)return i}return null}function J(t,e,r){let o=j(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 s=await m(e,"POST",r),n=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",DPoP:s},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 a=await n.json();t.set("accessToken",a.access_token),a.refresh_token&&t.set("refreshToken",a.refresh_token);let c=Date.now()+a.expires_in*1e3;return t.set("tokenExpiresAt",c.toString()),a.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 s="token_refresh",n=await B(e,s);if(!n){await se(100);let a=t.get("accessToken"),c=parseInt(t.get("tokenExpiresAt")||"0");if(a&&Date.now()<c-v)return a;throw new Error("Failed to refresh token")}try{let a=t.get("accessToken"),c=parseInt(t.get("tokenExpiresAt")||"0");return a&&Date.now()<c-v?a:await ae(t,e,r)}finally{J(e,s,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(),s=await L(i),n=V(),a=o.redirectUri||window.location.origin+window.location.pathname;t.set("codeVerifier",i),t.set("oauthState",n),t.set("clientId",r),t.set("redirectUri",a);let c=new URLSearchParams({client_id:r,redirect_uri:a,response_type:"code",code_challenge:s,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"),s=o.get("state"),n=o.get("error");if(n)throw new Error(`OAuth error: ${n} - ${o.get("error_description")||""}`);if(!i||!s)return!1;let a=t.get("oauthState");if(s!==a)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 u=await m(e,"POST",r),f=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",DPoP:u},body:new URLSearchParams({grant_type:"authorization_code",code:i,redirect_uri:l,client_id:g,code_verifier:c})});if(!f.ok){let G=await f.json().catch(()=>({}));throw new Error(`Token exchange failed: ${G.error_description||f.statusText}`)}let F=await f.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,s={},n=!1,a){let c={"Content-Type":"application/json"};if(n){let u=await k(t,e,o);if(!u)throw new Error("Not authenticated");let f=await m(e,"POST",r,u);c.Authorization=`DPoP ${u}`,c.DPoP=f}let g=await fetch(r,{method:"POST",headers:c,body:JSON.stringify({query:i,variables:s}),signal:a});if(!g.ok)throw new Error(`GraphQL request failed: ${g.statusText}`);let l=await g.json();if(l.errors&&l.errors.length>0)throw new Error(`GraphQL error: ${l.errors[0].message}`);return l.data}var w=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 I(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={},o={}){return await this.init(),await E(this.getStorage(),this.namespace,this.graphqlUrl,this.tokenUrl,e,r,!0,o.signal)}async mutate(e,r={},o={}){return this.query(e,r,o)}async publicQuery(e,r={},o={}){return await this.init(),await E(this.getStorage(),this.namespace,this.graphqlUrl,this.tokenUrl,e,r,!1,o.signal)}};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 w(t);return await e.init(),e}return te(ge);})();
+15 -6
quickslice-client-js/src/client.ts
··· 17 17 did: string; 18 18 } 19 19 20 + export interface QueryOptions { 21 + signal?: AbortSignal; 22 + } 23 + 20 24 export class QuicksliceClient { 21 25 private server: string; 22 26 private clientId: string; ··· 134 138 */ 135 139 async query<T = unknown>( 136 140 query: string, 137 - variables: Record<string, unknown> = {} 141 + variables: Record<string, unknown> = {}, 142 + options: QueryOptions = {} 138 143 ): Promise<T> { 139 144 await this.init(); 140 145 return await graphqlRequest<T>( ··· 144 149 this.tokenUrl, 145 150 query, 146 151 variables, 147 - true 152 + true, 153 + options.signal 148 154 ); 149 155 } 150 156 ··· 153 159 */ 154 160 async mutate<T = unknown>( 155 161 mutation: string, 156 - variables: Record<string, unknown> = {} 162 + variables: Record<string, unknown> = {}, 163 + options: QueryOptions = {} 157 164 ): Promise<T> { 158 - return this.query<T>(mutation, variables); 165 + return this.query<T>(mutation, variables, options); 159 166 } 160 167 161 168 /** ··· 163 170 */ 164 171 async publicQuery<T = unknown>( 165 172 query: string, 166 - variables: Record<string, unknown> = {} 173 + variables: Record<string, unknown> = {}, 174 + options: QueryOptions = {} 167 175 ): Promise<T> { 168 176 await this.init(); 169 177 return await graphqlRequest<T>( ··· 173 181 this.tokenUrl, 174 182 query, 175 183 variables, 176 - false 184 + false, 185 + options.signal 177 186 ); 178 187 } 179 188 }
+3 -1
quickslice-client-js/src/graphql.ts
··· 17 17 tokenUrl: string, 18 18 query: string, 19 19 variables: Record<string, unknown> = {}, 20 - requireAuth = false 20 + requireAuth = false, 21 + signal?: AbortSignal 21 22 ): Promise<T> { 22 23 const headers: Record<string, string> = { 23 24 'Content-Type': 'application/json', ··· 40 41 method: 'POST', 41 42 headers, 42 43 body: JSON.stringify({ query, variables }), 44 + signal, 43 45 }); 44 46 45 47 if (!response.ok) {
+1 -1
quickslice-client-js/src/index.ts
··· 1 - export { QuicksliceClient, QuicksliceClientOptions, User } from './client'; 1 + export { QuicksliceClient, QuicksliceClientOptions, QueryOptions, User } from './client'; 2 2 export { 3 3 QuicksliceError, 4 4 LoginRequiredError,
+6 -1
server/src/database/queries/aggregates.gleam
··· 74 74 True -> #(mut_where_parts, mut_bind_values) 75 75 False -> { 76 76 let #(where_sql, where_params) = 77 - where_clause.build_where_sql(exec, wc, True) 77 + where_clause.build_where_sql( 78 + exec, 79 + wc, 80 + True, 81 + list.length(mut_bind_values) + 1, 82 + ) 78 83 let new_where = list.append(mut_where_parts, [where_sql]) 79 84 let new_binds = list.append(mut_bind_values, where_params) 80 85 #(new_where, new_binds)
+49 -30
server/src/database/queries/pagination.gleam
··· 212 212 decoded_cursor: DecodedCursor, 213 213 sort_by: Option(List(#(String, String))), 214 214 is_before: Bool, 215 + start_index: Int, 215 216 ) -> #(String, List(String)) { 216 217 let sort_fields = case sort_by { 217 218 None -> [] ··· 228 229 decoded_cursor.field_values, 229 230 decoded_cursor.cid, 230 231 is_before, 232 + start_index, 231 233 ) 232 234 233 235 let sql = "(" <> string.join(clauses.0, " OR ") <> ")" ··· 243 245 field_values: List(String), 244 246 cid: String, 245 247 is_before: Bool, 248 + start_index: Int, 246 249 ) -> #(List(String), List(String)) { 247 - let #(clauses, params) = 248 - list.index_map(sort_fields, fn(field, i) { 249 - let #(equality_parts, equality_params) = case i { 250 - 0 -> #([], []) 251 - _ -> { 252 - list.range(0, i - 1) 253 - |> list.fold(#([], []), fn(eq_acc, j) { 254 - let #(eq_parts, eq_params) = eq_acc 255 - let prior_field = 256 - list_at(sort_fields, j) |> result.unwrap(#("", "")) 257 - let value = list_at(field_values, j) |> result.unwrap("") 250 + // Build clauses with tracked parameter index 251 + let #(clauses, params, next_index) = 252 + list.index_fold(sort_fields, #([], [], start_index), fn(acc, field, i) { 253 + let #(acc_clauses, acc_params, param_index) = acc 258 254 259 - let field_ref = build_cursor_field_reference(exec, prior_field.0) 260 - let new_part = field_ref <> " = ?" 261 - let new_params = list.append(eq_params, [value]) 262 - 263 - #(list.append(eq_parts, [new_part]), new_params) 264 - }) 255 + // Build equality parts for prior fields 256 + let #(equality_parts, equality_params, idx_after_eq) = case i { 257 + 0 -> #([], [], param_index) 258 + _ -> { 259 + list.index_fold( 260 + list.take(sort_fields, i), 261 + #([], [], param_index), 262 + fn(eq_acc, prior_field, j) { 263 + let #(eq_parts, eq_params, eq_idx) = eq_acc 264 + let value = list_at(field_values, j) |> result.unwrap("") 265 + let field_ref = build_cursor_field_reference(exec, prior_field.0) 266 + let placeholder = executor.placeholder(exec, eq_idx) 267 + let new_part = field_ref <> " = " <> placeholder 268 + #( 269 + list.append(eq_parts, [new_part]), 270 + list.append(eq_params, [value]), 271 + eq_idx + 1, 272 + ) 273 + }, 274 + ) 265 275 } 266 276 } 267 277 268 278 let value = list_at(field_values, i) |> result.unwrap("") 269 - 270 279 let comparison_op = get_comparison_operator(field.1, is_before) 271 280 let field_ref = build_cursor_field_reference(exec, field.0) 281 + let placeholder = executor.placeholder(exec, idx_after_eq) 272 282 273 - let comparison_part = field_ref <> " " <> comparison_op <> " ?" 283 + let comparison_part = 284 + field_ref <> " " <> comparison_op <> " " <> placeholder 274 285 let all_parts = list.append(equality_parts, [comparison_part]) 275 286 let all_params = list.append(equality_params, [value]) 276 287 277 288 let clause = "(" <> string.join(all_parts, " AND ") <> ")" 278 289 279 - #(clause, all_params) 290 + #( 291 + list.append(acc_clauses, [clause]), 292 + list.append(acc_params, all_params), 293 + idx_after_eq + 1, 294 + ) 280 295 }) 281 - |> list.unzip 282 - |> fn(unzipped) { 283 - let flattened_params = list.flatten(unzipped.1) 284 - #(unzipped.0, flattened_params) 285 - } 286 296 287 - let #(final_equality_parts, final_equality_params) = 288 - list.index_map(sort_fields, fn(field, j) { 297 + // Build final clause with all fields equal and CID comparison 298 + let #(final_equality_parts, final_equality_params, idx_after_final_eq) = 299 + list.index_fold(sort_fields, #([], [], next_index), fn(acc, field, j) { 300 + let #(parts, params, idx) = acc 289 301 let value = list_at(field_values, j) |> result.unwrap("") 290 302 let field_ref = build_cursor_field_reference(exec, field.0) 291 - #(field_ref <> " = ?", value) 303 + let placeholder = executor.placeholder(exec, idx) 304 + #( 305 + list.append(parts, [field_ref <> " = " <> placeholder]), 306 + list.append(params, [value]), 307 + idx + 1, 308 + ) 292 309 }) 293 - |> list.unzip 294 310 295 311 let last_field = list.last(sort_fields) |> result.unwrap(#("", "desc")) 296 312 let cid_comparison_op = get_comparison_operator(last_field.1, is_before) 313 + let cid_placeholder = executor.placeholder(exec, idx_after_final_eq) 297 314 298 315 let final_parts = 299 - list.append(final_equality_parts, ["cid " <> cid_comparison_op <> " ?"]) 316 + list.append(final_equality_parts, [ 317 + "cid " <> cid_comparison_op <> " " <> cid_placeholder, 318 + ]) 300 319 let final_params = list.append(final_equality_params, [cid]) 301 320 302 321 let final_clause = "(" <> string.join(final_parts, " AND ") <> ")"
+3 -1
server/src/database/queries/where_clause.gleam
··· 364 364 /// Builds WHERE clause SQL from a WhereClause 365 365 /// Returns tuple of (sql_string, parameters) 366 366 /// use_table_prefix: if True, prefixes table columns with "record." for joins 367 + /// start_index: the starting parameter index (1-based) for placeholders 367 368 pub fn build_where_sql( 368 369 exec: Executor, 369 370 clause: WhereClause, 370 371 use_table_prefix: Bool, 372 + start_index: Int, 371 373 ) -> #(String, List(Value)) { 372 374 case is_clause_empty(clause) { 373 375 True -> #("", []) 374 376 False -> { 375 377 let #(sql_parts, params, _) = 376 - build_where_clause_internal(exec, clause, use_table_prefix, 1) 378 + build_where_clause_internal(exec, clause, use_table_prefix, start_index) 377 379 let sql = string.join(sql_parts, " AND ") 378 380 #(sql, params) 379 381 }
+32 -4
server/src/database/repositories/records.gleam
··· 472 472 case where_clause.is_clause_empty(wc) { 473 473 True -> #(mut_where_parts, mut_bind_values) 474 474 False -> { 475 + // Start index is 2 because $1 is used for collection 475 476 let #(where_sql, where_params) = 476 - where_clause.build_where_sql(exec, wc, needs_actor_join) 477 + where_clause.build_where_sql( 478 + exec, 479 + wc, 480 + needs_actor_join, 481 + list.length(mut_bind_values) + 1, 482 + ) 477 483 let new_where = list.append(mut_where_parts, [where_sql]) 478 484 let new_binds = list.append(mut_bind_values, where_params) 479 485 #(new_where, new_binds) ··· 552 558 decoded_cursor, 553 559 sort_by, 554 560 !is_forward, 561 + list.length(bind_values) + 1, 555 562 ) 556 563 557 564 let new_where = list.append(where_parts, [cursor_where]) ··· 683 690 case where_clause.is_clause_empty(wc) { 684 691 True -> #(mut_where_parts, mut_bind_values) 685 692 False -> { 693 + // Start index accounts for prior bind values ($1 = collection) 686 694 let #(where_sql, where_params) = 687 - where_clause.build_where_sql(exec, wc, needs_actor_join) 695 + where_clause.build_where_sql( 696 + exec, 697 + wc, 698 + needs_actor_join, 699 + list.length(mut_bind_values) + 1, 700 + ) 688 701 let new_where = list.append(mut_where_parts, [where_sql]) 689 702 let new_binds = list.append(mut_bind_values, where_params) 690 703 #(new_where, new_binds) ··· 705 718 decoded_cursor, 706 719 sort_by, 707 720 !is_forward, 721 + list.length(bind_values) + 1, 708 722 ) 709 723 710 724 let new_where = list.append(where_parts, [cursor_where]) ··· 922 936 // Add where clause conditions if present 923 937 let #(with_where_parts, with_where_values) = case wc { 924 938 Some(clause) -> { 939 + // Start index accounts for prior bind values ($1 = collection, $2 = parent_uri, $3 = parent_uri) 925 940 let #(where_sql, where_params) = 926 - where_clause.build_where_sql(exec, clause, False) 941 + where_clause.build_where_sql( 942 + exec, 943 + clause, 944 + False, 945 + list.length(base_bind_values) + 1, 946 + ) 927 947 case where_sql { 928 948 "" -> #(base_where_parts, base_bind_values) 929 949 _ -> #( ··· 946 966 decoded_cursor, 947 967 sort_by, 948 968 !is_forward, 969 + list.length(with_where_values) + 1, 949 970 ) 950 971 951 972 let new_where = list.append(with_where_parts, [cursor_where]) ··· 1278 1299 // Add where clause conditions if present 1279 1300 let #(with_where_parts, with_where_values) = case wc { 1280 1301 Some(clause) -> { 1302 + // Start index accounts for prior bind values ($1 = did, $2 = collection) 1281 1303 let #(where_sql, where_params) = 1282 - where_clause.build_where_sql(exec, clause, False) 1304 + where_clause.build_where_sql( 1305 + exec, 1306 + clause, 1307 + False, 1308 + list.length(base_bind_values) + 1, 1309 + ) 1283 1310 case where_sql { 1284 1311 "" -> #(base_where_parts, base_bind_values) 1285 1312 _ -> #( ··· 1302 1329 decoded_cursor, 1303 1330 sort_by, 1304 1331 !is_forward, 1332 + list.length(with_where_values) + 1, 1305 1333 ) 1306 1334 1307 1335 let new_where = list.append(with_where_parts, [cursor_where])
+6 -6
server/test/pagination_test.gleam
··· 280 280 let sort_by = Some([#("indexed_at", "desc")]) 281 281 282 282 let #(sql, params) = 283 - pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 283 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 284 284 285 285 // For DESC: indexed_at < cursor_value OR (indexed_at = cursor_value AND cid < cursor_cid) 286 286 sql ··· 306 306 let sort_by = Some([#("indexed_at", "asc")]) 307 307 308 308 let #(sql, params) = 309 - pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 309 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 310 310 311 311 // For ASC: indexed_at > cursor_value OR (indexed_at = cursor_value AND cid > cursor_cid) 312 312 sql ··· 329 329 let sort_by = Some([#("text", "desc")]) 330 330 331 331 let #(sql, params) = 332 - pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 332 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 333 333 334 334 // JSON fields use json_extract 335 335 sql ··· 350 350 let sort_by = Some([#("author.name", "asc")]) 351 351 352 352 let #(sql, params) = 353 - pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 353 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 354 354 355 355 // Nested JSON fields use $.path.to.field 356 356 sql ··· 374 374 let sort_by = Some([#("text", "desc"), #("createdAt", "desc")]) 375 375 376 376 let #(sql, params) = 377 - pagination.build_cursor_where_clause(exec, decoded, sort_by, False) 377 + pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 378 378 379 379 // Multi-field: progressive equality checks 380 380 // (text < ?) OR (text = ? AND createdAt < ?) OR (text = ? AND createdAt = ? AND cid < ?) ··· 407 407 408 408 // is_before = True reverses the comparison operators 409 409 let #(sql, params) = 410 - pagination.build_cursor_where_clause(exec, decoded, sort_by, True) 410 + pagination.build_cursor_where_clause(exec, decoded, sort_by, True, 1) 411 411 412 412 // For before with DESC: indexed_at > cursor_value OR (indexed_at = cursor_value AND cid > cursor_cid) 413 413 sql
+18 -18
server/test/where_edge_cases_test.gleam
··· 24 24 let clause = 25 25 where_clause.WhereClause(conditions: dict.new(), and: None, or: None) 26 26 27 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 27 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 28 28 29 29 sql |> should.equal("") 30 30 list.length(params) |> should.equal(0) ··· 51 51 or: None, 52 52 ) 53 53 54 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 54 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 55 55 56 56 // Should produce no SQL since all conditions are None 57 57 sql |> should.equal("") ··· 79 79 or: None, 80 80 ) 81 81 82 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 82 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 83 83 84 84 // Empty IN list should produce no SQL 85 85 sql |> should.equal("") ··· 91 91 let clause = 92 92 where_clause.WhereClause(conditions: dict.new(), and: Some([]), or: None) 93 93 94 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 94 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 95 95 96 96 sql |> should.equal("") 97 97 list.length(params) |> should.equal(0) ··· 102 102 let clause = 103 103 where_clause.WhereClause(conditions: dict.new(), and: None, or: Some([])) 104 104 105 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 105 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 106 106 107 107 sql |> should.equal("") 108 108 list.length(params) |> should.equal(0) ··· 141 141 or: None, 142 142 ) 143 143 144 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 144 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 145 145 146 146 // Should use parameterized query with json_extract for non-table columns 147 147 sql |> should.equal("json_extract(json, '$.username') = ?") ··· 178 178 or: None, 179 179 ) 180 180 181 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 181 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 182 182 183 183 // Should use LIKE with parameterized value and json_extract for non-table fields 184 184 sql ··· 237 237 ) 238 238 239 239 let #(sql_large, params_large) = 240 - where_clause.build_where_sql(exec, clause_large, False) 240 + where_clause.build_where_sql(exec, clause_large, False, 1) 241 241 let #(sql_small, params_small) = 242 - where_clause.build_where_sql(exec, clause_small, False) 242 + where_clause.build_where_sql(exec, clause_small, False, 1) 243 243 244 244 // count is a JSON field, not a table column 245 245 sql_large |> should.equal("json_extract(json, '$.count') = ?") ··· 270 270 or: None, 271 271 ) 272 272 273 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 273 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 274 274 275 275 // Empty string is still valid - should use json_extract for non-table columns 276 276 sql |> should.equal("json_extract(json, '$.name') = ?") ··· 308 308 or: None, 309 309 ) 310 310 311 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 311 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 312 312 313 313 sql |> should.equal("json_extract(json, '$.text') = ?") 314 314 list.length(params) |> should.equal(1) ··· 364 364 or: None, 365 365 ) 366 366 367 - let #(sql, params) = where_clause.build_where_sql(exec, level5, False) 367 + let #(sql, params) = where_clause.build_where_sql(exec, level5, False, 1) 368 368 369 369 // With single condition at each level, no extra parentheses needed 370 370 // The implementation correctly doesn't add unnecessary parentheses for single conditions ··· 425 425 or: None, 426 426 ) 427 427 428 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 428 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 429 429 430 430 // Should only include non-empty conditions 431 431 list.length(params) |> should.equal(2) ··· 570 570 or: None, 571 571 ) 572 572 573 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 573 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 574 574 575 575 // Should combine both operators with json_extract and CAST for numeric comparison 576 576 sql ··· 602 602 or: None, 603 603 ) 604 604 605 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 605 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 606 606 607 607 // Should apply both (though logically this might not make sense) 608 608 list.length(params) |> should.equal(3) ··· 642 642 or: None, 643 643 ) 644 644 645 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 645 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 646 646 647 647 // Should generate correct number of placeholders 648 648 list.length(params) |> should.equal(100) ··· 683 683 or: None, 684 684 ) 685 685 686 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 686 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 687 687 688 688 // Should use json_extract for dotted field names 689 689 sql ··· 721 721 or: None, 722 722 ) 723 723 724 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 724 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 725 725 726 726 // Should use json_extract even for non-dotted field names 727 727 let expected = "json_extract(json, '$." <> field_name <> "') = ?"
+33 -33
server/test/where_sql_builder_test.gleam
··· 21 21 pub fn build_where_empty_clause_test() { 22 22 let exec = get_test_exec() 23 23 let clause = where_clause.empty_clause() 24 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 24 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 25 25 26 26 sql |> should.equal("") 27 27 list.length(params) |> should.equal(0) ··· 49 49 or: None, 50 50 ) 51 51 52 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 52 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 53 53 54 54 sql |> should.equal("collection = ?") 55 55 list.length(params) |> should.equal(1) ··· 81 81 or: None, 82 82 ) 83 83 84 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 84 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 85 85 86 86 sql |> should.equal("did IN (?, ?, ?)") 87 87 list.length(params) |> should.equal(3) ··· 109 109 or: None, 110 110 ) 111 111 112 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 112 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 113 113 114 114 sql |> should.equal("indexed_at > ?") 115 115 list.length(params) |> should.equal(1) ··· 137 137 or: None, 138 138 ) 139 139 140 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 140 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 141 141 142 142 // Now includes CAST for numeric comparisons on JSON fields 143 143 sql |> should.equal("CAST(json_extract(json, '$.year') AS INTEGER) >= ?") ··· 166 166 or: None, 167 167 ) 168 168 169 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 169 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 170 170 171 171 sql |> should.equal("indexed_at < ?") 172 172 list.length(params) |> should.equal(1) ··· 194 194 or: None, 195 195 ) 196 196 197 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 197 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 198 198 199 199 // Now includes CAST for numeric comparisons on JSON fields 200 200 sql |> should.equal("CAST(json_extract(json, '$.count') AS INTEGER) <= ?") ··· 223 223 or: None, 224 224 ) 225 225 226 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 226 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 227 227 228 228 // Should combine both conditions with AND 229 229 sql |> should.equal("indexed_at > ? AND indexed_at < ?") ··· 267 267 or: None, 268 268 ) 269 269 270 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 270 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 271 271 272 272 // Order might vary due to dict, but should have AND 273 273 should.be_true(string.contains(sql, "AND")) ··· 300 300 or: None, 301 301 ) 302 302 303 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 303 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 304 304 305 305 sql |> should.equal("json_extract(json, '$.text') = ?") 306 306 list.length(params) |> should.equal(1) ··· 328 328 or: None, 329 329 ) 330 330 331 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 331 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 332 332 333 333 sql |> should.equal("json_extract(json, '$.user.name') = ?") 334 334 list.length(params) |> should.equal(1) ··· 356 356 or: None, 357 357 ) 358 358 359 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 359 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 360 360 361 361 sql |> should.equal("json_extract(json, '$.metadata.tags.0') = ?") 362 362 list.length(params) |> should.equal(1) ··· 384 384 or: None, 385 385 ) 386 386 387 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 387 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 388 388 389 389 // Now includes CAST for numeric comparisons on JSON fields 390 390 sql ··· 431 431 or: None, 432 432 ) 433 433 434 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 434 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 435 435 436 436 // Should have both table column and JSON extract with CAST for numeric comparison 437 437 should.be_true(string.contains(sql, "collection = ?")) ··· 467 467 or: None, 468 468 ) 469 469 470 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 470 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 471 471 472 472 sql 473 473 |> should.equal( ··· 498 498 or: None, 499 499 ) 500 500 501 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 501 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 502 502 503 503 sql |> should.equal("uri LIKE '%' || ? || '%' COLLATE NOCASE") 504 504 list.length(params) |> should.equal(1) ··· 526 526 or: None, 527 527 ) 528 528 529 - let #(sql, _params) = where_clause.build_where_sql(exec, clause, False) 529 + let #(sql, _params) = where_clause.build_where_sql(exec, clause, False, 1) 530 530 531 531 // SQL should be generated (actual escaping would be handled by the parameter binding) 532 532 should.be_true(string.contains(sql, "LIKE")) ··· 570 570 or: None, 571 571 ) 572 572 573 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 573 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 574 574 575 575 // Should have both LIKE clauses 576 576 should.be_true(string.contains(sql, "LIKE")) ··· 600 600 or: None, 601 601 ) 602 602 603 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 603 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 604 604 605 605 // Should have both LIKE and > operator 606 606 should.be_true(string.contains(sql, "LIKE")) ··· 665 665 or: None, 666 666 ) 667 667 668 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 668 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 669 669 670 670 // Should have both conditions AND'ed together with parentheses 671 671 should.be_true(string.contains(sql, "collection = ?")) ··· 721 721 or: None, 722 722 ) 723 723 724 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 724 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 725 725 726 726 // Should have both root condition and nested condition 727 727 should.be_true(string.contains(sql, "collection = ?")) ··· 785 785 or: None, 786 786 ) 787 787 788 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 788 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 789 789 790 790 // Should have both conditions 791 791 should.be_true(string.contains(sql, "artist")) ··· 865 865 or: None, 866 866 ) 867 867 868 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 868 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 869 869 870 870 // Should have all three conditions 871 871 should.be_true(string.contains(sql, "collection = ?")) ··· 931 931 or: Some([clause1, clause2]), 932 932 ) 933 933 934 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 934 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 935 935 936 936 // Should have both conditions OR'ed together 937 937 should.be_true(string.contains(sql, "artist")) ··· 1026 1026 or: None, 1027 1027 ) 1028 1028 1029 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 1029 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 1030 1030 1031 1031 // Should have proper precedence: (artist LIKE OR genre =) AND year >= 1032 1032 should.be_true(string.contains(sql, "OR")) ··· 1172 1172 or: None, 1173 1173 ) 1174 1174 1175 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 1175 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 1176 1176 1177 1177 // Should have both OR and AND with proper nesting 1178 1178 should.be_true(string.contains(sql, "OR")) ··· 1260 1260 or: Some([clause1, clause2, clause3]), 1261 1261 ) 1262 1262 1263 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 1263 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 1264 1264 1265 1265 // Should have all three OR'ed together 1266 1266 should.be_true(string.contains(sql, "OR")) ··· 1292 1292 or: None, 1293 1293 ) 1294 1294 1295 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 1295 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 1296 1296 1297 1297 sql |> should.equal("json_extract(json, '$.replyParent') IS NULL") 1298 1298 list.length(params) |> should.equal(0) ··· 1320 1320 or: None, 1321 1321 ) 1322 1322 1323 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 1323 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 1324 1324 1325 1325 sql |> should.equal("json_extract(json, '$.replyParent') IS NOT NULL") 1326 1326 list.length(params) |> should.equal(0) ··· 1348 1348 or: None, 1349 1349 ) 1350 1350 1351 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 1351 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 1352 1352 1353 1353 sql |> should.equal("cid IS NULL") 1354 1354 list.length(params) |> should.equal(0) ··· 1376 1376 or: None, 1377 1377 ) 1378 1378 1379 - let #(sql, params) = where_clause.build_where_sql(exec, clause, False) 1379 + let #(sql, params) = where_clause.build_where_sql(exec, clause, False, 1) 1380 1380 1381 1381 sql |> should.equal("uri IS NOT NULL") 1382 1382 list.length(params) |> should.equal(0) ··· 1404 1404 or: None, 1405 1405 ) 1406 1406 1407 - let #(sql, params) = where_clause.build_where_sql(exec, clause, True) 1407 + let #(sql, params) = where_clause.build_where_sql(exec, clause, True, 1) 1408 1408 1409 1409 sql |> should.equal("json_extract(record.json, '$.text') IS NULL") 1410 1410 list.length(params) |> should.equal(0) ··· 1464 1464 or: None, 1465 1465 ) 1466 1466 1467 - let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False) 1467 + let #(sql, params) = where_clause.build_where_sql(exec, root_clause, False, 1) 1468 1468 1469 1469 should.be_true(string.contains(sql, "IS NULL")) 1470 1470 should.be_true(string.contains(sql, "LIKE"))
+3
www/src/www/config.gleam
··· 47 47 #("guides/queries.md", "/guides/queries", "Queries"), 48 48 #("guides/joins.md", "/guides/joins", "Joins"), 49 49 #("guides/mutations.md", "/guides/mutations", "Mutations"), 50 + #("guides/moderation.md", "/guides/moderation", "Moderation"), 51 + #("guides/notifications.md", "/guides/notifications", "Notifications"), 52 + #("guides/viewer-state.md", "/guides/viewer-state", "Viewer State"), 50 53 #( 51 54 "guides/authentication.md", 52 55 "/guides/authentication",
+1 -1
www/src/www/layout.gleam
··· 222 222 ]) 223 223 } 224 224 225 - const version = "v0.17.5" 225 + const version = "v0.20.0" 226 226 227 227 fn sidebar(current_path: String, pages: List(DocPage)) -> Element(Nil) { 228 228 html.aside([attribute.class("sidebar")], [
+8 -5
www/src/www/page.gleam
··· 93 93 94 94 /// Transform .md links to clean paths 95 95 fn transform_links(html: String) -> String { 96 - // Match href="./something.md" or href="something.md" and replace with href="/something" 97 - let assert Ok(re) = regexp.from_string("href=\"(?:\\./)?([^\"]+)\\.md\"") 96 + // Match href="./something.md" or href="something.md" with optional anchor, replace with clean path 97 + let assert Ok(re) = 98 + regexp.from_string("href=\"(?:\\./)?([^\"#]+)\\.md(#[^\"]*)?\"") 98 99 regexp.match_map(re, html, fn(m) { 99 100 case m.submatches { 100 - [Some(filename)] -> 101 + [Some(filename), anchor] -> { 102 + let anchor_str = option.unwrap(anchor, "") 101 103 case filename { 102 - "README" -> "href=\"/\"" 103 - _ -> "href=\"/" <> filename <> "\"" 104 + "README" -> "href=\"/" <> anchor_str <> "\"" 105 + _ -> "href=\"/" <> filename <> anchor_str <> "\"" 104 106 } 107 + } 105 108 _ -> m.content 106 109 } 107 110 })