@agent-native/core 0.201.1 → 0.202.0-nightly-20261006033736

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 (98) hide show
  1. package/dist/automation/index.d.ts +1 -1
  2. package/dist/client/analytics/index.d.ts +1 -1
  3. package/dist/client/analytics/index.js +1 -1
  4. package/dist/client/analytics.d.ts +23 -3
  5. package/dist/client/analytics.js +189 -46
  6. package/dist/client/client-status-requests.d.ts +4 -1
  7. package/dist/client/client-status-requests.js +21 -5
  8. package/dist/client/embed-auth.d.ts +6 -0
  9. package/dist/client/embed-auth.js +13 -0
  10. package/dist/client/session-replay.js +48 -22
  11. package/dist/client/sharing/index.d.ts +2 -0
  12. package/dist/client/sharing/index.js +2 -0
  13. package/dist/client/sharing/useAccessRequestReview.d.ts +80 -0
  14. package/dist/client/sharing/useAccessRequestReview.js +100 -0
  15. package/dist/client/sharing/useResourceAccessGate.d.ts +57 -0
  16. package/dist/client/sharing/useResourceAccessGate.js +76 -0
  17. package/dist/client/use-agent-engine-configured.d.ts +2 -0
  18. package/dist/client/use-agent-engine-configured.js +4 -1
  19. package/dist/email-catalog/registry.d.ts +1 -1
  20. package/dist/email-catalog/registry.js +1 -1
  21. package/dist/email-catalog/system-emails.d.ts +2 -2
  22. package/dist/email-catalog/system-emails.js +47 -2
  23. package/dist/email-catalog/templates.d.ts +5 -1
  24. package/dist/email-catalog/templates.js +11 -1
  25. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  26. package/dist/framework-tools.js +6 -0
  27. package/dist/identity/rekey.js +9 -0
  28. package/dist/jobs/run-history.d.ts +27 -8
  29. package/dist/jobs/run-history.js +17 -10
  30. package/dist/jobs/scheduler.js +39 -10
  31. package/dist/mcp/build-server.d.ts +1 -0
  32. package/dist/mcp/build-server.js +30 -7
  33. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  34. package/dist/notifications/routes.d.ts +2 -2
  35. package/dist/observability/routes.d.ts +7 -7
  36. package/dist/org/index.d.ts +1 -1
  37. package/dist/progress/routes.d.ts +1 -1
  38. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  39. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  40. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  41. package/dist/review/suggestions/actions.js +14 -35
  42. package/dist/review/suggestions/store.d.ts +0 -1
  43. package/dist/review/suggestions/store.js +0 -7
  44. package/dist/secrets/routes.d.ts +11 -11
  45. package/dist/server/action-discovery.js +24 -0
  46. package/dist/server/agent-access.js +7 -3
  47. package/dist/server/agent-chat/mcp-options.d.ts +1 -0
  48. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  49. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  50. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  51. package/dist/server/attribution.d.ts +33 -1
  52. package/dist/server/attribution.js +93 -21
  53. package/dist/server/auth.js +35 -11
  54. package/dist/server/email-templates.d.ts +34 -0
  55. package/dist/server/email-templates.js +40 -0
  56. package/dist/server/magic-link-attribution.js +2 -1
  57. package/dist/server/onboarding-html.js +35 -67
  58. package/dist/server/release-schema.js +4 -0
  59. package/dist/server/short-lived-token.d.ts +7 -0
  60. package/dist/server/short-lived-token.js +59 -0
  61. package/dist/shared/analytics-anonymous-id.d.ts +1 -0
  62. package/dist/shared/analytics-anonymous-id.js +1 -1
  63. package/dist/shared/attribution-source.d.ts +30 -0
  64. package/dist/shared/attribution-source.js +65 -0
  65. package/dist/sharing/access-request-store.d.ts +96 -0
  66. package/dist/sharing/access-request-store.js +213 -0
  67. package/dist/sharing/access-requests.d.ts +113 -0
  68. package/dist/sharing/access-requests.js +490 -0
  69. package/dist/sharing/access.d.ts +36 -2
  70. package/dist/sharing/access.js +74 -12
  71. package/dist/sharing/actions/approve-resource-access-request.d.ts +10 -0
  72. package/dist/sharing/actions/approve-resource-access-request.js +15 -0
  73. package/dist/sharing/actions/decline-resource-access-request.d.ts +7 -0
  74. package/dist/sharing/actions/decline-resource-access-request.js +14 -0
  75. package/dist/sharing/actions/get-resource-access-request.d.ts +4 -0
  76. package/dist/sharing/actions/get-resource-access-request.js +13 -0
  77. package/dist/sharing/actions/get-resource-access-status.d.ts +5 -0
  78. package/dist/sharing/actions/get-resource-access-status.js +19 -0
  79. package/dist/sharing/actions/list-resource-access-requests.d.ts +5 -0
  80. package/dist/sharing/actions/list-resource-access-requests.js +16 -0
  81. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  82. package/dist/sharing/actions/request-resource-access.d.ts +6 -0
  83. package/dist/sharing/actions/request-resource-access.js +17 -0
  84. package/dist/sharing/actions/share-resource.d.ts +1 -1
  85. package/dist/sharing/actions/share-resource.js +18 -144
  86. package/dist/sharing/grant.d.ts +49 -0
  87. package/dist/sharing/grant.js +195 -0
  88. package/dist/sharing/index.d.ts +2 -1
  89. package/dist/sharing/index.js +2 -1
  90. package/dist/sharing/registry.d.ts +36 -0
  91. package/dist/templates/chat/server/plugins/auth.ts +2 -2
  92. package/dist/templates/default/app/routes/access-requests.$requestId.tsx +7 -0
  93. package/dist/templates/default/server/plugins/auth.ts +2 -2
  94. package/dist/vite/action-types-plugin.js +24 -0
  95. package/package.json +11 -3
  96. package/src/templates/chat/server/plugins/auth.ts +2 -2
  97. package/src/templates/default/app/routes/access-requests.$requestId.tsx +7 -0
  98. package/src/templates/default/server/plugins/auth.ts +2 -2
