@agent-native/core 0.167.0 → 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.
Files changed (79) hide show
  1. package/corpus/templates/analytics/.agents/skills/agent-native-docs/SKILL.md +11 -0
  2. package/corpus/templates/assets/.agents/skills/agent-native-docs/SKILL.md +11 -0
  3. package/corpus/templates/brain/.agents/skills/agent-native-docs/SKILL.md +11 -0
  4. package/corpus/templates/calendar/.agents/skills/agent-native-docs/SKILL.md +11 -0
  5. package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  6. package/corpus/templates/clips/.agents/skills/agent-native-docs/SKILL.md +11 -0
  7. package/corpus/templates/clips/CHANGELOG.md +1 -0
  8. package/corpus/templates/clips/app/routes/download.tsx +78 -131
  9. package/corpus/templates/content/.agents/skills/agent-native-docs/SKILL.md +11 -0
  10. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +11 -0
  11. package/corpus/templates/design/.agents/skills/agent-native-docs/SKILL.md +11 -0
  12. package/corpus/templates/dispatch/.agents/skills/agent-native-docs/SKILL.md +11 -0
  13. package/corpus/templates/factory/.agents/skills/agent-native-docs/SKILL.md +11 -0
  14. package/corpus/templates/forms/.agents/skills/agent-native-docs/SKILL.md +11 -0
  15. package/corpus/templates/macros/.agents/skills/agent-native-docs/SKILL.md +11 -0
  16. package/corpus/templates/mail/.agents/skills/agent-native-docs/SKILL.md +11 -0
  17. package/corpus/templates/plan/.agents/skills/agent-native-docs/SKILL.md +11 -0
  18. package/corpus/templates/slides/.agents/skills/agent-native-docs/SKILL.md +11 -0
  19. package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +11 -0
  20. package/dist/changelog/parse.js +219 -8
  21. package/dist/chat-threads/store.d.ts +7 -0
  22. package/dist/chat-threads/store.js +15 -0
  23. package/dist/client/AgentPanel.d.ts +5 -1
  24. package/dist/client/AgentPanel.js +8 -8
  25. package/dist/client/AssistantChat.d.ts +3 -1
  26. package/dist/client/AssistantChat.js +22 -6
  27. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  28. package/dist/client/MultiTabAssistantChat.js +5 -4
  29. package/dist/client/agent-page/AgentJobsTab.js +39 -6
  30. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +10 -1
  31. package/dist/client/agent-page/AutomationScheduleDialog.js +3 -2
  32. package/dist/client/agent-page/ScheduledTriggerNotice.d.ts +22 -0
  33. package/dist/client/agent-page/ScheduledTriggerNotice.js +84 -0
  34. package/dist/client/agent-page/scheduled-trigger-state.d.ts +28 -0
  35. package/dist/client/agent-page/scheduled-trigger-state.js +10 -0
  36. package/dist/client/agent-page/use-jobs.d.ts +7 -0
  37. package/dist/client/agent-page/use-jobs.js +14 -0
  38. package/dist/client/blocks/library/diagram.config.d.ts +4 -1
  39. package/dist/client/blocks/library/diagram.js +1 -1
  40. package/dist/client/chat/run-recovery.js +15 -7
  41. package/dist/client/use-chat-threads.d.ts +7 -0
  42. package/dist/client/use-chat-threads.js +144 -40
  43. package/dist/deploy/build.d.ts +5 -0
  44. package/dist/deploy/build.js +12 -1
  45. package/dist/framework-tools.js +1 -0
  46. package/dist/jobs/actions/get-scheduled-trigger-status.d.ts +13 -0
  47. package/dist/jobs/actions/get-scheduled-trigger-status.js +21 -0
  48. package/dist/localization/default-messages.d.ts +14 -0
  49. package/dist/localization/default-messages.js +14 -0
  50. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  52. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  53. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  54. package/dist/secrets/routes.d.ts +9 -9
  55. package/dist/server/action-discovery.js +4 -0
  56. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +42 -1
  57. package/dist/server/agent-chat/recurring-jobs-runtime.js +102 -9
  58. package/dist/server/agent-chat-plugin.d.ts +3 -1
  59. package/dist/server/agent-chat-plugin.js +115 -24
  60. package/dist/server/attribution.d.ts +29 -0
  61. package/dist/server/attribution.js +68 -0
  62. package/dist/server/auth.js +58 -15
  63. package/dist/server/better-auth-instance.js +16 -4
  64. package/dist/server/identity-sso.js +14 -2
  65. package/dist/server/realtime-token.d.ts +1 -1
  66. package/dist/server/request-context.d.ts +8 -0
  67. package/dist/server/request-context.js +4 -0
  68. package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  69. package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
  70. package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
  71. package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +11 -0
  72. package/dist/vite/client.js +10 -0
  73. package/docs/content/deployment.mdx +3 -1
  74. package/docs/content/recurring-jobs.mdx +6 -0
  75. package/package.json +2 -2
  76. package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  77. package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
  78. package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
  79. 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 (options?.appId) {
2292
- await setThreadSourceIfMissing(threadId, {
2293
- appId: options.appId,
2293
+ if (!thread) {
2294
+ throw createError({
2295
+ statusCode: 404,
2296
+ statusMessage: "Thread not found",
2294
2297
  });
2295
2298
  }
2296
- if (!thread) {
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 both "detach" (scope: null) and "retag" flows.
4545
- // Send the field as `scope: undefined` (or omit it) when
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 || thread.ownerEmail !== owner) {
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
- return existing;
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: parseScopeFromBody(body?.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
- return existing;
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
+ }
@@ -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
- const sso = await readDesktopSsoSafely(event);
2728
- if (sso?.email)
2729
- return { email: sso.email, token: sso.token };
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
- const sso = await readDesktopSsoSafely(event);
2769
- if (sso?.email) {
2770
- return { email: sso.email, token: sso.token };
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 auth.handler(requestForAuth);
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: { email, password, name: email.split("@")[0], callbackURL },
4132
- headers: event.headers,
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 { readAnalyticsAnonymousId, signupAttributionFromCookieHeader, } from "./attribution.js";
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 = signupAttributionFromCookieHeader(cookieHeader);
1052
- attribution = magicLinkAttribution?.attribution ?? attribution;
1058
+ attribution =
1059
+ magicLinkAttribution?.attribution ??
1060
+ scopedSignupAttribution?.attribution ??
1061
+ headerSignupAttribution?.attribution ??
1062
+ requestSignupAttribution.attribution;
1053
1063
  anonymousId =
1054
1064
  magicLinkAttribution?.anonymousId ??
1055
- readAnalyticsAnonymousId(cookieHeader);
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);