@synonymdev/pubky 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -237,6 +237,20 @@ for compatibility and expiry behavior.
237
237
  await signer.approveAuthRequest("pubkyauth://signin?caps=...&secret=...&relay=...");
238
238
  ```
239
239
 
240
+ `approveAuthRequest()` approves every requested capability. To approve fewer
241
+ scopes or actions, pass a capabilities string to `approveAuthWithCapabilities()`:
242
+
243
+ ```js
244
+ // The request includes `/pub/example.app/:rwe`; the user approves only read.
245
+ await signer.approveAuthWithCapabilities(authUrl, "/pub/example.app/:r");
246
+ ```
247
+
248
+ See [selecting capabilities on the signing device][select-capabilities] for
249
+ validation rules, empty approvals, and encryption key delivery.
250
+
251
+ [select-capabilities]:
252
+ ../../../../docs/scoped-encryption-keys.md#select-capabilities-on-the-signing-device
253
+
240
254
  **Handle a pubkyauth deep link**
241
255
 
242
256
  General entry point for any supported `pubkyauth://` link. Auth request links
@@ -293,19 +307,31 @@ legacy `callback` parameter is accepted as an `xSuccess` fallback.
293
307
 
294
308
  #### Resume an auth flow after page refresh
295
309
 
296
- Grant auth flows are resumable by saving the pending flow state before refresh and restoring it afterwards:
310
+ Save pending grant auth state before a refresh, then restore it afterwards.
311
+ The browser example below uses a delegated signing key stored in IndexedDB.
312
+ It requires a secure context with WebCrypto Ed25519 support and IndexedDB.
313
+
314
+ For signed approvals, saved flow state includes a temporary HPKE private key
315
+ that decrypts the signer's response. The authorization URL carries the matching
316
+ public key in `epk`; a domain-separated hash of that key identifies the relay
317
+ channel. Keep the saved state confidential and delete it when the flow
318
+ completes or is abandoned.
297
319
 
298
320
  ```js
299
- const flow = pubky.startGrantAuthFlow(caps, AuthFlowKind.signin(), {
321
+ import { GrantAuthFlow } from "@synonymdev/pubky";
322
+
323
+ // Before refreshing:
324
+ const flow = await GrantAuthFlow.startDelegated(caps, AuthFlowKind.signin(), {
300
325
  clientId: "my-cool-app.example",
301
326
  relay,
302
327
  });
303
- sessionStorage.setItem("pubky-grant-auth", flow.save());
328
+ sessionStorage.setItem("pubky-grant-auth", flow.saveDelegated());
304
329
 
330
+ // After reloading:
305
331
  const saved = sessionStorage.getItem("pubky-grant-auth");
306
332
  if (saved) {
307
333
  try {
308
- const resumed = pubky.resumeGrantAuthFlow(saved);
334
+ const resumed = await pubky.resumeDelegatedGrantAuthFlow(saved);
309
335
  const session = await resumed.awaitApproval();
310
336
  } finally {
311
337
  sessionStorage.removeItem("pubky-grant-auth");
@@ -313,7 +339,96 @@ if (saved) {
313
339
  }
314
340
  ```
315
341
 
