@synonymdev/pubky 0.14.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 +142 -5
- package/index.cjs +786 -177
- package/index.js +788 -178
- package/package.json +5 -3
- package/pubky.d.ts +348 -27
- package/pubky_bg.wasm +0 -0
package/README.md
CHANGED
|
@@ -219,6 +219,18 @@ like a bearer token. Delegated browser grant sessions should use
|
|
|
219
219
|
> applications should use grant auth plus `browserSessionStore` or
|
|
220
220
|
> `exportLocalSecret()`.
|
|
221
221
|
|
|
222
|
+
**Multiple browser tabs**
|
|
223
|
+
|
|
224
|
+
`browserSessionStore.restore(id)` shares one bearer per grant across tabs on the
|
|
225
|
+
same origin, including after reloads. It uses the existing single-session protocol.
|
|
226
|
+
Browser persistence requires IndexedDB and Web Locks in a secure context.
|
|
227
|
+
|
|
228
|
+
Signout revokes the grant for every tab. Separate applications should obtain
|
|
229
|
+
their own grants: restoring the same exported grant outside browser coordination
|
|
230
|
+
replaces its current bearer.
|
|
231
|
+
See [grant session lifecycle](../../../../docs/grant-session-lifecycle.md)
|
|
232
|
+
for compatibility and expiry behavior.
|
|
233
|
+
|
|
222
234
|
**Approve a pubkyauth request URL**
|
|
223
235
|
|
|
224
236
|
```js
|
|
@@ -281,19 +293,31 @@ legacy `callback` parameter is accepted as an `xSuccess` fallback.
|
|
|
281
293
|
|
|
282
294
|
#### Resume an auth flow after page refresh
|
|
283
295
|
|
|
284
|
-
|
|
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.
|
|
285
305
|
|
|
286
306
|
```js
|
|
287
|
-
|
|
307
|
+
import { GrantAuthFlow } from "@synonymdev/pubky";
|
|
308
|
+
|
|
309
|
+
// Before refreshing:
|
|
310
|
+
const flow = await GrantAuthFlow.startDelegated(caps, AuthFlowKind.signin(), {
|
|
288
311
|
clientId: "my-cool-app.example",
|
|
289
312
|
relay,
|
|
290
313
|
});
|
|
291
|
-
sessionStorage.setItem("pubky-grant-auth", flow.
|
|
314
|
+
sessionStorage.setItem("pubky-grant-auth", flow.saveDelegated());
|
|
292
315
|
|
|
316
|
+
// After reloading:
|
|
293
317
|
const saved = sessionStorage.getItem("pubky-grant-auth");
|
|
294
318
|
if (saved) {
|
|
295
319
|
try {
|
|
296
|
-
const resumed = pubky.
|
|
320
|
+
const resumed = await pubky.resumeDelegatedGrantAuthFlow(saved);
|
|
297
321
|
const session = await resumed.awaitApproval();
|
|
298
322
|
} finally {
|
|
299
323
|
sessionStorage.removeItem("pubky-grant-auth");
|
|
@@ -301,7 +325,96 @@ if (saved) {
|
|
|
301
325
|
}
|
|
302
326
|
```
|
|
303
327
|
|
|
304
|
-
|
|
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.
|
|
305
418
|
|
|
306
419
|
#### Validate and normalize capabilities
|
|
307
420
|
|
|
@@ -406,8 +519,23 @@ await s.list("/pub/example.com/", null, false, 100, false);
|
|
|
406
519
|
|
|
407
520
|
// Delete
|
|
408
521
|
await s.delete("/pub/example.com/data.json");
|
|
522
|
+
|
|
523
|
+
// Locked writes (WebDAV LOCK/UNLOCK)
|
|
524
|
+
const lock = await s.lock("/pub/example.com/state.json", 30); // seconds
|
|
525
|
+
lock.path; // -> "/pub/example.com/state.json"
|
|
526
|
+
lock.token; // -> "opaquelocktoken:<uuid>"
|
|
527
|
+
lock.timeoutSeconds; // -> what the homeserver granted (it caps the lifetime)
|
|
528
|
+
await s.putTextLocked(lock, JSON.stringify({ count: 1 })); // only the holder can write; no putJsonLocked
|
|
529
|
+
await s.putBytesLocked(lock, new Uint8Array([1, 2, 3]));
|
|
530
|
+
await s.deleteLocked(lock);
|
|
531
|
+
await s.refreshLock(lock, 30); // restart the lifetime
|
|
532
|
+
await s.unlock(lock);
|
|
409
533
|
```
|
|
410
534
|
|
|
535
|
+
While a lock lives, a plain `putJson`/`putText`/`putBytes`/`delete` on that path rejects with `423 Locked`, and so does a second `lock()`. Once the lock is gone, the old value is refused with `412` (write, delete, refresh) and `409` on `unlock()`; take a new lock and read the file again. A homeserver without lock support answers `405`.
|
|
536
|
+
|
|
537
|
+
`timeoutSeconds` is rounded down to whole seconds, with a minimum of 1. If `lock()` rejects without a status code (a lost connection, say), the homeserver may still have granted the lock; nothing can release it, so the path answers `423` until it times out.
|
|
538
|
+
|
|
411
539
|
`get()` exposes the underlying `Response`, which is handy for streaming bodies or inspecting headers before consuming content.
|
|
412
540
|
|
|
413
541
|
Path rules:
|
|
@@ -445,6 +573,15 @@ await signer.pkdns.publishHomeserverForce(/* optional override homeserver*/);
|
|
|
445
573
|
await signer.pkdns.getHomeserver();
|
|
446
574
|
```
|
|
447
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
|
+
|
|
448
585
|
## Logging
|
|
449
586
|
|
|
450
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.
|