@synonymdev/pubky 0.9.3 → 0.10.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.
Files changed (6) hide show
  1. package/README.md +134 -46
  2. package/index.cjs +3336 -1129
  3. package/index.js +3337 -1130
  4. package/package.json +7 -2
  5. package/pubky.d.ts +790 -36
  6. package/pubky_bg.wasm +0 -0
package/README.md CHANGED
@@ -31,21 +31,29 @@ const homeserver = PublicKey.from(
31
31
  "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"
32
32
  );
33
33
  const signupToken = "<your-invite-code-or-null>";
34
- const session = await signer.signup(homeserver, signupToken);
34
+ await signer.signup(homeserver, signupToken);
35
35
 
36
- // 3) Write a public JSON file (session-scoped storage uses cookies automatically)
37
- const path = "/pub/example.com/hello.json";
36
+ // 3a) Signin with the signer directly.
37
+ let session = await signer.signin("example.com");
38
+
39
+ // 3b) Or, if you do not have the keypair available, authenticate on a 3rd-party app
40
+ // by delegating the authentication to a signer with a QR code.
41
+ const authFlow = await pubky.startGrantAuthFlow(
42
+ "/pub/my-cool-app/:rw",
43
+ AuthFlowKind.signin(),
44
+ { clientId: "my-cool-app.example" },
45
+ );
46
+ renderQr(authFlow.authorizationUrl); // Show to user the signin deeplink via a QR code
47
+ session = await authFlow.awaitApproval();
48
+
49
+ // 4) Write a public JSON file
50
+ const path = "/pub/my-cool-app/hello.json";
38
51
  await session.storage.putJson(path, { hello: "world" });
39
52
 
40
- // 4) Read it publicly (no auth needed)
53
+ // 5) Read it publicly (no auth needed)
41
54
  const userPk = session.info.publicKey.toString();
42
- const addr = `${userPk}/pub/example.com/hello.json`;
55
+ const addr = `${userPk}/pub/my-cool-app/hello.json`;
43
56
  const json = await pubky.publicStorage.getJson(addr); // -> { hello: "world" }
44
-
45
- // 5) Authenticate on a 3rd-party app
46
- const authFlow = pubky.startAuthFlow("/pub/my-cool-app/:rw", AuthFlowKind::signin()); // require permissions to read and write into `my.app`
47
- renderQr(authFlow.authorizationUrl); // show to user
48
- const session = await authFlow.awaitApproval();
49
57
  ```
50
58
 
51
59
  Find here [**ready-to-run examples**](https://github.com/pubky/pubky-core/tree/main/examples).
@@ -84,8 +92,12 @@ const pubkyLocal = Pubky.testnet("localhost");
84
92
  // Signer (bind your keypair to a new Signer actor)
85
93
  const signer = pubky.signer(Keypair.random());
86
94
 
87
- // Pubky Auth flow (with capabilities)
88
- const authFlow = pubky.startAuthFlow("/pub/my-cool-app/:rw", AuthFlowKind::signin());
95
+ // Grant Pubky Auth flow (with capabilities)
96
+ const authFlow = await pubky.startGrantAuthFlow(
97
+ "/pub/my-cool-app/:rw",
98
+ AuthFlowKind.signin(),
99
+ { clientId: "my-cool-app.example" },
100
+ );
89
101
 
90
102
  // Public storage (read-only)
91
103
  const publicStorage = pubky.publicStorage;
@@ -106,7 +118,7 @@ const client = new Client(); // or: pubky.client.fetch(); instead of constructin
106
118
 
107
119
  // Convert the identifier into a transport URL before fetching.
108
120
  const userId = PublicKey.from("pubky<z32>").toString();
109
- const url = resolvePubky(`${userId}/pub/example.com/file.txt`);
121
+ const url = resolvePubky(`${userId}/pub/my-cool-app/file.txt`);
110
122
  const res = await client.fetch(url);
111
123
  ```
112
124
 
@@ -155,12 +167,12 @@ const keypair = Keypair.random();
155
167
  const signer = pubky.signer(keypair);
