@synonymdev/pubky 0.15.0 → 0.16.0

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
@@ -293,19 +293,31 @@ legacy `callback` parameter is accepted as an `xSuccess` fallback.
293
293
 
294
294
  #### Resume an auth flow after page refresh
295
295
 
296
- Grant auth flows are resumable by saving the pending flow state before refresh and restoring it afterwards:
296
+ Save pending grant auth state before a refresh, then restore it afterwards.
297
+ The browser example below uses a delegated signing key stored in IndexedDB.
298
+ It requires a secure context with WebCrypto Ed25519 support and IndexedDB.
299
+
300
+ For signed approvals, saved flow state includes a temporary HPKE private key
301
+ that decrypts the signer's response. The authorization URL carries the matching
302
+ public key in `epk`; a domain-separated hash of that key identifies the relay
303
+ channel. Keep the saved state confidential and delete it when the flow
304
+ completes or is abandoned.
297
305
 
298
306
  ```js
299
- const flow = pubky.startGrantAuthFlow(caps, AuthFlowKind.signin(), {
307
+ import { GrantAuthFlow } from "@synonymdev/pubky";
308
+
309
+ // Before refreshing:
310
+ const flow = await GrantAuthFlow.startDelegated(caps, AuthFlowKind.signin(), {
300
311
  clientId: "my-cool-app.example",
301
312
  relay,
302
313
  });
303
- sessionStorage.setItem("pubky-grant-auth", flow.save());
314
+ sessionStorage.setItem("pubky-grant-auth", flow.saveDelegated());
304
315
 
316
+ // After reloading:
305
317
  const saved = sessionStorage.getItem("pubky-grant-auth");
306
318
  if (saved) {
307
319
  try {
308
- const resumed = pubky.resumeGrantAuthFlow(saved);
320
+ const resumed = await pubky.resumeDelegatedGrantAuthFlow(saved);
309
321
  const session = await resumed.awaitApproval();
310
322
  } finally {
311
323
  sessionStorage.removeItem("pubky-grant-auth");
@@ -313,7 +325,96 @@ if (saved) {
313
325
  }
314
326
  ```
315
327
 
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.
328
+ For local signing keys, use `GrantAuthFlow.start()`, `flow.saveLocal()`, and
329
+ `pubky.resumeGrantAuthFlow(saved)`.
330
+
331
+ Legacy cookie auth flows can be resumed with
332
+ `pubky.resumeCookieAuthFlow(authorizationUrl)`. Store pending auth state in
333
+ `sessionStorage`, not `localStorage`, and delete it once the flow completes or
334
+ is abandoned.
335
+
336
+ #### Request encryption keys
337
+
338
+ Apps can also ask the signer for encryption keys scoped to storage paths. Set
339
+ `approvalFormat: "signedApprovalV1"` and add the `e` action to each scope that
340
+ needs keys. Storage actions `r` and `w` don't deliver keys.
341
+
342
+ Keys stay usable after the grant expires or is revoked, and older homeservers
343
+ reject grants containing `e`. Read the
344
+ [scoped encryption keys guide](../../../../docs/scoped-encryption-keys.md)
345
+ before you use them.
346
+
347
+ ```js
348
+ const flow = await pubky.startGrantAuthFlow("/pub/chat/:rwe", AuthFlowKind.signin(), {
349
+ clientId: "chat.example",
350
+ approvalFormat: "signedApprovalV1",
351
+ });
352
+ renderQr(flow.authorizationUrl);
353
+ const session = await flow.awaitApproval();
354
+
355
+ const keys = session.grant.encryptionKeys; // undefined for bare grants
356
+ let key;
357
+ try {
358
+ // The signer may narrow scopes or decline `e`.
359
+ if (!keys.scopes.includes("/pub/chat/")) {
360
+ throw new Error("The signer didn't approve chat keys.");
361
+ }
362
+ key = await keys.deriveCryptoKeyForPath("/pub/chat/message");
363
+ } finally {
364
+ keys.free(); // The CryptoKey stays usable.
365
+ }
366
+
367
+ const iv = crypto.getRandomValues(new Uint8Array(12)); // Fresh IV every time.
368
+ const ciphertext = await crypto.subtle.encrypt(
369
+ { name: "AES-GCM", iv }, key, new TextEncoder().encode("Hello"),
370
+ );
371
+ ```
372
+
373
+ `EncryptionKeys` has these members:
374
+
375
+ | API | Returns |
376
+ | --- | --- |
377
+ | `keys.scopes` | Approved scope paths as `string[]` |
378
+ | `keys.deriveForPath(path)` | Raw key bytes as `Uint8Array`. Clear them after use. |
379
+ | `await keys.deriveCryptoKeyForPath(path)` | Non-extractable AES-GCM-256 `CryptoKey`. Requires WebCrypto in a secure context. |
380
+
381
+ Both derivation methods take a canonical file path and reject directories and
382
+ paths outside the approved scopes. Each read of `session.grant.encryptionKeys`
383
+ returns a separate copy; call `free()` when you're done with it. Signed
384
+ approvals without `e` scopes return an object with empty scopes.
385
+
386
+ `exportLocalSecret()` and `browserSessionStore.save()` keep the keys. The
387
+ browser store saves them as plaintext in IndexedDB, readable by any script on
388
+ the same origin. You can recover keys offline, even after the grant expires or
389
+ is revoked. Recovery creates no session:
390
+
391
+ ```js
392
+ import { EncryptionKeys } from "@synonymdev/pubky";
393
+
394
+ const recovered = EncryptionKeys.fromLocalSecret(savedToken);
395
+ if (recovered) {
396
+ try {
397
+ if (recovered.publicKey !== expectedUser.z32() ||
398
+ recovered.grantId !== expectedGrantId) {
399
+ throw new Error("This backup belongs to a different account or grant");
400
+ }
401
+ const key = await recovered.keys.deriveCryptoKeyForPath("/pub/chat/message");
402
+ // Use key with WebCrypto for locally saved ciphertext.
403
+ } finally {
404
+ recovered.keys.free();
405
+ }
406
+ }
407
+ ```
408
+
409
+ `fromLocalSecret()` returns `{ publicKey, grantId, keys }`, or `undefined` for
410
+ bare-grant tokens. `publicKey` is the authenticated account's z-base-32 key.
411
+ Compare it with the account you expect; also check `grantId` when restoring a
412
+ specific grant. Callers that previously received keys directly must now read
413
+ `recovered.keys` after checking the identity.
414
+
415
+ For a browser record, use
416
+ `await pubky.browserSessionStore.restoreEncryptionKeys(storedId)`. It checks
417
+ the account and grant against the record ID and returns the keys directly.
317
418
 
318
419
  #### Validate and normalize capabilities
319
420
 
@@ -472,6 +573,15 @@ await signer.pkdns.publishHomeserverForce(/* optional override homeserver*/);
472
573
  await signer.pkdns.getHomeserver();
473
574
  ```
474
575
 
576
+ ### Custom proofs
577
+
578
+ Use `session.grant.createCustomPop(data)` to sign JSON and return both the proof
579
+ and its root-signed grant. Use `verifyCustomGrantPop(credentials)` to verify the bundle
580
+ offline.
581
+
582
+ See [Sign and verify custom data](https://github.com/pubky/pubky-homeserver/blob/main/docs/CUSTOM_POP.md) for Rust and
583
+ JavaScript examples and the verification contract.
584
+
475
585
  ## Logging
476
586
 
477
587
  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.