@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.
- package/corpus/README.md +1 -1
- 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/brain/actions/list-sources.ts +17 -7
- package/corpus/templates/brain/app/components/brain/Surface.tsx +4 -2
- package/corpus/templates/brain/app/lib/brain.ts +13 -3
- package/corpus/templates/brain/server/lib/brain-health.ts +5 -8
- package/corpus/templates/brain/server/lib/brain.ts +1 -0
- package/corpus/templates/brain/server/lib/capture-sanitization.ts +5 -0
- package/corpus/templates/brain/server/routes/api/brain/queue/run.post.ts +3 -2
- 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/clips/desktop/src/app.tsx +11 -10
- package/corpus/templates/clips/desktop/src/lib/desktop-oauth-window.ts +23 -0
- 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/AboutAgentNativeDialog.d.ts +5 -0
- package/dist/client/AboutAgentNativeDialog.js +56 -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/CommandMenu.d.ts +6 -1
- package/dist/client/CommandMenu.js +46 -3
- package/dist/client/EnvironmentBadge.js +4 -1
- package/dist/client/MultiTabAssistantChat.d.ts +3 -1
- package/dist/client/MultiTabAssistantChat.js +5 -4
- package/dist/client/agent-native-version.d.ts +12 -0
- package/dist/client/agent-native-version.js +36 -0
- 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/routes.d.ts +1 -1
- package/dist/config.d.ts +3 -1
- package/dist/config.js +14 -9
- 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/core-messages/ar-SA.js +6 -0
- package/dist/localization/core-messages/de-DE.js +6 -0
- package/dist/localization/core-messages/en-US.d.ts +6 -0
- package/dist/localization/core-messages/en-US.js +6 -0
- package/dist/localization/core-messages/es-ES.js +6 -0
- package/dist/localization/core-messages/fr-FR.js +6 -0
- package/dist/localization/core-messages/hi-IN.js +6 -0
- package/dist/localization/core-messages/ja-JP.js +6 -0
- package/dist/localization/core-messages/ko-KR.js +6 -0
- package/dist/localization/core-messages/pt-BR.js +6 -0
- package/dist/localization/core-messages/zh-CN.js +6 -0
- package/dist/localization/core-messages/zh-TW.js +6 -0
- package/dist/localization/core-messages.d.ts +6 -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 +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +12 -12
- package/dist/resources/handlers.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/agent-engine-api-key-route.d.ts +1 -1
- 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/agent-native-config-loader.js +1 -1
- package/dist/vite/client.js +19 -8
- package/dist/vite/package-versions.d.ts +1 -0
- package/dist/vite/package-versions.js +97 -0
- package/docs/content/agent-native-config.mdx +55 -31
- package/docs/content/deployment.mdx +3 -1
- package/docs/content/environment-variables.mdx +51 -14
- 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
|
@@ -30,20 +30,27 @@ function isLoopbackAppUrl(value) {
|
|
|
30
30
|
}
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|
|
@@ -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
|
+
}
|