@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.
- package/dist/config.d.ts +2 -0
- package/dist/config.js +22 -0
- package/dist/esm/config.d.ts +2 -0
- package/dist/esm/config.js +3 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +9 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +23 -6
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +78 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +11 -6
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +2 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +9 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +23 -6
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +78 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +11 -6
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +2 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/package.json +13 -4
- package/src/config.ts +17 -0
- package/src/integrations/convex/component/README.md +1 -1
- package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +21 -6
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +108 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +18 -6
- 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.
|
|
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,
|
|
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.
|
|
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.
|
|
70
|
-
"@hexclave/
|
|
71
|
-
"@hexclave/
|
|
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.
|
|
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
|
-
|
|
108
|
-
|
|
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.
|
|
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: (
|
|
569
|
-
|
|
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
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
|
|
1507
|
-
this.
|
|
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 {
|