@agent-native/core 0.167.1 → 0.168.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.
Files changed (117) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/agent-native-docs/SKILL.md +11 -0
  3. package/corpus/templates/assets/.agents/skills/agent-native-docs/SKILL.md +11 -0
  4. package/corpus/templates/brain/.agents/skills/agent-native-docs/SKILL.md +11 -0
  5. package/corpus/templates/brain/actions/list-sources.ts +17 -7
  6. package/corpus/templates/brain/app/components/brain/Surface.tsx +4 -2
  7. package/corpus/templates/brain/app/lib/brain.ts +13 -3
  8. package/corpus/templates/brain/server/lib/brain-health.ts +5 -8
  9. package/corpus/templates/brain/server/lib/brain.ts +1 -0
  10. package/corpus/templates/brain/server/lib/capture-sanitization.ts +5 -0
  11. package/corpus/templates/brain/server/routes/api/brain/queue/run.post.ts +3 -2
  12. package/corpus/templates/calendar/.agents/skills/agent-native-docs/SKILL.md +11 -0
  13. package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  14. package/corpus/templates/clips/.agents/skills/agent-native-docs/SKILL.md +11 -0
  15. package/corpus/templates/clips/app/routes/download.tsx +78 -131
  16. package/corpus/templates/clips/desktop/src/app.tsx +11 -10
  17. package/corpus/templates/clips/desktop/src/lib/desktop-oauth-window.ts +23 -0
  18. package/corpus/templates/content/.agents/skills/agent-native-docs/SKILL.md +11 -0
  19. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +11 -0
  20. package/corpus/templates/design/.agents/skills/agent-native-docs/SKILL.md +11 -0
  21. package/corpus/templates/dispatch/.agents/skills/agent-native-docs/SKILL.md +11 -0
  22. package/corpus/templates/factory/.agents/skills/agent-native-docs/SKILL.md +11 -0
  23. package/corpus/templates/forms/.agents/skills/agent-native-docs/SKILL.md +11 -0
  24. package/corpus/templates/macros/.agents/skills/agent-native-docs/SKILL.md +11 -0
  25. package/corpus/templates/mail/.agents/skills/agent-native-docs/SKILL.md +11 -0
  26. package/corpus/templates/plan/.agents/skills/agent-native-docs/SKILL.md +11 -0
  27. package/corpus/templates/slides/.agents/skills/agent-native-docs/SKILL.md +11 -0
  28. package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +11 -0
  29. package/dist/chat-threads/store.d.ts +7 -0
  30. package/dist/chat-threads/store.js +15 -0
  31. package/dist/client/AboutAgentNativeDialog.d.ts +5 -0
  32. package/dist/client/AboutAgentNativeDialog.js +56 -0
  33. package/dist/client/AgentPanel.d.ts +5 -1
  34. package/dist/client/AgentPanel.js +8 -8
  35. package/dist/client/AssistantChat.d.ts +3 -1
  36. package/dist/client/AssistantChat.js +22 -6
  37. package/dist/client/CommandMenu.d.ts +6 -1
  38. package/dist/client/CommandMenu.js +46 -3
  39. package/dist/client/EnvironmentBadge.js +4 -1
  40. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  41. package/dist/client/MultiTabAssistantChat.js +5 -4
  42. package/dist/client/agent-native-version.d.ts +12 -0
  43. package/dist/client/agent-native-version.js +36 -0
  44. package/dist/client/agent-page/AgentJobsTab.js +39 -6
  45. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +10 -1
  46. package/dist/client/agent-page/AutomationScheduleDialog.js +3 -2
  47. package/dist/client/agent-page/ScheduledTriggerNotice.d.ts +22 -0
  48. package/dist/client/agent-page/ScheduledTriggerNotice.js +84 -0
  49. package/dist/client/agent-page/scheduled-trigger-state.d.ts +28 -0
  50. package/dist/client/agent-page/scheduled-trigger-state.js +10 -0
  51. package/dist/client/agent-page/use-jobs.d.ts +7 -0
  52. package/dist/client/agent-page/use-jobs.js +14 -0
  53. package/dist/client/blocks/library/diagram.config.d.ts +4 -1
  54. package/dist/client/blocks/library/diagram.js +1 -1
  55. package/dist/client/chat/run-recovery.js +15 -7
  56. package/dist/client/use-chat-threads.d.ts +7 -0
  57. package/dist/client/use-chat-threads.js +144 -40
  58. package/dist/collab/routes.d.ts +1 -1
  59. package/dist/config.d.ts +3 -1
  60. package/dist/config.js +14 -9
  61. package/dist/deploy/build.d.ts +5 -0
  62. package/dist/deploy/build.js +12 -1
  63. package/dist/framework-tools.js +1 -0
  64. package/dist/jobs/actions/get-scheduled-trigger-status.d.ts +13 -0
  65. package/dist/jobs/actions/get-scheduled-trigger-status.js +21 -0
  66. package/dist/localization/core-messages/ar-SA.js +6 -0
  67. package/dist/localization/core-messages/de-DE.js +6 -0
  68. package/dist/localization/core-messages/en-US.d.ts +6 -0
  69. package/dist/localization/core-messages/en-US.js +6 -0
  70. package/dist/localization/core-messages/es-ES.js +6 -0
  71. package/dist/localization/core-messages/fr-FR.js +6 -0
  72. package/dist/localization/core-messages/hi-IN.js +6 -0
  73. package/dist/localization/core-messages/ja-JP.js +6 -0
  74. package/dist/localization/core-messages/ko-KR.js +6 -0
  75. package/dist/localization/core-messages/pt-BR.js +6 -0
  76. package/dist/localization/core-messages/zh-CN.js +6 -0
  77. package/dist/localization/core-messages/zh-TW.js +6 -0
  78. package/dist/localization/core-messages.d.ts +6 -0
  79. package/dist/localization/default-messages.d.ts +14 -0
  80. package/dist/localization/default-messages.js +14 -0
  81. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  82. package/dist/notifications/routes.d.ts +1 -1
  83. package/dist/observability/routes.d.ts +3 -3
  84. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  85. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  86. package/dist/resources/handlers.d.ts +2 -2
  87. package/dist/server/action-discovery.js +4 -0
  88. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +42 -1
  89. package/dist/server/agent-chat/recurring-jobs-runtime.js +102 -9
  90. package/dist/server/agent-chat-plugin.d.ts +3 -1
  91. package/dist/server/agent-chat-plugin.js +115 -24
  92. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  93. package/dist/server/attribution.d.ts +29 -0
  94. package/dist/server/attribution.js +68 -0
  95. package/dist/server/auth.js +58 -15
  96. package/dist/server/better-auth-instance.js +16 -4
  97. package/dist/server/identity-sso.js +14 -2
  98. package/dist/server/realtime-token.d.ts +1 -1
  99. package/dist/server/request-context.d.ts +8 -0
  100. package/dist/server/request-context.js +4 -0
  101. package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  102. package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
  103. package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
  104. package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +11 -0
  105. package/dist/vite/agent-native-config-loader.js +1 -1
  106. package/dist/vite/client.js +19 -8
  107. package/dist/vite/package-versions.d.ts +1 -0
  108. package/dist/vite/package-versions.js +97 -0
  109. package/docs/content/agent-native-config.mdx +55 -31
  110. package/docs/content/deployment.mdx +3 -1
  111. package/docs/content/environment-variables.mdx +51 -14
  112. package/docs/content/recurring-jobs.mdx +6 -0
  113. package/package.json +2 -2
  114. package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  115. package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
  116. package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
  117. package/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +11 -0
