@hexclave/next 1.0.10 → 1.0.12

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 (41) hide show
  1. package/dist/config.d.ts +2 -0
  2. package/dist/config.js +22 -0
  3. package/dist/esm/config.d.ts +2 -0
  4. package/dist/esm/config.js +3 -0
  5. package/dist/esm/index.js +1 -1
  6. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +9 -1
  7. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  8. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +23 -6
  9. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  10. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +78 -0
  11. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  12. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +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 +11 -6
  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/index.js +1 -1
  21. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +9 -1
  22. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  23. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +23 -6
  24. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  25. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +78 -0
  26. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  27. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
  28. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  29. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +11 -6
  30. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  31. package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
  32. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  33. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +2 -1
  34. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  35. package/package.json +13 -4
  36. package/src/config.ts +17 -0
  37. package/src/integrations/convex/component/README.md +1 -1
  38. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +21 -6
  39. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +108 -0
  40. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +18 -6
  41. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +1 -0
@@ -17,7 +17,7 @@ let ____________generated_env_js = require("../../../../generated/env.js");
17
17
  let ______url_targets_js = require("../../url-targets.js");
18
18
 
19
19
  //#region src/lib/hexclave-app/apps/implementations/common.ts
20
- const clientVersion = "js @hexclave/next@1.0.10";
20
+ const clientVersion = "js @hexclave/next@1.0.12";
21
21
  if (clientVersion.startsWith("STACK_COMPILE_TIME")) throw new _hexclave_shared_dist_utils_errors.HexclaveAssertionError("Client version was not replaced. Something went wrong during build!");
22
22
  const replaceHexclavePortPrefix = (input) => {
23
23
  if (!input) return input;
@@ -1 +1 @@
1
- {"version":3,"file":"event-tracker.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"mappings":";;;KAiCY,gBAAA;EACV,SAAA;EACA,SAAA,GAAY,IAAA,UAAc,OAAA;IAAW,SAAA;EAAA,MAAyB,OAAA,CAAQ,MAAA,CAAO,QAAA,EAAU,KAAA;AAAA;AAAA,cAS5E,YAAA;EAAA,QACH,QAAA;EAAA,QACA,UAAA;EAAA,QACA,gBAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,QAAA;EAAA,iBACS,uBAAA;EAAA,iBACA,KAAA;EAAA,QAET,kBAAA;EAAA,QACA,qBAAA;cAEI,IAAA,EAAM,gBAAA;EAKlB,KAAA,CAAA;EAqBA,IAAA,CAAA;EAUA,WAAA,CAAA;EAAA,QAKQ,UAAA;EAAA,QAQA,gBAAA;EAAA,QA2BA,qBAAA;EAAA,iBA4BS,WAAA;EAAA,QAIT,cAAA;EAAA,QA0BA,sBAAA;EAAA,iBAWS,eAAA;EAAA,QAsBT,kBAAA;EAAA,iBAIS,WAAA;EAAA,QAIT,uBAAA;EAAA,QASA,SAAA;EAAA,QA0BM,MAAA;EAAA,QA+BN,KAAA;AAAA"}
1
+ {"version":3,"file":"event-tracker.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"mappings":";;;KAiCY,gBAAA;EACV,SAAA;EACA,SAAA,GAAY,IAAA,UAAc,OAAA;IAAW,SAAA;EAAA,MAAyB,OAAA,CAAQ,MAAA,CAAO,QAAA,EAAU,KAAA;AAAA;AAAA,cAS5E,YAAA;EAAA,QACH,QAAA;EAAA,QACA,UAAA;EAAA,QACA,gBAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,QAAA;EAAA,iBACS,uBAAA;EAAA,iBACA,KAAA;EAAA,QAET,kBAAA;EAAA,QACA,qBAAA;cAEI,IAAA,EAAM,gBAAA;EAKlB,KAAA,CAAA;EAqBA,IAAA,CAAA;EAUA,WAAA,CAAA;EAAA,QAKQ,UAAA;EAAA,QAQA,gBAAA;EAAA,QA4BA,qBAAA;EAAA,iBA4BS,WAAA;EAAA,QAIT,cAAA;EAAA,QA0BA,sBAAA;EAAA,iBAWS,eAAA;EAAA,QAsBT,kBAAA;EAAA,iBAIS,WAAA;EAAA,QAIT,uBAAA;EAAA,QASA,SAAA;EAAA,QA0BM,MAAA;EAAA,QA+BN,KAAA;AAAA"}
@@ -104,7 +104,8 @@ var EventTracker = class {
104
104
  viewport_width: window.innerWidth,
105
105
  viewport_height: window.innerHeight,
106
106
  screen_width: screenObject.width,
107
- screen_height: screenObject.height
107
+ screen_height: screenObject.height,
108
+ user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null
108
109
  }
109
110
  });
110
111
  }