156
168
 
157
169
  const homeserver = PublicKey.from("8pinxxgq…");
158
- const session = await signer.signup(homeserver, /* invite */ null);
170
+ await signer.signup(homeserver); // Add optional signup code here if required
159
171
 
160
- const session2 = await signer.signin(); // fast, prefer this; publishes PKDNS in background
161
- const session3 = await signer.signinBlocking(); // slower but safer; waits for PKDNS publish
172
+ const session1 = await signer.signin("example.com"); // fast, prefer this; publishes PKDNS in background
173
+ const session2 = await signer.signinBlocking("example.com"); // slower but safer; waits for PKDNS publish
162
174
 
163
- await session.signout(); // invalidates server session
175
+ await session1.signout(); // invalidates server session
164
176
  ```
165
177
 
166
178
  **Session details**
@@ -172,29 +184,60 @@ const caps = session.info.capabilities; // -> string[] permissions and paths
172
184
  const storage = session.storage; // -> This User's storage API (absolute paths)
173
185
  ```
174
186
 
175
- **Persist a session across tab refreshes (browser)**
187
+ **Persist a grant session**
176
188
 
177
189
  ```js
178
- // Save the session snapshot (no secrets inside; relies on the HTTP-only cookie).
179
- const snapshot = session.export();
180
- localStorage.setItem("pubky-session", snapshot);
190
+ // Persist explicitly when your app wants reload/returning-user restore.
191
+ // In browsers, this uses IndexedDB and supports multiple accounts.
192
+ const stored = await pubky.browserSessionStore.save(session);
193
+
194
+ const accounts = await pubky.browserSessionStore.list();
195
+ const restored = await pubky.browserSessionStore.restore(stored.id);
196
+ ```
197
+
198
+ `pubky.startGrantAuthFlow(...)` chooses delegated browser PoP keys
199
+ when the runtime supports them, then falls back to local PoP keys. Delegated
200
+ sessions keep the private PoP key non-extractable in browser storage; local
201
+ sessions store bearer-equivalent secret material in IndexedDB.
181
202
 
182
- // Later (after a reload), rehydrate using the browser's stored cookie.
183
- const restored = await pubky.restoreSession(localStorage.getItem("pubky-session")!);
203
+ ```js
204
+ const flow = await pubky.startGrantAuthFlow(
205
+ "/pub/example.com/:rw",
206
+ AuthFlowKind.signin(),
207
+ { clientId: "example.com" },
208
+ );
209
+ const session = await flow.awaitApproval();
210
+ await pubky.browserSessionStore.save(session);
184
211
  ```
185
212
 
186
- > The exported string contains only public session metadata. The browser must keep the
187
- > HTTP-only cookie alive for the restored session to remain authenticated.
213
+ For advanced/manual storage, local grant sessions still expose
214
+ `exportLocalSecret()` and `pubky.restoreSession(secret)`. Treat that string
215
+ like a bearer token. Delegated browser grant sessions should use
216
+ `browserSessionStore` because their PoP keys are non-extractable.
217
+
218
+ > `session.export()` is still available for legacy cookie sessions, but new
219
+ > applications should use grant auth plus `browserSessionStore` or
220
+ > `exportLocalSecret()`.
188
221
 
189
222
  **Approve a pubkyauth request URL**
190
223
 
191
224
  ```js
192
- await signer.approveAuthRequest("pubkyauth:///?caps=...&secret=...&relay=...");
225
+ await signer.approveAuthRequest("pubkyauth://signin?caps=...&secret=...&relay=...");
226
+ ```
227
+
228
+ **Handle a pubkyauth deep link**
229
+
230
+ General entry point for any supported `pubkyauth://` link. Auth request links
231
+ are approved through their relay (like `approveAuthRequest`), while a
232
+ `direct_signup` link creates the account directly on its target homeserver:
233
+
234
+ ```js
235
+ await signer.handleDeepLink("pubkyauth://direct_signup?hs=...");
193
236
  ```
194
237
 
195
238
  ---
196
239
 
