@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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@synonymdev/pubky",
3
3
  "type": "module",
4
4
  "description": "Decentralized identity, auth flows, and user-owned storage",
5
- "version": "0.15.0",
5
+ "version": "0.16.0",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "testnet": "cargo run -p pubky-testnet",
13
- "build:test": "tsc --project tsconfig.json && mkdir -p dist && cp index.js dist/index.js",
13
+ "build:test": "rm -rf dist/tests && tsc --project tsconfig.json && mkdir -p dist && cp index.js dist/index.js",
14
14
  "test": "npm run test-nodejs && npm run test-browser",
15
15
  "test-nodejs": "npm run build:test && node --require ./node-header.cjs node_modules/tape/bin/tape dist/tests/*.js -cov",
16
16
  "test-browser": "npm run build:test && browserify dist/tests/setup.js dist/tests/*.js -p esmify | npx tape-run",
package/pubky.d.ts CHANGED
@@ -12,6 +12,32 @@ export type Level = "error" | "warn" | "info" | "debug" | "trace";
12
12
  */
13
13
 
14
14
  export type ReadableStreamType = "bytes";
15
+
16
+ /** Offline recovery result. Check the identity before using the keys. */
17
+ export interface RecoveredEncryptionKeys {
18
+ /** Authenticated account public key, encoded as z-base-32. */
19
+ publicKey: string;
20
+ /** Authenticated grant identifier (jti). */
21
+ grantId: string;
22
+ /** Independently owned scoped keys. Call keys.free() when finished. */
23
+ keys: EncryptionKeys;
24
+ }
25
+
26
+
27
+
28
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
29
+ /** Root-signed grant claims. Public keys are z-base-32; times are Unix seconds. */
30
+ export interface VerifiedGrantClaims {
31
+ iss: string;
32
+ client_id: string;
33
+ caps: string[];
34
+ cnf: string;
35
+ jti: string;
36
+ iat: number;
37
+ exp: number;
38
+ }
39
+
40
+
15
41
  /**
16
42
  * A union type of all possible machine-readable codes for the `name` property
17
43
  * of a {@link PubkyError}.
@@ -63,6 +89,25 @@ export interface GrantAuthFlowOptions {
63
89
  * Optional return destinations for success, error, and cancellation.
64
90
  */
65
91
  xCallback?: XCallbackParams | null;
92
+ /**
93
+ * Relay approval format. `signedApprovalV1` delivers keys for approved `e` scopes
94
+ * and rejects bare-grant downgrades.
95
+ * Omitted or `bareGrant` selects a grant without an approval envelope.
96
+ * `signedApprovalV1` sets `af=v1` in the authorization link.
97
+ */
98
+ approvalFormat?: GrantApprovalFormat;
99
+ }
100
+
101
+ /**
102
+ * Options for verifying custom grant proofs.
103
+ */
104
+ export interface CustomPopVerificationOptions {
105
+ /**
106
+ * Allow the grant's issue time this many seconds ahead of the local clock.
107
+ * Defaults to 30; zero disables the allowance. Expiry remains strict.
108
+ * Must be an integer between 0 and 4294967295.
109
+ */
110
+ clockSkewSeconds?: number;
66
111
  }
67
112
 