@@ -30,20 +30,27 @@ function isLoopbackAppUrl(value) {
30
30
  }
31
31
  return false;
32
32
  }
33
- export function shouldDisableRecurringJobsRuntime(env = process.env) {
34
- if (isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS))
35
- return true;
36
- // A serverless isolate is not a durable scheduler. Keep this check separate
37
- // from the platform-specific scheduler branch below so a new sweep cannot
38
- // accidentally start an in-process timer before its platform trigger exists.
39
- const isServerlessRuntime = env.NETLIFY_LOCAL !== "true" &&
33
+ /**
34
+ * A serverless isolate is not a durable scheduler. Shared so
35
+ * `shouldDisableRecurringJobsRuntime` and `scheduledTriggerAvailability` cannot
36
+ * drift on what counts as serverless.
37
+ */
38
+ function isServerlessRecurringJobsRuntime(env) {
39
+ return (env.NETLIFY_LOCAL !== "true" &&
40
40
  (isTruthyEnv(env.NETLIFY) ||
41
41
  env.NITRO_PRESET === "netlify" ||
42
42
  Boolean(env.AWS_LAMBDA_FUNCTION_NAME) ||
43
43
  env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
44
44
  isTruthyEnv(env.CF_PAGES) ||
45
- isTruthyEnv(env.VERCEL));
46
- if (isServerlessRuntime)
45
+ isTruthyEnv(env.VERCEL)));
46
+ }
47
+ export function shouldDisableRecurringJobsRuntime(env = process.env) {
48
+ if (isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS))
49
+ return true;
50
+ // Keep this check separate from the platform-specific scheduler branch below
51
+ // so a new sweep cannot accidentally start an in-process timer before its
52
+ // platform trigger exists.
53
+ if (isServerlessRecurringJobsRuntime(env))
47
54
  return true;