197
- ### AuthFlow (pubkyauth)
240
+ ### GrantAuthFlow (pubkyauth)
198
241
 
199
242
  End-to-end auth (3rd-party app asks a user to approve via QR/deeplink, E.g. Pubky Ring).
200
243
 
@@ -208,8 +251,21 @@ const caps = "/pub/my-cool-app/:rw,/pub/another-app/folder/:w";
208
251
  // Optional relay; defaults to Synonym-hosted relay if omitted
209
252
  const relay = "https://httprelay.pubky.app/inbox/"; // optional (defaults to this)
210
253
 
211
- // Start the auth polling
212
- const flow = pubky.startAuthFlow(caps, AuthFlowKind::signin(), relay);
254
+ // Start grant auth polling
255
+ const flow = await pubky.startGrantAuthFlow(
256
+ caps,
257
+ AuthFlowKind.signin(),
258
+ {
259
+ clientId: "my-cool-app.example",
260
+ relay,
261
+ xCallback: {
262
+ xSource: "My Cool App",
263
+ xSuccess: "my-cool-app://auth/success?nonce=unique",
264
+ xError: "my-cool-app://auth/error?nonce=unique",
265
+ xCancel: "my-cool-app://auth/cancel?nonce=unique",
266
+ },
267
+ },
268
+ );
213
269
 
214
270
  renderQr(flow.authorizationUrl); // show to user
215
271
 
@@ -217,10 +273,40 @@ renderQr(flow.authorizationUrl); // show to user
217
273
  const session = await flow.awaitApproval();
218
274
  ```
219
275
 
276
+ The same callback object can be passed as the fourth argument to legacy cookie
277
+ auth: `startCookieAuthFlow(caps, kind, relay, xCallback)`. The SDK emits
278
+ `x-source`, `x-success`, `x-error`, and `x-cancel` using the encoding expected
279
+ by Pubky Ring. Parsed deep-link objects expose them through `xCallback`; the
280
+ legacy `callback` parameter is accepted as an `xSuccess` fallback.
281
+
282
+ #### Resume an auth flow after page refresh
283
+
284
+ Grant auth flows are resumable by saving the pending flow state before refresh and restoring it afterwards:
285
+
286
+ ```js
287
+ const flow = pubky.startGrantAuthFlow(caps, AuthFlowKind.signin(), {
288
+ clientId: "my-cool-app.example",
289
+ relay,
290
+ });
291
+ sessionStorage.setItem("pubky-grant-auth", flow.save());
292
+
293
+ const saved = sessionStorage.getItem("pubky-grant-auth");
294
+ if (saved) {
295
+ try {
296
+ const resumed = pubky.resumeGrantAuthFlow(saved);
297
+ const session = await resumed.awaitApproval();
298
+ } finally {
299
+ sessionStorage.removeItem("pubky-grant-auth");
300
+ }
301
+ }
302
+ ```
303
+
304
+ 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.
305
+
220
306
  #### Validate and normalize capabilities
221
307
 
222
308
  If you accept capability strings from user input (forms, CLI arguments, etc.),
223
- use `validateCapabilities` before calling `startAuthFlow`. The helper returns a
309
+ use `validateCapabilities` before calling `startGrantAuthFlow`. The helper returns a
224
310
  normalized string (ordering actions like `:rw`) and throws a structured error
225
311
  when the input is malformed.
226
312
 
@@ -233,7 +319,9 @@ const rawCaps = formData.get("caps");
233
319
 
234
320
  try {
235
321
  const caps = validateCapabilities(rawCaps ?? "");
236
- const flow = pubky.startAuthFlow(caps, AuthFlowKind::signin());
322
+ const flow = await pubky.startGrantAuthFlow(caps, AuthFlowKind.signin(), {
323
+ clientId: "my-cool-app.example",
324
+ });
237
325
  renderQr(flow.authorizationUrl);
238
326
  const session = await flow.awaitApproval();
239
327
  // ...
@@ -246,15 +334,18 @@ try {
246
334
  }
247
335
  ```
248
336
 