68
113
  /**
@@ -96,6 +141,11 @@ export interface PubkyClientConfig {
96
141
  maxErrorBodyBytes?: number;
97
142
  }
98
143
 
144
+ /**
145
+ * Relay payload format understood by the requesting app.
146
+ */
147
+ export type GrantApprovalFormat = "bareGrant" | "signedApprovalV1";
148
+
99
149
  /**
100
150
  * Represents the standard error structure for all exceptions thrown by the Pubky
101
151
  * WASM client.
@@ -202,9 +252,50 @@ export interface ResourceStats {
202
252
  etag?: string;
203
253
  }
204
254
 
255
+ /**
256
+ * Self-contained credentials for offline custom-proof verification.
257
+ * Unknown fields are ignored so future bundles remain readable by older verifiers.
258
+ */
259
+ export interface CustomPop {
260
+ /**
261
+ * Original root-signed grant JWS.
262
+ */
263
+ grant: string;
264
+ /**
265
+ * Client-signed custom proof JWS.
266
+ */
267
+ pop: string;
268
+ }
269
+
270
+ /**
271
+ * Verified provenance and data; applications validate the data's meaning.
272
+ */
273
+ export interface VerifiedCustomPop {
274
+ /**
275
+ * Root public key encoded as z-base-32.
276
+ */
277
+ identity: string;
278
+ /**
279
+ * Root-signed claims; storage capabilities do not grant service permissions.
280
+ */
281
+ grantClaims: VerifiedGrantClaims;
282
+ /**
283
+ * Unix seconds at which the client signed the proof. No maximum age is enforced.
284
+ */
285
+ iat: number;
286
+ /**
287
+ * Random per-proof value. Record it until grant expiry to detect replays.
288
+ */
289
+ nonce: string;
290
+ /**
291
+ * Signed JSON data, without application-specific validation.
292
+ */
293
+ data: JsonValue;
294
+ }
295
+
205
296
  export type Address = `pubky${string}/pub/${string}` | `pubky://${string}/pub/${string}`;
206
297
 
207
- export type CapabilityAction = "r" | "w" | "rw";
298
+ export type CapabilityAction = "r" | "w" | "e" | "rw" | "re" | "we" | "rwe";
208
299
  export type CapabilityScope = `/${string}`;
209
300
  export type CapabilityEntry = `${CapabilityScope}:${CapabilityAction}`;
210
301
  type CapabilitiesTail = `,${CapabilityEntry}${string}`;
@@ -481,6 +572,16 @@ export class BrowserSessionStore {
481
572
  * Remove local stored session metadata and any SDK-owned delegated key for that record.
482
573
  */
483
574
  remove(id: string): Promise<void>;
575
+ /**
576
+ * Recover a stored session's encryption keys without network access.
577
+ * Verifies the signed approval, exact grant binding, and requested session
578
+ * identity. Works after grant expiry/revocation and without the delegated
579
+ * WebCrypto signing key.
580
+ * Restore material is stored as plaintext in IndexedDB.
581
+ * Bare-grant records return undefined; signed approvals without `e` scopes
582
+ * return an empty bundle. Creates no session.
583
+ */
584
+ restoreEncryptionKeys(id: string): Promise<EncryptionKeys | undefined>;
484
585
  /**
485
586
  * Restore a specific stored session by id.
486
587
  *
@@ -489,7 +590,11 @@ export class BrowserSessionStore {
489
590
  */
490
591
  restore(id: string): Promise<Session>;
491
592
  /**
492
- * Persist a completed grant session in IndexedDB.
593
+ * Persist a completed grant session and its optional signed approval.
594
+ * Confidential restore material is stored as plaintext in IndexedDB.
595
+ * Delegated signing keys remain non-extractable WebCrypto keys.
596
+ * Records are identified by user and grant ID. Reauthorizing the same
597
+ * client creates a separate record; use the returned ID to restore it.
493
598
  */
494
599
  save(session: Session): Promise<StoredSessionInfo>;
495
600
  }
@@ -617,6 +722,42 @@ export class DirectSignupDeepLink {
617
722
  readonly xCallback: XCallbackParams;
618
723
  }
619
724
 
