@abraca/nuxt 2.24.0 → 2.25.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/module.d.mts CHANGED
@@ -77,6 +77,7 @@ declare module '@nuxt/schema' {
77
77
  resetToken: string | null;
78
78
  verifyToken: string | null;
79
79
  };
80
+ inviteQueryKey: string | false;
80
81
  schema: {
81
82
  validate: boolean;
82
83
  migrateOnRead: boolean;
@@ -301,6 +302,17 @@ interface ModuleOptions {
301
302
  /** Query param holding the email-verification token. Default: 'verify_token'. */
302
303
  verifyToken?: string | null;
303
304
  };
305
+ /**
306
+ * Query/hash param the client plugin sniffs at boot for an invite code to
307
+ * redeem during identity registration (`?invite=CODE`). Set to `false` to
308
+ * disable sniffing entirely — REQUIRED for apps that use an `invite` query
309
+ * param for their own flows (e.g. arcana's admin-setup deep link), because
310
+ * the boot-time guest registration would otherwise redeem the code on a
311
+ * throwaway soft identity before the app's flow runs.
312
+ *
313
+ * Default: `'invite'`.
314
+ */
315
+ inviteQueryKey?: string | false;
304
316
  /**
305
317
  * Optional `@abraca/schema` integration.
306
318
  *
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "2.24.0",
7
+ "version": "2.25.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -64,6 +64,7 @@ const module$1 = defineNuxtModule({
64
64
  resetToken: "reset_token",
65
65
  verifyToken: "verify_token"
66
66
  },
67
+ inviteQueryKey: "invite",
67
68
  schema: {
68
69
  validate: false,
69
70
  migrateOnRead: false,
@@ -112,6 +113,7 @@ const module$1 = defineNuxtModule({
112
113
  resetToken: options.authQueryKeys?.resetToken ?? "reset_token",
113
114
  verifyToken: options.authQueryKeys?.verifyToken ?? "verify_token"
114
115
  },
116
+ inviteQueryKey: options.inviteQueryKey ?? "invite",
115
117
  schema: {
116
118
  validate: options.schema?.validate ?? false,
117
119
  migrateOnRead: options.schema?.migrateOnRead ?? false,
@@ -11,7 +11,8 @@ const {
11
11
  userNeutralColorName,
12
12
  setNeutralColor,
13
13
  publicKeyB64,
14
- isClaimed
14
+ isClaimed,
15
+ identityState
15
16
  } = useAbracadabra();
16
17
  const { copy, copied } = useClipboard();
17
18
  const nameInput = ref(userName.value);
@@ -138,7 +139,7 @@ function setFont(value) {
138
139
  :icon="isClaimed ? 'i-lucide-shield-check' : 'i-lucide-shield-off'"
139
140
  :color="isClaimed ? 'success' : 'neutral'"
140
141
  variant="subtle"
141
- :label="isClaimed ? 'Hardware Protected' : 'Guest (Soft Key)'"
142
+ :label="isClaimed ? 'Hardware Protected' : identityState === 'account' ? 'Password Account' : 'Guest (Soft Key)'"
142
143
  />
143
144
  </ClientOnly>
144
145
  </div>
@@ -4,6 +4,7 @@ import { useAbracadabra } from "../../composables/useAbracadabra";
4
4
  import { usePasskeyAccounts } from "../../composables/usePasskeyAccounts";
5
5
  const {
6
6
  isClaimed,
7
+ identityState,
7
8
  claimAccount,
8
9
  loginWithHardware,
9
10
  logout,
@@ -98,10 +99,10 @@ const showLogoutConfirm = ref(false);
98
99
  />
99
100
  <div class="flex-1 min-w-0">
100
101
  <p class="text-sm font-medium">
101
- {{ isClaimed ? "Hardware Protected" : "Guest Identity" }}
102
+ {{ isClaimed ? "Hardware Protected" : identityState === "account" ? "Password Account" : "Guest Identity" }}
102
103
  </p>
103
104
  <p class="text-xs text-(--ui-text-muted)">
104
- {{ isClaimed ? "Bound to hardware security enclave." : "Key stored in localStorage only." }}
105
+ {{ isClaimed ? "Bound to hardware security enclave." : identityState === "account" ? "Signed in with a password. Add a passkey for hardware protection." : "Key stored in localStorage only." }}
105
106
  </p>
106
107
  </div>
107
108
  <div class="flex gap-2 shrink-0">
@@ -1,11 +1,12 @@
1
1
  import { defineNuxtRouteMiddleware, useRuntimeConfig, navigateTo } from "#imports";
2
2
  import { useAbracadabra } from "../composables/useAbracadabra.js";
3
- export default defineNuxtRouteMiddleware((to) => {
3
+ export default defineNuxtRouteMiddleware(async (to) => {
4
4
  if (import.meta.server) return;
5
- const { isReady, publicKeyB64 } = useAbracadabra();
5
+ const abra = useAbracadabra();
6
6
  const config = useRuntimeConfig();
7
7
  const loginPath = config.public?.abracadabra?.auth?.loginPath ?? "/login";
8
- if (isReady?.value && !publicKeyB64?.value) {
8
+ await abra.whenReady?.();
9
+ if (!abra.publicKeyB64?.value) {
9
10
  return navigateTo({ path: loginPath, query: { redirect: to.fullPath } });
10
11
  }
11
12
  });
@@ -138,6 +138,7 @@ const STORAGE_KEY_EXTERNAL_PLUGINS = "abracadabra_external_plugins";
138
138
  const STORAGE_KEY_DISABLED_BUILTINS = "abracadabra_disabled_builtins";
139
139
  const CLAIMED_FLAG_KEY = "abracadabra_was_claimed";
140
140
  const PUBKEY_KEY = "abracadabra_pubkey";
141
+ const ACCOUNT_SESSION_KEY = "abracadabra_account_session";
141
142
  const CURRENT_SERVER_KEY = "abracadabra_current_server";
142
143
  const REQUIRE_PASSKEY_KEY = "abracadabra_require_passkey";
143
144
  const SERVERS_KEY = "abracadabra_servers";
@@ -372,6 +373,7 @@ export default defineNuxtPlugin({
372
373
  let _initPromise = null;
373
374
  let _wsp = null;
374
375
  let _tokenManager = null;
376
+ let _signChallenge = null;
375
377
  let authFailureCount = 0;
376
378
  const AUTH_FAILURE_LIMIT = 3;
377
379
  let lastClientIds = /* @__PURE__ */ new Set();
@@ -479,6 +481,10 @@ export default defineNuxtPlugin({
479
481
  if (userStatusText.value) payload.statusText = userStatusText.value;
480
482
  provider.value?.setAwarenessField("user", payload);
481
483
  }
484
+ watch(
485
+ [userName, userColor, publicKeyB64, userStatusIcon, userStatusText],
486
+ () => publishAwarenessUser()
487
+ );
482
488
  function setUserName(name) {
483
489
  userName.value = name;
484
490
  localStorage.setItem("abracadabra_username", name);
@@ -538,6 +544,14 @@ export default defineNuxtPlugin({
538
544
  connectionError.value = null;
539
545
  _wsp.connect();
540
546
  }
547
+ function whenReady() {
548
+ const p = _initPromise;
549
+ if (!p) return Promise.resolve();
550
+ return p.then(
551
+ () => _initPromise !== p ? whenReady() : void 0,
552
+ () => void 0
553
+ );
554
+ }
541
555
  function _teardown() {
542
556
  if (_wsp) {
543
557
  _wsp.disconnect();
@@ -559,6 +573,7 @@ export default defineNuxtPlugin({
559
573
  }
560
574
  client.value = null;
561
575
  keystore.value = null;
576
+ _signChallenge = null;
562
577
  isReady.value = false;
563
578
  synced.value = false;
564
579
  status.value = "disconnected";
@@ -737,6 +752,7 @@ export default defineNuxtPlugin({
737
752
  localStorage.removeItem("abracadabra_username");
738
753
  localStorage.removeItem(CLAIMED_FLAG_KEY);
739
754
  localStorage.removeItem(PUBKEY_KEY);
755
+ localStorage.removeItem(ACCOUNT_SESSION_KEY);
740
756
  window.location.reload();
741
757
  }
742
758
  async function logoutServer() {
@@ -766,11 +782,31 @@ export default defineNuxtPlugin({
766
782
  await client.value.requestPasswordReset(opts);
767
783
  addLog("Password reset email requested", "auth");
768
784
  }
785
+ async function _adoptAccountSession(fallbackName) {
786
+ hasPassword.value = true;
787
+ if (identityState.value !== "claimed") {
788
+ identityState.value = "account";
789
+ try {
790
+ localStorage.setItem(ACCOUNT_SESSION_KEY, "1");
791
+ } catch {
792
+ }
793
+ }
794
+ try {
795
+ const me = await client.value?.getMe();
796
+ const name = me?.displayName || me?.username || fallbackName;
797
+ if (name) {
798
+ userName.value = name;
799
+ localStorage.setItem("abracadabra_username", name);
800
+ publishAwarenessUser();
801
+ }
802
+ } catch {
803
+ }
804
+ }
769
805
  async function loginWithPassword(opts) {
770
806
  if (!client.value) throw new Error("Not connected");
771
807
  await client.value.login(opts);
772
808
  addLog(`Logged in as ${opts.username}`, "auth");
773
- hasPassword.value = true;
809
+ await _adoptAccountSession(opts.username);
774
810
  if (_wsp) {
775
811
  _wsp.disconnect();
776
812
  setTimeout(() => _wsp?.connect(), 300);
@@ -781,7 +817,7 @@ export default defineNuxtPlugin({
781
817
  await client.value.register(opts);
782
818
  await client.value.login({ username: opts.username, password: opts.password });
783
819
  addLog(`Registered + logged in as ${opts.username}`, "auth");
784
- hasPassword.value = true;
820
+ await _adoptAccountSession(opts.displayName || opts.username);
785
821
  if (_wsp) {
786
822
  _wsp.disconnect();
787
823
  setTimeout(() => _wsp?.connect(), 300);
@@ -808,19 +844,10 @@ export default defineNuxtPlugin({
808
844
  await client.value.redeemInvite(code);
809
845
  addLog(`Invite redeemed: ${code}`, "auth");
810
846
  const pubKey = publicKeyB64.value;
811
- if (pubKey) {
847
+ const sign = _signChallenge;
848
+ if (pubKey && sign) {
812
849
  try {
813
- const ed = await import("@noble/ed25519");
814
- const storedPrivKey = localStorage.getItem("abracadabra_privkey");
815
- await client.value.loginWithKey(pubKey, async (ch) => {
816
- if (isClaimed.value && keystore.value) return keystore.value.sign(ch);
817
- if (storedPrivKey) {
818
- const privKey = fromBase64Url(storedPrivKey);
819
- const sig = await ed.sign(fromBase64Url(ch), privKey);
820
- return toBase64Url(sig);
821
- }
822
- throw new Error("No signing key available");
823
- });
850
+ await client.value.loginWithKey(pubKey, sign);
824
851
  if (_wsp) {
825
852
  _wsp.disconnect();
826
853
  setTimeout(() => _wsp?.connect(), 300);
@@ -881,10 +908,11 @@ export default defineNuxtPlugin({
881
908
  async function init(serverUrl) {
882
909
  currentServerUrl.value = serverUrl;
883
910
  addLog("Initializing Abracadabra...", "system");
884
- if (!pendingInviteCode.value) {
911
+ const inviteQueryKey = abraConfig.inviteQueryKey ?? "invite";
912
+ if (inviteQueryKey !== false && !pendingInviteCode.value) {
885
913
  const urlParams = new URLSearchParams(window.location.search);
886
914
  const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ""));
887
- const code = urlParams.get("invite") || hashParams.get("invite");
915
+ const code = urlParams.get(inviteQueryKey) || hashParams.get(inviteQueryKey);
888
916
  if (code) {
889
917
  pendingInviteCode.value = code.toUpperCase();
890
918
  addLog(`Invite code found in URL: ${pendingInviteCode.value}`, "auth");
@@ -989,6 +1017,7 @@ export default defineNuxtPlugin({
989
1017
  }
990
1018
  throw new Error("No identity available for signing");
991
1019
  };
1020
+ _signChallenge = signChallengeFn;
992
1021
  const _client = new AbracadabraClient({ url: serverUrl, persistAuth, storageKey: authStorageKey });
993
1022
  client.value = _client;
994
1023
  addLog(`Server: ${serverUrl}`, "connection");
@@ -1001,6 +1030,13 @@ export default defineNuxtPlugin({
1001
1030
  tm.on("session-expired", () => {
1002
1031
  addLog("Device session rejected \u2014 please re-authenticate", "auth");
1003
1032
  if (identityState.value === "claimed") identityState.value = "needsReauth";
1033
+ else if (identityState.value === "account") {
1034
+ try {
1035
+ localStorage.removeItem(ACCOUNT_SESSION_KEY);
1036
+ } catch {
1037
+ }
1038
+ identityState.value = "guest";
1039
+ }
1004
1040
  });
1005
1041
  if (tm.hasSession && !_client.isTokenValid()) {
1006
1042
  try {
@@ -1011,9 +1047,11 @@ export default defineNuxtPlugin({
1011
1047
  }
1012
1048
  }
1013
1049
  let useExistingToken = _client.isTokenValid();
1050
+ let resumedMe = null;
1014
1051
  if (useExistingToken) {
1015
1052
  try {
1016
1053
  const me = await _client.getMe();
1054
+ resumedMe = me ?? null;
1017
1055
  hasPassword.value = !!me?.hasPassword;
1018
1056
  } catch (e) {
1019
1057
  const status2 = e?.status ?? 0;
@@ -1034,11 +1072,28 @@ export default defineNuxtPlugin({
1034
1072
  if (useExistingToken && identityState.value === "claimed") {
1035
1073
  addLog("Resumed session from persisted token", "auth");
1036
1074
  } else if (useExistingToken) {
1037
- addLog("Resumed session from persisted token", "auth");
1075
+ if (localStorage.getItem(ACCOUNT_SESSION_KEY) === "1") {
1076
+ identityState.value = "account";
1077
+ const name = resumedMe?.displayName || resumedMe?.username;
1078
+ if (name) {
1079
+ userName.value = name;
1080
+ localStorage.setItem("abracadabra_username", name);
1081
+ }
1082
+ addLog("Resumed account session from persisted token", "auth");
1083
+ } else {
1084
+ addLog("Resumed session from persisted token", "auth");
1085
+ }
1038
1086
  } else if (identityState.value === "claimed" && !requirePasskeyOnLogin.value) {
1039
1087
  identityState.value = "needsReauth";
1040
1088
  addLog("Session expired \u2014 passkey sign-in available", "auth");
1041
1089
  } else {
1090
+ if (localStorage.getItem(ACCOUNT_SESSION_KEY) === "1") {
1091
+ try {
1092
+ localStorage.removeItem(ACCOUNT_SESSION_KEY);
1093
+ } catch {
1094
+ }
1095
+ addLog("Account session could not be refreshed \u2014 continuing as guest", "auth");
1096
+ }
1042
1097
  try {
1043
1098
  await _client.registerWithKey({
1044
1099
  publicKey: pubKey,
@@ -1288,23 +1343,6 @@ export default defineNuxtPlugin({
1288
1343
  });
1289
1344
  _provider.attach();
1290
1345
  provider.value = _provider;
1291
- try {
1292
- const onPointerLeave = (e) => {
1293
- if (e.relatedTarget !== null) return;
1294
- try {
1295
- _provider.awareness?.setLocalStateField("hover", null);
1296
- } catch {
1297
- }
1298
- };
1299
- document.addEventListener("pointerleave", onPointerLeave);
1300
- window.addEventListener("beforeunload", () => {
1301
- try {
1302
- _provider.awareness?.setLocalState(null);
1303
- } catch {
1304
- }
1305
- });
1306
- } catch {
1307
- }
1308
1346
  try {
1309
1347
  const { FileBlobStore } = await import("@abraca/dabra");
1310
1348
  const blobStore = new FileBlobStore(serverUrl, _client);
@@ -1321,20 +1359,7 @@ export default defineNuxtPlugin({
1321
1359
  _provider.ready.then(() => {
1322
1360
  isReady.value = true;
1323
1361
  }, () => null);
1324
- watch(
1325
- [userName, userColor, publicKeyB64, userStatusIcon, userStatusText],
1326
- () => {
1327
- const payload = {
1328
- name: userName.value,
1329
- color: userColor.value,
1330
- publicKey: publicKeyB64.value
1331
- };
1332
- if (userStatusIcon.value) payload.statusIcon = userStatusIcon.value;
1333
- if (userStatusText.value) payload.statusText = userStatusText.value;
1334
- _provider.setAwarenessField("user", payload);
1335
- },
1336
- { immediate: true }
1337
- );
1362
+ publishAwarenessUser();
1338
1363
  addLog("Provider attached, connecting...", "connection");
1339
1364
  } catch (e) {
1340
1365
  addLog(`Error: ${e instanceof Error ? e.message : String(e)}`, "system");
@@ -1411,7 +1436,8 @@ export default defineNuxtPlugin({
1411
1436
  recoverIdentity,
1412
1437
  unblockConnection,
1413
1438
  broadcastSyncActive: useBroadcastSync().isActive,
1414
- init
1439
+ init,
1440
+ whenReady
1415
1441
  };
1416
1442
  nuxtApp.provide("abracadabra", abra);
1417
1443
  if (debug) {
@@ -1440,11 +1466,27 @@ export default defineNuxtPlugin({
1440
1466
  }
1441
1467
  }
1442
1468
  }
1469
+ function _onVisibilityChange() {
1470
+ if (document.visibilityState === "visible") _kickConnection();
1471
+ }
1443
1472
  window.addEventListener("focus", _kickConnection);
1444
1473
  window.addEventListener("online", _kickConnection);
1445
- document.addEventListener("visibilitychange", () => {
1446
- if (document.visibilityState === "visible") _kickConnection();
1447
- });
1474
+ document.addEventListener("visibilitychange", _onVisibilityChange);
1475
+ function _onPointerLeave(e) {
1476
+ if (e.relatedTarget !== null) return;
1477
+ try {
1478
+ provider.value?.awareness?.setLocalStateField("hover", null);
1479
+ } catch {
1480
+ }
1481
+ }
1482
+ function _onBeforeUnload() {
1483
+ try {
1484
+ provider.value?.awareness?.setLocalState(null);
1485
+ } catch {
1486
+ }
1487
+ }
1488
+ document.addEventListener("pointerleave", _onPointerLeave);
1489
+ window.addEventListener("beforeunload", _onBeforeUnload);
1448
1490
  loadServers();
1449
1491
  let deepLinkServer = "";
1450
1492
  try {
@@ -1490,6 +1532,7 @@ export default defineNuxtPlugin({
1490
1532
  }
1491
1533
  }
1492
1534
  const shortcuts = registry.getAllKeyboardShortcuts();
1535
+ let removeKeydown = null;
1493
1536
  if (shortcuts.length > 0) {
1494
1537
  const handleKey = (e) => {
1495
1538
  for (const s of shortcuts) {
@@ -1506,17 +1549,19 @@ export default defineNuxtPlugin({
1506
1549
  }
1507
1550
  };
1508
1551
  document.addEventListener("keydown", handleKey);
1509
- nuxtApp.hook("app:beforeMount", () => {
1510
- nuxtApp.hook("app:suspense:resolve", () => {
1511
- });
1512
- });
1513
- const origUnmount = nuxtApp._cleanup;
1514
- nuxtApp._cleanup = () => {
1515
- document.removeEventListener("keydown", handleKey);
1516
- teardowns.forEach((fn) => fn());
1517
- origUnmount?.();
1518
- };
1552
+ removeKeydown = () => document.removeEventListener("keydown", handleKey);
1519
1553
  }
1554
+ const origUnmount = nuxtApp._cleanup;
1555
+ nuxtApp._cleanup = () => {
1556
+ removeKeydown?.();
1557
+ teardowns.forEach((fn) => fn());
1558
+ window.removeEventListener("focus", _kickConnection);
1559
+ window.removeEventListener("online", _kickConnection);
1560
+ document.removeEventListener("visibilitychange", _onVisibilityChange);
1561
+ document.removeEventListener("pointerleave", _onPointerLeave);
1562
+ window.removeEventListener("beforeunload", _onBeforeUnload);
1563
+ origUnmount?.();
1564
+ };
1520
1565
  }).catch((e) => console.error("[abracadabra] boot error:", e));
1521
1566
  }
1522
1567
  });
@@ -31,10 +31,16 @@ export default defineNuxtPlugin({
31
31
  userNeutralColorName: ref("zinc"),
32
32
  userCount: ref(0),
33
33
  publicKeyB64: ref(""),
34
+ identityState: ref("initializing"),
34
35
  isClaimed: ref(false),
35
36
  identityLost: ref(false),
36
37
  needsReauth: ref(false),
37
38
  requirePasskeyOnLogin: ref(false),
39
+ effectiveRole: ref(null),
40
+ unsyncedChanges: ref(0),
41
+ userStatusIcon: ref(""),
42
+ userStatusText: ref(""),
43
+ userStatusAsAvatar: ref(false),
38
44
  // Servers / spaces
39
45
  currentServerUrl: ref(""),
40
46
  savedServers: ref([]),
@@ -52,11 +58,24 @@ export default defineNuxtPlugin({
52
58
  setUserColor: noop,
53
59
  setNeutralColor: noop,
54
60
  setRequirePasskey: noop,
61
+ setUserStatusIcon: noop,
62
+ setUserStatusText: noop,
63
+ setUserStatusAsAvatar: noop,
55
64
  reconnect: noop,
56
65
  removeServer: noop,
66
+ unblockConnection: noop,
57
67
  claimAccount: noopAsync,
58
68
  loginWithHardware: noopAsync,
69
+ loginWithPassword: noopAsync,
70
+ registerWithPassword: noopAsync,
59
71
  logout: noopAsync,
72
+ logoutServer: noopAsync,
73
+ logoutAll: noopAsync,
74
+ requestPasswordReset: noopAsync,
75
+ confirmPasswordReset: noopAsync,
76
+ changePassword: noopAsync,
77
+ setPassword: noopAsync,
78
+ recoverIdentity: async () => false,
60
79
  addServer: noopAsync,
61
80
  switchServer: noopAsync,
62
81
  switchSpace: noopAsync,
@@ -70,7 +89,8 @@ export default defineNuxtPlugin({
70
89
  revokeInvite: noopAsync,
71
90
  hasPassword: ref(false),
72
91
  refreshHasPassword: noopAsync,
73
- init: noopAsync
92
+ init: noopAsync,
93
+ whenReady: () => Promise.resolve()
74
94
  });
75
95
  }
76
96
  });
@@ -378,15 +378,22 @@ export declare function deriveColorFromPubKey(pubKey: string): {
378
378
  * Transitions:
379
379
  * initializing → guest (soft-key generated)
380
380
  * initializing → claimed (passkey restored from cache)
381
+ * initializing → account (password-account JWT resumed from storage)
381
382
  * initializing → identityLost (was claimed but cached key missing)
382
383
  * guest → claimed (claimAccount succeeds)
384
+ * guest → account (loginWithPassword / registerWithPassword succeeds)
385
+ * account → guest (device session rejected / token unrecoverable)
383
386
  * claimed → needsReauth (token expired, passkey re-auth available)
384
387
  * claimed → identityLost (passkey/key cache lost)
385
388
  * needsReauth → claimed (loginWithHardware succeeds)
386
389
  * → connectionBlocked (AUTH_FAILURE_LIMIT reached or permanent denial)
387
390
  * connectionBlocked → (previous) (unblockConnection called)
391
+ *
392
+ * `account` = authenticated against a server account via password (JWT-backed,
393
+ * no local hardware key). The local soft key keeps identifying the client for
394
+ * CRDT/awareness purposes; REST authorization uses the account JWT.
388
395
  */
389
- export type IdentityState = 'initializing' | 'guest' | 'claimed' | 'identityLost' | 'needsReauth' | 'connectionBlocked';
396
+ export type IdentityState = 'initializing' | 'guest' | 'claimed' | 'account' | 'identityLost' | 'needsReauth' | 'connectionBlocked';
390
397
  /** Injected into nuxtApp as $abracadabra */
391
398
  export interface AbracadabraState {
392
399
  doc: ShallowRef<Y.Doc>;
@@ -526,6 +533,14 @@ export interface AbracadabraState {
526
533
  unblockConnection: () => void;
527
534
  broadcastSyncActive: Ref<boolean>;
528
535
  init: (serverUrl?: string) => Promise<void>;
536
+ /**
537
+ * Resolves once the current `init()` settles (following any re-inits that
538
+ * happen while waiting). Never rejects — init failures resolve too, so
539
+ * route guards can `await whenReady()` and then branch on state
540
+ * (`publicKeyB64`, `effectiveRole`, …) instead of racing a half-booted
541
+ * plugin. On the server this resolves immediately.
542
+ */
543
+ whenReady: () => Promise<void>;
529
544
  }
530
545
  /** Nitro storage interface (subset used by runners) */
531
546
  export interface NitroStorage {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/nuxt",
3
- "version": "2.24.0",
3
+ "version": "2.25.0",
4
4
  "description": "First-class Nuxt module for the Abracadabra CRDT collaboration platform",
5
5
  "repository": "abracadabra/abracadabra-nuxt",
6
6
  "license": "MIT",