@@ -39,7 +39,6 @@ export declare function recordDecision(client: DbExec, input: Omit<SuggestionDec
39
39
  }>;
40
40
  export declare function getDecision(client: DbExec, idempotencyKey: string): Promise<SuggestionDecisionRecord | null>;
41
41
  export declare function updateSuggestionStatus(client: DbExec, suggestionId: string, status: SuggestionStatus, observedRevision?: number): Promise<boolean>;
42
- export declare function replaceSuggestionStatus(client: DbExec, suggestionId: string, from: SuggestionStatus, to: SuggestionStatus): Promise<boolean>;
43
42
  export declare function getSuggestionProposal(client: DbExec, id: string): Promise<ResourceSuggestionProposal | null>;
44
43
  export declare function insertSuggestionProposal(client: DbExec, input: Omit<ResourceSuggestionProposal, "id" | "createdAt">): Promise<ResourceSuggestionProposal>;
45
44
  export declare function listProposalSuggestions(client: DbExec, proposalId: string): Promise<ResourceSuggestion[]>;
@@ -387,13 +387,6 @@ export async function updateSuggestionStatus(client, suggestionId, status, obser
387
387
  });
388
388
  return result.rowsAffected === 1;
389
389
  }
390
- export async function replaceSuggestionStatus(client, suggestionId, from, to) {
391
- const result = await client.execute({
392
- sql: "UPDATE agent_review_suggestions SET status = ?, updated_at = ? WHERE id = ? AND status = ?",
393
- args: [to, new Date().toISOString(), suggestionId, from],
394
- });
395
- return result.rowsAffected === 1;
396
- }
397
390
  export async function getSuggestionProposal(client, id) {
398
391
  const row = (await client.execute({
399
392
  sql: "SELECT * FROM agent_review_suggestion_proposals WHERE id = ?",
@@ -58,47 +58,47 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
58
58
  error: string;
59
59
  errorCode?: undefined;
60
60
  retryable?: undefined;
61
- status?: undefined;
62
61
  ok?: undefined;
62
+ status?: undefined;
63
63
  } | {
64
64
  error: string;
65
65
  errorCode: string;
66
66
  retryable?: undefined;
67
- status?: undefined;
68
67
  ok?: undefined;
68
+ status?: undefined;
69
69
  } | {
70
70
  error: string;
71
71
  errorCode: string;
72
72
  retryable: boolean;
73
- status?: undefined;
74
73
  ok?: undefined;
74
+ status?: undefined;
75
75
  } | {
76
+ error?: undefined;
76
77
  errorCode?: undefined;
77
78
  retryable?: undefined;
78
79
  ok: boolean;
79
80
  status: string;
80
- error?: undefined;
81
81
  } | {
82
+ ok?: undefined;
82
83
  error: string;
83
84
  removed?: undefined;
84
- ok?: undefined;
85
85
  } | {
86
+ error?: undefined;
86
87
  ok: boolean;
87
88
  removed: boolean;
88
- error?: undefined;
89
89
  }>>;
90
90
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
91
+ ok?: undefined;
91
92
  error: string;
92
93
  note?: undefined;
93
- ok?: undefined;
94
94
  } | {
95
+ error?: undefined;
95
96
  ok: boolean;
96
97
  note?: undefined;
97
- error?: undefined;
98
98
  } | {
99
+ error?: undefined;
99
100
  ok: boolean;
100
101
  note: string;
101
- error?: undefined;
102
102
  } | {
103
103
  note?: undefined;
104
104
  ok: boolean;
@@ -126,11 +126,11 @@ export declare function createAdHocSecretHandler(): import("h3").EventHandlerWit
126
126
  } | {
127
127
  error: string;
128
128
  } | {
129
+ error?: undefined;
129
130
  ok: boolean;
130
131
  key: string;
131
- error?: undefined;
132
132
  } | {
133
+ error?: undefined;
133
134
  ok: boolean;
134
135
  removed: boolean;
135
- error?: undefined;
136
136
  }>>;
@@ -423,6 +423,30 @@ export async function mergeCoreSharingActions(registry) {
423
423
  "list-resource-shares",
424
424
  () => import("../sharing/actions/list-resource-shares.js"),
425
425
  ],
426
+ [
427
+ "get-resource-access-status",
428
+ () => import("../sharing/actions/get-resource-access-status.js"),
429
+ ],
430
+ [
431
+ "request-resource-access",
432
+ () => import("../sharing/actions/request-resource-access.js"),
433
+ ],
434
+ [
435
+ "get-resource-access-request",
436
+ () => import("../sharing/actions/get-resource-access-request.js"),
437
+ ],
438
+ [
439
+ "list-resource-access-requests",
440
+ () => import("../sharing/actions/list-resource-access-requests.js"),
441
+ ],
442
+ [
443
+ "approve-resource-access-request",
444
+ () => import("../sharing/actions/approve-resource-access-request.js"),
445
+ ],
446
+ [
447
+ "decline-resource-access-request",
448
+ () => import("../sharing/actions/decline-resource-access-request.js"),
449
+ ],
426
450
  [
427
451
  "set-resource-visibility",
428
452
  () => import("../sharing/actions/set-resource-visibility.js"),
@@ -1,8 +1,8 @@
1
1
  import { DEFAULT_AGENT_ACCESS_TTL_SECONDS, scopedAgentAccessResourceId, } from "../shared/agent-access.js";
2
- import { signShortLivedToken, verifyShortLivedToken, } from "./short-lived-token.js";
2
+ import { signCompactShortLivedToken, verifyCompactShortLivedToken, verifyShortLivedToken, } from "./short-lived-token.js";
3
3
  export { AGENT_ACCESS_PARAM, DEFAULT_AGENT_ACCESS_TTL_SECONDS, appendAgentAccessParam, buildAgentAccessApiUrl, buildAgentAccessUrl, normalizeAgentAccessBasePath, normalizeAgentAccessOrigin, scopedAgentAccessResourceId, toAgentAccessUrl, } from "../shared/agent-access.js";
4
4
  export function signScopedAgentAccessToken({ resourceKind, resourceId, viewerEmail, agentLabel, ttlSeconds = DEFAULT_AGENT_ACCESS_TTL_SECONDS, }) {
5
- return signShortLivedToken({
5
+ return signCompactShortLivedToken({
6
6
  resourceId: scopedAgentAccessResourceId(resourceKind, resourceId),
7
7
  viewerEmail,
8
8
  agentLabel,
@@ -12,7 +12,11 @@ export function signScopedAgentAccessToken({ resourceKind, resourceId, viewerEma
12
12
  export function verifyScopedAgentAccessToken(token, scope) {
13
13
  if (!token)
14
14
  return { ok: false, reason: "missing" };
15
- return verifyShortLivedToken(token, scopedAgentAccessResourceId(scope.resourceKind, scope.resourceId));
15
+ const resourceId = scopedAgentAccessResourceId(scope.resourceKind, scope.resourceId);
16
+ const compact = verifyCompactShortLivedToken(token, resourceId);
17
+ if (compact.ok || compact.reason !== "bad_signature")
18
+ return compact;
19
+ return verifyShortLivedToken(token, resourceId);
16
20
  }
17
21
  export function createScopedAgentAccessGrant(options) {
18
22
  const ttlSeconds = options.ttlSeconds ?? DEFAULT_AGENT_ACCESS_TTL_SECONDS;
@@ -12,6 +12,7 @@ export interface AgentChatMcpOptions {
12
12
  directoryProfile?: {
13
13
  connectorCatalog: string[];
14
14
  instructions?: string;
15
+ widgets?: boolean;
15
16
  keyToolNames?: readonly string[];
16
17
  toolDescriptions?: Record<string, string>;
17
18
  toolParameterDescriptions?: Record<string, Record<string, string>>;
@@ -65,8 +65,8 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
65
65
  error: any;
66
66
  code: any;
67
67
  } | {
68
- error?: undefined;
69
68
  errorCode?: undefined;
69
+ error?: undefined;
70
70
  code?: undefined;
71
71
  ok: boolean;
72
72
  key: string;
@@ -8,9 +8,9 @@ export declare const AGENT_ENGINE_API_KEY_ACTION_NAME = "agent-engine-api-key";
8
8
  * resolveEngine's credential detection.
9
9
  */
10
10
  export declare function createAgentEngineDisconnectHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
- ok?: undefined;
12
11
  error: string;
13
12
  scope?: undefined;
13
+ ok?: undefined;
14
14
  } | {
15
15
  ok: boolean;
16
16
  error: any;
@@ -3,7 +3,7 @@ export declare function createAgentEngineOllamaModelsHandler(): import("h3").Eve
3
3
  models?: undefined;
4
4
  ok?: undefined;
5
5
  } | {
6
+ error?: undefined;
6
7
  ok: boolean;
7
8
  models: string[];
8
- error?: undefined;
9
9
  }>>;
@@ -8,6 +8,11 @@
8
8
  * request and enriches the canonical server-side `signup` event so we can
9
9
  * measure where new users came from and how apps spread (virality).
10
10
  *
11
+ * The browser also keeps the visitor's *last* visit that had a source in an
12
+ * `an_lt` cookie. First touch answers "how did they find us?" and credits
13
+ * acquisition; last touch answers "what brought them to sign up?" and credits
14
+ * the post, video, or person that converted them.
15
+ *
11
16
  * This module is intentionally pure and dependency-free so it is trivially
12
17
  * unit-testable and can never throw into the signup path. The single hard rule:
13
18
  * parsing untrusted cookie input must NEVER throw — every accessor is defensive.
@@ -36,6 +41,25 @@ export interface FirstTouchAttribution {
36
41
  landed_at?: string;
37
42
  capture_truncated?: string;
38
43
  }
44
+ /** The decoded `an_lt` cookie: the latest visit that had a source. */
45
+ export interface LastTouchAttribution {
46
+ ref?: string;
47
+ via?: string;
48
+ utm_source?: string;
49
+ utm_medium?: string;
50
+ utm_campaign?: string;
51
+ utm_content?: string;
52
+ utm_term?: string;
53
+ gclid?: string;
54
+ msclkid?: string;
55
+ vector_source?: string;
56
+ landing_referrer?: string;
57
+ site_referrer?: string;
58
+ site_landing_path?: string;
59
+ landing_path?: string;
60
+ touched_at?: string;
61
+ capture_truncated?: string;
62
+ }
39
63
  export type SignupOrigin = "browser_signup"
40
64
  /** The framework's Google OAuth callback owns this event. */
41
65
  | "google_oauth"
@@ -47,20 +71,28 @@ export interface SignupAttributionContext {
47
71
  }
48
72
  export declare const SIGNUP_ATTRIBUTION_HEADER_NAME = "x-agent-native-signup-attribution";
49
73
  export declare const FIRST_TOUCH_COOKIE_NAME = "an_ft";
74
+ export declare const LAST_TOUCH_COOKIE_NAME = "an_lt";
50
75
  /**
51
76
  * Parse a raw `Cookie:` header into a flat name→value map. Tolerates missing
52
77
  * input, extra whitespace, `=` inside values, and malformed pairs. Never throws.
53
78
  */
54
79
  export declare function parseCookieHeader(cookieHeader: string | null | undefined): Record<string, string>;
55
80
  export declare function decodeFirstTouchValue(value: string | null | undefined): FirstTouchAttribution | null;
81
+ export declare function decodeLastTouchValue(value: string | null | undefined): LastTouchAttribution | null;
56
82
  /**
57
83
  * Read the `an_ft` first-touch attribution out of a raw `Cookie:` header.
58
84
  * Returns `null` when the cookie is absent or unparseable. Never throws.
59
85
  */
60
86
  export declare function readFirstTouchAttribution(cookieHeader: string | null | undefined): FirstTouchAttribution | null;
87
+ /**
88
+ * Read the `an_lt` last-touch attribution out of a raw `Cookie:` header.
89
+ * Returns `null` when the cookie is absent or unparseable. Never throws.
90
+ */
91
+ export declare function readLastTouchAttribution(cookieHeader: string | null | undefined): LastTouchAttribution | null;
61
92
  export declare function readAnalyticsAnonymousId(cookieHeader: string | null | undefined): string | undefined;
62
- export declare function deriveReferralSource(ft: FirstTouchAttribution | null): string;
93
+ export declare function deriveReferralSource(ft: FirstTouchAttribution | LastTouchAttribution | null): string;
63
94
  export declare function deriveSignupAttribution(ft: FirstTouchAttribution | null): Record<string, string>;
95
+ export declare function deriveLastTouchAttribution(lt: LastTouchAttribution | null): Record<string, string>;
64
96
  /**
65
97
  * Convenience: read the cookie header and derive signup attribution in one
66
98
  * call. Never throws; falls back to `{ referral_source: "direct" }` on any
@@ -1,7 +1,9 @@
1
- import { ANALYTICS_ANONYMOUS_ID_COOKIE_NAME, normalizeAnalyticsAnonymousId, } from "../shared/analytics-anonymous-id.js";
1
+ import { ANALYTICS_ANONYMOUS_ID_COOKIE_NAME, ANALYTICS_ANONYMOUS_ID_MAX_LENGTH, normalizeAnalyticsAnonymousId, } from "../shared/analytics-anonymous-id.js";
2
+ import { isSourceReferrerHost, shareLandingSource, } from "../shared/attribution-source.js";
2
3
  export const SIGNUP_ATTRIBUTION_HEADER_NAME = "x-agent-native-signup-attribution";
3
4
  const SIGNUP_ATTRIBUTION_HEADER_MAX_LENGTH = 4096;
4
5
  export const FIRST_TOUCH_COOKIE_NAME = "an_ft";
6
+ export const LAST_TOUCH_COOKIE_NAME = "an_lt";
5
7
  const STRING_FIELDS = [
6
8
  "ref",
7
9
  "via",
@@ -20,6 +22,24 @@ const STRING_FIELDS = [
20
22
  "landed_at",
21
23
  "capture_truncated",
22
24
  ];
25
+ const LAST_TOUCH_STRING_FIELDS = [
26
+ "ref",
27
+ "via",
28
+ "utm_source",
29
+ "utm_medium",
30
+ "utm_campaign",
31
+ "utm_content",
32
+ "utm_term",
33
+ "gclid",
34
+ "msclkid",
35
+ "vector_source",
36
+ "landing_referrer",
37
+ "site_referrer",
38
+ "site_landing_path",
39
+ "landing_path",
40
+ "touched_at",
41
+ "capture_truncated",
42
+ ];
23
43
  /**
24
44
  * Parse a raw `Cookie:` header into a flat name→value map. Tolerates missing
25
45
  * input, extra whitespace, `=` inside values, and malformed pairs. Never throws.
@@ -44,6 +64,12 @@ export function parseCookieHeader(cookieHeader) {
44
64
  return out;
45
65
  }
46
66
  export function decodeFirstTouchValue(value) {
67
+ return decodeAttributionCookieValue(value, STRING_FIELDS);
68
+ }
69
+ export function decodeLastTouchValue(value) {
70
+ return decodeAttributionCookieValue(value, LAST_TOUCH_STRING_FIELDS);
71
+ }
72
+ function decodeAttributionCookieValue(value, fields) {
47
73
  if (!value || typeof value !== "string")
48
74
  return null;
49
75
  let decoded = value;
@@ -66,7 +92,7 @@ export function decodeFirstTouchValue(value) {
66
92
  const source = parsed;
67
93
  const result = {};
68
94
  let any = false;
69
- for (const field of STRING_FIELDS) {
95
+ for (const field of fields) {
70
96
  const raw = source[field];
71
97
  if (typeof raw === "string" && raw.length > 0) {
72
98
  result[field] = raw.slice(0, 120);
@@ -88,6 +114,14 @@ export function readFirstTouchAttribution(cookieHeader) {
88
114
  return null;
89
115
  }
90
116
  }
117
+ /**
118
+ * Read the `an_lt` last-touch attribution out of a raw `Cookie:` header.
119
+ * Returns `null` when the cookie is absent or unparseable. Never throws.
120
+ */
121
+ export function readLastTouchAttribution(cookieHeader) {
122
+ const cookies = parseCookieHeader(cookieHeader);
123
+ return decodeLastTouchValue(cookies[LAST_TOUCH_COOKIE_NAME]);
124
+ }
91
125
  export function readAnalyticsAnonymousId(cookieHeader) {
92
126
  try {
93
127
  return normalizeAnalyticsAnonymousId(parseCookieHeader(cookieHeader)[ANALYTICS_ANONYMOUS_ID_COOKIE_NAME]);
@@ -96,25 +130,14 @@ export function readAnalyticsAnonymousId(cookieHeader) {
96
130
  return undefined;
97
131
  }
98
132
  }
99
- function isExternalReferrerHost(host) {
100
- const trimmed = host?.trim();
101
- return !!trimmed && trimmed.length > 0;
102
- }
103
133
  export function deriveReferralSource(ft) {
104
134
  if (ft?.ref && ft.ref.trim())
105
135
  return ft.ref.trim();
106
- const path = ft?.landing_path ?? "";
107
- if (path.startsWith("/share/"))
108
- return "clip_share";
109
- if (path.includes("/p/") ||
110
- path.includes("/plan/") ||
111
- path.includes("/plans/") ||
112
- path.includes("/recaps/") ||
113
- path.includes("/share-plan/")) {
114
- return "plan_share";
115
- }
116
- if (isExternalReferrerHost(ft?.landing_referrer) ||
117
- isExternalReferrerHost(ft?.site_referrer)) {
136
+ const shareSource = shareLandingSource(ft?.landing_path);
137
+ if (shareSource)
138
+ return shareSource;
139
+ if (isSourceReferrerHost(ft?.landing_referrer) ||
140
+ isSourceReferrerHost(ft?.site_referrer)) {
118
141
  return "external";
119
142
  }
120
143
  return "direct";
@@ -149,6 +172,54 @@ export function deriveSignupAttribution(ft) {
149
172
  out.attribution_truncated = "true";
150
173
  return out;
151
174
  }
175
+ export function deriveLastTouchAttribution(lt) {
176
+ if (!lt)
177
+ return {};
178
+ const out = {
179
+ last_touch_source: deriveReferralSource(lt),
180
+ };
181
+ const setIf = (key, value) => {
182
+ const trimmed = value?.trim();
183
+ if (trimmed)
184
+ out[key] = trimmed;
185
+ };
186
+ setIf("last_touch_ref", lt.ref);
187
+ setIf("last_touch_via", lt.via);
188
+ setIf("last_touch_utm_source", lt.utm_source);
189
+ setIf("last_touch_utm_medium", lt.utm_medium);
190
+ setIf("last_touch_utm_campaign", lt.utm_campaign);
191
+ setIf("last_touch_utm_content", lt.utm_content);
192
+ setIf("last_touch_utm_term", lt.utm_term);
193
+ setIf("last_touch_gclid", lt.gclid);
194
+ setIf("last_touch_msclkid", lt.msclkid);
195
+ setIf("last_touch_vector_source", lt.vector_source);
196
+ setIf("last_touch_referrer", lt.landing_referrer);
197
+ setIf("last_touch_site_referrer", lt.site_referrer);
198
+ setIf("last_touch_path", lt.landing_path);
199
+ setIf("last_touch_site_path", lt.site_landing_path);
200
+ setIf("last_touch_at", lt.touched_at);
201
+ // The browser dropped fields to fit the cookie.
202
+ if (lt.capture_truncated === "1")
203
+ out.last_touch_truncated = "true";
204
+ return out;
205
+ }
206
+ /**
207
+ * Add last touch only while the handoff still fits its header. An oversized
208
+ * header is dropped whole, which would lose first touch with it.
209
+ */
210
+ function withLastTouchAttribution(attribution, lt) {
211
+ const lastTouch = deriveLastTouchAttribution(lt);
212
+ if (Object.keys(lastTouch).length === 0)
213
+ return attribution;
214
+ const merged = { ...attribution, ...lastTouch };
215
+ const largest = encodeSignupAttributionContext({
216
+ attribution: merged,
217
+ anonymousId: "a".repeat(ANALYTICS_ANONYMOUS_ID_MAX_LENGTH),
218
+ });
219
+ if (largest.length <= SIGNUP_ATTRIBUTION_HEADER_MAX_LENGTH)
220
+ return merged;
221
+ return { ...attribution, last_touch_truncated: "true" };
222
+ }
152
223
  /**
153
224
  * Convenience: read the cookie header and derive signup attribution in one
154
225
  * call. Never throws; falls back to `{ referral_source: "direct" }` on any
@@ -156,7 +227,7 @@ export function deriveSignupAttribution(ft) {
156
227
  */
157
228
  export function signupAttributionFromCookieHeader(cookieHeader) {
158
229
  try {
159
- return deriveSignupAttribution(readFirstTouchAttribution(cookieHeader));
230
+ return withLastTouchAttribution(deriveSignupAttribution(readFirstTouchAttribution(cookieHeader)), readLastTouchAttribution(cookieHeader));
160
231
  }
161
232
  catch {
162
233
  return { referral_source: "direct" };
@@ -176,11 +247,12 @@ export function signupAttributionFromCookieHeader(cookieHeader) {
176
247
  */
177
248
  export function signupAttributionContextFromCookieHeader(cookieHeader) {
178
249
  const firstTouch = readFirstTouchAttribution(cookieHeader);
250
+ const lastTouch = readLastTouchAttribution(cookieHeader);
179
251
  const anonymousId = readAnalyticsAnonymousId(cookieHeader);
180
- if (!firstTouch && !anonymousId)
252
+ if (!firstTouch && !lastTouch && !anonymousId)
181
253
  return undefined;
182
254
  return {
183
- attribution: deriveSignupAttribution(firstTouch),
255
+ attribution: withLastTouchAttribution(deriveSignupAttribution(firstTouch), lastTouch),
184
256
  ...(anonymousId ? { anonymousId } : {}),
185
257
  };
186
258
  }
@@ -44,7 +44,7 @@ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
44
44
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
45
45
  import { resolveLocaleFromRequest } from "../localization/server.js";
46
46
  import { readMcpOAuthFlowCookiePayload } from "../mcp-client/oauth-flow-cookie.js";
47
- import { MCP_LEGACY_ROUTE_PREFIX, MCP_PUBLIC_ROUTE_PREFIX, isMcpProtocolPath, } from "../mcp/route-paths.js";
47
+ import { MCP_DIRECTORY_ROUTE_PREFIX, MCP_LEGACY_ROUTE_PREFIX, MCP_PUBLIC_ROUTE_PREFIX, isMcpProtocolPath, } from "../mcp/route-paths.js";
48
48
  import { authProviderRequiredMessage, getRequiredAuthProviderForEmail, isGoogleSignInRequiredForEmail, } from "../org/auth-policy.js";
49
49
  import { readBody } from "../server/h3-helpers.js";
50
50
  import { putSetting } from "../settings/store.js";
@@ -62,7 +62,7 @@ import { injectAnalyticsIntoHtml } from "./analytics.js";
62
62
  import { getConfiguredAppBasePath, stripAppBasePath as stripConfiguredAppBasePath, } from "./app-base-path.js";
63
63
  import { getAppOriginClientConfigScript } from "./app-origin-config.js";
64
64
  import { getAppProductionUrl } from "./app-url.js";
65
- import { addSignupAttributionHeader, readAnalyticsAnonymousId, readFirstTouchAttribution, signupAttributionContextFromCookieHeader, signupAttributionFromCookieHeader, } from "./attribution.js";
65
+ import { addSignupAttributionHeader, readAnalyticsAnonymousId, signupAttributionContextFromCookieHeader, signupAttributionFromCookieHeader, } from "./attribution.js";
66
66
  import { getAuthLoginMode } from "./auth-login-mode.js";
67
67
  import { injectBetaOptOutPersistence } from "./beta-opt-out-html.js";
68
68
  import { createBetterAuthSessionForEmail, ensureGoogleAuthIdentity, getBetterAuthInternalAdapter, getBetterAuthUserIdForEmail, getAuthSecret, getBetterAuth, getBetterAuthSync, isDeployPreview, } from "./better-auth-instance.js";
@@ -2115,6 +2115,13 @@ export async function runAuthGuard(event) {
2115
2115
  return;
2116
2116
  return _authGuardFn(event);
2117
2117
  }
2118
+ const MCP_DIRECTORY_CORS_ORIGINS = new Set([
2119
+ "https://chatgpt.com",
2120
+ "https://chat.openai.com",
2121
+ "https://platform.openai.com",
2122
+ ]);
2123
+ const MCP_DIRECTORY_CORS_METHODS = "POST, GET, DELETE, OPTIONS";
2124
+ const MCP_DIRECTORY_CORS_HEADERS = "Authorization, Content-Type, Accept, MCP-Protocol-Version, MCP-Session-Id, Last-Event-Id";
2118
2125
  function applyCorsHeaders(event, publicCorsPaths = [], requestPath) {
2119
2126
  const origin = getHeader(event, "origin");
2120
2127
  if (!origin)
@@ -2717,6 +2724,29 @@ function createAuthGuardFn(app) {
2717
2724
  const callbackRelay = workspaceOAuthCallbackRelayResponse(event);
2718
2725
  if (callbackRelay)
2719
2726
  return callbackRelay;
2727
+ const isDirectoryMcpPath = p === MCP_DIRECTORY_ROUTE_PREFIX ||
2728
+ p === `${MCP_DIRECTORY_ROUTE_PREFIX}/`;
2729
+ const directoryPreflightOrigin = getMethod(event) === "OPTIONS" && isDirectoryMcpPath
2730
+ ? getHeader(event, "origin")
2731
+ : undefined;
2732
+ if (directoryPreflightOrigin) {
2733
+ if (!MCP_DIRECTORY_CORS_ORIGINS.has(directoryPreflightOrigin)) {
2734
+ setResponseStatus(event, 403);
2735
+ return "";
2736
+ }
2737
+ setResponseHeader(event, "Access-Control-Allow-Origin", directoryPreflightOrigin);
2738
+ setResponseHeader(event, "Vary", "Origin");
2739
+ setResponseHeader(event, "Access-Control-Allow-Methods", MCP_DIRECTORY_CORS_METHODS);
2740
+ setResponseHeader(event, "Access-Control-Allow-Headers", MCP_DIRECTORY_CORS_HEADERS);
2741
+ setResponseStatus(event, 204);
2742
+ return "";
2743
+ }
2744
+ const directoryResponseOrigin = isDirectoryMcpPath && getHeader(event, "origin");
2745
+ if (directoryResponseOrigin &&
2746
+ MCP_DIRECTORY_CORS_ORIGINS.has(directoryResponseOrigin)) {
2747
+ setResponseHeader(event, "Access-Control-Allow-Origin", directoryResponseOrigin);
2748
+ setResponseHeader(event, "Vary", "Origin");
2749
+ }
2720
2750
  const cors = applyCorsHeaders(event, config.publicCorsPaths, p);
2721
2751
  if (getMethod(event) === "OPTIONS") {
2722
2752
  if (cors.hasOrigin && !cors.allowed) {
@@ -4948,15 +4978,9 @@ async function mountBetterAuthRoutes(app, options) {
4948
4978
  callbackPath = withDesktopMagicLinkFlow(callbackPath, desktopFlow);
4949
4979
  }
4950
4980
  const callbackURL = betterAuthCallbackURL(callbackPath, true, event);
4951
- const cookieHeader = getHeader(event, "cookie") ?? null;
4952
- const signupAttribution = signupAttributionFromCookieHeader(cookieHeader);
4953
- const signupAnonymousId = readAnalyticsAnonymousId(cookieHeader);
4954
- const hasSignupAttribution = !!readFirstTouchAttribution(cookieHeader) || !!signupAnonymousId;
4955
- const attributionToken = hasSignupAttribution
4956
- ? encodeMagicLinkSignupAttribution({
4957
- attribution: signupAttribution,
4958
- anonymousId: signupAnonymousId,
4959
- }, getAuthSecret())
4981
+ const signupContext = signupAttributionContextFromCookieHeader(getHeader(event, "cookie") ?? null);
4982
+ const attributionToken = signupContext
4983
+ ? encodeMagicLinkSignupAttribution(signupContext, getAuthSecret())
4960
4984
  : undefined;
4961
4985
  const newUserCallbackUrl = new URL(`${getAppBasePath()}${publicFrameworkPath("/_agent-native/auth/magic-link/new-user")}?return=${encodeURIComponent(callbackPath)}`, getOrigin(event));
4962
4986
  if (attributionToken) {
@@ -73,3 +73,37 @@ export interface RenderResourceSharedEmailArgs {
73
73
  extras?: ShareEmailExtras;
74
74
  }
75
75
  export declare function renderResourceSharedEmail(args: RenderResourceSharedEmailArgs): RenderedEmailMessage;
76
+ export interface RenderAccessRequestedEmailArgs {
77
+ recipientEmail: string;
78
+ requester: {
79
+ name: string;
80
+ email: string;
81
+ };
82
+ resource: {
83
+ type: string;
84
+ label: string;
85
+ title: string;
86
+ url: string;
87
+ };
88
+ /** The page where someone who manages access allows or declines. */
89
+ reviewUrl: string;
90
+ note?: string;
91
+ app: EmailTemplateApp;
92
+ }
93
+ export declare function renderAccessRequestedEmail(args: RenderAccessRequestedEmailArgs): RenderedEmailMessage;
94
+ export interface RenderAccessGrantedEmailArgs {
95
+ recipientEmail: string;
96
+ approver: {
97
+ name: string;
98
+ email: string;
99
+ };
100
+ resource: {
101
+ type: string;
102
+ label: string;
103
+ title: string;
104
+ url: string;
105
+ };
106
+ role: "viewer" | "commenter" | "editor" | "admin";
107
+ app: EmailTemplateApp;
108
+ }
109
+ export declare function renderAccessGrantedEmail(args: RenderAccessGrantedEmailArgs): RenderedEmailMessage;
@@ -286,3 +286,43 @@ export function renderResourceSharedEmail(args) {
286
286
  });
287
287
  return { subject, html, text };
288
288
  }
289
+ export function renderAccessRequestedEmail(args) {
290
+ const requesterName = stripCrlf(args.requester.name);
291
+ const resourceTitle = stripCrlf(args.resource.title);
292
+ const resourceLabel = args.resource.label.toLowerCase();
293
+ const subject = `${requesterName} is asking for access to "${resourceTitle}"`;
294
+ const { html, text } = renderEmail({
295
+ brandName: args.app.name,
296
+ brandLogoUrl: args.app.logoUrl,
297
+ preheader: subject,
298
+ heading: `${requesterName} is asking for access`,
299
+ paragraphs: [
300
+ `${emailStrong(requesterName)} (${emailStrong(args.requester.email)}) is asking to open this ${resourceLabel}:`,
301
+ ...(args.note?.trim() ? [emailQuote(args.note)] : []),
302
+ ],
303
+ resourceBlock: { name: resourceTitle },
304
+ cta: { label: "Review request", url: args.reviewUrl },
305
+ closingParagraphs: [
306
+ `You're getting this because you can manage access to this ${resourceLabel}.`,
307
+ ],
308
+ });
309
+ return { subject, html, text };
310
+ }
311
+ export function renderAccessGrantedEmail(args) {
312
+ const approverName = stripCrlf(args.approver.name);
313
+ const resourceTitle = stripCrlf(args.resource.title);
314
+ const resourceLabel = args.resource.label.toLowerCase();
315
+ const subject = `You can now open "${resourceTitle}"`;
316
+ const { html, text } = renderEmail({
317
+ brandName: args.app.name,
318
+ brandLogoUrl: args.app.logoUrl,
319
+ preheader: subject,
320
+ heading: `${approverName} approved your request`,
321
+ paragraphs: [
322
+ `${emailStrong(approverName)} (${emailStrong(args.approver.email)}) gave you access to ${SHARE_ROLE_VERBS[args.role]} this ${resourceLabel}:`,
323
+ ],
324
+ resourceBlock: { name: resourceTitle },
325
+ cta: { label: "Open", url: args.resource.url },
326
+ });
327
+ return { subject, html, text };
328
+ }
@@ -2,7 +2,8 @@ import crypto from "node:crypto";
2
2
  import { normalizeAnalyticsAnonymousId } from "../shared/analytics-anonymous-id.js";
3
3
  export const MAGIC_LINK_ATTRIBUTION_PARAM = "signup_attribution";
4
4
  const MAGIC_LINK_ATTRIBUTION_TTL_SECONDS = 10 * 60;
5
- const MAX_ATTRIBUTION_FIELDS = 32;
5
+ // A signup carries up to 17 first-touch and 16 last-touch properties.
6
+ const MAX_ATTRIBUTION_FIELDS = 48;
6
7
  const MAX_ATTRIBUTION_VALUE_LENGTH = 200;
7
8
  function sanitizeAttribution(value) {
8
9
  if (!value || typeof value !== "object" || Array.isArray(value)) {