@abraca/nuxt 2.24.0 → 2.26.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 +12 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/components/ADocumentTree.vue +19 -1
- package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
- package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
- package/dist/runtime/components/settings/ASettingsProfilePanel.vue +3 -2
- package/dist/runtime/components/settings/ASettingsSecurityPanel.vue +3 -2
- package/dist/runtime/composables/useChildTree.js +15 -1
- package/dist/runtime/middleware/abracadabra-auth.js +4 -3
- package/dist/runtime/plugin-abracadabra.client.js +107 -62
- package/dist/runtime/plugin-abracadabra.server.js +21 -1
- package/dist/runtime/types.d.ts +16 -1
- package/package.json +1 -1
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
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,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed, nextTick, shallowRef, toRaw, watch } from "vue";
|
|
3
|
+
import { useVirtualizer } from "@tanstack/vue-virtual";
|
|
3
4
|
import { useAbracadabra, useTrash, useChat, useVoice, useToast, useAppConfig, useSyncedMap, useDocImport, useDocExport, useWindowManager, useAwareness } from "#imports";
|
|
4
5
|
import { resolveDocType, getAvailableDocTypes } from "../utils/docTypes";
|
|
5
6
|
import { avatarBorderStyle } from "../utils/avatarStyle";
|
|
@@ -225,6 +226,20 @@ const flatItems = computed(() => {
|
|
|
225
226
|
}
|
|
226
227
|
return result;
|
|
227
228
|
});
|
|
229
|
+
const scrollParent = ref(null);
|
|
230
|
+
const ROW_HEIGHT = 32;
|
|
231
|
+
const rowVirtualizer = useVirtualizer(
|
|
232
|
+
computed(() => ({
|
|
233
|
+
count: flatItems.value.length,
|
|
234
|
+
getScrollElement: () => scrollParent.value,
|
|
235
|
+
estimateSize: () => ROW_HEIGHT,
|
|
236
|
+
overscan: 12,
|
|
237
|
+
getItemKey: (index) => flatItems.value[index]?.id ?? index
|
|
238
|
+
}))
|
|
239
|
+
);
|
|
240
|
+
const virtualRows = computed(
|
|
241
|
+
() => rowVirtualizer.value.getVirtualItems().map((vRow) => ({ vRow, item: flatItems.value[vRow.index], idx: vRow.index })).filter((r) => r.item)
|
|
242
|
+
);
|
|
228
243
|
function getItemIcon(item) {
|
|
229
244
|
const meta = item.meta;
|
|
230
245
|
if (meta?.icon && typeof meta.icon === "string") return `i-lucide-${meta.icon}`;
|
|
@@ -983,6 +998,9 @@ function onTreeDragOver(e) {
|
|
|
983
998
|
onImportDragOver(e);
|
|
984
999
|
}
|
|
985
1000
|
const focusedIndex = ref(-1);
|
|
1001
|
+
watch(focusedIndex, (i) => {
|
|
1002
|
+
if (i >= 0 && i < flatItems.value.length) rowVirtualizer.value.scrollToIndex(i, { align: "auto" });
|
|
1003
|
+
});
|
|
986
1004
|
function onTreeKeydown(e) {
|
|
987
1005
|
const items = flatItems.value;
|
|
988
1006
|
if (!items.length) return;
|
|
@@ -1117,7 +1135,7 @@ defineExpose({
|
|
|
1117
1135
|
|
|
1118
1136
|
<div
|
|
1119
1137
|
v-else-if="!collapsed"
|
|
1120
|
-
class="flex flex-col min-h-0"
|
|
1138
|
+
class="flex flex-col min-h-0 h-full"
|
|
1121
1139
|
:class="externalDragActive ? 'ring-2 ring-inset ring-(--ui-primary)/30 rounded-(--ui-radius) bg-(--ui-primary)/3' : ''"
|
|
1122
1140
|
tabindex="0"
|
|
1123
1141
|
role="tree"
|
|
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
|
|
|
12
12
|
awareness: boolean;
|
|
13
13
|
tag: "video" | "audio";
|
|
14
14
|
live: boolean;
|
|
15
|
-
controls: boolean;
|
|
16
15
|
total: boolean;
|
|
16
|
+
controls: boolean;
|
|
17
17
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
18
|
declare const _default: typeof __VLS_export;
|
|
19
19
|
export default _default;
|
|
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
|
|
|
12
12
|
awareness: boolean;
|
|
13
13
|
tag: "video" | "audio";
|
|
14
14
|
live: boolean;
|
|
15
|
-
controls: boolean;
|
|
16
15
|
total: boolean;
|
|
16
|
+
controls: boolean;
|
|
17
17
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
18
|
declare const _default: typeof __VLS_export;
|
|
19
19
|
export default _default;
|
|
@@ -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">
|
|
@@ -23,9 +23,23 @@ export function useChildTree(rootDoc, parentDocId, options) {
|
|
|
23
23
|
}
|
|
24
24
|
return result;
|
|
25
25
|
});
|
|
26
|
+
const childrenByParent = computed(() => {
|
|
27
|
+
const map = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const e of entries.value) {
|
|
29
|
+
let arr = map.get(e.parentId);
|
|
30
|
+
if (!arr) {
|
|
31
|
+
arr = [];
|
|
32
|
+
map.set(e.parentId, arr);
|
|
33
|
+
}
|
|
34
|
+
arr.push(e);
|
|
35
|
+
}
|
|
36
|
+
for (const arr of map.values()) arr.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
37
|
+
return map;
|
|
38
|
+
});
|
|
26
39
|
function childrenOf(parentId) {
|
|
27
40
|
const target = parentId === null ? parentDocId : parentId;
|
|
28
|
-
|
|
41
|
+
const bucket = childrenByParent.value.get(target);
|
|
42
|
+
return bucket ? bucket.slice() : [];
|
|
29
43
|
}
|
|
30
44
|
function descendantsOf(id) {
|
|
31
45
|
const result = [];
|
|
@@ -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
|
|
5
|
+
const abra = useAbracadabra();
|
|
6
6
|
const config = useRuntimeConfig();
|
|
7
7
|
const loginPath = config.public?.abracadabra?.auth?.loginPath ?? "/login";
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
847
|
+
const sign = _signChallenge;
|
|
848
|
+
if (pubKey && sign) {
|
|
812
849
|
try {
|
|
813
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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 {
|