@hexclave/tanstack-start 1.0.24 → 1.0.25

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.
Files changed (70) hide show
  1. package/dist/components/elements/sidebar-layout.js +1 -1
  2. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  3. package/dist/dev-tool/dev-tool-core.js +3 -67
  4. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  5. package/dist/esm/components/elements/sidebar-layout.js +1 -1
  6. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  7. package/dist/esm/dev-tool/dev-tool-core.js +3 -67
  8. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  9. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  10. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  11. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +2 -0
  12. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  13. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  14. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +9 -3
  15. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  16. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
  17. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  18. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +2 -1
  19. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  20. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +26 -0
  21. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  22. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +7 -1
  23. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  24. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +11 -1
  25. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  26. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +43 -0
  27. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  28. package/dist/esm/lib/hexclave-app/projects/index.d.ts +6 -0
  29. package/dist/esm/lib/hexclave-app/projects/index.d.ts.map +1 -1
  30. package/dist/esm/lib/hexclave-app/projects/index.js.map +1 -1
  31. package/dist/esm/pushed-config-error-overlay/index.d.ts +7 -0
  32. package/dist/esm/pushed-config-error-overlay/index.d.ts.map +1 -0
  33. package/dist/esm/pushed-config-error-overlay/index.js +464 -0
  34. package/dist/esm/pushed-config-error-overlay/index.js.map +1 -0
  35. package/dist/generated/quetzal-translations.d.ts +2 -2
  36. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  37. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +2 -0
  38. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  39. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  40. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +9 -3
  41. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  42. package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
  43. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  44. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +1 -0
  45. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  46. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +26 -0
  47. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  48. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +7 -1
  49. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  50. package/dist/lib/hexclave-app/apps/implementations/session-replay.js +11 -0
  51. package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  52. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +43 -0
  53. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  54. package/dist/lib/hexclave-app/projects/index.d.ts +6 -0
  55. package/dist/lib/hexclave-app/projects/index.d.ts.map +1 -1
  56. package/dist/lib/hexclave-app/projects/index.js.map +1 -1
  57. package/dist/pushed-config-error-overlay/index.d.ts +7 -0
  58. package/dist/pushed-config-error-overlay/index.d.ts.map +1 -0
  59. package/dist/pushed-config-error-overlay/index.js +466 -0
  60. package/dist/pushed-config-error-overlay/index.js.map +1 -0
  61. package/package.json +3 -3
  62. package/src/dev-tool/dev-tool-core.ts +4 -58
  63. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +6 -0
  64. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +11 -3
  65. package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +33 -0
  66. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +6 -1
  67. package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +52 -0
  68. package/src/lib/hexclave-app/apps/implementations/session-replay.ts +20 -0
  69. package/src/lib/hexclave-app/projects/index.ts +2 -0
  70. package/src/pushed-config-error-overlay/index.ts +548 -0
@@ -638,11 +638,6 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
638
638
 
639
639
  const actions = h('div', { className: 'sdt-ov-actions' });
640
640
  const toast = h('div', { className: 'sdt-ov-toast', style: { display: 'none' } });
641
- const emailRow = h('div', { className: 'sdt-ov-email-input' });
642
- const emailInput = h('input', { type: 'email', placeholder: 'Sign in as email\u2026' }) as HTMLInputElement;
643
- const emailBtn = h('button', null);
644
- setHtml(emailBtn, '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>');
645
- emailRow.append(emailInput, emailBtn);
646
641
 
647
642
  function isBestEffortOverviewError(error: unknown) {
648
643
  if (error instanceof DOMException && error.name === 'AbortError') {
@@ -704,15 +699,14 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
704
699
  });
705
700
  actions.append(signOutBtn, randomBtn);
706
701
  } else {
707
- const quickBtn = h('button', { className: 'sdt-ov-btn sdt-ov-btn-primary sdt-ov-btn-wide' }, loading ? 'Working\u2026' : 'Quick Sign In');
702
+ const quickBtn = h('button', { className: 'sdt-ov-btn sdt-ov-btn-primary sdt-ov-btn-wide' }, loading ? 'Working\u2026' : 'Quick Sign Up');
708
703
  quickBtn.disabled = loading;
709
704
  quickBtn.addEventListener('click', () => {
710
705
  runAsynchronously(doQuickSignIn());
711
706
  });
712
707
  actions.appendChild(quickBtn);
713
708
  }
714
- emailInput.placeholder = currentUser ? 'Switch to email\u2026' : 'Sign in as email\u2026';
715
- actions.appendChild(emailRow);
709
+
716
710
  }
717
711
 
718
712
  async function doQuickSignIn() {
@@ -744,54 +738,6 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
744
738
  await refreshUser();
745
739
  }