725
+ /**
726
+ * Verified scoped encryption keys obtained from a session or offline recovery.
727
+ * This object owns its secrets independently of the session and grants no storage
728
+ * access. Call `free()` when finished; secret bytes are zeroized on free/drop.
729
+ */
730
+ export class EncryptionKeys {
731
+ private constructor();
732
+ free(): void;
733
+ [Symbol.dispose](): void;
734
+ /**
735
+ * Derive a non-extractable AES-GCM-256 key for WebCrypto encryption/decryption.
736
+ * Rejects directories and files outside approved scopes. Requires WebCrypto.
737
+ * Temporary JS key bytes are cleared after import, including on failure.
738
+ */
739
+ deriveCryptoKeyForPath(path: string): Promise<CryptoKey>;
740
+ /**
741
+ * Derive a 32-byte content key for a canonical decoded file path.
742
+ * Rejects directories and files outside the approved scopes. The caller
743
+ * owns the JS byte copy and should clear it after use.
744
+ */
745
+ deriveForPath(path: string): Uint8Array;
746
+ /**
747
+ * Recover authenticated identity and keys from an exportLocalSecret() token offline.
748
+ * Verifies the signed approval and grant binding even if the grant expired
749
+ * or was revoked. Bare-grant tokens return undefined; signed approvals
750
+ * without `e` scopes return a result with an empty key bundle.
751
+ * Check publicKey (z-base-32) against the expected account and grantId
752
+ * against the expected grant before using keys. Call keys.free() when done.
753
+ */
754
+ static fromLocalSecret(token: string): RecoveredEncryptionKeys | undefined;
755
+ /**
756
+ * Approved scopes; directory scopes cover descendant files.
757
+ */
758
+ readonly scopes: string[];
759
+ }
760
+
620
761
  /**
621
762
  * A single event from the event stream.
622
763
  *
@@ -867,7 +1008,8 @@ export class GrantAuthFlow {
867
1008
  * Resume a previously saved pending grant auth flow (standalone).
868
1009
  * Prefer `pubky.resumeGrantAuthFlow()` to reuse a facade client.
869
1010
  *
870
- * **Security:** `savedState` contains the relay secret and PoP client private key.
1011
+ * **Security:** `savedState` contains the PoP client private key and either
1012
+ * a shared relay secret or a temporary HPKE recipient secret.
871
1013
  * Store it only temporarily and delete it once the flow completes or is abandoned.
872
1014
  *
873
1015
  * @param {string} savedState A string produced by `grantFlow.saveLocal()`.
@@ -877,9 +1019,9 @@ export class GrantAuthFlow {
877
1019
  /**
878
1020
  * Save sensitive state required to resume this delegated pending grant flow.
879
1021
  *
880
- * This does not export the delegated private key, but it includes the relay
881
- * secret in the authorization URL. Store it only temporarily and delete it
882
- * once the flow completes or is abandoned.
1022
+ * This includes either a shared relay secret or a temporary HPKE recipient
1023
+ * secret. Store it only temporarily and delete it once the flow completes
1024
+ * or is abandoned.
883
1025
  */
884
1026
  saveDelegated(): string;
885
1027
  /**
@@ -1012,11 +1154,19 @@ export class GrantSession {
1012
1154
  private constructor();
1013
1155
  free(): void;
1014
1156
  [Symbol.dispose](): void;
1157
+ /**
1158
+ * Sign JSON data and return both the custom proof and its root-signed grant.
1159
+ * Data follows `JSON.stringify` semantics, e.g. `undefined` properties are omitted.
1160
+ * Each proof carries its signing time (`iat`) and a random `nonce`.
1161
+ * Makes no network requests. Your applications handle freshness and replay protection.
1162
+ */
1163
+ createCustomPop(data: JsonValue): Promise<CustomPop>;
1015
1164
  /**
1016
1165
  * Export the portable local secret material needed to restore this grant session.
1017
1166
  *
1018
1167
  * Treat the returned string as bearer-equivalent secret material until the
1019
- * grant expires or is revoked.
1168
+ * grant expires or is revoked. Included scoped keys remain sensitive after
1169
+ * expiry or revocation.
1020
1170
  *
1021
1171
  * @returns {Promise<string>}
1022
1172
  */
@@ -1033,6 +1183,15 @@ export class GrantSession {
1033
1183
  * @returns {Promise<GrantSessionInfo>}
1034
1184
  */
1035
1185
  sessionInfo(): Promise<GrantSessionInfo>;
1186
+ /**
1187
+ * Return scoped keys with the same derivation API as offline recovered keys.
1188
+ * Bare grants return `undefined`; signed approvals without `e` scopes
1189
+ * return an object with empty scopes.
1190
+ * Each access creates an owned copy. Keep it for repeated use and call
1191
+ * `free()` when finished. It remains usable after this view or its session
1192
+ * is freed, signed out, or revoked; freeing it does not affect the session.
1193
+ */
1194
+ readonly encryptionKeys: EncryptionKeys | undefined;
1036
1195
  }
1037
1196
 