@@ -1 +1 @@
1
- {"version":3,"file":"event-tracker.js","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { generateUuid } from \"./session-replay\";\n\nconst FLUSH_INTERVAL_MS = 10_000;\nconst MAX_EVENTS_PER_BATCH = 50;\nconst MAX_APPROX_BYTES_PER_BATCH = 64_000;\n\nfunction hasScreenDimensions(value: unknown): value is { width: number, height: number } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"width\" in value) || !(\"height\" in value)) {\n return false;\n }\n return typeof value.width === \"number\" && typeof value.height === \"number\";\n}\n\nfunction hasHistoryMethods(value: unknown): value is { pushState: History[\"pushState\"], replaceState: History[\"replaceState\"] } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"pushState\" in value) || !(\"replaceState\" in value)) {\n return false;\n }\n return typeof value.pushState === \"function\" && typeof value.replaceState === \"function\";\n}\n\nexport type EventTrackerDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\ntype TrackedEvent = {\n event_type: \"$page-view\" | \"$click\",\n event_at_ms: number,\n data: Record<string, unknown>,\n};\n\nexport class EventTracker {\n private _started = false;\n private _cancelled = false;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: TrackedEvent[] = [];\n private _approxBytes = 0;\n private _lastUrl: string | null = null;\n private readonly _sessionReplaySegmentId: string;\n private readonly _deps: EventTrackerDeps;\n\n private _originalPushState: History[\"pushState\"] | null = null;\n private _originalReplaceState: History[\"replaceState\"] | null = null;\n\n constructor(deps: EventTrackerDeps) {\n this._deps = deps;\n this._sessionReplaySegmentId = generateUuid();\n }\n\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n if (\n typeof window.addEventListener !== \"function\"\n || typeof window.removeEventListener !== \"function\"\n || typeof document.addEventListener !== \"function\"\n || typeof document.removeEventListener !== \"function\"\n || !hasScreenDimensions(window.screen)\n ) {\n return;\n }\n this._started = true;\n\n this._setupPageViewCapture();\n this._setupClickCapture();\n this._setupPageHideListeners();\n\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._teardown();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n }\n\n private _pushEvent(event: TrackedEvent) {\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n\n private _capturePageView(entryType: \"initial\" | \"push\" | \"replace\" | \"pop\") {\n const screenObject = window.screen;\n if (!hasScreenDimensions(screenObject)) {\n return;\n }\n\n const url = window.location.href;\n if (url === this._lastUrl && entryType !== \"initial\") return;\n this._lastUrl = url;\n\n this._pushEvent({\n event_type: \"$page-view\",\n event_at_ms: Date.now(),\n data: {\n url,\n path: window.location.pathname,\n referrer: document.referrer,\n title: document.title,\n entry_type: entryType,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n screen_width: screenObject.width,\n screen_height: screenObject.height,\n },\n });\n }\n\n private _setupPageViewCapture() {\n // Fire initial page-view\n this._capturePageView(\"initial\");\n const historyObject = window.history;\n if (!hasHistoryMethods(historyObject)) {\n return;\n }\n const originalPushState = historyObject.pushState;\n const originalReplaceState = historyObject.replaceState;\n\n // Monkey-patch history.pushState\n this._originalPushState = (...args: Parameters<History[\"pushState\"]>) => originalPushState.apply(historyObject, args);\n historyObject.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n this._originalPushState!(...args);\n this._capturePageView(\"push\");\n };\n\n // Monkey-patch history.replaceState\n this._originalReplaceState = (...args: Parameters<History[\"replaceState\"]>) => originalReplaceState.apply(historyObject, args);\n historyObject.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n this._originalReplaceState!(...args);\n this._capturePageView(\"replace\");\n };\n\n // Listen for popstate (back/forward navigation)\n window.addEventListener(\"popstate\", this._onPopState);\n }\n\n private readonly _onPopState = () => {\n this._capturePageView(\"pop\");\n };\n\n private _buildSelector(element: Element): string {\n const parts: string[] = [];\n let current: Element | null = element;\n let depth = 0;\n\n while (current && depth < 5) {\n let part = current.tagName.toLowerCase();\n if (current.id) {\n part += `#${current.id}`;\n parts.unshift(part);\n break;\n }\n if (current.className && typeof current.className === \"string\") {\n const classes = current.className.trim().split(/\\s+/).filter(Boolean);\n if (classes.length > 0) {\n part += `.${classes.join(\".\")}`;\n }\n }\n parts.unshift(part);\n current = current.parentElement;\n depth++;\n }\n\n return parts.join(\" > \");\n }\n\n private _findNearestAnchorHref(element: Element): string | null {\n let current: Element | null = element;\n while (current) {\n if (current.tagName === \"A\" && current.hasAttribute(\"href\")) {\n return current.getAttribute(\"href\");\n }\n current = current.parentElement;\n }\n return null;\n }\n\n private readonly _onClickCapture = (event: MouseEvent) => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n\n this._pushEvent({\n event_type: \"$click\",\n event_at_ms: Date.now(),\n data: {\n tag_name: target.tagName.toLowerCase(),\n text: target.textContent.trim().substring(0, 200),\n href: this._findNearestAnchorHref(target),\n selector: this._buildSelector(target),\n x: event.clientX,\n y: event.clientY,\n page_x: event.pageX,\n page_y: event.pageY,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n },\n });\n };\n\n private _setupClickCapture() {\n document.addEventListener(\"click\", this._onClickCapture, { capture: true });\n }\n\n private readonly _onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n\n private _setupPageHideListeners() {\n window.addEventListener(\"pagehide\", this._onPageHide);\n document.addEventListener(\"visibilitychange\", this._onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", this._onPageHide);\n document.removeEventListener(\"visibilitychange\", this._onPageHide);\n };\n }\n\n private _teardown() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n\n // Restore history methods\n const historyObject = window.history;\n if (hasHistoryMethods(historyObject)) {\n if (this._originalPushState) {\n historyObject.pushState = this._originalPushState;\n }\n if (this._originalReplaceState) {\n historyObject.replaceState = this._originalReplaceState;\n }\n }\n this._originalPushState = null;\n this._originalReplaceState = null;\n\n window.removeEventListener(\"popstate\", this._onPopState);\n document.removeEventListener(\"click\", this._onClickCapture, { capture: true });\n\n this._events = [];\n this._approxBytes = 0;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n if (this._events.length === 0) return;\n\n const nowMs = Date.now();\n\n const batchId = generateUuid();\n const payload = {\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n sent_at_ms: nowMs,\n events: this._events,\n };\n\n this._events = [];\n this._approxBytes = 0;\n\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n console.warn(\"EventTracker flush failed:\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n console.warn(\"EventTracker flush failed:\", res.data.status, await res.data.text());\n }\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"event-tracker.js","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { generateUuid } from \"./session-replay\";\n\nconst FLUSH_INTERVAL_MS = 10_000;\nconst MAX_EVENTS_PER_BATCH = 50;\nconst MAX_APPROX_BYTES_PER_BATCH = 64_000;\n\nfunction hasScreenDimensions(value: unknown): value is { width: number, height: number } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"width\" in value) || !(\"height\" in value)) {\n return false;\n }\n return typeof value.width === \"number\" && typeof value.height === \"number\";\n}\n\nfunction hasHistoryMethods(value: unknown): value is { pushState: History[\"pushState\"], replaceState: History[\"replaceState\"] } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"pushState\" in value) || !(\"replaceState\" in value)) {\n return false;\n }\n return typeof value.pushState === \"function\" && typeof value.replaceState === \"function\";\n}\n\nexport type EventTrackerDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\ntype TrackedEvent = {\n event_type: \"$page-view\" | \"$click\",\n event_at_ms: number,\n data: Record<string, unknown>,\n};\n\nexport class EventTracker {\n private _started = false;\n private _cancelled = false;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: TrackedEvent[] = [];\n private _approxBytes = 0;\n private _lastUrl: string | null = null;\n private readonly _sessionReplaySegmentId: string;\n private readonly _deps: EventTrackerDeps;\n\n private _originalPushState: History[\"pushState\"] | null = null;\n private _originalReplaceState: History[\"replaceState\"] | null = null;\n\n constructor(deps: EventTrackerDeps) {\n this._deps = deps;\n this._sessionReplaySegmentId = generateUuid();\n }\n\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n if (\n typeof window.addEventListener !== \"function\"\n || typeof window.removeEventListener !== \"function\"\n || typeof document.addEventListener !== \"function\"\n || typeof document.removeEventListener !== \"function\"\n || !hasScreenDimensions(window.screen)\n ) {\n return;\n }\n this._started = true;\n\n this._setupPageViewCapture();\n this._setupClickCapture();\n this._setupPageHideListeners();\n\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._teardown();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n }\n\n private _pushEvent(event: TrackedEvent) {\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n\n private _capturePageView(entryType: \"initial\" | \"push\" | \"replace\" | \"pop\") {\n const screenObject = window.screen;\n if (!hasScreenDimensions(screenObject)) {\n return;\n }\n\n const url = window.location.href;\n if (url === this._lastUrl && entryType !== \"initial\") return;\n this._lastUrl = url;\n\n this._pushEvent({\n event_type: \"$page-view\",\n event_at_ms: Date.now(),\n data: {\n url,\n path: window.location.pathname,\n referrer: document.referrer,\n title: document.title,\n entry_type: entryType,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n screen_width: screenObject.width,\n screen_height: screenObject.height,\n user_agent: typeof navigator !== \"undefined\" ? navigator.userAgent : null,\n },\n });\n }\n\n private _setupPageViewCapture() {\n // Fire initial page-view\n this._capturePageView(\"initial\");\n const historyObject = window.history;\n if (!hasHistoryMethods(historyObject)) {\n return;\n }\n const originalPushState = historyObject.pushState;\n const originalReplaceState = historyObject.replaceState;\n\n // Monkey-patch history.pushState\n this._originalPushState = (...args: Parameters<History[\"pushState\"]>) => originalPushState.apply(historyObject, args);\n historyObject.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n this._originalPushState!(...args);\n this._capturePageView(\"push\");\n };\n\n // Monkey-patch history.replaceState\n this._originalReplaceState = (...args: Parameters<History[\"replaceState\"]>) => originalReplaceState.apply(historyObject, args);\n historyObject.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n this._originalReplaceState!(...args);\n this._capturePageView(\"replace\");\n };\n\n // Listen for popstate (back/forward navigation)\n window.addEventListener(\"popstate\", this._onPopState);\n }\n\n private readonly _onPopState = () => {\n this._capturePageView(\"pop\");\n };\n\n private _buildSelector(element: Element): string {\n const parts: string[] = [];\n let current: Element | null = element;\n let depth = 0;\n\n while (current && depth < 5) {\n let part = current.tagName.toLowerCase();\n if (current.id) {\n part += `#${current.id}`;\n parts.unshift(part);\n break;\n }\n if (current.className && typeof current.className === \"string\") {\n const classes = current.className.trim().split(/\\s+/).filter(Boolean);\n if (classes.length > 0) {\n part += `.${classes.join(\".\")}`;\n }\n }\n parts.unshift(part);\n current = current.parentElement;\n depth++;\n }\n\n return parts.join(\" > \");\n }\n\n private _findNearestAnchorHref(element: Element): string | null {\n let current: Element | null = element;\n while (current) {\n if (current.tagName === \"A\" && current.hasAttribute(\"href\")) {\n return current.getAttribute(\"href\");\n }\n current = current.parentElement;\n }\n return null;\n }\n\n private readonly _onClickCapture = (event: MouseEvent) => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n\n this._pushEvent({\n event_type: \"$click\",\n event_at_ms: Date.now(),\n data: {\n tag_name: target.tagName.toLowerCase(),\n text: target.textContent.trim().substring(0, 200),\n href: this._findNearestAnchorHref(target),\n selector: this._buildSelector(target),\n x: event.clientX,\n y: event.clientY,\n page_x: event.pageX,\n page_y: event.pageY,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n },\n });\n };\n\n private _setupClickCapture() {\n document.addEventListener(\"click\", this._onClickCapture, { capture: true });\n }\n\n private readonly _onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n\n private _setupPageHideListeners() {\n window.addEventListener(\"pagehide\", this._onPageHide);\n document.addEventListener(\"visibilitychange\", this._onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", this._onPageHide);\n document.removeEventListener(\"visibilitychange\", this._onPageHide);\n };\n }\n\n private _teardown() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n\n // Restore history methods\n const historyObject = window.history;\n if (hasHistoryMethods(historyObject)) {\n if (this._originalPushState) {\n historyObject.pushState = this._originalPushState;\n }\n if (this._originalReplaceState) {\n historyObject.replaceState = this._originalReplaceState;\n }\n }\n this._originalPushState = null;\n this._originalReplaceState = null;\n\n window.removeEventListener(\"popstate\", this._onPopState);\n document.removeEventListener(\"click\", this._onClickCapture, { capture: true });\n\n this._events = [];\n this._approxBytes = 0;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n if (this._events.length === 0) return;\n\n const nowMs = Date.now();\n\n const batchId = generateUuid();\n const payload = {\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n sent_at_ms: nowMs,\n events: this._events,\n };\n\n this._events = [];\n this._approxBytes = 0;\n\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n console.warn(\"EventTracker flush failed:\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n console.warn(\"EventTracker flush failed:\", res.data.status, await res.data.text());\n }\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
3
3
  "name": "@hexclave/next",
4
- "version": "1.0.10",
4
+ "version": "1.0.12",
5
5
  "repository": "https://github.com/hexclave/hexclave",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/index.js",
@@ -16,6 +16,15 @@
16
16
  "default": "./dist/index.js"
17
17
  }