746
740
 
747
- async function doSignInAs(targetEmail: string) {
748
- if (!targetEmail.trim()) return;
749
- if (!isLocalhost(window.location.href)) {
750
- showToast('Quick sign-in is only available on localhost', 'error');
751
- return;
752
- }
753
- loading = true;
754
- rebuildActions();
755
- const trimmed = targetEmail.trim();
756
- try {
757
- const signInResult = await app.signInWithCredential({ email: trimmed, password: trimmed, noRedirect: true });
758
- if (signInResult.status === 'ok') {
759
- showToast(`Signed in as ${trimmed}`, 'success');
760
- emailInput.value = '';
761
- loading = false;
762
- await refreshUser();
763
- return;
764
- }
765
- const signUpResult = await app.signUpWithCredential({ email: trimmed, password: trimmed, noRedirect: true } as any);
766
- if (signUpResult.status === 'error') {
767
- showToast(`Failed: ${signUpResult.error.message}`, 'error');
768
- loading = false;
769
- rebuildActions();
770
- return;
771
- }
772
- const retryResult = await app.signInWithCredential({ email: trimmed, password: trimmed, noRedirect: true });
773
- if (retryResult.status === 'error') {
774
- showToast(`Sign in failed: ${retryResult.error.message}`, 'error');
775
- } else {
776
- showToast(`Signed in as ${trimmed}`, 'success');
777
- emailInput.value = '';
778
- }
779
- } catch (e: any) {
780
- showToast(e.message || 'Unknown error', 'error');
781
- }
782
- loading = false;
783
- await refreshUser();
784
- }
785
-
786
- emailBtn.addEventListener('click', () => {
787
- runAsynchronously(doSignInAs(emailInput.value));
788
- });
789
- emailInput.addEventListener('keydown', (e) => {
790
- if (e.key === 'Enter') {
791
- runAsynchronously(doSignInAs(emailInput.value));
792
- }
793
- });
794
-
795
741
  heroCard.append(actions, toast);
796
742
 
797
743
  // ── Auth methods card ──────────────────────────────────────────────────────