1038
1197
  /**
@@ -1370,8 +1529,9 @@ export class Pubky {
1370
1529
  /**
1371
1530
  * Resume a previously saved pending grant auth flow.
1372
1531
  *
1373
- * **Security:** `savedState` contains the relay secret and PoP client private key.
1374
- * Delete it from storage as soon as the resumed flow completes.
1532
+ * **Security:** `savedState` contains the PoP client private key and either
1533
+ * a shared relay secret or a temporary HPKE private key. Delete it from
1534
+ * storage when the flow completes or is abandoned.
1375
1535
  *
1376
1536
  * @param {string} savedState A string produced by `grantFlow.saveLocal()`.
1377
1537
  * @returns {GrantAuthFlow} A flow reconnected to the original relay channel.
@@ -1430,11 +1590,13 @@ export class Pubky {
1430
1590
  *
1431
1591
  * Grant auth uses a user-signed grant JWS plus Proof-of-Possession and
1432
1592
  * returns a self-refreshing session.
1593
+ * Select `approvalFormat: "signedApprovalV1"` and include `e` on scopes that need content
1594
+ * keys. Storage `r` and `w` alone do not deliver encryption keys.
1433
1595
  *
1434
1596
  * @param {string} capabilities Comma-separated caps, e.g. `"/pub/app/:rw,/pub/foo/file:r"`.
1435
1597
  * @param {AuthFlowKind} kind The kind of authentication flow to perform.
1436
1598
  * @param {GrantAuthFlowOptions} options Options for the grant flow:
1437
- * `{ clientId, relay?, xCallback? }`.
1599
+ * `{ clientId, relay?, xCallback?, approvalFormat? }`.
1438
1600
  * @returns {Promise<GrantAuthFlow>}
1439
1601
  * A running grant auth flow. Show `authorizationUrl` as QR/deeplink,
1440
1602
  * then `awaitApproval()` to obtain a grant-backed `Session`.
@@ -2001,7 +2163,8 @@ export class Signer {
2001
2163
  /**
2002
2164
  * Blocking sign-in. Waits for PKDNS publish to complete (slower; safer).
2003
2165
  *
2004
- * Creates a valid grant-backed homeserver Session with root capabilities.
2166
+ * Creates a grant-backed homeserver session with root storage access (`/:rw`).
2167
+ * Encryption keys require an explicit `e` request through a signed approval flow.
2005
2168
  * `clientId` is shown in the user's grant/session list.
2006
2169
  * @param {string} clientId App identifier, typically a domain.
2007
2170
  *
@@ -2023,7 +2186,8 @@ export class Signer {
2023
2186
  /**
2024
2187
  * Fast sign-in for a returning user. Publishes PKDNS in the background.
2025
2188
  *
2026
- * Creates a valid grant-backed homeserver Session with root capabilities.
2189
+ * Creates a grant-backed homeserver session with root storage access (`/:rw`).
2190
+ * Encryption keys require an explicit `e` request through a signed approval flow.
2027
2191
  * `clientId` is shown in the user's grant/session list.
2028
2192
  * @param {string} clientId App identifier, typically a domain.
2029
2193
  *
@@ -2108,6 +2272,10 @@ export class SigninGrantDeepLink {
2108
2272
  * @returns {string}
2109
2273
  */
2110
2274
  toString(): string;
2275
+ /**
2276
+ * Negotiated approval format: `bareGrant` or `signedApprovalV1`.
2277
+ */
2278
+ readonly approvalFormat: string;
2111
2279
  /**
2112
2280
  * Base HTTP relay inbox URL used by this auth request.
2113
2281
  *
@@ -2133,13 +2301,21 @@ export class SigninGrantDeepLink {
2133
2301
  */
2134
2302
  readonly clientPublicKey: PublicKey;
2135
2303
  /**
2136
- * Relay channel secret embedded in the deeplink.
2304
+ * The 32-byte ephemeral HPKE public key from `epk`, or `undefined`
2305
+ * for a shared-secret link. The relay channel ID is the unpadded
2306
+ * base64url encoding of BLAKE3("pubky-grant-relay-channel-v1" || 0x00 || epk),
2307
+ * where `||` concatenates bytes and `epk` is the raw 32-byte public key.
2137
2308
  *
2138
- * Treat this as sensitive temporary auth-flow material.
2309
+ * @returns {Uint8Array|undefined}
2310
+ */
2311
+ readonly ephemeralPublicKey: Uint8Array | undefined;
2312
+ /**
2313
+ * Shared relay secret, or `undefined` for an HPKE link.
2314
+ * Keep this secret confidential until the flow completes or is abandoned.
2139
2315
  *
2140
- * @returns {Uint8Array}
2316
+ * @returns {Uint8Array|undefined}
2141
2317
  */
