@synonymdev/pubky 0.6.0-rc.2 → 0.6.0-rc.7

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 +167 -45
  2. package/index.cjs +1335 -514
  3. package/index.js +1454 -601
  4. package/package.json +9 -5
  5. package/pubky.d.ts +397 -167
  6. package/pubky_bg.wasm +0 -0
package/README.md CHANGED
@@ -17,7 +17,7 @@ Module system + TS types: ESM and CommonJS both supported; TypeScript typings ge
17
17
  ## Getting Started
18
18
 
19
19
  ```js
20
- import { Pubky, PublicKey, Keypair } from "@synonymdev/pubky";
20
+ import { Pubky, PublicKey, Keypair, AuthFlowKind } from "@synonymdev/pubky";
21
21
 
22
22
  // Initiate a Pubky SDK facade wired for default mainnet Pkarr relays.
23
23
  const pubky = new Pubky(); // or: const pubky = Pubky.testnet(); for localhost testnet.
@@ -35,22 +35,31 @@ const session = await signer.signup(homeserver, signupToken);
35
35
 
36
36
  // 3) Write a public JSON file (session-scoped storage uses cookies automatically)
37
37
  const path = "/pub/example.com/hello.json";
38
- await session.storage().putJson(path, { hello: "world" });
38
+ await session.storage.putJson(path, { hello: "world" });
39
39
 
40
40
  // 4) Read it publicly (no auth needed)
41
- const userPk = session.info().publicKey();
42
- const addr = `${userPk.z32()}/pub/example.com/hello.json`;
43
- const json = await pubky.publicStorage().getJson(addr); // -> { hello: "world" }
41
+ const userPk = session.info.publicKey.z32();
42
+ const addr = `pubky${userPk}/pub/example.com/hello.json`;
43
+ 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();
44
49
  ```
45
50
 