48
55
  const isLocalRuntime = env.NODE_ENV === "development" ||
49
56
  env.NODE_ENV === "test" ||
@@ -74,3 +81,89 @@ export function isNetlifyRecurringJobsRuntime(env = process.env) {
74
81
  return false;
75
82
  return Boolean((env.NETLIFY && env.NETLIFY !== "false") || env.SITE_ID);
76
83
  }
84
+ /**
85
+ * Env name the build embeds its recurring-jobs decision under.
86
+ *
87
+ * Netlify's scheduled function is emitted — or omitted — while the build runs,
88
+ * and a deployed serverless runtime never sees the build environment. So the
89
+ * build has to hand the decision over explicitly; nothing observable at runtime
90
+ * can reconstruct it. Consumed below as a LITERAL `process.env.<name>` member
91
+ * expression because that is the only form Vite's `define` and Nitro's
92
+ * `replace` inline at build time.
93
+ */
94
+ export const RECURRING_JOBS_BUILD_MARKER_ENV_VAR = "AGENT_NATIVE_BUILD_RECURRING_JOBS";
95
+ /**
96
+ * Build-side resolver: what the build environment decided. Shared with
97
+ * `isRecurringJobsDeployEnabled` in the deploy build so the gate that emits the
98
+ * scheduled function and the marker that reports it cannot disagree.
99
+ */
100
+ export function resolveRecurringJobsBuildMarker(env = process.env) {
101
+ return isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS)
102
+ ? "disabled"
103
+ : "enabled";
104
+ }
105
+ function readRecurringJobsBuildMarker(env) {
106
+ const raw = env.AGENT_NATIVE_BUILD_RECURRING_JOBS ??
107
+ // config-ok: this value is INLINED at build time by Vite's `define` /
108
+ // Nitro's `replace`, which rewrite the literal `process.env.<NAME>` member
109
+ // expression and nothing else. A declared app-config field is read at
110
+ // runtime from the deployed environment, which is precisely the scope that
111
+ // cannot see the build's decision — the bug this marker exists to fix.
112
+ // Reading through the aliased `env` parameter would also survive the build
113
+ // unreplaced, so the literal form is load-bearing.
114
+ process.env.AGENT_NATIVE_BUILD_RECURRING_JOBS;
115
+ const value = raw?.trim();
116
+ return value === "enabled" || value === "disabled" ? value : undefined;
117
+ }
118
+ /**
119
+ * Whether ANY driver will actually fire a schedule-triggered automation in this
120
+ * deploy. Distinct from `shouldDisableRecurringJobsRuntime`, which answers the
121
+ * narrower "should THIS process run an in-process timer" and is therefore `true`
122
+ * on hosted Netlify even though schedules do fire there via the emitted
123
+ * scheduled function — reporting that value to a user would call the one
124
+ * working production runtime broken.
125
+ *
126
+ * Each branch reads the scope that actually decides its driver. The Netlify
127
+ * driver is a BUILD artifact, so it is answered by the build marker; every other
128
+ * driver is an in-process timer started from the RUNTIME env, so it is answered
129
+ * by the runtime flag. Reading runtime-only platform markers for the Netlify
130
+ * branch would report `available` off a build that emitted no trigger at all.
131
+ */
132
+ export function scheduledTriggerAvailability(env = process.env) {
133
+ const buildMarker = readRecurringJobsBuildMarker(env);
134
+ // Netlify: the driver IS the emitted scheduled function. It fires on the
135
+ // platform's clock without consulting the deployed env, and no runtime value
136
+ // can conjure it back once the build omitted it — so the build marker is the
137
+ // only honest input here, in both directions.
138
+ if (isNetlifyRecurringJobsRuntime(env)) {
139
+ if (buildMarker === "disabled") {
140
+ return { available: false, reason: "disabled-by-env" };
141
+ }
142
+ if (buildMarker === "enabled") {
143
+ return { available: true, driver: "netlify-scheduled-function" };
144
+ }
145
+ // No marker: a build predating it, or a caller passing a synthetic env.
146
+ // Fall back to the runtime flag, which only agrees with the build when the
147
+ // pipeline sets the same value in both scopes.
148
+ return isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS)
149
+ ? { available: false, reason: "disabled-by-env" }
150
+ : { available: true, driver: "netlify-scheduled-function" };
151
+ }
152
+ // Everything below is driven by an in-process timer, and the runtime flag is
153
+ // what `shouldDisableRecurringJobsRuntime` reads before starting it. A build
154
+ // marker cannot speak for this branch: a build with recurring jobs turned off
155
+ // still starts the timer if the deployed env does not repeat the switch.
156
+ if (isTruthyEnv(env.AGENT_NATIVE_DISABLE_RECURRING_JOBS)) {
157
+ return { available: false, reason: "disabled-by-env" };
158
+ }
159
+ // Every other serverless host: the build emits a scheduled trigger only for
160
+ // Netlify, and a frozen isolate cannot hold a timer.
161
+ if (isServerlessRecurringJobsRuntime(env)) {
162
+ return { available: false, reason: "no-platform-scheduler" };
163
+ }
164
+ // Long-lived host. Whatever is left of the runtime gate here is the
165
+ // local/loopback branch, which needs AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS.
166
+ return shouldDisableRecurringJobsRuntime(env)
167
+ ? { available: false, reason: "local-development" }
168
+ : { available: true, driver: "in-process" };
169
+ }
@@ -9,7 +9,7 @@ import { _agentChatPromptSectionsForTests } from "./agent-chat/framework-prompts
9
9
  import { type AgentChatPluginOptions, type NitroPluginDef } from "./agent-chat/plugin-options.js";
