@abraca/dabra 2.21.0 → 2.22.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/dist/index.d.ts CHANGED
@@ -357,7 +357,14 @@ declare class AbracadabraClient {
357
357
  listKeys(): Promise<PublicKeyInfo[]>;
358
358
  /** Rename a registered device key. */
359
359
  renameKey(keyId: string, deviceName: string): Promise<void>;
360
- /** Revoke a public key by its ID. */
360
+ /**
361
+ * Revoke a public key by its ID. The server treats this as a panic
362
+ * action: every outstanding JWT for the account is invalidated (a stolen
363
+ * device's token must die immediately, and tokens carry no per-key
364
+ * claim), and a fresh token for THIS session is returned and adopted
365
+ * automatically — so the device performing the revocation stays
366
+ * authenticated while every other device silently re-auths.
367
+ */
361
368
  revokeKey(keyId: string): Promise<void>;
362
369
  /** Create a single-use device invite code for pairing a new device to this account. */
363
370
  createDeviceInvite(opts?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "2.21.0",
3
+ "version": "2.22.0",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -41,7 +41,7 @@
41
41
  "yjs": "^13.6.8"
42
42
  },
43
43
  "devDependencies": {
44
- "@abraca/schema": "2.21.0"
44
+ "@abraca/schema": "2.22.0"
45
45
  },
46
46
  "scripts": {
47
47
  "test": "node --no-warnings --conditions=source --experimental-transform-types --test 'tests/*.test.ts'"
@@ -31,6 +31,21 @@ function fromBase64(b64: string): Uint8Array {
31
31
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
32
32
  }
33
33
 
34
+ /**
35
+ * Decode a base64url-encoded string (e.g. a JWT header/payload segment) to
36
+ * UTF-8 text. `atob` only accepts standard base64, so we translate the
37
+ * url-safe alphabet (`-`/`_`) and re-pad before decoding — otherwise a
38
+ * perfectly valid token whose payload happens to contain `-` or `_` throws
39
+ * and gets misclassified as invalid/expired.
40
+ */
41
+ function decodeBase64UrlToString(b64url: string): string {
42
+ let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
43
+ const pad = b64.length % 4;
44
+ if (pad) b64 += "=".repeat(4 - pad);
45
+ const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
46
+ return new TextDecoder().decode(bytes);
47
+ }
48
+
34
49
  /**
35
50
  * Reason classifications surfaced to `onAuthFailed`. Consumers can decide
36
51
  * whether to silently re-register the keypair (`user_not_found`) or
@@ -142,7 +157,10 @@ export class AbracadabraClient {
142
157
  if (!this._token) return false;
143
158
  try {
144
159
  const [, payload] = this._token.split(".");
145
- const { exp } = JSON.parse(atob(payload));
160
+ // JWT payloads are base64url — `atob` only handles standard base64,
161
+ // so a payload containing `-` or `_` would throw and a valid token
162
+ // would be misread as expired, forcing a spurious re-auth.
163
+ const { exp } = JSON.parse(decodeBase64UrlToString(payload));
146
164
  return typeof exp === "number" && exp * 1000 > Date.now();
147
165
  } catch {
148
166
  return false;
@@ -262,9 +280,23 @@ export class AbracadabraClient {
262
280
  await this.request("PATCH", `/auth/keys/${encodeURIComponent(keyId)}`, { body: { deviceName } });
263
281
  }
264
282
 
265
- /** Revoke a public key by its ID. */
283
+ /**
284
+ * Revoke a public key by its ID. The server treats this as a panic
285
+ * action: every outstanding JWT for the account is invalidated (a stolen
286
+ * device's token must die immediately, and tokens carry no per-key
287
+ * claim), and a fresh token for THIS session is returned and adopted
288
+ * automatically — so the device performing the revocation stays
289
+ * authenticated while every other device silently re-auths.
290
+ */
266
291
  async revokeKey(keyId: string): Promise<void> {
267
- await this.request("DELETE", `/auth/keys/${encodeURIComponent(keyId)}`);
292
+ const res = await this.request<{ token?: string } | null>(
293
+ "DELETE",
294
+ `/auth/keys/${encodeURIComponent(keyId)}`,
295
+ );
296
+ // Older servers respond 204 with no body — keep working against them.
297
+ if (res && typeof res === "object" && typeof res.token === "string") {
298
+ this.token = res.token;
299
+ }
268
300
  }
269
301
 
270
302
  // ── Device Invites ───────────────────────────────────────────────────────
@@ -14,7 +14,12 @@ import type { CryptoIdentityKeystore } from "./CryptoIdentityKeystore.ts";
14
14
  const HKDF_INFO = new TextEncoder().encode("abracadabra-dockey-v1");
15
15
 
16
16
  function fromBase64(b64: string): Uint8Array {
17
- return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
17
+ // Accept both standard base64 and base64url, padded or not — server
18
+ // payloads mix the two alphabets depending on the field.
19
+ let normalized = b64.replace(/-/g, "+").replace(/_/g, "/");
20
+ const pad = normalized.length % 4;
21
+ if (pad) normalized += "=".repeat(4 - pad);
22
+ return Uint8Array.from(atob(normalized), (c) => c.charCodeAt(0));
18
23
  }
19
24
 
20
25
  export class DocKeyManager {
@@ -84,7 +89,7 @@ export class DocKeyManager {
84
89
  const myKeys = await client.listUserKeys(me.id);
85
90
  const primaryKey = myKeys[0];
86
91
  if (primaryKey?.x25519Key) {
87
- const x25519Pub = fromBase64(primaryKey.x25519Key.replace(/-/g, "+").replace(/_/g, "/"));
92
+ const x25519Pub = fromBase64(primaryKey.x25519Key);
88
93
  const rewrapped = await this.wrapKeyForRecipient(docKey, x25519Pub, docId);
89
94
  const b64 = (() => {
90
95
  let s = "";
@@ -270,7 +270,15 @@ export class TokenManager extends EventEmitter {
270
270
  try {
271
271
  const [, payload] = jwt.split(".");
272
272
  if (!payload) return null;
273
- const { exp } = JSON.parse(atob(payload));
273
+ // base64url-safe: `atob` alone throws on `-`/`_`, which would make
274
+ // the proactive-refresh timer treat a valid token as unparseable.
275
+ let b64 = payload.replace(/-/g, "+").replace(/_/g, "/");
276
+ const pad = b64.length % 4;
277
+ if (pad) b64 += "=".repeat(4 - pad);
278
+ const json = new TextDecoder().decode(
279
+ Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)),
280
+ );
281
+ const { exp } = JSON.parse(json);
274
282
  return typeof exp === "number" ? exp : null;
275
283
  } catch {
276
284
  return null;