@@ -855,7 +801,7 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
855
801
  function buildChecklist() {
856
802
  checksCard.innerHTML = '';
857
803
  const currentUserCheck = hasPersistentTokenStore
858
- ? { ok: !!currentUser, label: 'Sign in a test user', hint: 'Use \u201cQuick Sign In\u201d above \u2192' }
804
+ ? { ok: !!currentUser, label: 'Sign in a test user', hint: 'Use \u201cQuick Sign Up\u201d above \u2192' }
859
805
  : { ok: true, label: 'Current-user tools unavailable', hint: null };
860
806
  const checks = [
861
807
  { ok: !!projectId && projectId !== 'default', label: 'Project configured', hint: null },
@@ -925,7 +871,7 @@ function createOverviewTab(app: StackClientApp<true>): TabResult {
925
871
  } else {
926
872
  avatar.textContent = initials;
927
873
  }
928
- userName.textContent = currentUser.displayName || 'Anonymous';
874
+ userName.textContent = currentUser.displayName || '(No display name)';
929
875
  userEmail.textContent = currentUser.primaryEmail || 'No email';
930
876
  authIndicator.style.display = '';
931
877
  } else {
@@ -200,6 +200,12 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
200
200
  logoFullUrl: data.logo_full_url,
201
201
  logoDarkModeUrl: data.logo_dark_mode_url,
202
202
  logoFullDarkModeUrl: data.logo_full_dark_mode_url,
203
+ pushedConfigError: data.pushed_config_error == null ? null : {
204
+ message: data.pushed_config_error.message,
205
+ },
206
+ configWarnings: data.config_warnings.map((warning) => ({
207
+ message: warning.message,
208
+ })),
203
209
  config: {
204
210
  signUpEnabled: data.config.sign_up_enabled,
205
211
  credentialEnabled: data.config.credential_enabled,
@@ -2,8 +2,7 @@
2
2
  //===========================================
3
3
  // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
4
  //===========================================
5
- import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
6
- import { KnownError, KnownErrors, HexclaveClientInterface } from "@hexclave/shared";
5
+ import { HexclaveClientInterface, KnownError, KnownErrors } from "@hexclave/shared";
7
6
  import type { RequestListener } from "@hexclave/shared/dist/interface/client-interface";
8
7
  import { ContactChannelsCrud } from "@hexclave/shared/dist/interface/crud/contact-channels";
9
8
  import { CurrentUserCrud } from "@hexclave/shared/dist/interface/crud/current-user";
@@ -42,14 +41,15 @@ import { BotChallengeExecutionFailedError, BotChallengeUserCancelledError, withB
42
41
  import { createUrlIfValid, getRelativePart, isRelative } from "@hexclave/shared/dist/utils/urls";
43
42
  import { generateUuid } from "@hexclave/shared/dist/utils/uuids";
44
43
  import * as tanstackStartServerContext from "@hexclave/tanstack-start/tanstack-start-server-context"; // THIS_LINE_PLATFORM tanstack-start
44
+ import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
45
45
  import * as TanStackRouter from "@tanstack/react-router"; // THIS_LINE_PLATFORM tanstack-start
46
46
  import * as cookie from "cookie";
47
47
  import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
48
48
  import type * as yup from "yup";
49
+ import { envVars } from "../../../../generated/env";
49
50
  import { constructRedirectUrl } from "../../../../utils/url";
50
51
  import { callOAuthCallback, getNewOAuthProviderOrScopeUrl } from "../../../auth";
51
52
  import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, getCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
52
- import { envVars } from "../../../../generated/env";
53
53
  import { ApiKey, ApiKeyCreationOptions, ApiKeyUpdateOptions, apiKeyCreationOptionsToCrud } from "../../api-keys";
54
54
  import { ConvexCtx, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, RequestLike, ResolvedHandlerUrls, TokenStoreInit, hexclaveAppInternalsSymbol } from "../../common";
55
55
  import { DeprecatedOAuthConnection, OAuthConnection } from "../../connected-accounts";
@@ -73,6 +73,7 @@ import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsO
73
73
  import { useAsyncCache } from "./common";
74
74
  import { mountClickmapOverlay } from "../../../../clickmap";
75
75
  import { mountDevTool } from "../../../../dev-tool";
76
+ import { mountPushedConfigErrorOverlay } from "../../../../pushed-config-error-overlay";
76
77
 
77
78
  let isReactServer = false;
78
79
 
@@ -742,6 +743,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
742
743
  // when a dashboard-minted token is handed over, so the listener is
743
744
  // mounted unconditionally (the heavy UI is lazy-loaded on demand).
744
745
  mountClickmapOverlay(this as any);
746
+ mountPushedConfigErrorOverlay(this as any);
745
747
  }
746
748
  }
747
749
 
@@ -1636,6 +1638,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
1636
1638
  return {
1637
1639
  id: crud.id,
1638
1640
  displayName: crud.display_name,
1641
+ pushedConfigError: crud.pushed_config_error == null ? null : {
1642
+ message: crud.pushed_config_error.message,
1643
+ },
1644
+ configWarnings: crud.config_warnings.map((warning) => ({
1645
+ message: warning.message,
1646
+ })),
1639
1647
  config: {
1640
1648
  signUpEnabled: crud.config.sign_up_enabled,
1641
1649
  credentialEnabled: crud.config.credential_enabled,
@@ -391,6 +391,39 @@ describe("EventTracker", () => {
391
391
  }
392
392
  });
393
393
 
394
+ it("silently ignores network errors caused by ad blockers", async () => {
395
+ vi.useFakeTimers();
396
+ document.body.innerHTML = "<button>Click me</button>";
397
+
398
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
399
+ const sentBodies: string[] = [];
400
+ const tracker = new EventTracker({
401
+ projectId: "internal",
402
+ sendBatch: async (body) => {
403
+ sentBodies.push(body);
404
+ return Result.error(new TypeError("Failed to fetch"));
405
+ },
406
+ });
407
+
408
+ try {
409
+ tracker.start();
410
+
411
+ await advancePastFlush();
412
+ expect(sentBodies).toHaveLength(1);
413
+ expect(warnSpy).not.toHaveBeenCalled();
414
+
415
+ // Unlike ANALYTICS_NOT_ENABLED, ad blocker errors do NOT disable the
416
+ // tracker — subsequent flushes continue attempting delivery.
417
+ document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
418
+ await advancePastFlush();
419
+ expect(sentBodies).toHaveLength(2);
420
+ expect(warnSpy).not.toHaveBeenCalled();
421
+ } finally {
422
+ tracker.stop();
423
+ warnSpy.mockRestore();
424
+ }
425
+ });
426
+
394
427
  it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
395
428
  vi.useFakeTimers();
396
429
  document.body.innerHTML = "<button>Click me</button>";
@@ -8,7 +8,7 @@ import { cssEscapeIdent } from "@hexclave/shared/dist/utils/dom";
8
8
  import { buildElementsChain, ELEMENTS_CHAIN_MAX_DEPTH } from "@hexclave/shared/dist/utils/elements-chain";
9
9
  import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
10
10
  import { Result } from "@hexclave/shared/dist/utils/results";
11
- import { generateUuid, isAnalyticsNotEnabledError } from "./session-replay";
11
+ import { generateUuid, isAdBlockerNetworkError, isAnalyticsNotEnabledError } from "./session-replay";
12
12
 
13
13
  const FLUSH_INTERVAL_MS = 10_000;
14
14
  const MAX_EVENTS_PER_BATCH = 50;
@@ -511,6 +511,11 @@ export class EventTracker {
511
511
  this._disable();
512
512
  return;
513
513
  }
514
+ // Ad blockers commonly block analytics endpoints, causing network
515
+ // errors. These are expected and should not pollute the console.
516
+ if (isAdBlockerNetworkError(res.error)) {
517
+ return;
518
+ }
514
519
  console.warn("EventTracker flush failed:", res.error);
515
520
  return;
516
521
  }
@@ -49,6 +49,58 @@ describe("analytics option JSON conversion", () => {
49
49
  });
50
50
 
51
51
  describe("SessionRecorder flush", () => {
52
+ it("silently ignores network errors caused by ad blockers", async () => {
53
+ vi.useFakeTimers();
54
+
55
+ const storageKey = `hexclave:session-replay:v1:test-project`;
56
+ localStorage.setItem(storageKey, JSON.stringify({
57
+ session_id: "test-session",
58
+ created_at_ms: Date.now(),
59
+ last_activity_ms: Date.now(),
60
+ }));
61
+
62
+ const sentBodies: string[] = [];
63
+ const recorder = new SessionRecorder(
64
+ {
65
+ projectId: "test-project",
66
+ sendBatch: async (body) => {
67
+ sentBodies.push(body);
68
+ return Result.error(new TypeError("Failed to fetch"));
69
+ },
70
+ },
71
+ {},
72
+ );
73
+
74
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
75
+
76
+ try {
77
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
78
+ (recorder as any)._events = [{ type: 2, timestamp: Date.now(), data: {} }];
79
+
80
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
81
+ (recorder as any)._tick();
82
+ await vi.advanceTimersByTimeAsync(0);
83
+
84
+ expect(sentBodies).toHaveLength(1);
85
+ expect(warnSpy).not.toHaveBeenCalled();
86
+
87
+ // Unlike ANALYTICS_NOT_ENABLED, ad blocker errors do NOT disable the
88
+ // recorder — subsequent flushes continue attempting delivery.
89
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
90
+ (recorder as any)._events = [{ type: 3, timestamp: Date.now(), data: {} }];
91
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
92
+ (recorder as any)._tick();
93
+ await vi.advanceTimersByTimeAsync(0);
94
+ expect(sentBodies).toHaveLength(2);
95
+ expect(warnSpy).not.toHaveBeenCalled();
96
+ } finally {
97
+ recorder.stop();
98
+ warnSpy.mockRestore();
99
+ localStorage.removeItem(storageKey);
100
+ vi.useRealTimers();
101
+ }
102
+ });
103
+
52
104
  it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
53
105
  vi.useFakeTimers();
54
106
 
@@ -170,6 +170,21 @@ export function isAnalyticsNotEnabledError(error: unknown): boolean {
170
170
  return KnownErrors.AnalyticsNotEnabled.isInstance(error);
171
171
  }
172
172
 
173
+ /**
174
+ * Whether the error looks like a network failure caused by an ad blocker or
175
+ * similar extension blocking analytics requests. These are expected in
176
+ * production and should be silently ignored rather than logged as warnings.
177
+ */
178
+ export function isAdBlockerNetworkError(error: unknown): boolean {
179
+ if (error instanceof Error) {
180
+ return error.message.includes("Failed to fetch")
181
+ || error.message.includes("NetworkError")
182
+ || error.message.includes("Load failed")
183
+ || error.message.includes("network connection");
184
+ }
185
+ return false;
186
+ }
187
+
173
188
  export class SessionRecorder {
174
189
  private _started = false;
175
190
  private _cancelled = false;
@@ -278,6 +293,11 @@ export class SessionRecorder {
278
293
  this._disable();
279
294
  return;
280
295
  }
296
+ // Ad blockers commonly block analytics endpoints, causing network
297
+ // errors. These are expected and should not pollute the console.
298
+ if (isAdBlockerNetworkError(res.error)) {
299
+ return;
300
+ }
281
301
  captureWarning("SessionRecorder.flush", res.error);
282
302
  return;
283
303
  }
@@ -30,6 +30,8 @@ export type PushConfigOptions = {
30
30
  export type Project = {
31
31
  readonly id: string,
32
32
  readonly displayName: string,
33
+ readonly pushedConfigError: { message: string } | null,
34
+ readonly configWarnings: { message: string }[],
33
35
  readonly config: ProjectConfig,
34
36
  };
35
37