46
51
  Find here [**ready-to-run examples**](https://github.com/pubky/pubky-core/tree/main/examples).
47
52
 
53
+ ### Initialization & events
54
+
55
+ The npm package bundles the WebAssembly module and **initializes it before exposing any APIs**. This avoids the common wasm-pack pitfall where events fire before the module finishes instantiating. Long-polling flows such as `authFlow.awaitApproval()` or `authFlow.tryPollOnce()` only start their relay calls after the underlying module is ready, so you won't miss approvals while the bundle is loading.
56
+
48
57
  ## API Overview
49
58
 
50
59
  Use `new Pubky()` to quickly get any flow started:
51
60
 
52
61
  ```js
53
- import { Pubky, Keypair } from "@synonymdev/pubky";
62
+ import { Pubky, Keypair, AuthFlowKind } from "@synonymdev/pubky";
54
63
 
55
64
  // Mainnet (default relays)
56
65
  const pubky = new Pubky();
@@ -62,25 +71,29 @@ const pubkyLocal = Pubky.testnet("localhost");
62
71
  // Signer (bind your keypair to a new Signer actor)
63
72
  const signer = pubky.signer(Keypair.random());
64
73
 
74
+ // Pubky Auth flow (with capabilities)
75
+ const authFlow = pubky.startAuthFlow("/pub/my-cool-app/:rw", AuthFlowKind::signin());
76
+
65
77
  // Public storage (read-only)
66
- const publicStorage = pubky.publicStorage();
78
+ const publicStorage = pubky.publicStorage;
67
79
 
68
- // PKDNS resolver (read-only)
69
- const pkdns = pubky.pkdns();
80
+ // Pkdns resolver
81
+ const pkdns = pubky.getHomeserverOf(publicKey);
70
82
 
71
83
  // Optional: raw HTTP client for advanced use
72
- const client = pubky.client();
84
+ const client = pubky.client;
73
85
  ```
74
86
 
75
87
  ### Client (HTTP bridge)
76
88
 
77
89
  ```js
78
- import { Client } from "@synonymdev/pubky";
90
+ import { Client, resolvePubky } from "@synonymdev/pubky";
79
91
 
80
- const client = new Client(); // or: pubky.client(); instead of constructing a client manually
92
+ const client = new Client(); // or: pubky.client.fetch(); instead of constructing a client manually
81
93
 
82
- // Works with both pubky:// and http(s)://
83
- const res = await client.fetch("pubky://<pubky>/pub/example.com/file.txt");
94
+ // Convert the identifier into a transport URL before fetching.
95
+ const url = resolvePubky("pubky<pubky>/pub/example.com/file.txt");
96
+ const res = await client.fetch(url);
84
97
  ```
85
98
 
86
99
  ---
@@ -91,7 +104,7 @@ const res = await client.fetch("pubky://<pubky>/pub/example.com/file.txt");
91
104
  import { Keypair, PublicKey } from "@synonymdev/pubky";
92
105
 
93
106
  const keypair = Keypair.random();
94
- const pubkey = keypair.publicKey();
107
+ const pubkey = keypair.publicKey;
95
108
 
96
109
  // z-base-32 roundtrip
97
110
  const parsed = PublicKey.from(pubkey.z32());
@@ -138,13 +151,26 @@ await session.signout(); // invalidates server session
138
151
  **Session details**
139
152
 
140
153
  ```js
141
- const info = session.info();
142
- const userPk = info.publicKey(); // -> PublicKey
143
- const caps = info.capabilities(); // -> string[]
154
+ const userPk = session.info.publicKey.z32(); // -> PublicKey as z32 string
155
+ const caps = session.info.capabilities; // -> string[] permissions and paths
156
+
157
+ const storage = session.storage; // -> This User's storage API (absolute paths)
158
+ ```
159
+
160
+ **Persist a session across tab refreshes (browser)**
161
+
162
+ ```js
163
+ // Save the session snapshot (no secrets inside; relies on the HTTP-only cookie).
164
+ const snapshot = session.export();
165
+ localStorage.setItem("pubky-session", snapshot);
144
166
 
145
- const storage = session.storage(); // -> SessionStorage (absolute paths)
167
+ // Later (after a reload), rehydrate using the browser's stored cookie.
168
+ const restored = await pubky.restoreSession(localStorage.getItem("pubky-session")!);
146
169
  ```
147
170
 
171
+ > The exported string contains only public session metadata. The browser must keep the
172
+ > HTTP-only cookie alive for the restored session to remain authenticated.
173
+
148
174
  **Approve a pubkyauth request URL**
149
175
 
150
176
  ```js
@@ -158,24 +184,57 @@ await signer.approveAuthRequest("pubkyauth:///?caps=...&secret=...&relay=...");
158
184
  End-to-end auth (3rd-party app asks a user to approve via QR/deeplink, E.g. Pubky Ring).
159
185
 
160
186
  ```js
161
- import { Pubky } from "@synonymdev/pubky";
187
+ import { Pubky, AuthFlowKind } from "@synonymdev/pubky";
162
188
  const pubky = new Pubky();
163
189
 
164
190
  // Comma-separated capabilities string
165
- const caps = "/pub/my.app/:rw,/pub/another.app/folder/:w";
191
+ const caps = "/pub/my-cool-app/:rw,/pub/another-app/folder/:w";
166
192
 
167
193
  // Optional relay; defaults to Synonym-hosted relay if omitted
168
194
  const relay = "https://httprelay.pubky.app/link/"; // optional (defaults to this)
169
195
 
170
196
  // Start the auth polling
171
- const flow = pubky.startAuthFlow(caps, relay);
197
+ const flow = pubky.startAuthFlow(caps, AuthFlowKind::signin(), relay);
172
198
 
173
- renderQr(flow.authorizationUrl()); // show to user
199
+ renderQr(flow.authorizationUrl); // show to user
174
200
 
175
201
  // Blocks until the signer approves; returns a ready Session
176
202
  const session = await flow.awaitApproval();
177
203
  ```
178
204
 
205
+ #### Validate and normalize capabilities
206
+
207
+ If you accept capability strings from user input (forms, CLI arguments, etc.),
208
+ use `validateCapabilities` before calling `startAuthFlow`. The helper returns a
209
+ normalized string (ordering actions like `:rw`) and throws a structured error
210
+ when the input is malformed.
211
+
212
+ ```js
213
+ import { Pubky, validateCapabilities, AuthFlowKind } from "@synonymdev/pubky";
214
+
215
+ const pubky = new Pubky();
216
+
217
+ const rawCaps = formData.get("caps");
218
+
219
+ try {
220
+ const caps = validateCapabilities(rawCaps ?? "");
221
+ const flow = pubky.startAuthFlow(caps, AuthFlowKind::signin());
222
+ renderQr(flow.authorizationUrl);
223
+ const session = await flow.awaitApproval();
224
+ // ...
225
+ } catch (error) {
226
+ if (error.name === "InvalidInput") {
227
+ surfaceValidationError(error.message);
228
+ return;
229
+ }
230
+ throw error;
231
+ }
232
+ ```
233
+
234
+ On invalid input, `validateCapabilities` throws a `PubkyError` with
235
+ `{ name: "InvalidInput", message: "Invalid capability entries: …" }`, so you can
236
+ surface precise feedback to the user.
237
+
179
238
  #### Http Relay & reliability
180
239
 
181
240
  - If you don’t specify a relay, `PubkyAuthFlow` defaults to a Synonym-hosted relay. If that relay is down, logins won’t complete.
@@ -189,26 +248,37 @@ const session = await flow.awaitApproval();
189
248
  #### PublicStorage (read-only)
190
249
 
191
250
  ```js
192
- const pub = pubky.publicStorage();
251
+ const pub = pubky.publicStorage;
193
252
 
194
253
  // Reads
195
- await pub.getJson(`${userPk.z32()}/pub/example.com/data.json`);
196
- await pub.getText(`${userPk.z32()}/pub/example.com/readme.txt`);
197
- await pub.getBytes(`${userPk.z32()}/pub/example.com/icon.png`); // Uint8Array
254
+ const response = await pub.get(
255
+ `pubky${userPk.z32()}/pub/example.com/data.json`
256
+ ); // -> Response (stream it)
257
+ await pub.getJson(`pubky${userPk.z32()}/pub/example.com/data.json`);
258
+ await pub.getText(`pubky${userPk.z32()}/pub/example.com/readme.txt`);
259
+ await pub.getBytes(`pubky${userPk.z32()}/pub/example.com/icon.png`); // Uint8Array
198
260
 
199
261
  // Metadata
200
- await pub.exists(`${userPk.z32()}/pub/example.com/foo`); // boolean
201
- await pub.stats(`${userPk.z32()}/pub/example.com/foo`); // { content_length, content_type, etag, last_modified } | null
262
+ await pub.exists(`pubky${userPk.z32()}/pub/example.com/foo`); // boolean
263
+ await pub.stats(`pubky${userPk.z32()}/pub/example.com/foo`); // { content_length, content_type, etag, last_modified } | null
202
264
 
203
265
  // List directory (addressed path "<pubky>/pub/.../") must include trailing `/`.
204
266
  // list(addr, cursor=null|suffix|fullUrl, reverse=false, limit?, shallow=false)
205
- await pub.list(`${userPk.z32()}/pub/example.com/`, null, false, 100, false);
267
+ await pub.list(
268
+ `pubky${userPk.z32()}/pub/example.com/`,
269
+ null,
270
+ false,
271
+ 100,
272
+ false
273
+ );
206
274
  ```
207
275
 
276
+ Use `get()` when you need the raw `Response` for streaming or custom parsing.
277
+
208
278
  #### SessionStorage (read/write; uses cookies)
209
279
 
210
280
  ```js
211
- const s = session.storage();
281
+ const s = session.storage;
212
282
 
213
283
  // Writes
214
284
  await s.putJson("/pub/example.com/data.json", { ok: true });
@@ -216,6 +286,7 @@ await s.putText("/pub/example.com/note.txt", "hello");
216
286
  await s.putBytes("/pub/example.com/img.bin", new Uint8Array([1, 2, 3]));
217
287
 
218
288
  // Reads
289
+ const response = await s.get("/pub/example.com/data.json"); // -> Response (stream it)
219
290
  await s.getJson("/pub/example.com/data.json");
220
291
  await s.getText("/pub/example.com/note.txt");
221
292
  await s.getBytes("/pub/example.com/img.bin");
@@ -231,12 +302,14 @@ await s.list("/pub/example.com/", null, false, 100, false);
231
302
  await s.delete("/pub/example.com/data.json");
232
303
  ```
233
304
 
305
+ `get()` exposes the underlying `Response`, which is handy for streaming bodies or inspecting headers before consuming content.
306
+
234
307
  Path rules:
235
308
 
236
309
  - Session storage uses **absolute** paths like `"/pub/app/file.txt"`.
237
- - Public storage uses **addressed** form `<user>/pub/app/file.txt` (or `pubky://<user>/...`).
310
+ - Public storage uses **addressed** form `pubky<user>/pub/app/file.txt` (preferred) or `pubky://<user>/...`.
238
311
 
239
- **Convention:** put your app’s public data under a domain-like folder in `/pub`, e.g. `/pub/mycoolnew.app/`.
312
+ **Convention:** put your app’s public data under a domain-like folder in `/pub`, e.g. `/pub/my-new-app/`.
240
313
 
241
314
  ---
242
315
 
@@ -250,26 +323,60 @@ import { Pubky, PublicKey, Keypair } from "@synonymdev/pubky";
250
323
  const pubky = new Pubky();
251
324
 
252
325
  // Read-only resolver
253
- const resolver = pubky.pkdns();
254
- const homeserver = await resolver.getHomeserverOf(PublicKey.from("<user-z32>")); // string | undefined
326
+ const homeserver = await pubky.getHomeserverOf(PublicKey.from("<user-z32>")); // string | undefined
255
327
 
256
328
  // With keys (signer-bound)
257
329
  const signer = pubky.signer(Keypair.random());
258
330
 
259
331
  // Republish if missing or stale (reuses current host unless overridden)
260
- await signer.pkdns().publishHomeserverIfStale();
332
+ await signer.pkdns.publishHomeserverIfStale();
261
333
  // Or force an override now:
262
- await signer.pkdns().publishHomeserverForce(/* optional override homeserver*/);
334
+ await signer.pkdns.publishHomeserverForce(/* optional override homeserver*/);
335
+ // Resolve your own homeserver:
336
+ await signer.pkdns.getHomeserver();
337
+ ```
338
+
339
+ ## Logging
340
+
341
+ 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.
342
+
343
+ ```js
344
+ import { setLogLevel } from "@synonymdev/pubky";
345
+
346
+ setLogLevel("debug"); // "error" | "warn" | "info" | "debug" | "trace"
347
+ ```
348
+
349
+ If the logger is already initialized, calling `setLogLevel` again will throw. Pick the most verbose level (`"debug"` or `"trace"`) while developing to see pkarr resolution, network requests and storage operations in the console.
350
+
351
+ #### Resolve `pubky` identifiers into transport URLs
352
+
353
+ Use `resolvePubky()` when you need to feed an addressed resource into a raw HTTP client:
354
+
355
+ ```js
356
+ import { resolvePubky } from "@synonymdev/pubky";
357
+
358
+ const identifier =
359
+ "pubkyoperrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0033X02JAN0SG";
360
+ const url = resolvePubky(identifier);
361
+ // -> "https://_pubky.operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0033X02JAN0SG"
263
362
  ```
264
363
 
364
+ Both `pubky<pk>/…` (preferred) and `pubky://<pk>/…` resolve to the same HTTPS endpoint.
365
+
366
+ ---
367
+
368
+ ## WASM memory (`free()` helpers)
369
+
370
+ `wasm-bindgen` generates `free()` methods on exported classes (for example `Pubky`, `AuthFlow` `PublicKey`). JavaScript's GC eventually releases the underlying Rust structs on its own, but calling `free()` lets you drop them **immediately** if you are creating many short-lived instances (e.g. in a long-running worker). It is safe to skip manual frees in typical browser or Node apps.
371
+
265
372
  ---
266
373
 
267
374
  ## Errors
268
375
 
269
- All async methods throw a structured `PubkyJsError`:
376
+ All async methods throw a structured `PubkyError`:
270
377
 
271
378
  ```ts
272
- type PubkyJsError = {
379
+ interface PubkyError extends Error {
273
380
  name:
274
381
  | "RequestError" // network/server/validation/JSON
275
382
  | "InvalidInput"
@@ -277,8 +384,8 @@ type PubkyJsError = {
277
384
  | "PkarrError"
278
385
  | "InternalError";
279
386
  message: string;
280
- statusCode?: number; // present for HTTP server errors (4xx/5xx)
281
- };
387
+ data?: unknown; // structured context when available (e.g. { statusCode: number })
388
+ }
282
389
  ```
283
390
 
284
391
  Example:
@@ -287,28 +394,43 @@ Example:
287
394
  try {
288
395
  await publicStorage.getJson(`${pk}/pub/example.com/missing.json`);
289
396
  } catch (e) {
290
- if (e.name === "RequestError" && e.statusCode === 404) {
397
+ const error = e as PubkyError;
398
+ if (
399
+ error.name === "RequestError" &&
400
+ typeof error.data === "object" &&
401
+ error.data !== null &&
402
+ "statusCode" in error.data &&
403
+ typeof (error.data as { statusCode?: number }).statusCode === "number" &&
404
+ (error.data as { statusCode?: number }).statusCode === 404
405
+ ) {
291
406
  // handle not found
292
407
  }
293
408
  }
294
409
  ```
295
410
 
411
+ ## Browser environment notes
412
+
413
+ - 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.
414
+ - 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).
415
+ - When troubleshooting auth/session caching: open a fresh incognito window, clear site data for the target origin, and verify the request includes credentials.
416
+
296
417
  ---
297
418
 
298
419
  ## Local Test & Development
299
420
 
300
421
  For test and development, you can run a local homeserver in a test network.
301
422
 
302
- 1. Install Rust (for wasm builds):
423
+ 1. Install Rust (for wasm and testnet builds):
303
424
 
304
425
  ```bash
305
426
  curl https://sh.rustup.rs -sSf | sh
306
427
  ```
307
428
 
308
- 2. Run the local testnet:
429
+ 2. Install and run the local testnet:
309
430
 
310
431
  ```bash
311
- npm run testnet
432
+ cargo install pubky-testnet
433
+ pubky-testnet
312
434
  ```
313
435
 
314
436
  3. Point the SDK at testnet: