@agent-native/core 0.69.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +48 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/changelog/parse.ts +245 -0
- package/corpus/core/src/cli/changelog.ts +199 -0
- package/corpus/core/src/cli/index.ts +19 -0
- package/corpus/core/src/cli/recap.ts +24 -1
- package/corpus/core/src/cli/skills.ts +72 -45
- package/corpus/core/src/client/CommandMenu.tsx +183 -75
- package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
- package/corpus/core/src/client/analytics.ts +181 -0
- package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
- package/corpus/core/src/client/index.ts +11 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
- package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
- package/corpus/core/src/mcp/build-server.ts +1 -1
- package/corpus/core/src/mcp/embed-app.ts +112 -6
- package/corpus/core/src/server/attribution.ts +211 -0
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/server/better-auth-instance.ts +49 -5
- package/corpus/core/src/server/builder-browser.ts +27 -1
- package/corpus/core/src/server/core-routes-plugin.ts +58 -0
- package/corpus/core/src/server/google-oauth.ts +19 -6
- package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
- package/corpus/core/src/styles/agent-conversation.css +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/CHANGELOG.md +10 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +2 -2
- package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +5 -3
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
- package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -213
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -3
- package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
- package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
- package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
- package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
- package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
- package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
- package/corpus/templates/analytics/server/db/index.ts +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
- package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
- package/corpus/templates/assets/CHANGELOG.md +10 -0
- package/corpus/templates/assets/app/root.tsx +7 -1
- package/corpus/templates/assets/app/routes/settings.tsx +6 -0
- package/corpus/templates/brain/CHANGELOG.md +10 -0
- package/corpus/templates/brain/app/root.tsx +7 -1
- package/corpus/templates/brain/app/routes/settings.tsx +8 -1
- package/corpus/templates/calendar/CHANGELOG.md +10 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
- package/corpus/templates/calendar/app/root.tsx +7 -1
- package/corpus/templates/chat/CHANGELOG.md +10 -0
- package/corpus/templates/chat/app/root.tsx +7 -1
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
- package/corpus/templates/clips/app/root.tsx +7 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
- package/corpus/templates/clips/app/routes/record.tsx +78 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
- package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
- package/corpus/templates/clips/desktop/src/styles.css +49 -0
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +67 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +51 -1
- package/corpus/templates/clips/shared/share-attribution.ts +79 -0
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/app/root.tsx +7 -1
- package/corpus/templates/design/CHANGELOG.md +10 -0
- package/corpus/templates/design/actions/generate-design.ts +4 -0
- package/corpus/templates/design/app/root.tsx +7 -1
- package/corpus/templates/design/app/routes/settings.tsx +10 -2
- package/corpus/templates/dispatch/CHANGELOG.md +10 -0
- package/corpus/templates/dispatch/app/root.tsx +7 -1
- package/corpus/templates/forms/CHANGELOG.md +10 -0
- package/corpus/templates/forms/app/root.tsx +7 -1
- package/corpus/templates/macros/CHANGELOG.md +10 -0
- package/corpus/templates/macros/app/root.tsx +7 -1
- package/corpus/templates/mail/CHANGELOG.md +10 -0
- package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
- package/corpus/templates/plan/AGENTS.md +4 -0
- package/corpus/templates/plan/CHANGELOG.md +10 -0
- package/corpus/templates/plan/actions/view-screen.ts +5 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
- package/corpus/templates/plan/app/root.tsx +7 -1
- package/corpus/templates/plan/shared/share-attribution.ts +72 -0
- package/corpus/templates/slides/CHANGELOG.md +10 -0
- package/corpus/templates/slides/app/root.tsx +7 -1
- package/corpus/templates/videos/CHANGELOG.md +4 -151
- package/corpus/templates/videos/app/root.tsx +7 -1
- package/dist/changelog/parse.d.ts +70 -0
- package/dist/changelog/parse.d.ts.map +1 -0
- package/dist/changelog/parse.js +200 -0
- package/dist/changelog/parse.js.map +1 -0
- package/dist/cli/changelog.d.ts +4 -0
- package/dist/cli/changelog.d.ts.map +1 -0
- package/dist/cli/changelog.js +166 -0
- package/dist/cli/changelog.js.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +22 -1
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -3
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +72 -45
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +17 -1
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +49 -13
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +43 -6
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +18 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +177 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/changelog/Changelog.d.ts +47 -0
- package/dist/client/changelog/Changelog.d.ts.map +1 -0
- package/dist/client/changelog/Changelog.js +138 -0
- package/dist/client/changelog/Changelog.js.map +1 -0
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +2 -0
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +58 -10
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/mcp/build-server.js +1 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +112 -6
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/server/attribution.d.ts +83 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +178 -0
- package/dist/server/attribution.js.map +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +9 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +32 -2
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +20 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +38 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +16 -2
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/styles/agent-conversation.css +21 -0
- package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
|
@@ -195,6 +195,8 @@ const BUILDER_STATE_PARAM = "_an_state";
|
|
|
195
195
|
const BUILDER_SIGNUP_SOURCE_PARAM = "signupSource";
|
|
196
196
|
const BUILDER_AGENT_NATIVE_FLOW_PARAM = "agentNativeFlow";
|
|
197
197
|
const BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM = "agentNativeConnectSource";
|
|
198
|
+
const BUILDER_AGENT_NATIVE_APP_PARAM = "agentNativeApp";
|
|
199
|
+
const BUILDER_AGENT_NATIVE_TEMPLATE_PARAM = "agentNativeTemplate";
|
|
198
200
|
const BUILDER_SIGNUP_SOURCE = "agent-native";
|
|
199
201
|
const STATUS_CONNECT_URL_TTL_MS = 9 * 60 * 1000;
|
|
200
202
|
|
|
@@ -224,42 +226,102 @@ function inferBuilderConnectTrackingFlow(source: string | undefined): string {
|
|
|
224
226
|
return "connect_llm";
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
function normalizeTrackingSlug(
|
|
230
|
+
value: string | null | undefined,
|
|
231
|
+
): string | null {
|
|
232
|
+
const trimmed = value?.trim();
|
|
233
|
+
if (!trimmed) return null;
|
|
234
|
+
const unscoped = trimmed.startsWith("@")
|
|
235
|
+
? (trimmed.split("/").pop() ?? trimmed)
|
|
236
|
+
: trimmed;
|
|
237
|
+
const slug = unscoped
|
|
238
|
+
.toLowerCase()
|
|
239
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
240
|
+
.replace(/^-+|-+$/g, "");
|
|
241
|
+
return slug || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function inferBuilderConnectTrackingIdentity(options: {
|
|
245
|
+
app?: string;
|
|
246
|
+
template?: string;
|
|
247
|
+
}): { app: string | null; template: string | null } {
|
|
248
|
+
const env = (import.meta.env as Record<string, string | undefined>) ?? {};
|
|
249
|
+
const app =
|
|
250
|
+
normalizeTrackingSlug(options.app) ??
|
|
251
|
+
normalizeTrackingSlug(env.VITE_AGENT_NATIVE_APP) ??
|
|
252
|
+
(typeof window !== "undefined"
|
|
253
|
+
? normalizeTrackingSlug(window.location.hostname.split(".")[0])
|
|
254
|
+
: null);
|
|
255
|
+
const template =
|
|
256
|
+
normalizeTrackingSlug(options.template) ??
|
|
257
|
+
normalizeTrackingSlug(env.VITE_AGENT_NATIVE_TEMPLATE) ??
|
|
258
|
+
normalizeTrackingSlug(env.VITE_APP_TEMPLATE) ??
|
|
259
|
+
(app?.startsWith("agent-native-")
|
|
260
|
+
? normalizeTrackingSlug(app.slice("agent-native-".length))
|
|
261
|
+
: app && app !== "localhost"
|
|
262
|
+
? app
|
|
263
|
+
: null);
|
|
264
|
+
|
|
265
|
+
return { app, template };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function applyBuilderConnectTrackingParams(
|
|
269
|
+
params: URLSearchParams,
|
|
270
|
+
tracking: {
|
|
271
|
+
source?: string | null;
|
|
272
|
+
flow: string;
|
|
273
|
+
app?: string | null;
|
|
274
|
+
template?: string | null;
|
|
275
|
+
},
|
|
276
|
+
) {
|
|
277
|
+
params.set(BUILDER_SIGNUP_SOURCE_PARAM, BUILDER_SIGNUP_SOURCE);
|
|
278
|
+
params.set(BUILDER_AGENT_NATIVE_FLOW_PARAM, tracking.flow);
|
|
279
|
+
if (tracking.source) {
|
|
280
|
+
params.set(BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM, tracking.source);
|
|
281
|
+
}
|
|
282
|
+
if (tracking.app) {
|
|
283
|
+
params.set(BUILDER_AGENT_NATIVE_APP_PARAM, tracking.app);
|
|
284
|
+
}
|
|
285
|
+
if (tracking.template) {
|
|
286
|
+
params.set(BUILDER_AGENT_NATIVE_TEMPLATE_PARAM, tracking.template);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
227
290
|
export function withBuilderConnectTrackingParams(
|
|
228
291
|
url: string,
|
|
229
|
-
options: {
|
|
292
|
+
options: {
|
|
293
|
+
source?: string;
|
|
294
|
+
flow?: string;
|
|
295
|
+
app?: string;
|
|
296
|
+
template?: string;
|
|
297
|
+
} = {},
|
|
230
298
|
): string {
|
|
231
299
|
const source = cleanTrackingParam(options.source);
|
|
232
300
|
const flow =
|
|
233
301
|
cleanTrackingParam(options.flow) ??
|
|
234
302
|
inferBuilderConnectTrackingFlow(source ?? undefined);
|
|
303
|
+
const { app, template } = inferBuilderConnectTrackingIdentity(options);
|
|
235
304
|
const origin =
|
|
236
305
|
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
237
306
|
|
|
238
307
|
try {
|
|
239
308
|
const parsed = new URL(url, origin);
|
|
240
|
-
parsed.searchParams
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
);
|
|
247
|
-
}
|
|
309
|
+
applyBuilderConnectTrackingParams(parsed.searchParams, {
|
|
310
|
+
source,
|
|
311
|
+
flow,
|
|
312
|
+
app,
|
|
313
|
+
template,
|
|
314
|
+
});
|
|
248
315
|
|
|
249
316
|
const redirectUrl = parsed.searchParams.get("redirect_url");
|
|
250
317
|
if (redirectUrl) {
|
|
251
318
|
const parsedRedirect = new URL(redirectUrl);
|
|
252
|
-
parsedRedirect.searchParams
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
parsedRedirect.searchParams.set(
|
|
259
|
-
BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM,
|
|
260
|
-
source,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
319
|
+
applyBuilderConnectTrackingParams(parsedRedirect.searchParams, {
|
|
320
|
+
source,
|
|
321
|
+
flow,
|
|
322
|
+
app,
|
|
323
|
+
template,
|
|
324
|
+
});
|
|
263
325
|
parsed.searchParams.set("redirect_url", parsedRedirect.toString());
|
|
264
326
|
}
|
|
265
327
|
|
|
@@ -708,7 +708,7 @@ function safeUiSegment(value: string | undefined, fallback: string): string {
|
|
|
708
708
|
|
|
709
709
|
// ChatGPT and Claude cache MCP App resource HTML by `ui://` URI. Bump this
|
|
710
710
|
// when the shared shell changes in a way that must invalidate host caches.
|
|
711
|
-
const MCP_APP_RESOURCE_SHELL_VERSION = "shell-
|
|
711
|
+
const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v45";
|
|
712
712
|
|
|
713
713
|
function legacyDefaultMcpAppUri(config: MCPConfig, actionName: string): string {
|
|
714
714
|
const app = safeUiSegment(config.appId ?? config.name, "agent-native");
|
|
@@ -455,6 +455,49 @@ export function embedApp(
|
|
|
455
455
|
};
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
function externalOpenUrlForAppUrl(appUrl) {
|
|
459
|
+
if (openUrl) return openUrl;
|
|
460
|
+
try {
|
|
461
|
+
const url = new URL(appUrl.href);
|
|
462
|
+
url.searchParams.delete("embedded");
|
|
463
|
+
url.searchParams.delete("__an_embed_token");
|
|
464
|
+
url.searchParams.delete(chatBridgeParam);
|
|
465
|
+
return url.toString();
|
|
466
|
+
} catch (_err) {
|
|
467
|
+
return "";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function installExternalOpenControl(appUrl) {
|
|
472
|
+
const href = externalOpenUrlForAppUrl(appUrl);
|
|
473
|
+
if (!href || !document.body || document.getElementById("agent-native-external-open-control")) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const host = document.createElement("div");
|
|
477
|
+
host.id = "agent-native-external-open-control";
|
|
478
|
+
host.style.position = "fixed";
|
|
479
|
+
host.style.top = "10px";
|
|
480
|
+
host.style.right = "10px";
|
|
481
|
+
host.style.zIndex = "2147483647";
|
|
482
|
+
host.style.pointerEvents = "auto";
|
|
483
|
+
const root = host.attachShadow ? host.attachShadow({ mode: "open" }) : host;
|
|
484
|
+
root.innerHTML =
|
|
485
|
+
'<style>' +
|
|
486
|
+
':host{all:initial;}' +
|
|
487
|
+
'button{box-sizing:border-box;min-height:30px;border:1px solid color-mix(in srgb, CanvasText 18%, Canvas);border-radius:7px;background:Canvas;color:CanvasText;box-shadow:0 6px 22px color-mix(in srgb, CanvasText 14%, transparent);cursor:pointer;font:700 12px ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:0 10px;white-space:nowrap;}' +
|
|
488
|
+
'button:hover{background:color-mix(in srgb, CanvasText 6%, Canvas);}' +
|
|
489
|
+
'</style>' +
|
|
490
|
+
'<button type="button">Open in new tab</button>';
|
|
491
|
+
const button = root.querySelector("button");
|
|
492
|
+
if (button) {
|
|
493
|
+
button.addEventListener("click", (event) => {
|
|
494
|
+
event.preventDefault();
|
|
495
|
+
void openHostLink({ url: href });
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
document.body.appendChild(host);
|
|
499
|
+
}
|
|
500
|
+
|
|
458
501
|
function installReactRefreshPreambleFallback() {
|
|
459
502
|
window.__vite_plugin_react_preamble_installed__ = true;
|
|
460
503
|
if (typeof window.$RefreshReg$ !== "function") {
|
|
@@ -795,6 +838,7 @@ export function embedApp(
|
|
|
795
838
|
base.href = config.baseHref;
|
|
796
839
|
document.head.prepend(base);
|
|
797
840
|
importChildren(parsed.body, document.body);
|
|
841
|
+
installExternalOpenControl(appUrl);
|
|
798
842
|
for (const script of scripts) {
|
|
799
843
|
if (isRunnableClassicScript(script)) runClassicScript(script);
|
|
800
844
|
}
|
|
@@ -987,6 +1031,42 @@ export function embedApp(
|
|
|
987
1031
|
}
|
|
988
1032
|
}
|
|
989
1033
|
|
|
1034
|
+
function renderAppLaunchError(message) {
|
|
1035
|
+
clearFrameReadyTimer();
|
|
1036
|
+
clearFrameLoadTimer();
|
|
1037
|
+
appFrame = null;
|
|
1038
|
+
const fallbackCopy = openUrl
|
|
1039
|
+
? "The inline MCP app could not load in this chat host. You can open the same app route in a new tab or retry the inline load."
|
|
1040
|
+
: "The inline MCP app could not load in this chat host.";
|
|
1041
|
+
const copyHtml = message
|
|
1042
|
+
? '<div>' + esc(message) + '</div><div>' + esc(fallbackCopy) + '</div>'
|
|
1043
|
+
: esc(fallbackCopy);
|
|
1044
|
+
stage.innerHTML =
|
|
1045
|
+
'<div class="fallback">' +
|
|
1046
|
+
'<div class="fallback-title">App did not load</div>' +
|
|
1047
|
+
'<div class="fallback-copy">' + copyHtml + '</div>' +
|
|
1048
|
+
'<div class="fallback-actions">' +
|
|
1049
|
+
'<button type="button" data-fallback-open>Open in new tab</button>' +
|
|
1050
|
+
'<button type="button" data-fallback-retry>Retry</button>' +
|
|
1051
|
+
'</div>' +
|
|
1052
|
+
(openUrl ? '<a class="fallback-url" href="' + esc(openUrl) + '" target="_blank" rel="noreferrer">' + esc(openUrl) + '</a>' : '') +
|
|
1053
|
+
'</div>';
|
|
1054
|
+
const fallbackOpen = stage.querySelector("[data-fallback-open]");
|
|
1055
|
+
const fallbackRetry = stage.querySelector("[data-fallback-retry]");
|
|
1056
|
+
if (fallbackOpen) {
|
|
1057
|
+
fallbackOpen.disabled = !openUrl;
|
|
1058
|
+
fallbackOpen.onclick = () => {
|
|
1059
|
+
if (openUrl) void openHostLink({ url: openUrl });
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
if (fallbackRetry) {
|
|
1063
|
+
fallbackRetry.onclick = () => {
|
|
1064
|
+
startedFor = "";
|
|
1065
|
+
void launchEmbed();
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
990
1070
|
async function openFallbackExternal() {
|
|
991
1071
|
if (!openUrl) return;
|
|
992
1072
|
let url = withChatBridgeParam(openUrl);
|
|
@@ -1051,12 +1131,29 @@ export function embedApp(
|
|
|
1051
1131
|
return true;
|
|
1052
1132
|
}
|
|
1053
1133
|
|
|
1134
|
+
// We have connected to a standards-track MCP Apps host (Codex, Cursor,
|
|
1135
|
+
// Claude over the SDK, our own renderer, …) through the postMessage
|
|
1136
|
+
// \`ui/*\` bridge rather than ChatGPT's \`window.openai\` global. These hosts
|
|
1137
|
+
// render the resource in a strict sandboxed iframe (typically
|
|
1138
|
+
// \`sandbox="allow-scripts"\`, opaque origin). Self-navigating that iframe to
|
|
1139
|
+
// the real app origin tears down the host bridge and loses the opaque-origin
|
|
1140
|
+
// auth context, which shows up as a permanent / flashing loading state.
|
|
1141
|
+
// Transplanting the app document into the shell keeps the bridge alive and
|
|
1142
|
+
// works under the opaque origin via embed-token auth, exactly like Claude.
|
|
1143
|
+
function isNativeMcpAppsBridgeHost() {
|
|
1144
|
+
return !!app && !openAiBridge;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1054
1147
|
function shouldTransplantAppDocument() {
|
|
1055
1148
|
const render = renderModeSource();
|
|
1056
1149
|
const mode = render.mode;
|
|
1150
|
+
if (mode === "iframe" || mode === "nested" || render.frame === "iframe" || render.nested) {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1057
1153
|
return (
|
|
1058
1154
|
isClaudeMcpContentHost() ||
|
|
1059
1155
|
isChatGptSandboxHost() ||
|
|
1156
|
+
isNativeMcpAppsBridgeHost() ||
|
|
1060
1157
|
mode === "transplant" ||
|
|
1061
1158
|
render.frame === "transplant"
|
|
1062
1159
|
);
|
|
@@ -1074,7 +1171,7 @@ export function embedApp(
|
|
|
1074
1171
|
try {
|
|
1075
1172
|
const host = window.location.hostname || "";
|
|
1076
1173
|
const appParam = new URL(window.location.href).searchParams.get("app");
|
|
1077
|
-
return /^[^.]+\\.web-sandbox\\.oaiusercontent\\.com$/i.test(host) || appParam === "chatgpt";
|
|
1174
|
+
return /^(?:[^.]+\\.)?web-sandbox\\.oaiusercontent\\.com$/i.test(host) || appParam === "chatgpt";
|
|
1078
1175
|
} catch {
|
|
1079
1176
|
return false;
|
|
1080
1177
|
}
|
|
@@ -1297,7 +1394,7 @@ export function embedApp(
|
|
|
1297
1394
|
async function launchEmbed() {
|
|
1298
1395
|
const launchUrl = openStartUrl || openUrl;
|
|
1299
1396
|
if (!launchUrl) {
|
|
1300
|
-
|
|
1397
|
+
renderAppLaunchError("Open link was not available.");
|
|
1301
1398
|
return;
|
|
1302
1399
|
}
|
|
1303
1400
|
if (!wantsEmbed()) {
|
|
@@ -1328,7 +1425,7 @@ export function embedApp(
|
|
|
1328
1425
|
const data = parseToolResult(result);
|
|
1329
1426
|
if (typeof data.startUrl !== "string" || !data.startUrl) {
|
|
1330
1427
|
startedFor = "";
|
|
1331
|
-
|
|
1428
|
+
renderAppLaunchError(data.error || "This app can be opened, but not embedded from this MCP server.");
|
|
1332
1429
|
return;
|
|
1333
1430
|
}
|
|
1334
1431
|
const startUrl = withChatBridgeParam(data.startUrl);
|
|
@@ -1345,7 +1442,7 @@ export function embedApp(
|
|
|
1345
1442
|
}
|
|
1346
1443
|
} catch (err) {
|
|
1347
1444
|
startedFor = "";
|
|
1348
|
-
|
|
1445
|
+
renderAppLaunchError(err && err.message ? err.message : "Could not launch embedded app.");
|
|
1349
1446
|
}
|
|
1350
1447
|
}
|
|
1351
1448
|
|
|
@@ -1395,6 +1492,9 @@ export function embedApp(
|
|
|
1395
1492
|
try {
|
|
1396
1493
|
if (new URLSearchParams(window.location.search).get("app") === "chatgpt") return true;
|
|
1397
1494
|
} catch (_err) {}
|
|
1495
|
+
try {
|
|
1496
|
+
if (/^(?:[^.]+\\.)?web-sandbox\\.oaiusercontent\\.com$/i.test(window.location.hostname || "")) return true;
|
|
1497
|
+
} catch (_err) {}
|
|
1398
1498
|
try {
|
|
1399
1499
|
return /chatgpt/i.test(String(navigator.userAgent || ""));
|
|
1400
1500
|
} catch (_err) {
|
|
@@ -1578,8 +1678,13 @@ export function embedApp(
|
|
|
1578
1678
|
async openLink(params) {
|
|
1579
1679
|
const url = typeof (params && params.url) === "string" ? params.url : "";
|
|
1580
1680
|
if (!url) return { isError: true };
|
|
1581
|
-
|
|
1582
|
-
|
|
1681
|
+
try {
|
|
1682
|
+
return await rpcRequest("ui/open-link", { url }, wrapperRequestTimeoutMs);
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
console.warn("[agent-native] MCP host open-link request failed", err);
|
|
1685
|
+
}
|
|
1686
|
+
const opened = window.open(url, "_blank", "noopener,noreferrer");
|
|
1687
|
+
return opened ? { ok: true } : { isError: true };
|
|
1583
1688
|
},
|
|
1584
1689
|
async requestDisplayMode(params) {
|
|
1585
1690
|
return await rpcRequest("ui/request-display-mode", objectValue(params));
|
|
@@ -1615,6 +1720,7 @@ export function embedApp(
|
|
|
1615
1720
|
return;
|
|
1616
1721
|
}
|
|
1617
1722
|
if (
|
|
1723
|
+
message.method === "ui/notifications/host-context-changed" ||
|
|
1618
1724
|
message.method === "ui/notifications/host-context" ||
|
|
1619
1725
|
message.method === "ui/notifications/context"
|
|
1620
1726
|
) {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-touch referral attribution — server side.
|
|
3
|
+
*
|
|
4
|
+
* The browser captures an anonymous visitor's *first* landing context (referral
|
|
5
|
+
* source, UTM params, referring host, landing path) and persists it across the
|
|
6
|
+
* signup boundary in a first-party cookie named `an_ft` (see
|
|
7
|
+
* `client/analytics.ts`). On signup, the auth hook reads that cookie off the
|
|
8
|
+
* request and enriches the canonical server-side `signup` event so we can
|
|
9
|
+
* measure where new users came from and how apps spread (virality).
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally pure and dependency-free so it is trivially
|
|
12
|
+
* unit-testable and can never throw into the signup path. The single hard rule:
|
|
13
|
+
* parsing untrusted cookie input must NEVER throw — every accessor is defensive.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The decoded first-touch attribution object. Mirrors the compact JSON the
|
|
18
|
+
* client writes into the `an_ft` cookie / `an_attribution` localStorage key.
|
|
19
|
+
* Every field is optional — the client omits empty fields to keep the cookie
|
|
20
|
+
* small, and a malformed/absent cookie yields `null`.
|
|
21
|
+
*/
|
|
22
|
+
export interface FirstTouchAttribution {
|
|
23
|
+
/** Referral source bucket, e.g. "clip_share", "plan_share". */
|
|
24
|
+
ref?: string;
|
|
25
|
+
/** Referrer's stable user id (the clip/plan owner who shared the link). */
|
|
26
|
+
via?: string;
|
|
27
|
+
utm_source?: string;
|
|
28
|
+
utm_medium?: string;
|
|
29
|
+
utm_campaign?: string;
|
|
30
|
+
utm_content?: string;
|
|
31
|
+
utm_term?: string;
|
|
32
|
+
/** `window.location.pathname` of the first page the visitor landed on. */
|
|
33
|
+
landing_path?: string;
|
|
34
|
+
/** Host of `document.referrer` (scrubbed; host only, never a full URL). */
|
|
35
|
+
landing_referrer?: string;
|
|
36
|
+
/** ISO timestamp of when the visitor first landed. */
|
|
37
|
+
landed_at?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Cookie name written by the client (non-HttpOnly; non-sensitive). */
|
|
41
|
+
export const FIRST_TOUCH_COOKIE_NAME = "an_ft";
|
|
42
|
+
|
|
43
|
+
const STRING_FIELDS: Array<keyof FirstTouchAttribution> = [
|
|
44
|
+
"ref",
|
|
45
|
+
"via",
|
|
46
|
+
"utm_source",
|
|
47
|
+
"utm_medium",
|
|
48
|
+
"utm_campaign",
|
|
49
|
+
"utm_content",
|
|
50
|
+
"utm_term",
|
|
51
|
+
"landing_path",
|
|
52
|
+
"landing_referrer",
|
|
53
|
+
"landed_at",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a raw `Cookie:` header into a flat name→value map. Tolerates missing
|
|
58
|
+
* input, extra whitespace, `=` inside values, and malformed pairs. Never throws.
|
|
59
|
+
*/
|
|
60
|
+
export function parseCookieHeader(
|
|
61
|
+
cookieHeader: string | null | undefined,
|
|
62
|
+
): Record<string, string> {
|
|
63
|
+
const out: Record<string, string> = {};
|
|
64
|
+
if (!cookieHeader || typeof cookieHeader !== "string") return out;
|
|
65
|
+
for (const part of cookieHeader.split(";")) {
|
|
66
|
+
const eq = part.indexOf("=");
|
|
67
|
+
if (eq === -1) continue;
|
|
68
|
+
const name = part.slice(0, eq).trim();
|
|
69
|
+
if (!name) continue;
|
|
70
|
+
const rawValue = part.slice(eq + 1).trim();
|
|
71
|
+
// First write wins so a duplicate cookie name can't clobber the first.
|
|
72
|
+
if (name in out) continue;
|
|
73
|
+
out[name] = rawValue;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decode a single cookie value into a `FirstTouchAttribution`. The value is the
|
|
80
|
+
* URL-encoded compact JSON written by the client. Returns `null` for empty,
|
|
81
|
+
* malformed, or non-object input. Only known string fields are copied through,
|
|
82
|
+
* each clamped to a sane max length as a defense against an oversized cookie.
|
|
83
|
+
*/
|
|
84
|
+
export function decodeFirstTouchValue(
|
|
85
|
+
value: string | null | undefined,
|
|
86
|
+
): FirstTouchAttribution | null {
|
|
87
|
+
if (!value || typeof value !== "string") return null;
|
|
88
|
+
let decoded = value;
|
|
89
|
+
try {
|
|
90
|
+
decoded = decodeURIComponent(value);
|
|
91
|
+
} catch {
|
|
92
|
+
// Not valid percent-encoding — fall back to the raw value and let the JSON
|
|
93
|
+
// parse below decide whether it's usable.
|
|
94
|
+
decoded = value;
|
|
95
|
+
}
|
|
96
|
+
let parsed: unknown;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(decoded);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const source = parsed as Record<string, unknown>;
|
|
106
|
+
const result: FirstTouchAttribution = {};
|
|
107
|
+
let any = false;
|
|
108
|
+
for (const field of STRING_FIELDS) {
|
|
109
|
+
const raw = source[field];
|
|
110
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
111
|
+
result[field] = raw.slice(0, 120);
|
|
112
|
+
any = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return any ? result : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read the `an_ft` first-touch attribution out of a raw `Cookie:` header.
|
|
120
|
+
* Returns `null` when the cookie is absent or unparseable. Never throws.
|
|
121
|
+
*/
|
|
122
|
+
export function readFirstTouchAttribution(
|
|
123
|
+
cookieHeader: string | null | undefined,
|
|
124
|
+
): FirstTouchAttribution | null {
|
|
125
|
+
try {
|
|
126
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
127
|
+
return decodeFirstTouchValue(cookies[FIRST_TOUCH_COOKIE_NAME]);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isExternalReferrerHost(host: string | undefined): boolean {
|
|
134
|
+
const trimmed = host?.trim();
|
|
135
|
+
return !!trimmed && trimmed.length > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Derive the `referral_source` bucket from first-touch attribution per the
|
|
140
|
+
* contract:
|
|
141
|
+
* 1. explicit `ref` wins;
|
|
142
|
+
* 2. landing path under `/share/` => "clip_share";
|
|
143
|
+
* 3. landing path that looks like a public plan page
|
|
144
|
+
* (contains `/p/`, `/plan/`, or `/share-plan/`) => "plan_share";
|
|
145
|
+
* 4. a non-empty external referring host => "external";
|
|
146
|
+
* 5. otherwise => "direct".
|
|
147
|
+
*/
|
|
148
|
+
export function deriveReferralSource(ft: FirstTouchAttribution | null): string {
|
|
149
|
+
if (ft?.ref && ft.ref.trim()) return ft.ref.trim();
|
|
150
|
+
const path = ft?.landing_path ?? "";
|
|
151
|
+
if (path.startsWith("/share/")) return "clip_share";
|
|
152
|
+
if (
|
|
153
|
+
path.includes("/p/") ||
|
|
154
|
+
path.includes("/plan/") ||
|
|
155
|
+
path.includes("/share-plan/")
|
|
156
|
+
) {
|
|
157
|
+
return "plan_share";
|
|
158
|
+
}
|
|
159
|
+
if (isExternalReferrerHost(ft?.landing_referrer)) return "external";
|
|
160
|
+
return "direct";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Compute the snake_case signup-event properties from first-touch attribution.
|
|
165
|
+
* Returns a clean object with `undefined` values omitted, ready to merge into
|
|
166
|
+
* the `signup` track call. Always sets `referral_source` (defaults to "direct").
|
|
167
|
+
*
|
|
168
|
+
* Pure and total — given any (or no) input it returns a well-formed object and
|
|
169
|
+
* never throws.
|
|
170
|
+
*/
|
|
171
|
+
export function deriveSignupAttribution(
|
|
172
|
+
ft: FirstTouchAttribution | null,
|
|
173
|
+
): Record<string, string> {
|
|
174
|
+
const out: Record<string, string> = {
|
|
175
|
+
referral_source: deriveReferralSource(ft),
|
|
176
|
+
};
|
|
177
|
+
if (!ft) return out;
|
|
178
|
+
|
|
179
|
+
const setIf = (key: string, value: string | undefined) => {
|
|
180
|
+
const trimmed = value?.trim();
|
|
181
|
+
if (trimmed) out[key] = trimmed;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
setIf("referrer_user", ft.via);
|
|
185
|
+
setIf("referral_medium", ft.utm_medium);
|
|
186
|
+
setIf("referral_campaign", ft.utm_campaign);
|
|
187
|
+
setIf("utm_source", ft.utm_source);
|
|
188
|
+
setIf("utm_medium", ft.utm_medium);
|
|
189
|
+
setIf("utm_campaign", ft.utm_campaign);
|
|
190
|
+
setIf("utm_content", ft.utm_content);
|
|
191
|
+
setIf("utm_term", ft.utm_term);
|
|
192
|
+
setIf("first_touch_path", ft.landing_path);
|
|
193
|
+
setIf("landing_referrer", ft.landing_referrer);
|
|
194
|
+
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convenience: read the cookie header and derive signup attribution in one
|
|
200
|
+
* call. Never throws; falls back to `{ referral_source: "direct" }` on any
|
|
201
|
+
* error.
|
|
202
|
+
*/
|
|
203
|
+
export function signupAttributionFromCookieHeader(
|
|
204
|
+
cookieHeader: string | null | undefined,
|
|
205
|
+
): Record<string, string> {
|
|
206
|
+
try {
|
|
207
|
+
return deriveSignupAttribution(readFirstTouchAttribution(cookieHeader));
|
|
208
|
+
} catch {
|
|
209
|
+
return { referral_source: "direct" };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -1796,6 +1796,12 @@ function createAuthGuardFn(): (
|
|
|
1796
1796
|
// route tree, no per-user data.
|
|
1797
1797
|
if (p === "/__manifest") return;
|
|
1798
1798
|
if (p === "/_agent-native/speculation-rules.json") return;
|
|
1799
|
+
// Liveness probe: always public so uptime monitors and the keep-warm cron
|
|
1800
|
+
// can reach the DB-warmup route without a session. It exposes no per-user
|
|
1801
|
+
// data (just ok/db/ms) and runs a trivial `SELECT 1`. Without this bypass
|
|
1802
|
+
// the gate below 401s anonymous /_agent-native/* requests before any DB
|
|
1803
|
+
// query, so the database would never get warmed.
|
|
1804
|
+
if (p === "/_agent-native/health") return;
|
|
1799
1805
|
if (isPublicPath(normalizedUrl, publicPaths)) return;
|
|
1800
1806
|
if (shouldBypassAuthForBuilderConnect(event, p)) return;
|
|
1801
1807
|
if (isPublicWorkspacePageRequest(event, p, config)) {
|
|
@@ -23,6 +23,7 @@ import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
|
|
|
23
23
|
import { autoJoinDomainMatchingOrgs } from "../org/auto-join-domain.js";
|
|
24
24
|
import { saveOAuthTokens } from "../oauth-tokens/store.js";
|
|
25
25
|
import { flushTracking, identify, track } from "../tracking/index.js";
|
|
26
|
+
import { signupAttributionFromCookieHeader } from "./attribution.js";
|
|
26
27
|
import { TEMPLATES } from "../cli/templates-meta.js";
|
|
27
28
|
import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
|
|
28
29
|
import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
|
|
@@ -72,23 +73,41 @@ export async function trackSignupEvent({
|
|
|
72
73
|
authUserId,
|
|
73
74
|
email,
|
|
74
75
|
name,
|
|
76
|
+
attribution,
|
|
75
77
|
}: {
|
|
76
78
|
authProvider: string;
|
|
77
79
|
authUserId?: string;
|
|
78
80
|
email: string;
|
|
79
81
|
name?: string | null;
|
|
82
|
+
/**
|
|
83
|
+
* First-touch referral attribution derived from the visitor's `an_ft`
|
|
84
|
+
* cookie (see `server/attribution.ts`). Snake_case keys such as
|
|
85
|
+
* `referral_source`, `referrer_user`, and the UTM passthrough are merged
|
|
86
|
+
* into the `signup` event so we can measure where new users came from.
|
|
87
|
+
* `undefined` values are dropped; a missing object is a clean no-op.
|
|
88
|
+
*/
|
|
89
|
+
attribution?: Record<string, string | undefined>;
|
|
80
90
|
}): Promise<void> {
|
|
81
91
|
identify(email, {
|
|
82
92
|
email,
|
|
83
93
|
name: name ?? undefined,
|
|
84
94
|
authUserId,
|
|
85
95
|
});
|
|
96
|
+
const cleanAttribution: Record<string, string> = {};
|
|
97
|
+
if (attribution) {
|
|
98
|
+
for (const [key, value] of Object.entries(attribution)) {
|
|
99
|
+
if (typeof value === "string" && value.length > 0) {
|
|
100
|
+
cleanAttribution[key] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
86
104
|
track(
|
|
87
105
|
"signup",
|
|
88
106
|
{
|
|
89
107
|
...resolveSignupTrackingProperties(),
|
|
90
108
|
auth_provider: authProvider,
|
|
91
109
|
...(authUserId ? { auth_user_id: authUserId } : {}),
|
|
110
|
+
...cleanAttribution,
|
|
92
111
|
},
|
|
93
112
|
{ userId: email },
|
|
94
113
|
);
|
|
@@ -929,22 +948,47 @@ async function createBetterAuthInstance(
|
|
|
929
948
|
databaseHooks: {
|
|
930
949
|
user: {
|
|
931
950
|
create: {
|
|
932
|
-
after: async (
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
951
|
+
after: async (
|
|
952
|
+
user: {
|
|
953
|
+
id?: string;
|
|
954
|
+
email?: string;
|
|
955
|
+
name?: string | null;
|
|
956
|
+
},
|
|
957
|
+
// Better Auth (1.6.x) passes the endpoint context as the 2nd arg.
|
|
958
|
+
// It carries the originating request's headers (and on OAuth
|
|
959
|
+
// signups the callback request's headers), which is where the
|
|
960
|
+
// browser's `an_ft` first-touch cookie rides in.
|
|
961
|
+
context?: {
|
|
962
|
+
headers?: Headers | null;
|
|
963
|
+
request?: { headers?: Headers | null } | null;
|
|
964
|
+
} | null,
|
|
965
|
+
) => {
|
|
937
966
|
// When a newly-created user's email has pending org invitations
|
|
938
967
|
// (common when someone is invited *before* they've signed up),
|
|
939
968
|
// auto-accept them so the user lands in the org on their very
|
|
940
969
|
// first page load instead of a blank-slate workspace.
|
|
941
970
|
const email = user?.email;
|
|
942
971
|
if (!email) return;
|
|
972
|
+
// Derive first-touch referral attribution from the request's
|
|
973
|
+
// cookie header. Never let attribution parsing throw or block
|
|
974
|
+
// signup — on any error fall back to `direct`.
|
|
975
|
+
let attribution: Record<string, string> | undefined;
|
|
976
|
+
try {
|
|
977
|
+
const cookieHeader =
|
|
978
|
+
context?.headers?.get("cookie") ??
|
|
979
|
+
context?.request?.headers?.get("cookie") ??
|
|
980
|
+
null;
|
|
981
|
+
attribution = signupAttributionFromCookieHeader(cookieHeader);
|
|
982
|
+
} catch (err) {
|
|
983
|
+
console.error("[auth] failed to derive signup attribution", err);
|
|
984
|
+
attribution = undefined;
|
|
985
|
+
}
|
|
943
986
|
await trackSignupEvent({
|
|
944
987
|
authProvider: "better-auth",
|
|
945
988
|
authUserId: user.id,
|
|
946
989
|
email,
|
|
947
990
|
name: user.name,
|
|
991
|
+
attribution,
|
|
948
992
|
});
|
|
949
993
|
try {
|
|
950
994
|
await acceptPendingInvitationsForEmail(email);
|