18
18
  },
19
+ "./config": {
20
+ "types": "./dist/config.d.ts",
21
+ "import": {
22
+ "default": "./dist/esm/config.js"
23
+ },
24
+ "require": {
25
+ "default": "./dist/config.js"
26
+ }
27
+ },
19
28
  "./convex.config": {
20
29
  "types": "./dist/integrations/convex/component/convex.config.d.ts",
21
30
  "import": {
@@ -66,9 +75,9 @@
66
75
  "rrweb": "^1.1.3",
67
76
  "tsx": "^4.21.0",
68
77
  "yup": "^1.7.1",
69
- "@hexclave/shared": "1.0.10",
70
- "@hexclave/ui": "1.0.10",
71
- "@hexclave/sc": "1.0.10"
78
+ "@hexclave/shared": "1.0.12",
79
+ "@hexclave/sc": "1.0.12",
80
+ "@hexclave/ui": "1.0.12"
72
81
  },
73
82
  "peerDependencies": {
74
83
  "@types/react": ">=18.0.0",
package/src/config.ts ADDED
@@ -0,0 +1,17 @@
1
+
2
+ //===========================================
3
+ // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
+ //===========================================
5
+ // Lightweight, side-effect-free entrypoint for authoring `hexclave.config.ts`
6
+ // files. Importing from here (e.g. `@hexclave/next/config`) gives you the
7
+ // `defineHexclaveConfig` helper and config types WITHOUT pulling in the
8
+ // framework runtime (React, server-only, Next.js internals). That matters
9
+ // because tooling such as the local dashboard evaluates your config file in a
10
+ // plain Node context — importing `defineHexclaveConfig` from the package root
11
+ // would drag in the whole SDK and fail to load.
12
+ //
13
+ // Hexclave aliases and legacy Stack* names — @deprecated JSDoc lives on the
14
+ // original declarations in @hexclave/shared/config so it survives dts bundling
15
+ // (per-specifier JSDoc on re-exports does not).
16
+ export type { HexclaveConfig, StackConfig } from "@hexclave/shared/config";
17
+ export { defineHexclaveConfig, defineStackConfig, showOnboardingHexclaveConfigValue } from "@hexclave/shared/config";
@@ -23,7 +23,7 @@ import { getConvexProvidersConfig } from "@hexclave/js/convex-auth.config"; //
23
23
 
24
24
  export default {
25
25
  providers: getConvexProvidersConfig({
26
- projectId: process.env.STACK_PROJECT_ID, // or: process.env.NEXT_PUBLIC_STACK_PROJECT_ID
26
+ projectId: process.env.HEXCLAVE_PROJECT_ID, // or: process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID
27
27
  }),
28
28
  }
29
29
  ```
@@ -104,8 +104,12 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
104
104
  private readonly _svixTokenCache = createCache(async () => {
105
105
  return await this._interface.getSvixToken();
106
106
  });
107
- private readonly _metricsCache = createCache(async ([includeAnonymous]: [boolean]) => {
108
- return await this._interface.getMetrics(includeAnonymous);
107
+ // Cache key serializes filters via URLSearchParams (sorted keys) so
108
+ // DependenciesMap (identity-keyed per array slot) treats two equal
109
+ // filter objects as the same deterministic string entry.
110
+ private readonly _metricsCache = createCache(async ([includeAnonymous, filtersKey]: [boolean, string]) => {
111
+ const filters = filtersKey ? Object.fromEntries(new URLSearchParams(filtersKey)) : undefined;
112
+ return await this._interface.getMetrics(includeAnonymous, filters);
109
113
  });
110
114
  private readonly _userActivityCache = createCache(async ([userId]: [string]) => {
111
115
  return await this._interface.getUserActivity(userId);
@@ -556,8 +560,7 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
556
560
  protected override async _refreshUsers() {
557
561
  await Promise.all([
558
562
  super._refreshUsers(),
559
- this._metricsCache.refresh([false]),
560
- this._metricsCache.refresh([true]),
563
+ this._metricsCache.refreshWhere(() => true),
561
564
  this._metricsUserCountsCache.refresh([]),
562
565
  ]);
563
566
  }
@@ -565,8 +568,20 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
565
568
  get [hexclaveAppInternalsSymbol]() {
566
569
  return {
567
570
  ...super[hexclaveAppInternalsSymbol],
568
- useMetrics: (includeAnonymous: boolean = false): MetricsResponse => {
569
- return useAsyncCache(this._metricsCache, [includeAnonymous] as const, "adminApp.useMetrics()") as MetricsResponse;
571
+ useMetrics: (
572
+ includeAnonymous: boolean = false,
573
+ filters?: { country_code?: string, referrer?: string, browser?: string, os?: string, device?: string, since?: string, until?: string },
574
+ ): MetricsResponse => {
575
+ const filtersKey = (() => {
576
+ if (filters == null) return "";
577
+ const params = new URLSearchParams();
578
+ for (const key of ["browser", "country_code", "device", "os", "referrer", "since", "until"] as const) {
579
+ const v = filters[key];
580
+ if (v != null) params.set(key, v);
581
+ }
582
+ return params.toString();
583
+ })();
584
+ return useAsyncCache(this._metricsCache, [includeAnonymous, filtersKey] as const, "adminApp.useMetrics()") as MetricsResponse;
570
585
  },
571
586
  useUserActivity: (userId: string): UserActivityResponse => {
572
587
  return useAsyncCache(this._userActivityCache, [userId] as const, "adminApp.useUserActivity()") as UserActivityResponse;
@@ -287,6 +287,12 @@ describe("StackClientApp cross-domain auth", () => {
287
287
  const originalFetchNewAccessToken = Reflect.get(clientInterface, "fetchNewAccessToken");
288
288
  const refreshedRawRefreshTokens: string[] = [];
289
289
 
290
+ // Cookie-store writes queue a background trusted-parent-domain lookup. Without this stub, that
291
+ // lookup fetches the (unreachable) baseUrl with retries while holding the global store lock,
292
+ // which starves any later test that needs the write lock (e.g. signOut). Not restored on
293
+ // purpose: queued tasks can still run after this test body finishes.
294
+ vi.spyOn(clientApp as any, "_getTrustedParentDomain").mockResolvedValue(null);
295
+
290
296
  try {
291
297
  const getBrowserCookieTokenStore = Reflect.get(clientApp, "_getBrowserCookieTokenStore");
292
298
  if (typeof getBrowserCookieTokenStore !== "function") {
@@ -329,6 +335,108 @@ describe("StackClientApp cross-domain auth", () => {
329
335
  expect(refreshedRawRefreshTokens).toEqual(["new-refresh-token"]);
330
336
  });
331
337
 
338
+ it("does not re-bounce nested cross-domain auth after the OAuth callback consumed code+state from the URL", async () => {
339
+ const projectId = "00000000-0000-4000-8000-000000000008";
340
+ const previousWindow = globalThis.window;
341
+ const previousDocument = globalThis.document;
342
+
343
+ const strippedUrl = new URL(`https://${projectId}.example-stack-hosted.test/handler/sign-in`);
344
+ strippedUrl.searchParams.set("stack_nested_cross_domain_auth_refresh_token_id", "source-refresh-token-id");
345
+ strippedUrl.searchParams.set("stack_nested_cross_domain_auth_callback_url", "https://demo.stack-auth.com/");
346
+ const urlAtConstructionTime = new URL(strippedUrl);
347
+ urlAtConstructionTime.searchParams.set("code", "one-time-code");
348
+ urlAtConstructionTime.searchParams.set("state", "nested-oauth-state");
349
+
350
+ // Construct before installing the window mock so the constructor does not schedule its own
351
+ // nested-auth resolution; the assertions below drive the handler explicitly.
352
+ const clientApp = new StackClientApp({
353
+ baseUrl: "http://localhost:12345",
354
+ projectId,
355
+ publishableClientKey: "stack-pk-test",
356
+ tokenStore: "memory",
357
+ redirectMethod: "window",
358
+ noAutomaticPrefetch: true,
359
+ });
360
+
361
+ globalThis.document = createMockDocument();
362
+ globalThis.window = {
363
+ location: {
364
+ href: strippedUrl.toString(),
365
+ replace: () => {
366
+ throw new Error("INTENTIONAL_TEST_ABORT");
367
+ },
368
+ },
369
+ } as any;
370
+
371
+ vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null);
372
+ vi.spyOn(clientApp as any, "_getCrossDomainHandoffParamsForRedirect").mockResolvedValue({
373
+ state: "fresh-nested-state",
374
+ codeChallenge: "fresh-nested-code-challenge",
375
+ });
376
+ vi.spyOn(clientApp as any, "_isTrusted").mockResolvedValue(true);
377
+
378
+ try {
379
+ // Without the construction-time URL, the handler re-bounces (location.replace aborts).
380
+ await expect((clientApp as any)._maybeHandleNestedCrossDomainAuth()).rejects.toThrowError("INTENTIONAL_TEST_ABORT");
381
+ // With it, the in-flight OAuth callback wins and the handler stands down.
382
+ await expect((clientApp as any)._maybeHandleNestedCrossDomainAuth(urlAtConstructionTime)).resolves.toBe(false);
383
+ // The live-URL guard must also stand down on its own when code+state are still present.
384
+ (globalThis.window as any).location.href = urlAtConstructionTime.toString();
385
+ await expect((clientApp as any)._maybeHandleNestedCrossDomainAuth()).resolves.toBe(false);
386
+ } finally {
387
+ globalThis.window = previousWindow;
388
+ globalThis.document = previousDocument;
389
+ }
390
+ });
391
+
392
+ it("passes the construction-time URL to the nested cross-domain auth handler", async () => {
393
+ const projectId = "00000000-0000-4000-8000-000000000009";
394
+ const previousWindow = globalThis.window;
395
+ const previousDocument = globalThis.document;
396
+
397
+ const callbackUrl = new URL(`https://${projectId}.example-stack-hosted.test/handler/sign-in`);
398
+ callbackUrl.searchParams.set("stack_nested_cross_domain_auth_refresh_token_id", "source-refresh-token-id");
399
+ callbackUrl.searchParams.set("code", "one-time-code");
400
+ callbackUrl.searchParams.set("state", "nested-oauth-state");
401
+ const strippedUrl = new URL(callbackUrl);
402
+ strippedUrl.searchParams.delete("code");
403
+ strippedUrl.searchParams.delete("state");
404
+
405
+ globalThis.document = createMockDocument();
406
+ globalThis.window = {
407
+ location: {
408
+ href: callbackUrl.toString(),
409
+ },
410
+ } as any;
411
+
412
+ const nestedAuthSpy = vi.spyOn(StackClientApp.prototype as any, "_maybeHandleNestedCrossDomainAuth").mockResolvedValue(false);
413
+
414
+ try {
415
+ new StackClientApp({
416
+ baseUrl: "http://localhost:12345",
417
+ projectId,
418
+ publishableClientKey: "stack-pk-test",
419
+ tokenStore: "memory",
420
+ redirectMethod: "window",
421
+ noAutomaticPrefetch: true,
422
+ });
423
+
424
+ // Simulate consumeOAuthCallbackQueryParams stripping code+state before microtasks run.
425
+ (globalThis.window as any).location.href = strippedUrl.toString();
426
+ await new Promise((resolve) => setTimeout(resolve, 0));
427
+
428
+ expect(nestedAuthSpy).toHaveBeenCalledTimes(1);
429
+ const urlArgument = nestedAuthSpy.mock.calls[0][0] as URL;
430
+ expect(urlArgument).toBeInstanceOf(URL);
431
+ expect(urlArgument.searchParams.get("code")).toBe("one-time-code");
432
+ expect(urlArgument.searchParams.get("state")).toBe("nested-oauth-state");
433
+ } finally {
434
+ nestedAuthSpy.mockRestore();
435
+ globalThis.window = previousWindow;
436
+ globalThis.document = previousDocument;
437
+ }
438
+ });
439
+
332
440
  it("uses direct sign-out instead of hosted sign-out redirects when code execution is available", async () => {
333
441
  const clientApp = new StackClientApp({
334
442
  baseUrl: "http://localhost:12345",
@@ -690,8 +690,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
690
690
  }
691
691
 
692
692
  if (isBrowserLike()) {
693
+ // The OAuth callback resolution scheduled above synchronously strips `code` and `state`
694
+ // from the URL before its token exchange, so the nested handler must decide based on the
695
+ // URL the page was loaded with, not whatever is in the address bar when it runs.
696
+ const urlAtConstructionTime = new URL(window.location.href);
693
697
  this._trackPendingAuthResolution(async () => {
694
- await this._maybeHandleNestedCrossDomainAuth();
698
+ await this._maybeHandleNestedCrossDomainAuth(urlAtConstructionTime);
695
699
  });
696
700
  }
697
701
 
@@ -858,11 +862,15 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
858
862
  return targetUrl.toString();
859
863
  }
860
864
 
861
- protected async _maybeHandleNestedCrossDomainAuth(): Promise<boolean> {
865
+ protected async _maybeHandleNestedCrossDomainAuth(urlAtConstructionTime?: URL): Promise<boolean> {
862
866
  if (typeof window === "undefined") return false;
863
867
  const currentUrl = new URL(window.location.href);
864
868
  // A real OAuth callback wins over nested handoff detection on the final return to b.com.
869
+ // The OAuth callback resolution strips `code` and `state` from the live URL before this
870
+ // runs, so the check must also consult the URL captured at construction time — otherwise
871
+ // we'd re-bounce to the source domain while the token exchange is still in flight.
865
872
  if (currentUrl.searchParams.has("code") && currentUrl.searchParams.has("state")) return false;
873
+ if (urlAtConstructionTime != null && urlAtConstructionTime.searchParams.has("code") && urlAtConstructionTime.searchParams.has("state")) return false;
866
874
  const refreshTokenId = currentUrl.searchParams.get(nestedCrossDomainAuthQueryParams.refreshTokenId);
867
875
  if (refreshTokenId == null) return false;
868
876
 
@@ -1501,10 +1509,14 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
1501
1509
  const tokenStore = this._getOrCreateTokenStore(await this._createCookieHelper());
1502
1510
  tokenStore.set(tokens);
1503
1511
 
1504
- // Pre-fetch the current user for the new session so the cache is already
1505
- // populated when useUser() re-renders, avoiding a stale-cache render cycle.
1506
- const newSession = this._getSessionFromTokenStore(tokenStore);
1507
- this._currentUserCache.getOrWait([newSession], "write-only").catch(() => {});
1512
+ // If these tokens resolve to a session we already have (eg. the RDE dashboard re-installing a freshly minted
1513
+ // access token for the same access-only session), push the new token into it in place; constructing a new
1514
+ // session here would cold-invalidate every session-scoped cache and suspend the UI on each refresh.
1515
+ const session = this._getSessionFromTokenStore(tokenStore);
1516
+ session.updateAccessToken(tokens);
1517
+
1518
+ // Pre-fetch the current user so the cache is warm when useUser() re-renders (write-only, so it never suspends).
1519
+ runAsynchronously(this._currentUserCache.getOrWait([session], "write-only"));
1508
1520
  }
1509
1521
 
1510
1522
  protected _getTokenStoreInitForFreshTokens(tokens: { accessToken: string | null, refreshToken: string }): TokenStoreInit | undefined {
@@ -128,6 +128,7 @@ export class EventTracker {
128
128
  viewport_height: window.innerHeight,
129
129
  screen_width: screenObject.width,
130
130
  screen_height: screenObject.height,
131
+ user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
131
132
  },
132
133
  });
133
134
  }