249
- On invalid input, `validateCapabilities` throws a `PubkyError` with
250
- `{ name: "InvalidInput", message: "Invalid capability entries: …" }`, so you can
251
- surface precise feedback to the user.
337
+ On invalid input, `validateCapabilities` throws a `PubkyError` identifying the
338
+ first malformed entry and its position. Its `data.invalidEntries` array contains
339
+ that entry so applications can surface precise feedback to the user.
340
+
341
+ Capability scopes must be canonical absolute paths. Repeated separators and
342
+ `.` or `..` segments are rejected rather than normalized. Percent sequences are
343
+ literal scope characters; URL encoding is handled by the enclosing deep link.
252
344
 
253
345
  #### Http Relay & reliability
254
346
 
255
- - If you don’t specify a relay, `PubkyAuthFlow` defaults to a Synonym-hosted relay. If that relay is down, logins won’t complete.
256
- - For production and larger apps, run **your own http relay** (MIT, Docker): [https://httprelay.io](https://httprelay.io).
257
- The channel is derived as `base64url(hash(secret))`; the token is end-to-end encrypted with the `secret` and cannot be decrypted by the relay.
347
+ - If you don’t specify a relay, the auth flow defaults to a Synonym-hosted relay. If that relay is down, logins won’t complete.
348
+ - For production and larger apps, run **your own http relay** (MIT, Docker): [https://github.com/pubky/http-relay](https://github.com/pubky/http-relay).
258
349
 
259
350
  ---
260
351
 
@@ -290,7 +381,7 @@ await pub.list(
290
381
 
291
382
  Use `get()` when you need the raw `Response` for streaming or custom parsing.
292
383
 
293
- #### SessionStorage (read/write; uses cookies)
384
+ #### SessionStorage (read/write)
294
385
 
295
386
  ```js
296
387
  const s = session.storage;
@@ -321,11 +412,13 @@ await s.delete("/pub/example.com/data.json");
321
412
 
322
413
  Path rules:
323
414
 
324
- - Session storage uses **absolute** paths like `"/pub/app/file.txt"`.
415
+ - Session storage uses **absolute** paths under `/pub/` or `/priv/`.
325
416
  - Public storage uses **addressed** form `pubky<user>/pub/app/file.txt` (preferred) or `pubky://<user>/...`.
326
417
 
327
418
  **Convention:** put your app’s public data under a domain-like folder in `/pub`, e.g. `/pub/my-new-app/`.
328
419
 
420
+ See [Private Storage](https://github.com/pubky/pubky-core/blob/main/docs/PRIVATE_STORAGE.md) for `/priv/` access and events.
421
+
329
422
  ---
330
423
 
331
424
  ### PKDNS (Pkarr)
@@ -338,7 +431,8 @@ import { Pubky, PublicKey, Keypair } from "@synonymdev/pubky";
338
431
  const pubky = new Pubky();
339
432
 
340
433
  // Read-only resolver
341
- const homeserver = await pubky.getHomeserverOf(PublicKey.from("pubky<z32>")); // string | undefined
434
+ const homeserver = await pubky.getHomeserverOf(PublicKey.from("pubky<z32>")); // PublicKey | undefined
435
+ // Rejects if PKARR resolution fails or the `_pubky` target is malformed.
342
436
 
343
437
  // With keys (signer-bound)
344
438
  const signer = pubky.signer(Keypair.random());
@@ -423,12 +517,6 @@ try {
423
517
  }
424
518
  ```
425
519
 
426
- ## Browser environment notes
427
-
428
- - Keep the Pubky client UI and the homeserver on the **same origin family** (both local or both remote). Browsers partition cookies by scheme/host, and cross-site requests (e.g., http://localhost calling https://staging…​) can silently drop or cache `SameSite`/`Secure` session cookies.
429
- - If you must mix environments, use a reverse proxy so the browser always talks to one consistent origin (or disable caching via devtools and clear cookies between switches).
430
- - When troubleshooting auth/session caching: open a fresh incognito window, clear site data for the target origin, and verify the request includes credentials.
431
-
432
520
  ---
433
521
 
434
522
  ## Local Test & Development