316
- Legacy cookie auth flows can be resumed with `pubky.resumeCookieAuthFlow(authorizationUrl)`. Store pending auth state in `sessionStorage`, not `localStorage`, and delete it once the flow completes or is abandoned.
342
+ For local signing keys, use `GrantAuthFlow.start()`, `flow.saveLocal()`, and
343
+ `pubky.resumeGrantAuthFlow(saved)`.
344
+
345
+ Legacy cookie auth flows can be resumed with
346
+ `pubky.resumeCookieAuthFlow(authorizationUrl)`. Store pending auth state in
347
+ `sessionStorage`, not `localStorage`, and delete it once the flow completes or
348
+ is abandoned.
349
+
350
+ #### Request encryption keys
351
+
352
+ Apps can also ask the signer for encryption keys scoped to storage paths. Set
353
+ `approvalFormat: "signedApprovalV1"` and add the `e` action to each scope that
354
+ needs keys. Storage actions `r` and `w` don't deliver keys.
355
+
356
+ Keys stay usable after the grant expires or is revoked, and older homeservers
357
+ reject grants containing `e`. Read the
358
+ [scoped encryption keys guide](../../../../docs/scoped-encryption-keys.md)
359
+ before you use them.
360
+
361
+ ```js
362
+ const flow = await pubky.startGrantAuthFlow("/pub/chat/:rwe", AuthFlowKind.signin(), {
363
+ clientId: "chat.example",
364
+ approvalFormat: "signedApprovalV1",
365
+ });
366
+ renderQr(flow.authorizationUrl);
367
+ const session = await flow.awaitApproval();
368
+
369
+ const keys = session.grant.encryptionKeys; // undefined for bare grants
370
+ let key;
371
+ try {
372
+ // The signer may narrow scopes or decline `e`.
373
+ if (!keys.scopes.includes("/pub/chat/")) {
374
+ throw new Error("The signer didn't approve chat keys.");
375
+ }
376
+ key = await keys.deriveCryptoKeyForPath("/pub/chat/message");
377
+ } finally {
378
+ keys.free(); // The CryptoKey stays usable.
379
+ }
380
+
381
+ const iv = crypto.getRandomValues(new Uint8Array(12)); // Fresh IV every time.
382
+ const ciphertext = await crypto.subtle.encrypt(
383
+ { name: "AES-GCM", iv }, key, new TextEncoder().encode("Hello"),
384
+ );
385
+ ```
386
+
387
+ `EncryptionKeys` has these members:
388
+
389
+ | API | Returns |
390
+ | --- | --- |
391
+ | `keys.scopes` | Approved scope paths as `string[]` |
392
+ | `keys.deriveForPath(path)` | Raw key bytes as `Uint8Array`. Clear them after use. |
393
+ | `await keys.deriveCryptoKeyForPath(path)` | Non-extractable AES-GCM-256 `CryptoKey`. Requires WebCrypto in a secure context. |
394
+
395
+ Both derivation methods take a canonical file path and reject directories and
396
+ paths outside the approved scopes. Each read of `session.grant.encryptionKeys`
397
+ returns a separate copy; call `free()` when you're done with it. Signed
398
+ approvals without `e` scopes return an object with empty scopes.
399
+
400
+ `exportLocalSecret()` and `browserSessionStore.save()` keep the keys. The
401
+ browser store saves them as plaintext in IndexedDB, readable by any script on
402
+ the same origin. You can recover keys offline, even after the grant expires or
403
+ is revoked. Recovery creates no session:
404
+
405
+ ```js
406
+ import { EncryptionKeys } from "@synonymdev/pubky";
407
+
408
+ const recovered = EncryptionKeys.fromLocalSecret(savedToken);
409
+ if (recovered) {
410
+ try {
411
+ if (recovered.publicKey !== expectedUser.z32() ||
412
+ recovered.grantId !== expectedGrantId) {
413
+ throw new Error("This backup belongs to a different account or grant");
414
+ }
415
+ const key = await recovered.keys.deriveCryptoKeyForPath("/pub/chat/message");
416
+ // Use key with WebCrypto for locally saved ciphertext.
417
+ } finally {
418
+ recovered.keys.free();
419
+ }
420
+ }
421
+ ```
422
+
423
+ `fromLocalSecret()` returns `{ publicKey, grantId, keys }`, or `undefined` for
424
+ bare-grant tokens. `publicKey` is the authenticated account's z-base-32 key.
425
+ Compare it with the account you expect; also check `grantId` when restoring a
426
+ specific grant. Callers that previously received keys directly must now read
427
+ `recovered.keys` after checking the identity.
428
+
429
+ For a browser record, use
430
+ `await pubky.browserSessionStore.restoreEncryptionKeys(storedId)`. It checks
431
+ the account and grant against the record ID and returns the keys directly.
317
432
 
318
433
  #### Validate and normalize capabilities
319
434
 
@@ -472,6 +587,15 @@ await signer.pkdns.publishHomeserverForce(/* optional override homeserver*/);
472
587
  await signer.pkdns.getHomeserver();
473
588
  ```
474
589
 
590
+ ### Custom proofs
591
+
592
+ Use `session.grant.createCustomPop(data)` to sign JSON and return both the proof
593
+ and its root-signed grant. Use `verifyCustomGrantPop(credentials)` to verify the bundle
594
+ offline.
595
+
596
+ See [Sign and verify custom data](https://github.com/pubky/pubky-homeserver/blob/main/docs/CUSTOM_POP.md) for Rust and
597
+ JavaScript examples and the verification contract.
598
+
475
599
  ## Logging
476
600
 
477
601
  The SDK ships with a WASM logger that bridges Rust `log` output into the browser or Node console. Call `setLogLevel` **once at application start**, before constructing `Pubky` or other SDK actors, to choose how verbose the logs should be.