2142
- readonly secret: Uint8Array;
2318
+ readonly secret: Uint8Array | undefined;
2143
2319
  /**
2144
2320
  * Optional x-callback-url metadata carried by this deep link.
2145
2321
  */
@@ -2187,6 +2363,10 @@ export class SignupGrantDeepLink {
2187
2363
  * @returns {string}
2188
2364
  */
2189
2365
  toString(): string;
2366
+ /**
2367
+ * Negotiated approval format: `bareGrant` or `signedApprovalV1`.
2368
+ */
2369
+ readonly approvalFormat: string;
2190
2370
  /**
2191
2371
  * Base HTTP relay inbox URL used by this auth request.
2192
2372
  *
@@ -2211,6 +2391,15 @@ export class SignupGrantDeepLink {
2211
2391
  * @returns {PublicKey}
2212
2392
  */
2213
2393
  readonly clientPublicKey: PublicKey;
2394
+ /**
2395
+ * The 32-byte ephemeral HPKE public key from `epk`, or `undefined`
2396
+ * for a shared-secret link. The relay channel ID is the unpadded
2397
+ * base64url encoding of BLAKE3("pubky-grant-relay-channel-v1" || 0x00 || epk),
2398
+ * where `||` concatenates bytes and `epk` is the raw 32-byte public key.
2399
+ *
2400
+ * @returns {Uint8Array|undefined}
2401
+ */
2402
+ readonly ephemeralPublicKey: Uint8Array | undefined;
2214
2403
  /**
2215
2404
  * Homeserver public key requested for account creation.
2216
2405
  *
@@ -2218,13 +2407,12 @@ export class SignupGrantDeepLink {
2218
2407
  */
2219
2408
  readonly homeserver: PublicKey;
2220
2409
  /**
2221
- * Relay channel secret embedded in the deeplink.
2222
- *
2223
- * Treat this as sensitive temporary auth-flow material.
2410
+ * Shared relay secret, or `undefined` for an HPKE link.
2411
+ * Keep this secret confidential until the flow completes or is abandoned.
2224
2412
  *
2225
- * @returns {Uint8Array}
2413
+ * @returns {Uint8Array|undefined}
2226
2414
  */
2227
- readonly secret: Uint8Array;
2415
+ readonly secret: Uint8Array | undefined;
2228
2416
  /**
2229
2417
  * Optional signup token included in the request.
2230
2418
  *
@@ -2362,7 +2550,8 @@ export function setLogLevel(level: Level): void;
2362
2550
  /**
2363
2551
  * Validate and normalize a capabilities string.
2364
2552
  *
2365
- * - Normalizes action order (`wr` -> `rw`)
2553
+ * - Normalizes action order (`wr` -> `rw`, `ewr` -> `rwe`).
2554
+ * - `e` requests scoped encryption/decryption keys in V1 grant approvals.
2366
2555
  * - Throws `InvalidInput` identifying the first malformed entry.
2367
2556
  *
2368
2557
  * @param {string} input
@@ -2371,3 +2560,10 @@ export function setLogLevel(level: Level): void;
2371
2560
  * The error's `data` field is `{ invalidEntries: string[] }` containing the malformed token.
2372
2561
  */
2373
2562
  export function validateCapabilities(input: string): string;
2563
+
2564
+ /**
2565
+ * Verify both signatures, grant binding, and grant validity against the local clock.
2566
+ * Allows 30 seconds of future clock skew by default; never extends grant expiry.
2567
+ * Does not check replay, application authorization, or homeserver revocation.
2568
+ */
2569
+ export function verifyCustomGrantPop(credentials: CustomPop, options?: CustomPopVerificationOptions | null): VerifiedCustomPop;
package/pubky_bg.wasm CHANGED
Binary file