10
10
  import { finalizeClaimedAgentChatProcessRunFailure } from "./agent-chat/process-run-failure.js";
11
11
  import { loadResourcesForPrompt } from "./agent-chat/prompt-resources.js";
12
- import { isNetlifyRecurringJobsRuntime, shouldDisableRecurringJobsRuntime } from "./agent-chat/recurring-jobs-runtime.js";
12
+ import { isNetlifyRecurringJobsRuntime, resolveRecurringJobsBuildMarker, scheduledTriggerAvailability, shouldDisableRecurringJobsRuntime } from "./agent-chat/recurring-jobs-runtime.js";
13
13
  import { shouldBlockInProductCodeEditingSurface } from "./agent-chat/request-surface.js";
14
14
  import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
15
15
  export { loadResourcesForPrompt };
@@ -34,6 +34,8 @@ export { DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKEN
34
34
  export { shouldBlockInProductCodeEditingSurface };
35
35
  export { loadRunCodeToolEntries };
36
36
  export { isNetlifyRecurringJobsRuntime };
37
+ export { resolveRecurringJobsBuildMarker };
38
+ export { scheduledTriggerAvailability };
37
39
  export { shouldDisableRecurringJobsRuntime };
38
40
  export { finalizeClaimedAgentChatProcessRunFailure };
39
41
  export declare function resolveInteractiveAgentRunOptions(options?: Pick<AgentChatPluginOptions, "runSoftTimeoutMs" | "runNoProgressTimeoutMs" | "durableBackgroundRuns">): {
@@ -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
  }
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -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
+ }