@glyphteck/veyl 0.66.3 → 0.67.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/account.js CHANGED
@@ -7281,7 +7281,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
7281
7281
  close(vaultAccess);
7282
7282
  close(vaultSigner);
7283
7283
  close(localCache);
7284
- pending.push(lockWallet(wallet));
7284
+ lockWallet(wallet);
7285
7285
  lockChat(chatPrivateKey, chatPK);
7286
7286
  cleanBytes(chatSigningSecret, notificationPrivateKey, notificationPublicKey);
7287
7287
  return Promise.allSettled(pending).then(() => {
@@ -7857,6 +7857,8 @@ function settingsState(settings = defaultSettings) {
7857
7857
  var defaultUser = {
7858
7858
  uid: null,
7859
7859
  authReady: false,
7860
+ authSessionReady: false,
7861
+ authSessionActive: false,
7860
7862
  authCredential: null,
7861
7863
  profileReady: false,
7862
7864
  username: null,
@@ -7878,12 +7880,16 @@ var defaultUser = {
7878
7880
  settingsReady: false,
7879
7881
  settings: settingsState()
7880
7882
  };
7883
+ function isAccountAuthenticationError(error) {
7884
+ const code = typeof error?.code === "string" ? error.code.toLowerCase() : "";
7885
+ return code === "permission-denied" || code.endsWith("/permission-denied") || code === "unauthenticated" || code.endsWith("/unauthenticated");
7886
+ }
7881
7887
  function createUser({ cloud, network, avatarCache = null, diag = null, onSessionRevoked = null }) {
7882
7888
  if (!cloud) {
7883
7889
  throw new Error("createUser requires { cloud }");
7884
7890
  }
7885
- if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove, avatarCache.removeAll].some((method) => typeof method !== "function")) {
7886
- throw new Error("avatarCache requires read, write, remove, and removeAll");
7891
+ if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove].some((method) => typeof method !== "function")) {
7892
+ throw new Error("avatarCache requires read, write, and remove");
7887
7893
  }
7888
7894
  let activeNetwork = network;
7889
7895
  let state = defaultUser;
@@ -8021,9 +8027,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8021
8027
  return;
8022
8028
  runCacheTask("user.avatar.cache.remove", () => avatarCache.remove(uid));
8023
8029
  }
8024
- function clearCachedAvatars() {
8025
- runCacheTask("user.avatar.cache.clear", () => avatarCache.removeAll());
8026
- }
8027
8030
  async function fetchAvatar(uid, { version = null, force = false, clear = false, persist = true } = {}) {
8028
8031
  if (!uid)
8029
8032
  return;
@@ -8106,6 +8109,54 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8106
8109
  function isCurrentUser(uid, session) {
8107
8110
  return started && authSession === session && userUid === uid;
8108
8111
  }
8112
+ function revokeAuthentication(authUser, {
8113
+ code = "",
8114
+ local = false,
8115
+ reason = "session-inactive"
8116
+ } = {}) {
8117
+ if (revokingSession)
8118
+ return;
8119
+ revokingSession = true;
8120
+ let callback;
8121
+ try {
8122
+ callback = onSessionRevoked?.({
8123
+ uid: authUser.uid,
8124
+ code,
8125
+ reason
8126
+ });
8127
+ } catch (error) {
8128
+ callback = Promise.reject(error);
8129
+ }
8130
+ markDiag(diag, "user.auth.revoked", {
8131
+ code,
8132
+ local,
8133
+ reason
8134
+ });
8135
+ setState((user) => ({
8136
+ ...user,
8137
+ authSessionReady: true,
8138
+ authSessionActive: false,
8139
+ authCredential: null
8140
+ }));
8141
+ let operation = Promise.resolve(callback).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error));
8142
+ if (!local) {
8143
+ operation = operation.then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error));
8144
+ }
8145
+ operation.finally(() => {
8146
+ if (userUid === authUser.uid) {
8147
+ revokingSession = false;
8148
+ }
8149
+ });
8150
+ }
8151
+ function revokeAuthenticationError(authUser, error, reason) {
8152
+ if (!isAccountAuthenticationError(error))
8153
+ return false;
8154
+ revokeAuthentication(authUser, {
8155
+ code: error?.code || "",
8156
+ reason
8157
+ });
8158
+ return true;
8159
+ }
8109
8160
  function watchUser(authUser) {
8110
8161
  if (!started)
8111
8162
  return;
@@ -8115,7 +8166,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8115
8166
  closeUserWatches();
8116
8167
  markDiag(diag, "user.auth.state", { signedIn: !!authUser });
8117
8168
  if (!authUser) {
8118
- const signedOutUid = userUid;
8119
8169
  revokingSession = false;
8120
8170
  userUid = null;
8121
8171
  clearSettingsKey(settingsKey);
@@ -8124,17 +8174,11 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8124
8174
  agreementStored = null;
8125
8175
  agreementOverride = null;
8126
8176
  agreementAcceptance = null;
8127
- if (signedOutUid) {
8128
- clearCachedAvatars();
8129
- }
8130
8177
  avatarFetch = { uid: null, key: null, promise: null };
8131
8178
  setState({ ...defaultUser, authReady: true });
8132
8179
  return;
8133
8180
  }
8134
8181
  const authUidChanged = userUid !== authUser.uid;
8135
- if (userUid && authUidChanged) {
8136
- clearCachedAvatars();
8137
- }
8138
8182
  userUid = authUser.uid;
8139
8183
  if (authUidChanged) {
8140
8184
  clearSettingsKey(settingsKey);
@@ -8164,32 +8208,28 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8164
8208
  userWatches.push(cloud.auth.session.watch(authUser.uid, current((authSessionState) => {
8165
8209
  if (authSessionState?.active) {
8166
8210
  const authCredential = authSessionState.authCredential;
8167
- setState((user) => user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : { ...user, authCredential });
8168
- return;
8169
- }
8170
- if (revokingSession)
8171
- return;
8172
- revokingSession = true;
8173
- if (cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation)) {
8174
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).finally(() => {
8175
- if (userUid === authUser.uid) {
8176
- revokingSession = false;
8177
- }
8211
+ setState((user) => user.authSessionReady && user.authSessionActive && user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : {
8212
+ ...user,
8213
+ authSessionReady: true,
8214
+ authSessionActive: true,
8215
+ authCredential
8178
8216
  });
8179
8217
  return;
8180
8218
  }
8181
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error)).finally(() => {
8182
- if (userUid === authUser.uid) {
8183
- revokingSession = false;
8184
- }
8219
+ revokeAuthentication(authUser, {
8220
+ local: cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation) === true,
8221
+ reason: "session-inactive"
8185
8222
  });
8186
8223
  }), current((error) => {
8187
8224
  markError(diag, "user.session.listen", authStartedAt, error);
8225
+ revokeAuthenticationError(authUser, error, "session-listener");
8188
8226
  })));
8189
8227
  userWatches.push(cloud.user.admin.watch(authUser.uid, current((allowed) => {
8190
8228
  setState((user) => ({ ...user, isAdmin: allowed, adminReady: true }));
8191
8229
  }), current((error) => {
8192
8230
  markError(diag, "user.admin.listen", authStartedAt, error);
8231
+ if (revokeAuthenticationError(authUser, error, "admin-listener"))
8232
+ return;
8193
8233
  setState((user) => ({ ...user, isAdmin: false, adminReady: true }));
8194
8234
  })));
8195
8235
  userWatches.push(cloud.user.private.watch(authUser.uid, current((privateData, info = {}) => {
@@ -8213,6 +8253,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8213
8253
  }));
8214
8254
  }), current((error) => {
8215
8255
  markError(diag, "user.settings.snapshot", authStartedAt, error);
8256
+ if (revokeAuthenticationError(authUser, error, "settings-listener"))
8257
+ return;
8216
8258
  settingsStored = null;
8217
8259
  agreementStored = null;
8218
8260
  setState((user) => ({
@@ -8225,6 +8267,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8225
8267
  setState((user) => ({ ...user, banned: banned ?? null }));
8226
8268
  }), current((error) => {
8227
8269
  markError(diag, "user.moderation.listen", authStartedAt, error);
8270
+ if (revokeAuthenticationError(authUser, error, "moderation-listener"))
8271
+ return;
8228
8272
  setState((user) => ({ ...user, banned: null }));
8229
8273
  })));
8230
8274
  userWatches.push(cloud.user.blocked.watch(authUser.uid, current((blocked, info = {}) => {
@@ -8237,6 +8281,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8237
8281
  });
8238
8282
  }), current((error) => {
8239
8283
  markError(diag, "user.blocked.listen", authStartedAt, error);
8284
+ if (revokeAuthenticationError(authUser, error, "blocked-listener"))
8285
+ return;
8240
8286
  setState((user) => ({ ...user, blockedReady: false }));
8241
8287
  })));
