@agent-native/core 0.167.1 → 0.167.4
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/templates/analytics/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/assets/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/brain/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/calendar/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/clips/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/clips/app/routes/download.tsx +78 -131
- package/corpus/templates/content/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/design/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/dispatch/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/factory/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/forms/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/macros/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/mail/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/plan/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/slides/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/dist/chat-threads/store.d.ts +7 -0
- package/dist/chat-threads/store.js +15 -0
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +8 -8
- package/dist/client/AssistantChat.d.ts +3 -1
- package/dist/client/AssistantChat.js +22 -6
- package/dist/client/MultiTabAssistantChat.d.ts +3 -1
- package/dist/client/MultiTabAssistantChat.js +5 -4
- package/dist/client/agent-page/AgentJobsTab.js +39 -6
- package/dist/client/agent-page/AutomationScheduleDialog.d.ts +10 -1
- package/dist/client/agent-page/AutomationScheduleDialog.js +3 -2
- package/dist/client/agent-page/ScheduledTriggerNotice.d.ts +22 -0
- package/dist/client/agent-page/ScheduledTriggerNotice.js +84 -0
- package/dist/client/agent-page/scheduled-trigger-state.d.ts +28 -0
- package/dist/client/agent-page/scheduled-trigger-state.js +10 -0
- package/dist/client/agent-page/use-jobs.d.ts +7 -0
- package/dist/client/agent-page/use-jobs.js +14 -0
- package/dist/client/blocks/library/diagram.config.d.ts +4 -1
- package/dist/client/blocks/library/diagram.js +1 -1
- package/dist/client/chat/run-recovery.js +15 -7
- package/dist/client/use-chat-threads.d.ts +7 -0
- package/dist/client/use-chat-threads.js +144 -40
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +5 -0
- package/dist/deploy/build.js +12 -1
- package/dist/framework-tools.js +1 -0
- package/dist/jobs/actions/get-scheduled-trigger-status.d.ts +13 -0
- package/dist/jobs/actions/get-scheduled-trigger-status.js +21 -0
- package/dist/localization/default-messages.d.ts +14 -0
- package/dist/localization/default-messages.js +14 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +42 -1
- package/dist/server/agent-chat/recurring-jobs-runtime.js +102 -9
- package/dist/server/agent-chat-plugin.d.ts +3 -1
- package/dist/server/agent-chat-plugin.js +115 -24
- package/dist/server/attribution.d.ts +29 -0
- package/dist/server/attribution.js +68 -0
- package/dist/server/auth.js +58 -15
- package/dist/server/better-auth-instance.js +16 -4
- package/dist/server/identity-sso.js +14 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/request-context.js +4 -0
- package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/dist/vite/client.js +10 -0
- package/docs/content/deployment.mdx +3 -1
- package/docs/content/recurring-jobs.mdx +6 -0
- package/package.json +2 -2
- package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
- package/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +11 -0
|
@@ -23,7 +23,7 @@ import { attachToolSearch } from "../agent/tool-search.js";
|
|
|
23
23
|
import { getAppConfig } from "../app-config/index.js";
|
|
24
24
|
import { readAppStateForCurrentTab } from "../application-state/script-helpers.js";
|
|
25
25
|
import { runChatThreadDataMigrations } from "../chat-threads/migrations.js";
|
|
26
|
-
import { adoptThreadScopeIfUnscoped, createThread, forkThread, getThread, registerChatThreadsShareable, resolveRunThreadScope, resolveThreadAccess, listThreads, searchThreads, renameThread, createThreadShareLink, getThreadByShareToken, getThreadShareState, revokeThreadShareLink, setThreadArchived, setThreadPinned, setThreadScope, updateThreadData, withThreadDataLock, deleteThread, setThreadQueuedMessages, setThreadSourceIfMissing, } from "../chat-threads/store.js";
|
|
26
|
+
import { adoptThreadScopeIfUnscoped, createThread, forkThread, getThread, registerChatThreadsShareable, resolveRunThreadScope, resolveThreadAccess, listThreads, searchThreads, renameThread, createThreadShareLink, getThreadByShareToken, getThreadShareState, revokeThreadShareLink, setThreadArchived, setThreadPinned, setThreadScope, updateThreadData, withThreadDataLock, deleteThread, setThreadQueuedMessages, setThreadSourceIfMissing, isAppOwnedChatScope, threadScopeMismatch, } from "../chat-threads/store.js";
|
|
27
27
|
import { isCheckpointRestorePath } from "../checkpoints/route-match.js";
|
|
28
28
|
import { createDbAdminAgentTools } from "../db-admin/agent-tools.js";
|
|
29
29
|
import { isProductionServerlessFunctionRuntime, isTransientDatabaseError, } from "../db/client.js";
|
|
@@ -120,7 +120,7 @@ import { resolveAgentChatMcpOptions } from "./agent-chat/mcp-options.js";
|
|
|
120
120
|
import { resolveA2AAgentDelegationEnabled, } from "./agent-chat/plugin-options.js";
|
|
121
121
|
import { finalizeClaimedAgentChatProcessRunFailure } from "./agent-chat/process-run-failure.js";
|
|
122
122
|
import { loadResourcesForPrompt, promptResourceManifestSections, resourceScopeForOwner, } from "./agent-chat/prompt-resources.js";
|
|
123
|
-
import { isNetlifyRecurringJobsRuntime, shouldDisableRecurringJobsRuntime, } from "./agent-chat/recurring-jobs-runtime.js";
|
|
123
|
+
import { isNetlifyRecurringJobsRuntime, resolveRecurringJobsBuildMarker, scheduledTriggerAvailability, shouldDisableRecurringJobsRuntime, } from "./agent-chat/recurring-jobs-runtime.js";
|
|
124
124
|
import { isLocalhost, shouldBlockInProductCodeEditingSurface, } from "./agent-chat/request-surface.js";
|
|
125
125
|
import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
|
|
126
126
|
import { createAgentEngineScriptEntries, createAgentLoopSettingsScriptEntries, createCallAgentScriptEntry, createChatScriptEntries, createDbScriptEntries, createDocsScriptEntries, createResourceScriptEntries, } from "./agent-chat/script-entries.js";
|
|
@@ -147,6 +147,8 @@ export { DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKEN
|
|
|
147
147
|
export { shouldBlockInProductCodeEditingSurface };
|
|
148
148
|
export { loadRunCodeToolEntries };
|
|
149
149
|
export { isNetlifyRecurringJobsRuntime };
|
|
150
|
+
export { resolveRecurringJobsBuildMarker };
|
|
151
|
+
export { scheduledTriggerAvailability };
|
|
150
152
|
export { shouldDisableRecurringJobsRuntime };
|
|
151
153
|
export { finalizeClaimedAgentChatProcessRunFailure };
|
|
152
154
|
export function resolveInteractiveAgentRunOptions(options) {
|
|
@@ -2288,17 +2290,23 @@ export function createAgentChatPlugin(options) {
|
|
|
2288
2290
|
thread = await getThread(threadId);
|
|
2289
2291
|
}
|
|
2290
2292
|
}
|
|
2291
|
-
if (
|
|
2292
|
-
|
|
2293
|
-
|
|
2293
|
+
if (!thread) {
|
|
2294
|
+
throw createError({
|
|
2295
|
+
statusCode: 404,
|
|
2296
|
+
statusMessage: "Thread not found",
|
|
2294
2297
|
});
|
|
2295
2298
|
}
|
|
2296
|
-
if (
|
|
2299
|
+
if (threadScopeMismatch(thread.scope, runScope)) {
|
|
2297
2300
|
throw createError({
|
|
2298
2301
|
statusCode: 404,
|
|
2299
2302
|
statusMessage: "Thread not found",
|
|
2300
2303
|
});
|
|
2301
2304
|
}
|
|
2305
|
+
if (options?.appId) {
|
|
2306
|
+
await setThreadSourceIfMissing(threadId, {
|
|
2307
|
+
appId: options.appId,
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2302
2310
|
const access = await resolveThreadAccess(ownerEmail, threadId, "editor", { orgId: getRequestOrgId() });
|
|
2303
2311
|
if (!access) {
|
|
2304
2312
|
throw createError({
|
|
@@ -2818,6 +2826,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
2818
2826
|
if (details.threadId && details.ownerEmail) {
|
|
2819
2827
|
const existingThread = await getThread(details.threadId);
|
|
2820
2828
|
if (existingThread) {
|
|
2829
|
+
if (threadScopeMismatch(existingThread.scope, getRequestRunContext()?.chatScope)) {
|
|
2830
|
+
throw createError({
|
|
2831
|
+
statusCode: 404,
|
|
2832
|
+
statusMessage: "Thread not found",
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2821
2835
|
const access = await resolveThreadAccess(details.ownerEmail, details.threadId, "editor", { orgId: await getOrgIdFromEvent(details.event) });
|
|
2822
2836
|
if (!access) {
|
|
2823
2837
|
throw createError({
|
|
@@ -3094,6 +3108,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
3094
3108
|
if (details.threadId && details.ownerEmail) {
|
|
3095
3109
|
const existingThread = await getThread(details.threadId);
|
|
3096
3110
|
if (existingThread) {
|
|
3111
|
+
if (threadScopeMismatch(existingThread.scope, getRequestRunContext()?.chatScope)) {
|
|
3112
|
+
throw createError({
|
|
3113
|
+
statusCode: 404,
|
|
3114
|
+
statusMessage: "Thread not found",
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3097
3117
|
const access = await resolveThreadAccess(details.ownerEmail, details.threadId, "editor", { orgId: await getOrgIdFromEvent(details.event) });
|
|
3098
3118
|
if (!access) {
|
|
3099
3119
|
throw createError({
|
|
@@ -4471,11 +4491,16 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4471
4491
|
const method = getMethod(event);
|
|
4472
4492
|
const { threadId, tail: threadTail } = parseThreadRoute(event);
|
|
4473
4493
|
const isThreadSubroute = (subroute) => threadTail[0] === subroute;
|
|
4494
|
+
const requestedScope = parseScopeFromQuery(getQuery(event));
|
|
4495
|
+
const scopesMatch = (left, right) => Boolean(left && right && left.type === right.type && left.id === right.id);
|
|
4496
|
+
const threadMatchesRequestedScope = (thread, incomingScope) => !requestedScope ||
|
|
4497
|
+
scopesMatch(thread.scope, requestedScope) ||
|
|
4498
|
+
(!thread.scope && scopesMatch(incomingScope, requestedScope));
|
|
4474
4499
|
// ── Specific thread: GET/PUT/DELETE /threads/:id ──
|
|
4475
4500
|
if (threadId) {
|
|
4476
4501
|
if (method === "GET") {
|
|
4477
4502
|
const thread = await resolveThreadAccess(owner, threadId, "viewer", { orgId });
|
|
4478
|
-
if (!thread) {
|
|
4503
|
+
if (!thread || !threadMatchesRequestedScope(thread)) {
|
|
4479
4504
|
setResponseStatus(event, 404);
|
|
4480
4505
|
return { error: "Thread not found" };
|
|
4481
4506
|
}
|
|
@@ -4489,12 +4514,33 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4489
4514
|
// run could clobber the assistant message the server just
|
|
4490
4515
|
// appended (and vice versa).
|
|
4491
4516
|
return await withThreadDataLock(threadId, async () => {
|
|
4517
|
+
const body = await readBody(event);
|
|
4518
|
+
const bodyIncludesScope = Boolean(body &&
|
|
4519
|
+
typeof body === "object" &&
|
|
4520
|
+
Object.prototype.hasOwnProperty.call(body, "scope"));
|
|
4521
|
+
const incomingScope = bodyIncludesScope
|
|
4522
|
+
? parseScopeFromBody(body.scope)
|
|
4523
|
+
: undefined;
|
|
4492
4524
|
const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
|
|
4493
|
-
if (!thread
|
|
4525
|
+
if (!thread ||
|
|
4526
|
+
!threadMatchesRequestedScope(thread, incomingScope)) {
|
|
4527
|
+
setResponseStatus(event, 404);
|
|
4528
|
+
return { error: "Thread not found" };
|
|
4529
|
+
}
|
|
4530
|
+
const bodyScopeMatchesRequestedScope = !requestedScope ||
|
|
4531
|
+
!incomingScope ||
|
|
4532
|
+
scopesMatch(incomingScope, requestedScope);
|
|
4533
|
+
const unauthorizedScopeChange = bodyIncludesScope &&
|
|
4534
|
+
((incomingScope !== null &&
|
|
4535
|
+
(threadScopeMismatch(thread.scope, incomingScope) ||
|
|
4536
|
+
!bodyScopeMatchesRequestedScope)) ||
|
|
4537
|
+
(incomingScope === null &&
|
|
4538
|
+
isAppOwnedChatScope(thread.scope) &&
|
|
4539
|
+
!requestedScope));
|
|
4540
|
+
if (unauthorizedScopeChange) {
|
|
4494
4541
|
setResponseStatus(event, 404);
|
|
4495
4542
|
return { error: "Thread not found" };
|
|
4496
4543
|
}
|
|
4497
|
-
const body = await readBody(event);
|
|
4498
4544
|
let newThreadData = body.threadData || thread.threadData;
|
|
4499
4545
|
let newMessageCount = body.messageCount ?? thread.messageCount;
|
|
4500
4546
|
let nextTitle = typeof body.title === "string" ? body.title : thread.title;
|
|
@@ -4541,9 +4587,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4541
4587
|
}
|
|
4542
4588
|
await updateThreadData(threadId, newThreadData, nextTitle, nextPreview, newMessageCount, { ignoreConflicts: true });
|
|
4543
4589
|
// Scope updates piggyback on the PUT — the client uses this
|
|
4544
|
-
// path for
|
|
4545
|
-
//
|
|
4546
|
-
// you don't want to touch the existing scope.
|
|
4590
|
+
// path for detach and for claiming a legacy unscoped thread.
|
|
4591
|
+
// A scoped thread cannot be retagged across resources here.
|
|
4547
4592
|
if (Object.prototype.hasOwnProperty.call(body, "scope")) {
|
|
4548
4593
|
const incomingScope = parseScopeFromBody(body.scope);
|
|
4549
4594
|
await setThreadScope(threadId, incomingScope);
|
|
@@ -4557,7 +4602,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4557
4602
|
// on full-thread saves.
|
|
4558
4603
|
if (method === "POST" && isThreadSubroute("queued")) {
|
|
4559
4604
|
const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
|
|
4560
|
-
if (!thread) {
|
|
4605
|
+
if (!thread || !threadMatchesRequestedScope(thread)) {
|
|
4561
4606
|
setResponseStatus(event, 404);
|
|
4562
4607
|
return { error: "Thread not found" };
|
|
4563
4608
|
}
|
|
@@ -4574,7 +4619,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4574
4619
|
}
|
|
4575
4620
|
if (method === "POST" && isThreadSubroute("rename")) {
|
|
4576
4621
|
const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
|
|
4577
|
-
if (!thread) {
|
|
4622
|
+
if (!thread || !threadMatchesRequestedScope(thread)) {
|
|
4578
4623
|
setResponseStatus(event, 404);
|
|
4579
4624
|
return { error: "Thread not found" };
|
|
4580
4625
|
}
|
|
@@ -4595,7 +4640,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4595
4640
|
}
|
|
4596
4641
|
if (method === "POST" && isThreadSubroute("pin")) {
|
|
4597
4642
|
const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
|
|
4598
|
-
if (!thread) {
|
|
4643
|
+
if (!thread || !threadMatchesRequestedScope(thread)) {
|
|
4599
4644
|
setResponseStatus(event, 404);
|
|
4600
4645
|
return { error: "Thread not found" };
|
|
4601
4646
|
}
|
|
@@ -4613,7 +4658,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4613
4658
|
}
|
|
4614
4659
|
if (method === "POST" && isThreadSubroute("archive")) {
|
|
4615
4660
|
const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
|
|
4616
|
-
if (!thread) {
|
|
4661
|
+
if (!thread || !threadMatchesRequestedScope(thread)) {
|
|
4617
4662
|
setResponseStatus(event, 404);
|
|
4618
4663
|
return { error: "Thread not found" };
|
|
4619
4664
|
}
|
|
@@ -4632,7 +4677,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4632
4677
|
// POST /threads/:id/fork — duplicate a thread with all its messages
|
|
4633
4678
|
if (method === "POST" && isThreadSubroute("fork")) {
|
|
4634
4679
|
const thread = await resolveThreadAccess(owner, threadId, "viewer", { orgId });
|
|
4635
|
-
if (!thread) {
|
|
4680
|
+
if (!thread || !threadMatchesRequestedScope(thread)) {
|
|
4636
4681
|
setResponseStatus(event, 404);
|
|
4637
4682
|
return { error: "Thread not found" };
|
|
4638
4683
|
}
|
|
@@ -4650,7 +4695,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4650
4695
|
}
|
|
4651
4696
|
if (isThreadSubroute("share")) {
|
|
4652
4697
|
const thread = await resolveThreadAccess(owner, threadId, "admin", { orgId });
|
|
4653
|
-
if (!thread) {
|
|
4698
|
+
if (!thread || !threadMatchesRequestedScope(thread)) {
|
|
4654
4699
|
setResponseStatus(event, 404);
|
|
4655
4700
|
return { error: "Thread not found" };
|
|
4656
4701
|
}
|
|
@@ -4684,7 +4729,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4684
4729
|
}
|
|
4685
4730
|
if (method === "DELETE") {
|
|
4686
4731
|
const thread = await getThread(threadId);
|
|
4687
|
-
if (!thread ||
|
|
4732
|
+
if (!thread ||
|
|
4733
|
+
thread.ownerEmail !== owner ||
|
|
4734
|
+
!threadMatchesRequestedScope(thread)) {
|
|
4688
4735
|
setResponseStatus(event, 404);
|
|
4689
4736
|
return { error: "Thread not found" };
|
|
4690
4737
|
}
|
|
@@ -4725,6 +4772,46 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4725
4772
|
}
|
|
4726
4773
|
if (method === "POST") {
|
|
4727
4774
|
const body = await readBody(event);
|
|
4775
|
+
const requestedScope = parseScopeFromQuery(getQuery(event));
|
|
4776
|
+
const bodyIncludesScope = Boolean(body &&
|
|
4777
|
+
typeof body === "object" &&
|
|
4778
|
+
Object.prototype.hasOwnProperty.call(body, "scope"));
|
|
4779
|
+
const bodyScope = bodyIncludesScope
|
|
4780
|
+
? parseScopeFromBody(body.scope)
|
|
4781
|
+
: undefined;
|
|
4782
|
+
const bodyScopeMatchesRequestedScope = !requestedScope ||
|
|
4783
|
+
!bodyIncludesScope ||
|
|
4784
|
+
(bodyScope !== null && scopesMatch(bodyScope, requestedScope));
|
|
4785
|
+
if (!bodyScopeMatchesRequestedScope) {
|
|
4786
|
+
setResponseStatus(event, 404);
|
|
4787
|
+
return { error: "Thread not found" };
|
|
4788
|
+
}
|
|
4789
|
+
const resolveExistingOwnedThread = async (existing) => {
|
|
4790
|
+
if ((requestedScope &&
|
|
4791
|
+
threadScopeMismatch(existing.scope, requestedScope)) ||
|
|
4792
|
+
(bodyIncludesScope &&
|
|
4793
|
+
threadScopeMismatch(existing.scope, bodyScope))) {
|
|
4794
|
+
setResponseStatus(event, 404);
|
|
4795
|
+
return null;
|
|
4796
|
+
}
|
|
4797
|
+
const scopeToAdopt = bodyIncludesScope
|
|
4798
|
+
? bodyScope
|
|
4799
|
+
: requestedScope;
|
|
4800
|
+
if (!existing.scope && scopeToAdopt) {
|
|
4801
|
+
const adoptedScope = await adoptThreadScopeIfUnscoped(existing.id, scopeToAdopt);
|
|
4802
|
+
if (!scopesMatch(adoptedScope, scopeToAdopt)) {
|
|
4803
|
+
setResponseStatus(event, 404);
|
|
4804
|
+
return null;
|
|
4805
|
+
}
|
|
4806
|
+
const adopted = await getThread(existing.id);
|
|
4807
|
+
if (!adopted) {
|
|
4808
|
+
setResponseStatus(event, 404);
|
|
4809
|
+
return null;
|
|
4810
|
+
}
|
|
4811
|
+
return adopted;
|
|
4812
|
+
}
|
|
4813
|
+
return existing;
|
|
4814
|
+
};
|
|
4728
4815
|
// Idempotent: when the caller supplies an id and a thread with
|
|
4729
4816
|
// that id already exists for this owner, return it instead of
|
|
4730
4817
|
// 500'ing on the UNIQUE constraint. The client can race with
|
|
@@ -4735,8 +4822,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4735
4822
|
if (body?.id) {
|
|
4736
4823
|
const existing = await getThread(body.id);
|
|
4737
4824
|
if (existing) {
|
|
4738
|
-
if (existing.ownerEmail === owner)
|
|
4739
|
-
|
|
4825
|
+
if (existing.ownerEmail === owner) {
|
|
4826
|
+
const resolved = await resolveExistingOwnedThread(existing);
|
|
4827
|
+
return resolved ?? { error: "Thread not found" };
|
|
4828
|
+
}
|
|
4740
4829
|
setResponseStatus(event, 409);
|
|
4741
4830
|
return { error: "Thread id already in use" };
|
|
4742
4831
|
}
|
|
@@ -4745,7 +4834,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4745
4834
|
const thread = await createThread(owner, {
|
|
4746
4835
|
id: body?.id,
|
|
4747
4836
|
title: body?.title ?? "",
|
|
4748
|
-
scope:
|
|
4837
|
+
scope: bodyIncludesScope ? bodyScope : requestedScope,
|
|
4749
4838
|
source: options?.appId ? { appId: options.appId } : null,
|
|
4750
4839
|
});
|
|
4751
4840
|
return thread;
|
|
@@ -4756,8 +4845,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4756
4845
|
// return the row that actually landed.
|
|
4757
4846
|
if (body?.id) {
|
|
4758
4847
|
const existing = await getThread(body.id);
|
|
4759
|
-
if (existing && existing.ownerEmail === owner)
|
|
4760
|
-
|
|
4848
|
+
if (existing && existing.ownerEmail === owner) {
|
|
4849
|
+
const resolved = await resolveExistingOwnedThread(existing);
|
|
4850
|
+
return resolved ?? { error: "Thread not found" };
|
|
4851
|
+
}
|
|
4761
4852
|
}
|
|
4762
4853
|
throw err;
|
|
4763
4854
|
}
|
|
@@ -35,6 +35,18 @@ export interface FirstTouchAttribution {
|
|
|
35
35
|
/** ISO timestamp of when the visitor first landed. */
|
|
36
36
|
landed_at?: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Request-scoped browser attribution captured at the signup boundary.
|
|
40
|
+
* Better Auth may create the user in a later async callback where the
|
|
41
|
+
* original request headers are no longer available, so the values must be
|
|
42
|
+
* carried explicitly through that boundary.
|
|
43
|
+
*/
|
|
44
|
+
export interface SignupAttributionContext {
|
|
45
|
+
attribution: Record<string, string>;
|
|
46
|
+
anonymousId?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Internal Better Auth handoff header; attribution is analytics-only. */
|
|
49
|
+
export declare const SIGNUP_ATTRIBUTION_HEADER_NAME = "x-agent-native-signup-attribution";
|
|
38
50
|
/** Cookie name written by the client (non-HttpOnly; non-sensitive). */
|
|
39
51
|
export declare const FIRST_TOUCH_COOKIE_NAME = "an_ft";
|
|
40
52
|
/**
|
|
@@ -87,3 +99,20 @@ export declare function deriveSignupAttribution(ft: FirstTouchAttribution | null
|
|
|
87
99
|
* error.
|
|
88
100
|
*/
|
|
89
101
|
export declare function signupAttributionFromCookieHeader(cookieHeader: string | null | undefined): Record<string, string>;
|
|
102
|
+
/**
|
|
103
|
+
* Capture all browser attribution needed by the server-side signup event.
|
|
104
|
+
* Keep this as one boundary helper so every signup entry point carries the
|
|
105
|
+
* same values into Better Auth's user-create hook.
|
|
106
|
+
*/
|
|
107
|
+
export declare function signupAttributionContextFromCookieHeader(cookieHeader: string | null | undefined): SignupAttributionContext;
|
|
108
|
+
/** Serialize the bounded attribution handoff for a Better Auth request. */
|
|
109
|
+
export declare function encodeSignupAttributionContext(context: SignupAttributionContext): string;
|
|
110
|
+
/**
|
|
111
|
+
* Decode the internal handoff header. Invalid input is absent, not a direct
|
|
112
|
+
* attribution, so the caller can safely fall back to the request cookie.
|
|
113
|
+
*/
|
|
114
|
+
export declare function decodeSignupAttributionContext(value: string | null | undefined): SignupAttributionContext | undefined;
|
|
115
|
+
/** Add the explicit handoff to a Better Auth request/API header set. */
|
|
116
|
+
export declare function addSignupAttributionHeader(headers: HeadersInit | undefined, context: SignupAttributionContext): Headers;
|
|
117
|
+
/** Read the explicit handoff from Better Auth's request context headers. */
|
|
118
|
+
export declare function signupAttributionContextFromHeaders(headers: Headers | null | undefined): SignupAttributionContext | undefined;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { ANALYTICS_ANONYMOUS_ID_COOKIE_NAME, normalizeAnalyticsAnonymousId, } from "../shared/analytics-anonymous-id.js";
|
|
2
|
+
/** Internal Better Auth handoff header; attribution is analytics-only. */
|
|
3
|
+
export const SIGNUP_ATTRIBUTION_HEADER_NAME = "x-agent-native-signup-attribution";
|
|
4
|
+
const SIGNUP_ATTRIBUTION_HEADER_MAX_LENGTH = 4096;
|
|
2
5
|
/** Cookie name written by the client (non-HttpOnly; non-sensitive). */
|
|
3
6
|
export const FIRST_TOUCH_COOKIE_NAME = "an_ft";
|
|
4
7
|
const STRING_FIELDS = [
|
|
@@ -178,3 +181,68 @@ export function signupAttributionFromCookieHeader(cookieHeader) {
|
|
|
178
181
|
return { referral_source: "direct" };
|
|
179
182
|
}
|
|
180
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Capture all browser attribution needed by the server-side signup event.
|
|
186
|
+
* Keep this as one boundary helper so every signup entry point carries the
|
|
187
|
+
* same values into Better Auth's user-create hook.
|
|
188
|
+
*/
|
|
189
|
+
export function signupAttributionContextFromCookieHeader(cookieHeader) {
|
|
190
|
+
const anonymousId = readAnalyticsAnonymousId(cookieHeader);
|
|
191
|
+
return {
|
|
192
|
+
attribution: signupAttributionFromCookieHeader(cookieHeader),
|
|
193
|
+
...(anonymousId ? { anonymousId } : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Serialize the bounded attribution handoff for a Better Auth request. */
|
|
197
|
+
export function encodeSignupAttributionContext(context) {
|
|
198
|
+
return encodeURIComponent(JSON.stringify(context));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Decode the internal handoff header. Invalid input is absent, not a direct
|
|
202
|
+
* attribution, so the caller can safely fall back to the request cookie.
|
|
203
|
+
*/
|
|
204
|
+
export function decodeSignupAttributionContext(value) {
|
|
205
|
+
if (!value || value.length > SIGNUP_ATTRIBUTION_HEADER_MAX_LENGTH) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(decodeURIComponent(value));
|
|
210
|
+
const rawAttribution = parsed?.attribution;
|
|
211
|
+
if (!rawAttribution ||
|
|
212
|
+
typeof rawAttribution !== "object" ||
|
|
213
|
+
Array.isArray(rawAttribution)) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const attribution = {};
|
|
217
|
+
for (const [key, rawValue] of Object.entries(rawAttribution)) {
|
|
218
|
+
if (/^[A-Za-z0-9_]+$/.test(key) &&
|
|
219
|
+
key.length <= 64 &&
|
|
220
|
+
typeof rawValue === "string" &&
|
|
221
|
+
rawValue.length > 0) {
|
|
222
|
+
attribution[key] = rawValue.slice(0, 120);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (Object.keys(attribution).length === 0)
|
|
226
|
+
return undefined;
|
|
227
|
+
const anonymousId = normalizeAnalyticsAnonymousId(parsed?.anonymousId);
|
|
228
|
+
return {
|
|
229
|
+
attribution,
|
|
230
|
+
...(anonymousId ? { anonymousId } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// `undefined` distinguishes an unreadable handoff from direct attribution.
|
|
235
|
+
void error;
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Add the explicit handoff to a Better Auth request/API header set. */
|
|
240
|
+
export function addSignupAttributionHeader(headers, context) {
|
|
241
|
+
const result = new Headers(headers);
|
|
242
|
+
result.set(SIGNUP_ATTRIBUTION_HEADER_NAME, encodeSignupAttributionContext(context));
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
/** Read the explicit handoff from Better Auth's request context headers. */
|
|
246
|
+
export function signupAttributionContextFromHeaders(headers) {
|
|
247
|
+
return decodeSignupAttributionContext(headers?.get(SIGNUP_ATTRIBUTION_HEADER_NAME));
|
|
248
|
+
}
|
package/dist/server/auth.js
CHANGED
|
@@ -58,7 +58,7 @@ import { isValidWorkspaceAppIdFormat } from "../shared/workspace-app-id.js";
|
|
|
58
58
|
import { injectAnalyticsIntoHtml } from "./analytics.js";
|
|
59
59
|
import { getConfiguredAppBasePath } from "./app-base-path.js";
|
|
60
60
|
import { getAppProductionUrl } from "./app-url.js";
|
|
61
|
-
import { readAnalyticsAnonymousId, readFirstTouchAttribution, signupAttributionFromCookieHeader, } from "./attribution.js";
|
|
61
|
+
import { addSignupAttributionHeader, readAnalyticsAnonymousId, readFirstTouchAttribution, signupAttributionContextFromCookieHeader, signupAttributionFromCookieHeader, } from "./attribution.js";
|
|
62
62
|
import { getAuthLoginMode } from "./auth-login-mode.js";
|
|
63
63
|
import { injectBetaOptOutPersistence } from "./beta-opt-out-html.js";
|
|
64
64
|
import { createBetterAuthSessionForEmail, ensureGoogleAuthIdentity, getAuthSecret, getBetterAuth, getBetterAuthSync, isDeployPreview, } from "./better-auth-instance.js";
|
|
@@ -73,6 +73,7 @@ import { ensureCanonicalUserForLegacySession } from "./legacy-auth-migration.js"
|
|
|
73
73
|
import { encodeMagicLinkSignupAttribution, MAGIC_LINK_ATTRIBUTION_PARAM, } from "./magic-link-attribution.js";
|
|
74
74
|
import { safeOAuthReturnUrl } from "./oauth-return-url.js";
|
|
75
75
|
import { getOnboardingHtml, getResetPasswordHtml, } from "./onboarding-html.js";
|
|
76
|
+
import { getRequestContext, hasContinuationLocalRequestContext, runWithRequestContext, } from "./request-context.js";
|
|
76
77
|
import { captureAuthError } from "./sentry.js";
|
|
77
78
|
import { forgetCachedSessionEmail, getCachedSessionEmail, invalidateSessionEmailCache, setCachedSessionEmail, } from "./session-email-cache.js";
|
|
78
79
|
import { isWorkspaceOAuthCallbackRelayEnabled } from "./workspace-oauth.js";
|
|
@@ -83,6 +84,23 @@ import { isWorkspaceOAuthCallbackRelayEnabled } from "./workspace-oauth.js";
|
|
|
83
84
|
export function getSessionMaxAge() {
|
|
84
85
|
return sessionMaxAge;
|
|
85
86
|
}
|
|
87
|
+
function withSignupAttributionContext(cookieHeader, fn) {
|
|
88
|
+
const signupAttribution = signupAttributionContextFromCookieHeader(cookieHeader);
|
|
89
|
+
if (!hasContinuationLocalRequestContext())
|
|
90
|
+
return fn();
|
|
91
|
+
return runWithRequestContext({
|
|
92
|
+
...(getRequestContext() ?? {}),
|
|
93
|
+
signupAttribution,
|
|
94
|
+
}, fn);
|
|
95
|
+
}
|
|
96
|
+
function requestWithSignupAttribution(request, signupAttribution) {
|
|
97
|
+
return new Request(request, {
|
|
98
|
+
headers: addSignupAttributionHeader(request.headers, signupAttribution),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function headersWithSignupAttribution(headers, cookieHeader) {
|
|
102
|
+
return addSignupAttributionHeader(headers, signupAttributionContextFromCookieHeader(cookieHeader));
|
|
103
|
+
}
|
|
86
104
|
// ---------------------------------------------------------------------------
|
|
87
105
|
// Constants
|
|
88
106
|
// ---------------------------------------------------------------------------
|
|
@@ -416,6 +434,9 @@ async function readDesktopSsoSafely(event) {
|
|
|
416
434
|
return null;
|
|
417
435
|
return await readDesktopSso();
|
|
418
436
|
}
|
|
437
|
+
function isDesktopSessionCookieOnlyCheck(event) {
|
|
438
|
+
return getHeader(event, "x-agent-native-session-check") === "cookie-only";
|
|
439
|
+
}
|
|
419
440
|
/**
|
|
420
441
|
* Extract the framework session token from a Better Auth response's
|
|
421
442
|
* Set-Cookie headers, if any. Used by the password-reset path to skip
|
|
@@ -2688,6 +2709,7 @@ export async function getSession(event) {
|
|
|
2688
2709
|
})());
|
|
2689
2710
|
}
|
|
2690
2711
|
async function resolveSessionUncached(event) {
|
|
2712
|
+
const cookieOnlyDesktopCheck = isDesktopSessionCookieOnlyCheck(event);
|
|
2691
2713
|
// 1. MCP App embed session. This is a short-lived browser session minted
|
|
2692
2714
|
// from a one-time ticket that was scoped to the authenticated MCP caller.
|
|
2693
2715
|
// It lets an inline MCP App iframe load the real app without reusing the
|
|
@@ -2724,9 +2746,11 @@ async function resolveSessionUncached(event) {
|
|
|
2724
2746
|
// templates too. Gated on `readDesktopSsoSafely` so a non-loopback
|
|
2725
2747
|
// request that spoofs `User-Agent: ... Electron/...` cannot read the
|
|
2726
2748
|
// home-dir broker file (and so production builds never consult it).
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2749
|
+
if (!cookieOnlyDesktopCheck) {
|
|
2750
|
+
const sso = await readDesktopSsoSafely(event);
|
|
2751
|
+
if (sso?.email)
|
|
2752
|
+
return { email: sso.email, token: sso.token };
|
|
2753
|
+
}
|
|
2730
2754
|
// Fall through to mobile _session check
|
|
2731
2755
|
}
|
|
2732
2756
|
else {
|
|
@@ -2765,9 +2789,11 @@ async function resolveSessionUncached(event) {
|
|
|
2765
2789
|
// a loopback (127.0.0.1 / ::1) source IP, and a non-production NODE_ENV
|
|
2766
2790
|
// — anything else is rejected so a hostile network request cannot
|
|
2767
2791
|
// impersonate whichever email last signed into the desktop app.
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2792
|
+
if (!cookieOnlyDesktopCheck) {
|
|
2793
|
+
const sso = await readDesktopSsoSafely(event);
|
|
2794
|
+
if (sso?.email) {
|
|
2795
|
+
return { email: sso.email, token: sso.token };
|
|
2796
|
+
}
|
|
2771
2797
|
}
|
|
2772
2798
|
}
|
|
2773
2799
|
// 8. Mobile WebView bridge — _session query param
|
|
@@ -3699,11 +3725,18 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3699
3725
|
getMethod(event) === "POST";
|
|
3700
3726
|
const isMagicLinkRequest = reqPath.includes("/sign-in/magic-link") && getMethod(event) === "POST";
|
|
3701
3727
|
const isMagicLinkVerification = reqPath.includes("/magic-link/verify") && getMethod(event) === "GET";
|
|
3728
|
+
const isEmailSignup = reqPath.includes("/sign-up/email") && getMethod(event) === "POST";
|
|
3702
3729
|
const isSignOut = reqPath.includes("sign-out") && getMethod(event) === "POST";
|
|
3703
3730
|
if (isSignOut)
|
|
3704
3731
|
optOutOfAuthDisabledSession(event);
|
|
3705
3732
|
const authRequest = toWebRequest(event);
|
|
3706
3733
|
let requestForAuth = authRequest;
|
|
3734
|
+
const signupCookieHeader = isEmailSignup
|
|
3735
|
+
? authRequest.headers.get("cookie")
|
|
3736
|
+
: undefined;
|
|
3737
|
+
const signupAttribution = isEmailSignup
|
|
3738
|
+
? signupAttributionContextFromCookieHeader(signupCookieHeader)
|
|
3739
|
+
: undefined;
|
|
3707
3740
|
let magicLinkPreConsume;
|
|
3708
3741
|
if (isMagicLinkVerification) {
|
|
3709
3742
|
const verificationURL = new URL(authRequest.url);
|
|
@@ -3820,9 +3853,14 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3820
3853
|
// validation error.
|
|
3821
3854
|
}
|
|
3822
3855
|
}
|
|
3856
|
+
if (signupAttribution) {
|
|
3857
|
+
requestForAuth = requestWithSignupAttribution(requestForAuth, signupAttribution);
|
|
3858
|
+
}
|
|
3823
3859
|
let response;
|
|
3824
3860
|
try {
|
|
3825
|
-
response = await
|
|
3861
|
+
response = await (isEmailSignup
|
|
3862
|
+
? withSignupAttributionContext(signupCookieHeader, () => auth.handler(requestForAuth))
|
|
3863
|
+
: auth.handler(requestForAuth));
|
|
3826
3864
|
}
|
|
3827
3865
|
catch (error) {
|
|
3828
3866
|
if (isMagicLinkVerification) {
|
|
@@ -4127,10 +4165,15 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
4127
4165
|
return { error: GOOGLE_AUTH_REQUIRED_MESSAGE };
|
|
4128
4166
|
}
|
|
4129
4167
|
try {
|
|
4130
|
-
await auth.api.signUpEmail({
|
|
4131
|
-
body: {
|
|
4132
|
-
|
|
4133
|
-
|
|
4168
|
+
await withSignupAttributionContext(getHeader(event, "cookie") ?? null, () => auth.api.signUpEmail({
|
|
4169
|
+
body: {
|
|
4170
|
+
email,
|
|
4171
|
+
password,
|
|
4172
|
+
name: email.split("@")[0],
|
|
4173
|
+
callbackURL,
|
|
4174
|
+
},
|
|
4175
|
+
headers: headersWithSignupAttribution(event.headers, getHeader(event, "cookie") ?? null),
|
|
4176
|
+
}));
|
|
4134
4177
|
setFirstRunOnboardingCookie(event);
|
|
4135
4178
|
return { ok: true };
|
|
4136
4179
|
}
|
|
@@ -4384,10 +4427,10 @@ function mountAuthFallbackRoutes(app) {
|
|
|
4384
4427
|
}
|
|
4385
4428
|
try {
|
|
4386
4429
|
const auth = await getBetterAuth();
|
|
4387
|
-
await auth.api.signUpEmail({
|
|
4430
|
+
await withSignupAttributionContext(getHeader(event, "cookie") ?? null, () => auth.api.signUpEmail({
|
|
4388
4431
|
body: { email, password, name: email.split("@")[0] },
|
|
4389
|
-
headers: event.headers,
|
|
4390
|
-
});
|
|
4432
|
+
headers: headersWithSignupAttribution(event.headers, getHeader(event, "cookie") ?? null),
|
|
4433
|
+
}));
|
|
4391
4434
|
setFirstRunOnboardingCookie(event);
|
|
4392
4435
|
return { ok: true };
|
|
4393
4436
|
}
|
|
@@ -26,13 +26,14 @@ import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, } from "../shared/password-po
|
|
|
26
26
|
import { formatRuntimeConfigReport, getRuntimeConfigReport, } from "../shared/runtime-config.js";
|
|
27
27
|
import { flushTracking, identify, track } from "../tracking/index.js";
|
|
28
28
|
import { getAppProductionUrl } from "./app-url.js";
|
|
29
|
-
import {
|
|
29
|
+
import { signupAttributionContextFromCookieHeader, signupAttributionContextFromHeaders, } from "./attribution.js";
|
|
30
30
|
import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
|
|
31
31
|
import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
|
|
32
32
|
import { renderMagicLinkEmail, renderResetPasswordEmail, renderVerifySignupEmail, } from "./email-templates.js";
|
|
33
33
|
import { getEmailReadiness, sendEmail } from "./email.js";
|
|
34
34
|
import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
|
|
35
35
|
import { readMagicLinkSignupAttribution } from "./magic-link-attribution.js";
|
|
36
|
+
import { getRequestContext, hasContinuationLocalRequestContext, } from "./request-context.js";
|
|
36
37
|
export { getAuthLoginMode, resolveAuthLoginMode, resolveAuthLoginModeFromReadiness, } from "./auth-login-mode.js";
|
|
37
38
|
async function flushSignupTracking() {
|
|
38
39
|
try {
|
|
@@ -1045,14 +1046,25 @@ async function createBetterAuthInstance(config) {
|
|
|
1045
1046
|
const cookieHeader = context?.headers?.get("cookie") ??
|
|
1046
1047
|
context?.request?.headers?.get("cookie") ??
|
|
1047
1048
|
null;
|
|
1049
|
+
const scopedSignupAttribution = hasContinuationLocalRequestContext()
|
|
1050
|
+
? getRequestContext()?.signupAttribution
|
|
1051
|
+
: undefined;
|
|
1052
|
+
const requestSignupAttribution = signupAttributionContextFromCookieHeader(cookieHeader);
|
|
1053
|
+
const headerSignupAttribution = signupAttributionContextFromHeaders(context?.headers) ??
|
|
1054
|
+
signupAttributionContextFromHeaders(context?.request?.headers);
|
|
1048
1055
|
const magicLinkAttribution = context?.request?.url?.includes("newUserCallbackURL")
|
|
1049
1056
|
? readMagicLinkSignupAttribution(context.request.url, getAuthSecret())
|
|
1050
1057
|
: undefined;
|
|
1051
|
-
attribution =
|
|
1052
|
-
|
|
1058
|
+
attribution =
|
|
1059
|
+
magicLinkAttribution?.attribution ??
|
|
1060
|
+
scopedSignupAttribution?.attribution ??
|
|
1061
|
+
headerSignupAttribution?.attribution ??
|
|
1062
|
+
requestSignupAttribution.attribution;
|
|
1053
1063
|
anonymousId =
|
|
1054
1064
|
magicLinkAttribution?.anonymousId ??
|
|
1055
|
-
|
|
1065
|
+
scopedSignupAttribution?.anonymousId ??
|
|
1066
|
+
headerSignupAttribution?.anonymousId ??
|
|
1067
|
+
requestSignupAttribution.anonymousId;
|
|
1056
1068
|
}
|
|
1057
1069
|
catch (err) {
|
|
1058
1070
|
console.error("[auth] failed to derive signup attribution", err);
|