8242
8288
  userWatches.push(cloud.user.profile.watch(authUser.uid, current((profileData, info = {}) => {
@@ -8283,6 +8329,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8283
8329
  }
8284
8330
  }), current((error) => {
8285
8331
  markError(diag, "user.profile.snapshot", authStartedAt, error);
8332
+ if (revokeAuthenticationError(authUser, error, "profile-listener"))
8333
+ return;
8286
8334
  avatarFetch = { uid: null, key: null, promise: null };
8287
8335
  setState((user) => ({
8288
8336
  ...user,
@@ -41653,20 +41701,36 @@ function openAccount(options = {}) {
41653
41701
  let closePromise = null;
41654
41702
  let state;
41655
41703
  let chatSession = null;
41704
+ let revokedUid = null;
41705
+ let revocationPromise = null;
41656
41706
  let nextAgreement = agreementContract(options.agreement || CURRENT_AGREEMENT);
41657
41707
  const listeners = new Set;
41658
41708
  let activeWriteTail = Promise.resolve();
41659
41709
  const sessionCloseWork = new Set;
41660
41710
  const operationBarrier = createAccountOperationCloseBarrier();
41711
+ function revokeSession(details = {}) {
41712
+ const { uid = null } = details;
41713
+ const accountUid = uid || state?.uid || null;
41714
+ if (accountUid && revokedUid === accountUid) {
41715
+ return revocationPromise || Promise.resolve();
41716
+ }
41717
+ revokedUid = accountUid;
41718
+ let callback;
41719
+ try {
41720
+ callback = options.onSessionRevoked?.({ ...details, uid: accountUid });
41721
+ } catch (error) {
41722
+ callback = Promise.reject(error);
41723
+ }
41724
+ lock();
41725
+ revocationPromise = Promise.resolve(callback);
41726
+ return revocationPromise;
41727
+ }
41661
41728
  const user = options.user || createUser({
41662
41729
  cloud,
41663
41730
  network: options.network || defaultNetwork,
41664
41731
  avatarCache,
41665
41732
  diag,
41666
- onSessionRevoked: ({ uid } = {}) => {
41667
- lock();
41668
- options.onSessionRevoked?.({ uid: uid || state?.uid || null });
41669
- }
41733
+ onSessionRevoked: revokeSession
41670
41734
  });
41671
41735
  state = emptyState(user.getSnapshot(), options.network || defaultNetwork);
41672
41736
  const chat = createChatSession({
@@ -41915,6 +41979,24 @@ function openAccount(options = {}) {
41915
41979
  code: error?.code || "",
41916
41980
  message: error?.message || String(error)
41917
41981
  });
41982
+ if (isAccountAuthenticationError(error)) {
41983
+ revokeSession({
41984
+ uid,
41985
+ code: error?.code || "",
41986
+ reason: "vault-listener"
41987
+ }).catch((revocationError) => {
41988
+ diag?.("account.session.revoke.error", {
41989
+ code: revocationError?.code || "",
41990
+ message: revocationError?.message || String(revocationError)
41991
+ });
41992
+ }).then(() => cloud.auth.logout()).catch((logoutError) => {
41993
+ diag?.("account.session.revoke.auth.error", {
41994
+ code: logoutError?.code || "",
41995
+ message: logoutError?.message || String(logoutError)
41996
+ });
41997
+ });
41998
+ return;
41999
+ }
41918
42000
  lock();
41919
42001
  emit2({ vault: null, vaultReady: true, vaultError: error });
41920
42002
  });
@@ -249,6 +249,57 @@ function cleanBytes(...values) {
249
249
  }
250
250
  }
251
251
 
252
+ // ../../core/passkey.js
253
+ var PASSKEY_ENVIRONMENT_MISMATCH = "passkey-environment-mismatch";
254
+ var PASSKEY_PROVIDER_ALREADY_LINKED = "passkey-provider-already-linked";
255
+ var PASSKEY_REGISTER_INVALID = "passkey-register-invalid";
256
+ var PASSKEY_RP_MISMATCH = "passkey-rp-mismatch";
257
+ var PASSKEY_UNLINKED = "passkey-unlinked";
258
+ var PASSKEY_CANCELLED_CODES = new Set(["ERR_USER_CANCELLED"]);
259
+ var PASSKEY_CANCELLED_NAMES = new Set(["UserCancelledException"]);
260
+ var PASSKEY_CONTROLLED_ERRORS = new Set([
261
+ PASSKEY_ENVIRONMENT_MISMATCH,
262
+ PASSKEY_PROVIDER_ALREADY_LINKED,
263
+ PASSKEY_REGISTER_INVALID,
264
+ PASSKEY_RP_MISMATCH,
265
+ PASSKEY_UNLINKED
266
+ ]);
267
+ function canDeletePasskey(row, passkeys, currentPasskeyId) {
268
+ if (row?.type === "pending")
269
+ return true;
270
+ return row?.type === "passkey" && Array.isArray(passkeys) && passkeys.length > 1 && row.id !== cleanText(currentPasskeyId);
271
+ }
272
+ function passkeyError(code, message, cause) {
273
+ const error = new Error(message);
274
+ error.code = code;
275
+ error.cause = cause;
276
+ return error;
277
+ }
278
+ function normalizePasskeyLoginError(error, options = {}) {
279
+ if (options.localhostSilo === true && error?.code === "functions/failed-precondition" && cleanText(error?.message).includes("Localhost uses a separate passkey silo")) {
280
+ return passkeyError(PASSKEY_ENVIRONMENT_MISMATCH, "This passkey belongs to glyphteck.com, not localhost.", error);
281
+ }
282
+ if (error?.code === "functions/not-found") {
283
+ return passkeyError(PASSKEY_UNLINKED, "This passkey is no longer linked to an account.", error);
284
+ }
285
+ if (error?.code === "functions/failed-precondition") {
286
+ return passkeyError(PASSKEY_RP_MISMATCH, "This passkey belongs to a different Glyphteck passkey setup.", error);
287
+ }
288
+ return error;
289
+ }
290
+ function normalizePasskeyRegisterError(error, options = {}) {
291
+ if (error?.name === "CredentialAlreadyExistsException" || error?.name === "InvalidStateError" || error?.code === "functions/already-exists") {
292
+ return passkeyError(PASSKEY_PROVIDER_ALREADY_LINKED, "you already have a passkey for this account", error);
293
+ }
294
+ if (error?.code === "functions/failed-precondition") {
295
+ return passkeyError(PASSKEY_RP_MISMATCH, "This passkey belongs to a different Glyphteck passkey setup.", error);
296
+ }
297
+ if (options.invalidArgument === true && error?.code === "functions/invalid-argument" && error?.message) {
298
+ return passkeyError(PASSKEY_REGISTER_INVALID, error.message, error);
299
+ }
300
+ return error;
301
+ }
302
+
252
303
  // ../../core/account/profiles.js
253
304
  var ACCOUNT_PROFILES_VERSION = 1;
254
305
  var PROFILE_ID_RE = /^[0-9a-f]{32}$/;
@@ -606,7 +657,7 @@ function requirePorts(options) {
606
657
  onError: typeof options.onError === "function" ? options.onError : null
607
658
  };
608
659
  }
609
- async function resolveAuthenticatedProfile(owner, generation, reason) {
660
+ async function resolveAuthenticatedProfile(owner, generation, reason, details = {}) {
610
661
  ensureGeneration(owner, generation);
611
662
  const registry = owner.registry;
612
663
  const context = Object.freeze({
@@ -614,6 +665,7 @@ async function resolveAuthenticatedProfile(owner, generation, reason) {
614
665
  generation,
615
666
  reason,
616
667
  requestedProfileId: owner.requestedProfileId,
668
+ rejectedProfileId: details.rejectedProfileId || null,
617
669
  isCurrent: () => !owner.closing && !owner.closed && owner.generation === generation
618
670
  });
619
671
  const profileId = await owner.ports.resolveAuthenticatedProfile(registry, context);
@@ -981,7 +1033,9 @@ async function applyAuthenticatedProfile(owner, profile, generation, reason, opt
981
1033
  }
982
1034
  async function resumeAuthenticatedProfile(owner, generation, reason, options = {}) {
983
1035
  publish(owner, reason, null);
984
- const profile = await resolveAuthenticatedProfile(owner, generation, reason);
1036
+ const profile = await resolveAuthenticatedProfile(owner, generation, reason, {
1037
+ rejectedProfileId: options.rejectedProfileId
1038
+ });
985
1039
  return applyAuthenticatedProfile(owner, profile, generation, reason, options);
986
1040
  }
987
1041
  function transitionFailure(owner, error, generation, profileId) {
@@ -1009,12 +1063,15 @@ async function initialize(owner) {
1009
1063
  throw error;
1010
1064
  }
1011
1065
  }
1012
- function requestAuthenticatedProfileResume(owner) {
1066
+ function requestAuthenticatedProfileResume(owner, options = {}) {
1013
1067
  ensureReady(owner);
1068
+ const rejectedProfileId = options.rejectedProfileId == null ? null : readAccountProfileId(options.rejectedProfileId);
1014
1069
  const generation = requestSelection(owner, null, false);
1015
1070
  return enqueue(owner, async () => {
1016
1071
  try {
1017
- return await resumeAuthenticatedProfile(owner, generation, "restoring");
1072
+ return await resumeAuthenticatedProfile(owner, generation, "restoring", {
1073
+ rejectedProfileId
1074
+ });
1018
1075
  } catch (error) {
1019
1076
  if (!isStaleAccountProfileTransition(error))
1020
1077
  publish(owner, "error", error);
@@ -1353,7 +1410,7 @@ function createAccountProfiles(options = {}) {
1353
1410
  const generation = requestSelection(owner, profileId, false);
1354
1411
  return enqueue(owner, () => switchExisting(owner, profileId, generation));
1355
1412
  },
1356
- resumeAuthenticated: () => requestAuthenticatedProfileResume(owner),
1413
+ resumeAuthenticated: (options2) => requestAuthenticatedProfileResume(owner, options2),
1357
1414
  refresh: () => requestProfileRefresh(owner),
1358
1415
  signOut: (profileId) => requestProfileSignOut(owner, profileId),
1359
1416
  remove: (profileId) => requestProfileRemoval(owner, profileId),
package/dist/auth.js CHANGED
@@ -82,8 +82,8 @@ function normalizePasskeyLoginError(error, options = {}) {
82
82
  return error;
83
83
  }
84
84
  function normalizePasskeyRegisterError(error, options = {}) {
85
- if (error?.name === "InvalidStateError" || error?.code === "functions/already-exists") {
86
- return passkeyError(PASSKEY_PROVIDER_ALREADY_LINKED, "this provider already has a passkey for this account. choose another provider, device, or security key", error);
85
+ if (error?.name === "CredentialAlreadyExistsException" || error?.name === "InvalidStateError" || error?.code === "functions/already-exists") {
86
+ return passkeyError(PASSKEY_PROVIDER_ALREADY_LINKED, "you already have a passkey for this account", error);
87
87
  }
88
88
  if (error?.code === "functions/failed-precondition") {
89
89
  return passkeyError(PASSKEY_RP_MISMATCH, "This passkey belongs to a different Glyphteck passkey setup.", error);
package/dist/cli.js CHANGED
@@ -7922,7 +7922,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
7922
7922
  close(vaultAccess);
7923
7923
  close(vaultSigner);
7924
7924
  close(localCache);
7925
- pending.push(lockWallet(wallet));
7925
+ lockWallet(wallet);
7926
7926
  lockChat(chatPrivateKey, chatPK);
7927
7927
  cleanBytes(chatSigningSecret, notificationPrivateKey, notificationPublicKey);
7928
7928
  return Promise.allSettled(pending).then(() => {
@@ -8551,12 +8551,16 @@ function settingsState(settings = defaultSettings) {
8551
8551
  }
8552
8552
  };
8553
8553
  }
8554
+ function isAccountAuthenticationError(error) {
8555
+ const code = typeof error?.code === "string" ? error.code.toLowerCase() : "";
8556
+ return code === "permission-denied" || code.endsWith("/permission-denied") || code === "unauthenticated" || code.endsWith("/unauthenticated");
8557
+ }
8554
8558
  function createUser({ cloud, network, avatarCache = null, diag = null, onSessionRevoked = null }) {
8555
8559
  if (!cloud) {
8556
8560
  throw new Error("createUser requires { cloud }");
8557
8561
  }
8558
- if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove, avatarCache.removeAll].some((method) => typeof method !== "function")) {
8559
- throw new Error("avatarCache requires read, write, remove, and removeAll");
8562
+ if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove].some((method) => typeof method !== "function")) {
8563
+ throw new Error("avatarCache requires read, write, and remove");
8560
8564
  }
8561
8565
  let activeNetwork = network;
8562
8566
  let state = defaultUser;
@@ -8694,9 +8698,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8694
8698
  return;
8695
8699
  runCacheTask("user.avatar.cache.remove", () => avatarCache.remove(uid));
8696
8700
  }
8697
- function clearCachedAvatars() {
8698
- runCacheTask("user.avatar.cache.clear", () => avatarCache.removeAll());
8699
- }
8700
8701
  async function fetchAvatar(uid, { version = null, force = false, clear = false, persist = true } = {}) {
8701
8702
  if (!uid)
8702
8703
  return;
@@ -8779,6 +8780,54 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8779
8780
  function isCurrentUser(uid, session) {
8780
8781
  return started && authSession === session && userUid === uid;
8781
8782
  }
8783
+ function revokeAuthentication(authUser, {
8784
+ code = "",
8785
+ local = false,
8786
+ reason = "session-inactive"
8787
+ } = {}) {
8788
+ if (revokingSession)
8789
+ return;
8790
+ revokingSession = true;
8791
+ let callback;
8792
+ try {
8793
+ callback = onSessionRevoked?.({
8794
+ uid: authUser.uid,
8795
+ code,
8796
+ reason
8797
+ });
8798
+ } catch (error) {
8799
+ callback = Promise.reject(error);
8800
+ }
8801
+ markDiag(diag, "user.auth.revoked", {
8802
+ code,
8803
+ local,
8804
+ reason
8805
+ });
8806
+ setState((user) => ({
8807
+ ...user,
8808
+ authSessionReady: true,
8809
+ authSessionActive: false,
8810
+ authCredential: null
8811
+ }));
8812
+ let operation = Promise.resolve(callback).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error));
8813
+ if (!local) {
8814
+ operation = operation.then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error));
8815
+ }
8816
+ operation.finally(() => {
8817
+ if (userUid === authUser.uid) {
8818
+ revokingSession = false;
8819
+ }
8820
+ });
8821
+ }
8822
+ function revokeAuthenticationError(authUser, error, reason) {
8823
+ if (!isAccountAuthenticationError(error))
8824
+ return false;
8825
+ revokeAuthentication(authUser, {
8826
+ code: error?.code || "",
8827
+ reason
8828
+ });
8829
+ return true;
8830
+ }
8782
8831
  function watchUser(authUser) {
8783
8832
  if (!started)
8784
8833
  return;
@@ -8788,7 +8837,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8788
8837
  closeUserWatches();
8789
8838
  markDiag(diag, "user.auth.state", { signedIn: !!authUser });
8790
8839
  if (!authUser) {
8791
- const signedOutUid = userUid;
8792
8840
  revokingSession = false;
8793
8841
  userUid = null;
8794
8842
  clearSettingsKey(settingsKey);
@@ -8797,17 +8845,11 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8797
8845
  agreementStored = null;
8798
8846
  agreementOverride = null;
8799
8847
  agreementAcceptance = null;
8800
- if (signedOutUid) {
8801
- clearCachedAvatars();
8802
- }
8803
8848
  avatarFetch = { uid: null, key: null, promise: null };
8804
8849
  setState({ ...defaultUser, authReady: true });
8805
8850
  return;
8806
8851
  }
8807
8852
  const authUidChanged = userUid !== authUser.uid;
8808
- if (userUid && authUidChanged) {
8809
- clearCachedAvatars();
8810
- }
8811
8853
  userUid = authUser.uid;
8812
8854
  if (authUidChanged) {
8813
8855
  clearSettingsKey(settingsKey);
@@ -8837,32 +8879,28 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8837
8879
  userWatches.push(cloud.auth.session.watch(authUser.uid, current((authSessionState) => {
8838
8880
  if (authSessionState?.active) {
8839
8881
  const authCredential = authSessionState.authCredential;
8840
- setState((user) => user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : { ...user, authCredential });
8841
- return;
8842
- }
8843
- if (revokingSession)
8844
- return;
8845
- revokingSession = true;
8846
- if (cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation)) {
8847
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).finally(() => {
8848
- if (userUid === authUser.uid) {
8849
- revokingSession = false;
8850
- }
8882
+ setState((user) => user.authSessionReady && user.authSessionActive && user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : {
8883
+ ...user,
8884
+ authSessionReady: true,
8885
+ authSessionActive: true,
8886
+ authCredential
8851
8887
  });
8852
8888
  return;
8853
8889
  }
8854
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error)).finally(() => {
8855
- if (userUid === authUser.uid) {
8856
- revokingSession = false;
8857
- }
8890
+ revokeAuthentication(authUser, {
8891
+ local: cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation) === true,
8892
+ reason: "session-inactive"
8858
8893
  });
8859
8894
  }), current((error) => {
8860
8895
  markError(diag, "user.session.listen", authStartedAt, error);
8896
+ revokeAuthenticationError(authUser, error, "session-listener");
8861
8897
  })));
8862
8898
  userWatches.push(cloud.user.admin.watch(authUser.uid, current((allowed) => {
8863
8899
  setState((user) => ({ ...user, isAdmin: allowed, adminReady: true }));
8864
8900
  }), current((error) => {
8865
8901
  markError(diag, "user.admin.listen", authStartedAt, error);
8902
+ if (revokeAuthenticationError(authUser, error, "admin-listener"))
8903
+ return;
8866
8904
  setState((user) => ({ ...user, isAdmin: false, adminReady: true }));
8867
8905
  })));
8868
8906
  userWatches.push(cloud.user.private.watch(authUser.uid, current((privateData, info = {}) => {
@@ -8886,6 +8924,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8886
8924
  }));
8887
8925
  }), current((error) => {
8888
8926
  markError(diag, "user.settings.snapshot", authStartedAt, error);
8927
+ if (revokeAuthenticationError(authUser, error, "settings-listener"))
8928
+ return;
8889
8929
  settingsStored = null;
8890
8930
  agreementStored = null;
8891
8931
  setState((user) => ({
@@ -8898,6 +8938,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8898
8938
  setState((user) => ({ ...user, banned: banned ?? null }));
8899
8939
  }), current((error) => {
8900
8940
  markError(diag, "user.moderation.listen", authStartedAt, error);
8941
+ if (revokeAuthenticationError(authUser, error, "moderation-listener"))
8942
+ return;
8901
8943
  setState((user) => ({ ...user, banned: null }));
8902
8944
  })));
8903
8945
  userWatches.push(cloud.user.blocked.watch(authUser.uid, current((blocked, info = {}) => {
@@ -8910,6 +8952,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8910
8952
  });
8911
8953
  }), current((error) => {
8912
8954
  markError(diag, "user.blocked.listen", authStartedAt, error);
8955
+ if (revokeAuthenticationError(authUser, error, "blocked-listener"))
8956
+ return;
8913
8957
  setState((user) => ({ ...user, blockedReady: false }));
8914
8958
  })));
8915
8959
  userWatches.push(cloud.user.profile.watch(authUser.uid, current((profileData, info = {}) => {
@@ -8956,6 +9000,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8956
9000
  }
8957
9001
  }), current((error) => {
8958
9002
  markError(diag, "user.profile.snapshot", authStartedAt, error);
9003
+ if (revokeAuthenticationError(authUser, error, "profile-listener"))
9004
+ return;
8959
9005
  avatarFetch = { uid: null, key: null, promise: null };
8960
9006
  setState((user) => ({
8961
9007
  ...user,
@@ -9186,6 +9232,8 @@ var init_user = __esm(() => {
9186
9232
  defaultUser = {
9187
9233
  uid: null,
9188
9234
  authReady: false,
9235
+ authSessionReady: false,
9236
+ authSessionActive: false,
9189
9237
  authCredential: null,
9190
9238
  profileReady: false,
9191
9239
  username: null,
@@ -45674,20 +45722,36 @@ function openAccount(options = {}) {
45674
45722
  let closePromise = null;
45675
45723
  let state;
45676
45724
  let chatSession = null;
45725
+ let revokedUid = null;
45726
+ let revocationPromise = null;
45677
45727
  let nextAgreement = agreementContract(options.agreement || CURRENT_AGREEMENT);
45678
45728
  const listeners = new Set;
45679
45729
  let activeWriteTail = Promise.resolve();
45680
45730
  const sessionCloseWork = new Set;
45681
45731
  const operationBarrier = createAccountOperationCloseBarrier();
45732
+ function revokeSession(details = {}) {
45733
+ const { uid = null } = details;
45734
+ const accountUid = uid || state?.uid || null;
45735
+ if (accountUid && revokedUid === accountUid) {
45736
+ return revocationPromise || Promise.resolve();
45737
+ }
45738
+ revokedUid = accountUid;
45739
+ let callback;
45740
+ try {
45741
+ callback = options.onSessionRevoked?.({ ...details, uid: accountUid });
45742
+ } catch (error) {
45743
+ callback = Promise.reject(error);
45744
+ }
45745
+ lock();
45746
+ revocationPromise = Promise.resolve(callback);
45747
+ return revocationPromise;
45748
+ }
45682
45749
  const user = options.user || createUser({
45683
45750
  cloud,
45684
45751
  network: options.network || defaultNetwork,
45685
45752
  avatarCache,
45686
45753
  diag,
45687
- onSessionRevoked: ({ uid } = {}) => {
45688
- lock();
45689
- options.onSessionRevoked?.({ uid: uid || state?.uid || null });
45690
- }
45754
+ onSessionRevoked: revokeSession
45691
45755
  });
45692
45756
  state = emptyState(user.getSnapshot(), options.network || defaultNetwork);
45693
45757
  const chat = createChatSession({
@@ -45936,6 +46000,24 @@ function openAccount(options = {}) {
45936
46000
  code: error?.code || "",
45937
46001
  message: error?.message || String(error)
45938
46002
  });
46003
+ if (isAccountAuthenticationError(error)) {
46004
+ revokeSession({
46005
+ uid,
46006
+ code: error?.code || "",
46007
+ reason: "vault-listener"
46008
+ }).catch((revocationError) => {
46009
+ diag?.("account.session.revoke.error", {
46010
+ code: revocationError?.code || "",
46011
+ message: revocationError?.message || String(revocationError)
46012
+ });
46013
+ }).then(() => cloud.auth.logout()).catch((logoutError) => {
46014
+ diag?.("account.session.revoke.auth.error", {
46015
+ code: logoutError?.code || "",
46016
+ message: logoutError?.message || String(logoutError)
46017
+ });
46018
+ });
46019
+ return;
46020
+ }
45939
46021
  lock();
45940
46022
  emit2({ vault: null, vaultReady: true, vaultError: error });
45941
46023
  });
@@ -46400,8 +46482,8 @@ function normalizePasskeyLoginError(error, options = {}) {
46400
46482
  return error;
46401
46483
  }
46402
46484
  function normalizePasskeyRegisterError(error, options = {}) {
46403
- if (error?.name === "InvalidStateError" || error?.code === "functions/already-exists") {
46404
- return passkeyError(PASSKEY_PROVIDER_ALREADY_LINKED, "this provider already has a passkey for this account. choose another provider, device, or security key", error);
46485
+ if (error?.name === "CredentialAlreadyExistsException" || error?.name === "InvalidStateError" || error?.code === "functions/already-exists") {
46486
+ return passkeyError(PASSKEY_PROVIDER_ALREADY_LINKED, "you already have a passkey for this account", error);
46405
46487
  }
46406
46488
  if (error?.code === "functions/failed-precondition") {
46407
46489
  return passkeyError(PASSKEY_RP_MISMATCH, "This passkey belongs to a different Glyphteck passkey setup.", error);
@@ -56424,7 +56506,7 @@ var package_default;
56424
56506
  var init_package2 = __esm(() => {
56425
56507
  package_default = {
56426
56508
  name: "veyl",
56427
- version: "0.66.3",
56509
+ version: "0.67.0",
56428
56510
  packageManager: "bun@1.4.0",
56429
56511
  private: true,
56430
56512
  license: "UNLICENSED",
@@ -201665,18 +201747,25 @@ class Fleet {
201665
201747
  const client2 = this.client(profile);
201666
201748
  return operation(client2, publicAccount(record));
201667
201749
  }
201750
+ failRecord(record, error) {
201751
+ const message = error?.message || String(error);
201752
+ record.error ||= message;
201753
+ record.status = "error";
201754
+ record.controller.abort(error);
201755
+ if (record.failureEmitted)
201756
+ return;
201757
+ record.failureEmitted = true;
201758
+ this.emit("account-error", {
201759
+ account: publicAccount(record),
201760
+ error: record.error
201761
+ });
201762
+ }
201668
201763
  queuePolicies(record, event2) {
201669
201764
  record.pendingEvents += 1;
201670
201765
  if (record.pendingEvents > this.maxPendingEvents) {
201671
201766
  record.pendingEvents -= 1;
201672
201767
  const error = new Error(`fleet event backlog exceeded for ${record.profile.profile}`);
201673
- record.error = error.message;
201674
- record.status = "error";
201675
- record.controller.abort(error);
201676
- this.emit("account-error", {
201677
- account: publicAccount(record),
201678
- error: error.message
201679
- });
201768
+ this.failRecord(record, error);
201680
201769
  return;
201681
201770
  }
201682
201771
  if (this.policyReloading) {
@@ -201725,69 +201814,64 @@ class Fleet {
201725
201814
  unsubscribeRevoked: () => {},
201726
201815
  startedPolicies: [],
201727
201816
  reloadEvents: [],
201728
- closed: false
201817
+ closed: false,
201818
+ failureEmitted: false
201729
201819
  };
201730
201820
  this.records.set(profile.profile, record);
201731
201821
  this.emit("account-starting", { account: publicAccount(record) });
201732
- const baseOptions = typeof this.clientOptions === "function" ? await this.clientOptions(profile) : this.clientOptions;
201733
- record.client = await this.openClient({
201734
- ...baseOptions || {},
201735
- profile: profile.profile,
201736
- ...profile.network ? { network: profile.network } : {}
201737
- }, profile);
201738
- await record.client.ensureUnlocked();
201739
- for (const policy of this.policies) {
201740
- record.startedPolicies.push(policy);
201741
- await policy.start?.({
201742
- account: profile,
201743
- client: record.client,
201744
- fleet: this
201745
- });
201746
- }
201747
- let readyResolve;
201748
- let readyReject;
201749
- const ready2 = new Promise((resolve2, reject) => {
201750
- readyResolve = resolve2;
201751
- readyReject = reject;
201752
- });
201753
- record.unsubscribeRevoked = record.client.subscribeSessionRevoked?.(() => {
201754
- const error = new Error("account session revoked");
201755
- record.error = error.message;
201756
- record.status = "error";
201757
- controller.abort(error);
201758
- readyReject(error);
201759
- this.emit("account-error", {
201760
- account: publicAccount(record),
201761
- error: error.message
201762
- });
201763
- }) || (() => {});
201764
- const accountEventOptions = typeof this.eventOptions === "function" ? await this.eventOptions(profile) : this.eventOptions;
201765
- record.listenTask = record.client.listen({
201766
- ...accountEventOptions || {},
201767
- signal: controller.signal,
201768
- onEvent: (event2) => {
201769
- if (event2?.type === "ready")
201770
- readyResolve();
201771
- this.emit("account-event", {
201772
- account: publicAccount(record),
201773
- event: event2
201822
+ try {
201823
+ const baseOptions = typeof this.clientOptions === "function" ? await this.clientOptions(profile) : this.clientOptions;
201824
+ record.client = await this.openClient({
201825
+ ...baseOptions || {},
201826
+ profile: profile.profile,
201827
+ ...profile.network ? { network: profile.network } : {}
201828
+ }, profile);
201829
+ await record.client.ensureUnlocked();
201830
+ for (const policy of this.policies) {
201831
+ record.startedPolicies.push(policy);
201832
+ await policy.start?.({
201833
+ account: profile,
201834
+ client: record.client,
201835
+ fleet: this
201774
201836
  });
201775
- this.queuePolicies(record, event2);
201776
201837
  }
201777
- }).catch((error) => {
201778
- if (controller.signal.aborted)
201779
- return;
201780
- record.error = error?.message || String(error);
201781
- record.status = "error";
201782
- readyReject(error);
201783
- this.emit("account-error", {
201784
- account: publicAccount(record),
201785
- error: record.error
201838
+ let readyResolve;
201839
+ let readyReject;
201840
+ const ready2 = new Promise((resolve2, reject) => {
201841
+ readyResolve = resolve2;
201842
+ readyReject = reject;
201843
+ });
201844
+ record.unsubscribeRevoked = record.client.subscribeSessionRevoked?.(() => {
201845
+ const error = new Error("account session revoked");
201846
+ this.failRecord(record, error);
201847
+ readyReject(error);
201848
+ }) || (() => {});
201849
+ const accountEventOptions = typeof this.eventOptions === "function" ? await this.eventOptions(profile) : this.eventOptions;
201850
+ record.listenTask = record.client.listen({
201851
+ ...accountEventOptions || {},
201852
+ signal: controller.signal,
201853
+ onEvent: (event2) => {
201854
+ if (event2?.type === "ready")
201855
+ readyResolve();
201856
+ this.emit("account-event", {
201857
+ account: publicAccount(record),
201858
+ event: event2
201859
+ });
201860
+ this.queuePolicies(record, event2);
201861
+ }
201862
+ }).catch((error) => {
201863
+ if (controller.signal.aborted)
201864
+ return;
201865
+ this.failRecord(record, error);
201866
+ readyReject(error);
201786
201867
  });
201787
- });
201788
- await waitFor3(ready2, this.startTimeoutMs, `fleet profile start timed out: ${profile.profile}`);
201789
- record.status = "ready";
201790
- this.emit("account-ready", { account: publicAccount(record) });
201868
+ await waitFor3(ready2, this.startTimeoutMs, `fleet profile start timed out: ${profile.profile}`);
201869
+ record.status = "ready";
201870
+ this.emit("account-ready", { account: publicAccount(record) });
201871
+ } catch (error) {
201872
+ this.failRecord(record, error);
201873
+ throw error;
201874
+ }
201791
201875
  }
201792
201876
  async stopRecordPolicies(record) {
201793
201877
  const errors = [];
@@ -201999,10 +202083,13 @@ class Fleet {
201999
202083
  }
202000
202084
  };
202001
202085
  record.controller.abort();
202002
- await attempt(() => record.listenTask);
202003
- errors.push(...await this.stopRecordPolicies(record));
202004
202086
  await attempt(() => record.unsubscribeRevoked());
202005
- await attempt(() => record.client?.close());
202087
+ errors.push(...await this.stopRecordPolicies(record));
202088
+ const closeTask = Promise.resolve().then(() => record.client?.close());
202089
+ await Promise.all([
202090
+ attempt(() => record.listenTask),
202091
+ attempt(() => closeTask)
202092
+ ]);
202006
202093
  record.closed = true;
202007
202094
  record.status = errors.length || record.error ? "error" : "stopped";
202008
202095
  if (errors.length) {
package/dist/index.js CHANGED
@@ -7921,7 +7921,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
7921
7921
  close(vaultAccess);
7922
7922
  close(vaultSigner);
7923
7923
  close(localCache);
7924
- pending.push(lockWallet(wallet));
7924
+ lockWallet(wallet);
7925
7925
  lockChat(chatPrivateKey, chatPK);
7926
7926
  cleanBytes(chatSigningSecret, notificationPrivateKey, notificationPublicKey);
7927
7927
  return Promise.allSettled(pending).then(() => {
@@ -8550,12 +8550,16 @@ function settingsState(settings = defaultSettings) {
8550
8550
  }
8551
8551
  };
8552
8552
  }
8553
+ function isAccountAuthenticationError(error) {
8554
+ const code = typeof error?.code === "string" ? error.code.toLowerCase() : "";
8555
+ return code === "permission-denied" || code.endsWith("/permission-denied") || code === "unauthenticated" || code.endsWith("/unauthenticated");
8556
+ }
8553
8557
  function createUser({ cloud, network, avatarCache = null, diag = null, onSessionRevoked = null }) {
8554
8558
  if (!cloud) {
8555
8559
  throw new Error("createUser requires { cloud }");
8556
8560
  }
8557
- if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove, avatarCache.removeAll].some((method) => typeof method !== "function")) {
8558
- throw new Error("avatarCache requires read, write, remove, and removeAll");
8561
+ if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove].some((method) => typeof method !== "function")) {
8562
+ throw new Error("avatarCache requires read, write, and remove");
8559
8563
  }
8560
8564
  let activeNetwork = network;
8561
8565
  let state = defaultUser;
@@ -8693,9 +8697,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8693
8697
  return;
8694
8698
  runCacheTask("user.avatar.cache.remove", () => avatarCache.remove(uid));
8695
8699
  }
8696
- function clearCachedAvatars() {
8697
- runCacheTask("user.avatar.cache.clear", () => avatarCache.removeAll());
8698
- }
8699
8700
  async function fetchAvatar(uid, { version = null, force = false, clear = false, persist = true } = {}) {
8700
8701
  if (!uid)
8701
8702
  return;
@@ -8778,6 +8779,54 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8778
8779
  function isCurrentUser(uid, session) {
8779
8780
  return started && authSession === session && userUid === uid;
8780
8781
  }
8782
+ function revokeAuthentication(authUser, {
8783
+ code = "",
8784
+ local = false,
8785
+ reason = "session-inactive"
8786
+ } = {}) {
8787
+ if (revokingSession)
8788
+ return;
8789
+ revokingSession = true;
8790
+ let callback;
8791
+ try {
8792
+ callback = onSessionRevoked?.({
8793
+ uid: authUser.uid,
8794
+ code,
8795
+ reason
8796
+ });
8797
+ } catch (error) {
8798
+ callback = Promise.reject(error);
8799
+ }
8800
+ markDiag(diag, "user.auth.revoked", {
8801
+ code,
8802
+ local,
8803
+ reason
8804
+ });
8805
+ setState((user) => ({
8806
+ ...user,
8807
+ authSessionReady: true,
8808
+ authSessionActive: false,
8809
+ authCredential: null
8810
+ }));
8811
+ let operation = Promise.resolve(callback).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error));
8812
+ if (!local) {
8813
+ operation = operation.then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error));
8814
+ }
8815
+ operation.finally(() => {
8816
+ if (userUid === authUser.uid) {
8817
+ revokingSession = false;
8818
+ }
8819
+ });
8820
+ }
8821
+ function revokeAuthenticationError(authUser, error, reason) {
8822
+ if (!isAccountAuthenticationError(error))
8823
+ return false;
8824
+ revokeAuthentication(authUser, {
8825
+ code: error?.code || "",
8826
+ reason
8827
+ });
8828
+ return true;
8829
+ }
8781
8830
  function watchUser(authUser) {
8782
8831
  if (!started)
8783
8832
  return;
@@ -8787,7 +8836,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8787
8836
  closeUserWatches();
8788
8837
  markDiag(diag, "user.auth.state", { signedIn: !!authUser });
8789
8838
  if (!authUser) {
8790
- const signedOutUid = userUid;
8791
8839
  revokingSession = false;
8792
8840
  userUid = null;
8793
8841
  clearSettingsKey(settingsKey);
@@ -8796,17 +8844,11 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8796
8844
  agreementStored = null;
8797
8845
  agreementOverride = null;
8798
8846
  agreementAcceptance = null;
8799
- if (signedOutUid) {
8800
- clearCachedAvatars();
8801
- }
8802
8847
  avatarFetch = { uid: null, key: null, promise: null };
8803
8848
  setState({ ...defaultUser, authReady: true });
8804
8849
  return;
8805
8850
  }
8806
8851
  const authUidChanged = userUid !== authUser.uid;
8807
- if (userUid && authUidChanged) {
8808
- clearCachedAvatars();
8809
- }
8810
8852
  userUid = authUser.uid;
8811
8853
  if (authUidChanged) {
8812
8854
  clearSettingsKey(settingsKey);
@@ -8836,32 +8878,28 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8836
8878
  userWatches.push(cloud.auth.session.watch(authUser.uid, current((authSessionState) => {
8837
8879
  if (authSessionState?.active) {
8838
8880
  const authCredential = authSessionState.authCredential;
8839
- setState((user) => user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : { ...user, authCredential });
8840
- return;
8841
- }
8842
- if (revokingSession)
8843
- return;
8844
- revokingSession = true;
8845
- if (cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation)) {
8846
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).finally(() => {
8847
- if (userUid === authUser.uid) {
8848
- revokingSession = false;
8849
- }
8881
+ setState((user) => user.authSessionReady && user.authSessionActive && user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : {
8882
+ ...user,
8883
+ authSessionReady: true,
8884
+ authSessionActive: true,
8885
+ authCredential
8850
8886
  });
8851
8887
  return;
8852
8888
  }
8853
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error)).finally(() => {
8854
- if (userUid === authUser.uid) {
8855
- revokingSession = false;
8856
- }
8889
+ revokeAuthentication(authUser, {
8890
+ local: cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation) === true,
8891
+ reason: "session-inactive"
8857
8892
  });
8858
8893
  }), current((error) => {
8859
8894
  markError(diag, "user.session.listen", authStartedAt, error);
8895
+ revokeAuthenticationError(authUser, error, "session-listener");
8860
8896
  })));
8861
8897
  userWatches.push(cloud.user.admin.watch(authUser.uid, current((allowed) => {
8862
8898
  setState((user) => ({ ...user, isAdmin: allowed, adminReady: true }));
8863
8899
  }), current((error) => {
8864
8900
  markError(diag, "user.admin.listen", authStartedAt, error);
8901
+ if (revokeAuthenticationError(authUser, error, "admin-listener"))
8902
+ return;
8865
8903
  setState((user) => ({ ...user, isAdmin: false, adminReady: true }));
8866
8904
  })));
8867
8905
  userWatches.push(cloud.user.private.watch(authUser.uid, current((privateData, info = {}) => {
@@ -8885,6 +8923,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8885
8923
  }));
8886
8924
  }), current((error) => {
8887
8925
  markError(diag, "user.settings.snapshot", authStartedAt, error);
8926
+ if (revokeAuthenticationError(authUser, error, "settings-listener"))
8927
+ return;
8888
8928
  settingsStored = null;
8889
8929
  agreementStored = null;
8890
8930
  setState((user) => ({
@@ -8897,6 +8937,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8897
8937
  setState((user) => ({ ...user, banned: banned ?? null }));
8898
8938
  }), current((error) => {
8899
8939
  markError(diag, "user.moderation.listen", authStartedAt, error);
8940
+ if (revokeAuthenticationError(authUser, error, "moderation-listener"))
8941
+ return;
8900
8942
  setState((user) => ({ ...user, banned: null }));
8901
8943
  })));
8902
8944
  userWatches.push(cloud.user.blocked.watch(authUser.uid, current((blocked, info = {}) => {
@@ -8909,6 +8951,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8909
8951
  });
8910
8952
  }), current((error) => {
8911
8953
  markError(diag, "user.blocked.listen", authStartedAt, error);
8954
+ if (revokeAuthenticationError(authUser, error, "blocked-listener"))
8955
+ return;
8912
8956
  setState((user) => ({ ...user, blockedReady: false }));
8913
8957
  })));
8914
8958
  userWatches.push(cloud.user.profile.watch(authUser.uid, current((profileData, info = {}) => {
@@ -8955,6 +8999,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8955
8999
  }
8956
9000
  }), current((error) => {
8957
9001
  markError(diag, "user.profile.snapshot", authStartedAt, error);
9002
+ if (revokeAuthenticationError(authUser, error, "profile-listener"))
9003
+ return;
8958
9004
  avatarFetch = { uid: null, key: null, promise: null };
8959
9005
  setState((user) => ({
8960
9006
  ...user,
@@ -9185,6 +9231,8 @@ var init_user = __esm(() => {
9185
9231
  defaultUser = {
9186
9232
  uid: null,
9187
9233
  authReady: false,
9234
+ authSessionReady: false,
9235
+ authSessionActive: false,
9188
9236
  authCredential: null,
9189
9237
  profileReady: false,
9190
9238
  username: null,
@@ -45673,20 +45721,36 @@ function openAccount(options = {}) {
45673
45721
  let closePromise = null;
45674
45722
  let state;
45675
45723
  let chatSession = null;
45724
+ let revokedUid = null;
45725
+ let revocationPromise = null;
45676
45726
  let nextAgreement = agreementContract(options.agreement || CURRENT_AGREEMENT);
45677
45727
  const listeners = new Set;
45678
45728
  let activeWriteTail = Promise.resolve();
45679
45729
  const sessionCloseWork = new Set;
45680
45730
  const operationBarrier = createAccountOperationCloseBarrier();
45731
+ function revokeSession(details = {}) {
45732
+ const { uid = null } = details;
45733
+ const accountUid = uid || state?.uid || null;
45734
+ if (accountUid && revokedUid === accountUid) {
45735
+ return revocationPromise || Promise.resolve();
45736
+ }
45737
+ revokedUid = accountUid;
45738
+ let callback;
45739
+ try {
45740
+ callback = options.onSessionRevoked?.({ ...details, uid: accountUid });
45741
+ } catch (error) {
45742
+ callback = Promise.reject(error);
45743
+ }
45744
+ lock();
45745
+ revocationPromise = Promise.resolve(callback);
45746
+ return revocationPromise;
45747
+ }
45681
45748
  const user = options.user || createUser({
45682
45749
  cloud,
45683
45750
  network: options.network || defaultNetwork,
45684
45751
  avatarCache,
45685
45752
  diag,
45686
- onSessionRevoked: ({ uid } = {}) => {
45687
- lock();
45688
- options.onSessionRevoked?.({ uid: uid || state?.uid || null });
45689
- }
45753
+ onSessionRevoked: revokeSession
45690
45754
  });
45691
45755
  state = emptyState(user.getSnapshot(), options.network || defaultNetwork);
45692
45756
  const chat = createChatSession({
@@ -45935,6 +45999,24 @@ function openAccount(options = {}) {
45935
45999
  code: error?.code || "",
45936
46000
  message: error?.message || String(error)
45937
46001
  });
46002
+ if (isAccountAuthenticationError(error)) {
46003
+ revokeSession({
46004
+ uid,
46005
+ code: error?.code || "",
46006
+ reason: "vault-listener"
46007
+ }).catch((revocationError) => {
46008
+ diag?.("account.session.revoke.error", {
46009
+ code: revocationError?.code || "",
46010
+ message: revocationError?.message || String(revocationError)
46011
+ });
46012
+ }).then(() => cloud.auth.logout()).catch((logoutError) => {
46013
+ diag?.("account.session.revoke.auth.error", {
46014
+ code: logoutError?.code || "",
46015
+ message: logoutError?.message || String(logoutError)
46016
+ });
46017
+ });
46018
+ return;
46019
+ }
45938
46020
  lock();
45939
46021
  emit2({ vault: null, vaultReady: true, vaultError: error });
45940
46022
  });
@@ -46399,8 +46481,8 @@ function normalizePasskeyLoginError(error, options = {}) {
46399
46481
  return error;
46400
46482
  }
46401
46483
  function normalizePasskeyRegisterError(error, options = {}) {
46402
- if (error?.name === "InvalidStateError" || error?.code === "functions/already-exists") {
46403
- return passkeyError(PASSKEY_PROVIDER_ALREADY_LINKED, "this provider already has a passkey for this account. choose another provider, device, or security key", error);
46484
+ if (error?.name === "CredentialAlreadyExistsException" || error?.name === "InvalidStateError" || error?.code === "functions/already-exists") {
46485
+ return passkeyError(PASSKEY_PROVIDER_ALREADY_LINKED, "you already have a passkey for this account", error);
46404
46486
  }
46405
46487
  if (error?.code === "functions/failed-precondition") {
46406
46488
  return passkeyError(PASSKEY_RP_MISMATCH, "This passkey belongs to a different Glyphteck passkey setup.", error);
@@ -56423,7 +56505,7 @@ var package_default;
56423
56505
  var init_package2 = __esm(() => {
56424
56506
  package_default = {
56425
56507
  name: "veyl",
56426
- version: "0.66.3",
56508
+ version: "0.67.0",
56427
56509
  packageManager: "bun@1.4.0",
56428
56510
  private: true,
56429
56511
  license: "UNLICENSED",
@@ -201664,18 +201746,25 @@ class Fleet {
201664
201746
  const client2 = this.client(profile);
201665
201747
  return operation(client2, publicAccount(record));
201666
201748
  }
201749
+ failRecord(record, error) {
201750
+ const message = error?.message || String(error);
201751
+ record.error ||= message;
201752
+ record.status = "error";
201753
+ record.controller.abort(error);
201754
+ if (record.failureEmitted)
201755
+ return;
201756
+ record.failureEmitted = true;
201757
+ this.emit("account-error", {
201758
+ account: publicAccount(record),
201759
+ error: record.error
201760
+ });
201761
+ }
201667
201762
  queuePolicies(record, event2) {
201668
201763
  record.pendingEvents += 1;
201669
201764
  if (record.pendingEvents > this.maxPendingEvents) {
201670
201765
  record.pendingEvents -= 1;
201671
201766
  const error = new Error(`fleet event backlog exceeded for ${record.profile.profile}`);
201672
- record.error = error.message;
201673
- record.status = "error";
201674
- record.controller.abort(error);
201675
- this.emit("account-error", {
201676
- account: publicAccount(record),
201677
- error: error.message
201678
- });
201767
+ this.failRecord(record, error);
201679
201768
  return;
201680
201769
  }
201681
201770
  if (this.policyReloading) {
@@ -201724,69 +201813,64 @@ class Fleet {
201724
201813
  unsubscribeRevoked: () => {},
201725
201814
  startedPolicies: [],
201726
201815
  reloadEvents: [],
201727
- closed: false
201816
+ closed: false,
201817
+ failureEmitted: false
201728
201818
  };
201729
201819
  this.records.set(profile.profile, record);
201730
201820
  this.emit("account-starting", { account: publicAccount(record) });
201731
- const baseOptions = typeof this.clientOptions === "function" ? await this.clientOptions(profile) : this.clientOptions;
201732
- record.client = await this.openClient({
201733
- ...baseOptions || {},
201734
- profile: profile.profile,
201735
- ...profile.network ? { network: profile.network } : {}
201736
- }, profile);
201737
- await record.client.ensureUnlocked();
201738
- for (const policy of this.policies) {
201739
- record.startedPolicies.push(policy);
201740
- await policy.start?.({
201741
- account: profile,
201742
- client: record.client,
201743
- fleet: this
201744
- });
201745
- }
201746
- let readyResolve;
201747
- let readyReject;
201748
- const ready2 = new Promise((resolve2, reject) => {
201749
- readyResolve = resolve2;
201750
- readyReject = reject;
201751
- });
201752
- record.unsubscribeRevoked = record.client.subscribeSessionRevoked?.(() => {
201753
- const error = new Error("account session revoked");
201754
- record.error = error.message;
201755
- record.status = "error";
201756
- controller.abort(error);
201757
- readyReject(error);
201758
- this.emit("account-error", {
201759
- account: publicAccount(record),
201760
- error: error.message
201761
- });
201762
- }) || (() => {});
201763
- const accountEventOptions = typeof this.eventOptions === "function" ? await this.eventOptions(profile) : this.eventOptions;
201764
- record.listenTask = record.client.listen({
201765
- ...accountEventOptions || {},
201766
- signal: controller.signal,
201767
- onEvent: (event2) => {
201768
- if (event2?.type === "ready")
201769
- readyResolve();
201770
- this.emit("account-event", {
201771
- account: publicAccount(record),
201772
- event: event2
201821
+ try {
201822
+ const baseOptions = typeof this.clientOptions === "function" ? await this.clientOptions(profile) : this.clientOptions;
201823
+ record.client = await this.openClient({
201824
+ ...baseOptions || {},
201825
+ profile: profile.profile,
201826
+ ...profile.network ? { network: profile.network } : {}
201827
+ }, profile);
201828
+ await record.client.ensureUnlocked();
201829
+ for (const policy of this.policies) {
201830
+ record.startedPolicies.push(policy);
201831
+ await policy.start?.({
201832
+ account: profile,
201833
+ client: record.client,
201834
+ fleet: this
201773
201835
  });
201774
- this.queuePolicies(record, event2);
201775
201836
  }
201776
- }).catch((error) => {
201777
- if (controller.signal.aborted)
201778
- return;
201779
- record.error = error?.message || String(error);
201780
- record.status = "error";
201781
- readyReject(error);
201782
- this.emit("account-error", {
201783
- account: publicAccount(record),
201784
- error: record.error
201837
+ let readyResolve;
201838
+ let readyReject;
201839
+ const ready2 = new Promise((resolve2, reject) => {
201840
+ readyResolve = resolve2;
201841
+ readyReject = reject;
201842
+ });
201843
+ record.unsubscribeRevoked = record.client.subscribeSessionRevoked?.(() => {
201844
+ const error = new Error("account session revoked");
201845
+ this.failRecord(record, error);
201846
+ readyReject(error);
201847
+ }) || (() => {});
201848
+ const accountEventOptions = typeof this.eventOptions === "function" ? await this.eventOptions(profile) : this.eventOptions;
201849
+ record.listenTask = record.client.listen({
201850
+ ...accountEventOptions || {},
201851
+ signal: controller.signal,
201852
+ onEvent: (event2) => {
201853
+ if (event2?.type === "ready")
201854
+ readyResolve();
201855
+ this.emit("account-event", {
201856
+ account: publicAccount(record),
201857
+ event: event2
201858
+ });
201859
+ this.queuePolicies(record, event2);
201860
+ }
201861
+ }).catch((error) => {
201862
+ if (controller.signal.aborted)
201863
+ return;
201864
+ this.failRecord(record, error);
201865
+ readyReject(error);
201785
201866
  });
201786
- });
201787
- await waitFor3(ready2, this.startTimeoutMs, `fleet profile start timed out: ${profile.profile}`);
201788
- record.status = "ready";
201789
- this.emit("account-ready", { account: publicAccount(record) });
201867
+ await waitFor3(ready2, this.startTimeoutMs, `fleet profile start timed out: ${profile.profile}`);
201868
+ record.status = "ready";
201869
+ this.emit("account-ready", { account: publicAccount(record) });
201870
+ } catch (error) {
201871
+ this.failRecord(record, error);
201872
+ throw error;
201873
+ }
201790
201874
  }
201791
201875
  async stopRecordPolicies(record) {
201792
201876
  const errors = [];
@@ -201998,10 +202082,13 @@ class Fleet {
201998
202082
  }
201999
202083
  };
202000
202084
  record.controller.abort();
202001
- await attempt(() => record.listenTask);
202002
- errors.push(...await this.stopRecordPolicies(record));
202003
202085
  await attempt(() => record.unsubscribeRevoked());
202004
- await attempt(() => record.client?.close());
202086
+ errors.push(...await this.stopRecordPolicies(record));
202087
+ const closeTask = Promise.resolve().then(() => record.client?.close());
202088
+ await Promise.all([
202089
+ attempt(() => record.listenTask),
202090
+ attempt(() => closeTask)
202091
+ ]);
202005
202092
  record.closed = true;
202006
202093
  record.status = errors.length || record.error ? "error" : "stopped";
202007
202094
  if (errors.length) {
package/package.json CHANGED
@@ -42,5 +42,5 @@
42
42
  "start": "node src/cli.js",
43
43
  "lint": "eslint src --quiet"
44
44
  },
45
- "version": "0.66.3"
45
+ "version": "0.67.0"
46
46
  }