@agent-native/core 0.201.1 → 0.201.2-nightly-20261006010750

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.
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
+ error?: undefined;
142
143
  accepted: boolean;
143
144
  duplicate: boolean;
144
145
  eventId: string;
145
- error?: undefined;
146
146
  }>;
@@ -1,2 +1,2 @@
1
- export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureAttribution, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackAnonymousEvent, trackEvent, trackLifecycleEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
1
+ export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureAttribution, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getLastTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackAnonymousEvent, trackEvent, trackLifecycleEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type LastTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
2
2
  export { track } from "../track.js";
@@ -1,2 +1,2 @@
1
- export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureAttribution, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackAnonymousEvent, trackEvent, trackLifecycleEvent, trackSessionStatus, } from "../analytics.js";
1
+ export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureAttribution, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getLastTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackAnonymousEvent, trackEvent, trackLifecycleEvent, trackSessionStatus, } from "../analytics.js";
2
2
  export { track } from "../track.js";
@@ -86,14 +86,34 @@ export interface FirstTouchAttribution {
86
86
  landed_at?: string;
87
87
  capture_truncated?: string;
88
88
  }
89
+ /** The latest visit that had a source; see `captureLastTouchAttribution`. */
90
+ export interface LastTouchAttribution {
91
+ ref?: string;
92
+ via?: string;
93
+ utm_source?: string;
94
+ utm_medium?: string;
95
+ utm_campaign?: string;
96
+ utm_content?: string;
97
+ utm_term?: string;
98
+ gclid?: string;
99
+ msclkid?: string;
100
+ vector_source?: string;
101
+ landing_referrer?: string;
102
+ site_referrer?: string;
103
+ site_landing_path?: string;
104
+ landing_path?: string;
105
+ touched_at?: string;
106
+ capture_truncated?: string;
107
+ }
89
108
  export declare function getAnalyticsIdentityKey(): string | undefined;
90
109
  export declare function getAnalyticsAnonymousId(): string | undefined;
91
110
  export declare function getAnalyticsSessionId(): string | undefined;
92
111
  export declare function getFirstTouchAttribution(): FirstTouchAttribution | null;
112
+ export declare function getLastTouchAttribution(): LastTouchAttribution | null;
93
113
  /**
94
- * Store the visitor's first touch now instead of in `configureTracking()`,
95
- * for a page that reads it before tracking starts. It runs once per page
96
- * load, so tracking's own capture is then a no-op.
114
+ * Store the visitor's first and last touch now instead of in
115
+ * `configureTracking()`, for a page that reads them before tracking starts.
116
+ * It runs once per page load, so tracking's own capture is then a no-op.
97
117
  */
98
118
  export declare function captureAttribution(): void;
99
119
  export declare function setSentryUser(user: TrackingIdentityUser | null, orgId?: string | null): void;
@@ -1,6 +1,7 @@
1
1
  import { recordTrackingEvent } from "../observability/tracing.js";
2
2
  import { AGENT_NATIVE_LIFECYCLE_EVENTS, canonicalTrackingEvent, legacyLifecycleEvent, normalizeTrackingDimension, withCanonicalTrackingProperties, } from "../shared/analytics-events.js";
3
3
  import { ANALYTICS_CLIENT_PLATFORM_PROPERTY, } from "../shared/analytics-platform.js";
4
+ import { hasAttributionSource } from "../shared/attribution-source.js";
4
5
  import { resolveLaneEndpoint } from "../shared/environment-lanes.js";
5
6
  import { classifyErrorNoise, } from "../shared/error-noise.js";
6
7
  import { llmConnectionTrackingProperties, } from "../shared/llm-connection.js";
@@ -101,6 +102,72 @@ const FIRST_TOUCH_COOKIE_FIELD_PRIORITY = [
101
102
  "site_landing_path",
102
103
  "landed_at",
103
104
  ];
105
+ const LAST_TOUCH_STORAGE_KEY = "an_last_touch";
106
+ const LAST_TOUCH_COOKIE_NAME = "an_lt";
107
+ // Small enough that both cookies still fit the signup handoff header.
108
+ const LAST_TOUCH_MAX_COOKIE_BYTES = 700;
109
+ // What a sourced visit keeps as last touch, besides its path and time.
110
+ const LAST_TOUCH_SOURCE_FIELDS = [
111
+ "ref",
112
+ "via",
113
+ "utm_source",
114
+ "utm_medium",
115
+ "utm_campaign",
116
+ "utm_content",
117
+ "utm_term",
118
+ "gclid",
119
+ "msclkid",
120
+ "vector_source",
121
+ "landing_referrer",
122
+ "site_referrer",
123
+ ];
124
+ // Which fields the cookie keeps first when they don't all fit.
125
+ const LAST_TOUCH_COOKIE_FIELD_PRIORITY = [
126
+ "ref",
127
+ "gclid",
128
+ "msclkid",
129
+ "vector_source",
130
+ "utm_source",
131
+ "utm_medium",
132
+ "utm_campaign",
133
+ "via",
134
+ "utm_content",
135
+ "utm_term",
136
+ "landing_referrer",
137
+ "site_referrer",
138
+ "landing_path",
139
+ "touched_at",
140
+ "site_landing_path",
141
+ ];
142
+ // The marketing site forwards its own last touch under these names when it
143
+ // differs from the first touch it forwards as plain campaign params.
144
+ const FORWARDED_LAST_TOUCH_FIELDS = {
145
+ last_ref: "ref",
146
+ last_via: "via",
147
+ last_utm_source: "utm_source",
148
+ last_utm_medium: "utm_medium",
149
+ last_utm_campaign: "utm_campaign",
150
+ last_utm_content: "utm_content",
151
+ last_utm_term: "utm_term",
152
+ last_gclid: "gclid",
153
+ last_msclkid: "msclkid",
154
+ last_vector_source: "vector_source",
155
+ last_referrer: "site_referrer",
156
+ last_landing_path: "site_landing_path",
157
+ };
158
+ // When the site's latest sourced visit happened, so an older site visit can't
159
+ // replace a newer one the app already has.
160
+ const FORWARDED_LAST_TOUCH_AT_PARAM = "last_at";
161
+ const FIRST_TOUCH_COOKIE = {
162
+ name: FIRST_TOUCH_COOKIE_NAME,
163
+ maxBytes: FIRST_TOUCH_MAX_COOKIE_BYTES,
164
+ priority: FIRST_TOUCH_COOKIE_FIELD_PRIORITY,
165
+ };
166
+ const LAST_TOUCH_COOKIE = {
167
+ name: LAST_TOUCH_COOKIE_NAME,
168
+ maxBytes: LAST_TOUCH_MAX_COOKIE_BYTES,
169
+ priority: LAST_TOUCH_COOKIE_FIELD_PRIORITY,
170
+ };
104
171
  let _firstTouchCaptured = false;
105
172
  function safeStorageGet(key) {
106
173
  try {
@@ -390,7 +457,7 @@ function buildFirstTouchAttribution() {
390
457
  attribution.landed_at = new Date().toISOString();
391
458
  return attribution;
392
459
  }
393
- function readFirstTouchCookie() {
460
+ function readAttributionCookie(cookieName) {
394
461
  if (typeof document === "undefined")
395
462
  return null;
396
463
  try {
@@ -400,7 +467,7 @@ function readFirstTouchCookie() {
400
467
  if (eq === -1)
401
468
  continue;
402
469
  const name = part.slice(0, eq).trim();
403
- if (name === FIRST_TOUCH_COOKIE_NAME) {
470
+ if (name === cookieName) {
404
471
  return part.slice(eq + 1).trim();
405
472
  }
406
473
  }
@@ -410,15 +477,16 @@ function readFirstTouchCookie() {
410
477
  }
411
478
  return null;
412
479
  }
413
- function firstTouchCookieAssignment(encodedValue) {
414
- return (`${FIRST_TOUCH_COOKIE_NAME}=${encodedValue}; path=/; ` +
480
+ function attributionCookieAssignment(cookieName, encodedValue) {
481
+ return (`${cookieName}=${encodedValue}; path=/; ` +
415
482
  `max-age=${FIRST_TOUCH_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`);
416
483
  }
417
- function fitFirstTouchCookieValue(value) {
484
+ function fitAttributionCookieValue(value, spec) {
418
485
  const source = JSON.parse(value);
419
486
  const compact = {};
420
487
  let truncated = false;
421
- for (const field of FIRST_TOUCH_COOKIE_FIELD_PRIORITY) {
488
+ const fits = (encoded) => attributionCookieAssignment(spec.name, encoded).length <= spec.maxBytes;
489
+ for (const field of spec.priority) {
422
490
  const rawValue = source[field];
423
491
  if (typeof rawValue !== "string" || !rawValue)
424
492
  continue;
@@ -426,9 +494,8 @@ function fitFirstTouchCookieValue(value) {
426
494
  ...compact,
427
495
  [field]: rawValue.slice(0, FIRST_TOUCH_MAX_FIELD_LENGTH),
428
496
  };
429
- const encoded = encodeURIComponent(JSON.stringify(candidate));
430
- if (firstTouchCookieAssignment(encoded).length <= FIRST_TOUCH_MAX_COOKIE_BYTES) {
431
- Object.assign(compact, { [field]: candidate[field] });
497
+ if (fits(encodeURIComponent(JSON.stringify(candidate)))) {
498
+ compact[field] = candidate[field];
432
499
  }
433
500
  else {
434
501
  truncated = true;
@@ -437,38 +504,63 @@ function fitFirstTouchCookieValue(value) {
437
504
  if (truncated) {
438
505
  compact.capture_truncated = "1";
439
506
  // Keep the auth handoff under its 4 KB header limit after re-encoding.
440
- for (const field of [...FIRST_TOUCH_COOKIE_FIELD_PRIORITY].reverse()) {
507
+ for (const field of [...spec.priority].reverse()) {
441
508
  const encoded = encodeURIComponent(JSON.stringify(compact));
442
- if (firstTouchCookieAssignment(encoded).length <=
443
- FIRST_TOUCH_MAX_COOKIE_BYTES) {
509
+ if (fits(encoded))
444
510
  return encoded;
445
- }
446
511
  delete compact[field];
447
512
  }
448
513
  }
449
514
  const encoded = encodeURIComponent(JSON.stringify(compact));
450
- if (firstTouchCookieAssignment(encoded).length > FIRST_TOUCH_MAX_COOKIE_BYTES) {
451
- throw new Error("First-touch attribution exceeded the cookie budget");
515
+ if (!fits(encoded)) {
516
+ throw new Error(`Attribution exceeded the ${spec.name} cookie budget`);
452
517
  }
453
518
  return encoded;
454
519
  }
455
- function writeFirstTouchCookie(value) {
520
+ function writeAttributionCookie(value, spec) {
456
521
  if (typeof document === "undefined")
457
522
  return;
458
- const encodedValue = fitFirstTouchCookieValue(value);
523
+ const encodedValue = fitAttributionCookieValue(value, spec);
459
524
  try {
460
- document.cookie = firstTouchCookieAssignment(encodedValue);
525
+ document.cookie = attributionCookieAssignment(spec.name, encodedValue);
461
526
  }
462
527
  catch {
463
528
  // best-effort
464
529
  }
465
530
  }
531
+ function storeAttribution(storageKey, spec, attribution) {
532
+ const json = JSON.stringify(attribution);
533
+ safeStorageSet(storageKey, json);
534
+ writeAttributionCookie(json, spec);
535
+ }
536
+ /**
537
+ * Restore a cookie that expired or was cleared from its stored value, so the
538
+ * signup boundary still sees it.
539
+ */
540
+ function backfillAttributionCookie(storageKey, spec) {
541
+ if (readAttributionCookie(spec.name))
542
+ return;
543
+ const stored = safeStorageGet(storageKey);
544
+ if (!stored)
545
+ return;
546
+ try {
547
+ writeAttributionCookie(stored, spec);
548
+ }
549
+ catch {
550
+ // coercion-ok: localStorage still holds the value; only this page's
551
+ // signup handoff goes without it.
552
+ }
553
+ }
466
554
  /**
467
- * Capture the visitor's first-touch referral attribution exactly once. Reads
468
- * the current URL query params + landing info and, IF no attribution is
469
- * already stored (first-write-wins), persists it to both `localStorage`
470
- * (`an_attribution`) and the first-party `an_ft` cookie. Fully defensive and
471
- * SSR-safe — any failure is swallowed so it can never break app boot.
555
+ * Capture the visitor's referral attribution once per page load, into both
556
+ * `localStorage` and a first-party cookie the signup boundary reads. Fully
557
+ * defensive and SSR-safe — any failure is swallowed so it can never break app
558
+ * boot.
559
+ *
560
+ * First touch (`an_attribution` / `an_ft`) is first-write-wins, except over a
561
+ * visit that had no source: the first visit that says where this person came
562
+ * from replaces it. Without that, an untagged first visit would hide every
563
+ * tagged visit after it.
472
564
  */
473
565
  function captureFirstTouchAttribution() {
474
566
  if (_firstTouchCaptured)
@@ -477,35 +569,80 @@ function captureFirstTouchAttribution() {
477
569
  if (typeof window === "undefined")
478
570
  return;
479
571
  try {
480
- const existing = safeStorageGet(FIRST_TOUCH_STORAGE_KEY);
481
- if (existing) {
482
- // Already captured in a prior visit. Backfill the cookie if it expired
483
- // or was cleared so the signup boundary still sees first-touch data, but
484
- // never overwrite the stored value itself (first-write-wins).
485
- if (!readFirstTouchCookie()) {
486
- try {
487
- writeFirstTouchCookie(existing);
488
- }
489
- catch {
490
- // ignore
491
- }
492
- }
493
- return;
572
+ const current = buildFirstTouchAttribution();
573
+ const existing = getFirstTouchAttribution();
574
+ if (existing &&
575
+ (hasAttributionSource(existing) || !hasAttributionSource(current))) {
576
+ backfillAttributionCookie(FIRST_TOUCH_STORAGE_KEY, FIRST_TOUCH_COOKIE);
577
+ }
578
+ else {
579
+ storeAttribution(FIRST_TOUCH_STORAGE_KEY, FIRST_TOUCH_COOKIE, current);
494
580
  }
495
- const attribution = buildFirstTouchAttribution();
496
- const json = JSON.stringify(attribution);
497
- safeStorageSet(FIRST_TOUCH_STORAGE_KEY, json);
498
- writeFirstTouchCookie(json);
581
+ captureLastTouchAttribution(current);
499
582
  }
500
583
  catch {
501
584
  // Attribution is best-effort telemetry; never let it break boot.
502
585
  }
503
586
  }
504
- export function getFirstTouchAttribution() {
587
+ /**
588
+ * Last touch (`an_last_touch` / `an_lt`) is the latest visit that had a
589
+ * source. When the marketing site forwards its own last touch, that visit is
590
+ * the one to keep, not the site's first touch riding on the same link. A site
591
+ * visit keeps the time it happened, and loses to a newer visit the app has
592
+ * already recorded.
593
+ */
594
+ function captureLastTouchAttribution(current) {
595
+ const forwarded = readForwardedLastTouch();
596
+ const source = forwarded ?? (hasAttributionSource(current) ? current : null);
597
+ const forwardedAt = readForwardedLastTouchAt();
598
+ const existing = getLastTouchAttribution();
599
+ const existingAt = Date.parse(existing?.touched_at ?? "");
600
+ if (!source ||
601
+ (forwardedAt &&
602
+ hasAttributionSource(existing) &&
603
+ existingAt > Date.parse(forwardedAt))) {
604
+ backfillAttributionCookie(LAST_TOUCH_STORAGE_KEY, LAST_TOUCH_COOKIE);
605
+ return;
606
+ }
607
+ const lastTouch = {};
608
+ for (const field of LAST_TOUCH_SOURCE_FIELDS) {
609
+ const value = source[field];
610
+ if (value)
611
+ lastTouch[field] = value;
612
+ }
613
+ // As for first touch, `landing_path` is where the visitor entered this app
614
+ // and `site_landing_path` the marketing-site page the touch landed on.
615
+ if (current.landing_path)
616
+ lastTouch.landing_path = current.landing_path;
617
+ if (source.site_landing_path) {
618
+ lastTouch.site_landing_path = source.site_landing_path;
619
+ }
620
+ lastTouch.touched_at = forwardedAt ?? current.landed_at;
621
+ storeAttribution(LAST_TOUCH_STORAGE_KEY, LAST_TOUCH_COOKIE, lastTouch);
622
+ }
623
+ function readForwardedLastTouchAt() {
624
+ const raw = new URLSearchParams(window.location.search).get(FORWARDED_LAST_TOUCH_AT_PARAM);
625
+ const time = Date.parse(raw ?? "");
626
+ // A visit can't be in the future; a bad clock or value means "now".
627
+ if (!Number.isFinite(time) || time > Date.now())
628
+ return undefined;
629
+ return new Date(time).toISOString();
630
+ }
631
+ function readForwardedLastTouch() {
632
+ const params = new URLSearchParams(window.location.search);
633
+ const forwarded = {};
634
+ for (const [param, field] of Object.entries(FORWARDED_LAST_TOUCH_FIELDS)) {
635
+ const value = truncateFirstTouchField(params.get(param));
636
+ if (value)
637
+ forwarded[field] = value;
638
+ }
639
+ return hasAttributionSource(forwarded) ? forwarded : null;
640
+ }
641
+ function readStoredAttribution(storageKey) {
505
642
  if (typeof window === "undefined")
506
643
  return null;
507
644
  try {
508
- const raw = safeStorageGet(FIRST_TOUCH_STORAGE_KEY);
645
+ const raw = safeStorageGet(storageKey);
509
646
  if (!raw)
510
647
  return null;
511
648
  const parsed = JSON.parse(raw);
@@ -518,10 +655,16 @@ export function getFirstTouchAttribution() {
518
655
  return null;
519
656
  }
520
657
  }
658
+ export function getFirstTouchAttribution() {
659
+ return readStoredAttribution(FIRST_TOUCH_STORAGE_KEY);
660
+ }
661
+ export function getLastTouchAttribution() {
662
+ return readStoredAttribution(LAST_TOUCH_STORAGE_KEY);
663
+ }
521
664
  /**
522
- * Store the visitor's first touch now instead of in `configureTracking()`,
523
- * for a page that reads it before tracking starts. It runs once per page
524
- * load, so tracking's own capture is then a no-op.
665
+ * Store the visitor's first and last touch now instead of in
666
+ * `configureTracking()`, for a page that reads them before tracking starts.
667
+ * It runs once per page load, so tracking's own capture is then a no-op.
525
668
  */
526
669
  export function captureAttribution() {
527
670
  if (isSyntheticBrowserTraffic())
@@ -1,6 +1,6 @@
1
1
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
- error: string;
3
2
  ok?: undefined;
3
+ error: string;
4
4
  } | {
5
5
  error?: undefined;
6
6
  ok: boolean;
@@ -140,13 +140,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
140
140
  clientHint?: undefined;
141
141
  timestamp?: undefined;
142
142
  chapterId?: undefined;
143
+ count?: undefined;
143
144
  startAt?: undefined;
144
145
  endAt?: undefined;
145
146
  reason?: undefined;
146
147
  includeMicrophone?: undefined;
147
148
  includeSystemAudio?: undefined;
148
149
  requestId?: undefined;
149
- count?: undefined;
150
150
  };
151
151
  required?: undefined;
152
152
  };
@@ -171,13 +171,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
171
171
  clientHint?: undefined;
172
172
  timestamp?: undefined;
173
173
  chapterId?: undefined;
174
+ count?: undefined;
174
175
  startAt?: undefined;
175
176
  endAt?: undefined;
176
177
  reason?: undefined;
177
178
  includeMicrophone?: undefined;
178
179
  includeSystemAudio?: undefined;
179
180
  requestId?: undefined;
180
- count?: undefined;
181
181
  };
182
182
  required?: undefined;
183
183
  };
@@ -196,13 +196,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
196
196
  clientHint?: undefined;
197
197
  timestamp?: undefined;
198
198
  chapterId?: undefined;
199
+ count?: undefined;
199
200
  startAt?: undefined;
200
201
  endAt?: undefined;
201
202
  reason?: undefined;
202
203
  includeMicrophone?: undefined;
203
204
  includeSystemAudio?: undefined;
204
205
  requestId?: undefined;
205
- count?: undefined;
206
206
  };
207
207
  required?: undefined;
208
208
  };
@@ -231,13 +231,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
231
231
  };
232
232
  timestamp?: undefined;
233
233
  chapterId?: undefined;
234
+ count?: undefined;
234
235
  startAt?: undefined;
235
236
  endAt?: undefined;
236
237
  reason?: undefined;
237
238
  includeMicrophone?: undefined;
238
239
  includeSystemAudio?: undefined;
239
240
  requestId?: undefined;
240
- count?: undefined;
241
241
  };
242
242
  };
243
243
  } | {
@@ -260,12 +260,12 @@ export declare function screenMemoryMcpToolDefinitions(): ({
260
260
  description: string;
261
261
  };
262
262
  chapterId?: undefined;
263
+ count?: undefined;
263
264
  startAt?: undefined;
264
265
  endAt?: undefined;
265
266
  includeMicrophone?: undefined;
266
267
  includeSystemAudio?: undefined;
267
268
  requestId?: undefined;
268
- count?: undefined;
269
269
  };
270
270
  };
271
271
  } | {
@@ -317,6 +317,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
317
317
  clientHint?: undefined;
318
318
  timestamp?: undefined;
319
319
  chapterId?: undefined;
320
+ count?: undefined;
320
321
  startAt: {
321
322
  type: string;
322
323
  description: string;
@@ -336,7 +337,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
336
337
  type: string;
337
338
  };
338
339
  requestId?: undefined;
339
- count?: undefined;
340
340
  };
341
341
  };
342
342
  } | {
@@ -352,6 +352,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
352
352
  clientHint?: undefined;
353
353
  timestamp?: undefined;
354
354
  chapterId?: undefined;
355
+ count?: undefined;
355
356
  startAt?: undefined;
356
357
  endAt?: undefined;
357
358
  reason?: undefined;
@@ -360,7 +361,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
360
361
  requestId: {
361
362
  type: string;
362
363
  };
363
- count?: undefined;
364
364
  };
365
365
  };
366
366
  })[];
@@ -1,13 +1,13 @@
1
1
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
2
2
  count: number;
3
3
  updated?: undefined;
4
- ok?: undefined;
5
4
  error?: undefined;
5
+ ok?: undefined;
6
6
  } | {
7
7
  count?: undefined;
8
8
  updated: number;
9
- ok?: undefined;
10
9
  error?: undefined;
10
+ ok?: undefined;
11
11
  } | {
12
12
  count?: undefined;
13
13
  updated?: undefined;
@@ -16,6 +16,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  } | {
17
17
  count?: undefined;
18
18
  updated?: undefined;
19
- ok: boolean;
20
19
  error?: undefined;
20
+ ok: boolean;
21
21
  }>>;
@@ -27,38 +27,38 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
27
27
  thumbsUpRate: number;
28
28
  avgEvalScore: number;
29
29
  } | {
30
+ ok?: undefined;
30
31
  error: string;
31
32
  message: string;
32
33
  summary?: undefined;
33
34
  spans?: undefined;
34
35
  id?: undefined;
35
- ok?: undefined;
36
36
  } | {
37
+ error?: undefined;
38
+ ok?: undefined;
37
39
  message?: undefined;
38
40
  summary: import("./types.js").TraceSummary;
39
41
  spans: import("./types.js").TraceSpan[];
40
42
  id?: undefined;
43
+ } | {
41
44
  error?: undefined;
42
45
  ok?: undefined;
43
- } | {
44
46
  message?: undefined;
45
47
  summary?: undefined;
46
48
  spans?: undefined;
47
49
  id: string;
48
- error?: undefined;
49
- ok?: undefined;
50
50
  } | {
51
+ ok?: undefined;
51
52
  message?: undefined;
52
53
  summary?: undefined;
53
54
  spans?: undefined;
54
55
  id?: undefined;
55
56
  error: any;
56
- ok?: undefined;
57
57
  } | {
58
+ error?: undefined;
58
59
  message?: undefined;
59
60
  summary?: undefined;
60
61
  spans?: undefined;
61
62
  id?: undefined;
62
- error?: undefined;
63
63
  ok: boolean;
64
64
  }>>;
@@ -39,8 +39,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
39
39
  error: string;
40
40
  }>;
41
41
  export declare function handleDeleteResource(event: any): Promise<{
42
- error: string;
43
42
  ok?: undefined;
43
+ error: string;
44
44
  } | {
45
45
  error?: undefined;
46
46
  ok: boolean;
@@ -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
  }>>;
@@ -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;
@@ -16,9 +16,9 @@ export declare function createAgentEngineDisconnectHandler(): import("h3").Event
16
16
  error: any;
17
17
  scope?: undefined;
18
18
  } | {
19
+ error?: undefined;
19
20
  ok: boolean;
20
21
  scope: "org" | "user";
21
- error?: undefined;
22
22
  }>>;
23
23
  export interface DefaultModelSelectionRequest {
24
24
  engine: string;
@@ -1,9 +1,9 @@
1
1
  export declare function createAgentEngineOllamaModelsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
2
  error: string;
3
- models?: undefined;
4
3
  ok?: undefined;
4
+ models?: 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
  }
@@ -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";
@@ -4948,15 +4948,9 @@ async function mountBetterAuthRoutes(app, options) {
4948
4948
  callbackPath = withDesktopMagicLinkFlow(callbackPath, desktopFlow);
4949
4949
  }
4950
4950
  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())
4951
+ const signupContext = signupAttributionContextFromCookieHeader(getHeader(event, "cookie") ?? null);
4952
+ const attributionToken = signupContext
4953
+ ? encodeMagicLinkSignupAttribution(signupContext, getAuthSecret())
4960
4954
  : undefined;
4961
4955
  const newUserCallbackUrl = new URL(`${getAppBasePath()}${publicFrameworkPath("/_agent-native/auth/magic-link/new-user")}?return=${encodeURIComponent(callbackPath)}`, getOrigin(event));
4962
4956
  if (attributionToken) {
@@ -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)) {
@@ -31,6 +31,13 @@ export declare function signShortLivedToken(claims: ShortLivedTokenClaims): stri
31
31
  * not surface the reason to viewers (it's useful for server-side logs only).
32
32
  */
33
33
  export declare function verifyShortLivedToken(token: string, expectedResourceId: string): VerifyResult;
34
+ export declare function signCompactShortLivedToken(claims: ShortLivedTokenClaims): string;
35
+ /**
36
+ * Verify a token produced by {@link signCompactShortLivedToken}. A token for a
37
+ * different resource, and any legacy token, fails as `bad_signature` — the id
38
+ * is not in the payload, so a mismatch is indistinguishable from tampering.
39
+ */
40
+ export declare function verifyCompactShortLivedToken(token: string, expectedResourceId: string): VerifyResult;
34
41
  /** Payload `typ` discriminator for realtime subscribe tokens. */
35
42
  export declare const REALTIME_SUBSCRIBE_TOKEN_TYPE = "rt-subscribe";
36
43
  export interface RealtimeSubscribeClaims {
@@ -94,6 +94,53 @@ export function verifyShortLivedToken(token, expectedResourceId) {
94
94
  agentLabel: claims.agentLabel,
95
95
  };
96
96
  }
97
+ function compactSignature(resourceId, payloadStr) {
98
+ return hmacB64(`agent-access\n${resourceId}\n${payloadStr}`, getSigningKey());
99
+ }
100
+ export function signCompactShortLivedToken(claims) {
101
+ const ttl = claims.ttlSeconds ?? DEFAULT_TTL_SECONDS;
102
+ const payload = {
103
+ e: Math.floor(Date.now() / 1000) + ttl,
104
+ };
105
+ if (claims.viewerEmail)
106
+ payload.v = claims.viewerEmail;
107
+ if (claims.agentLabel)
108
+ payload.l = claims.agentLabel;
109
+ const payloadStr = base64UrlEncode(JSON.stringify(payload));
110
+ return `${payloadStr}.${compactSignature(claims.resourceId, payloadStr)}`;
111
+ }
112
+ /**
113
+ * Verify a token produced by {@link signCompactShortLivedToken}. A token for a
114
+ * different resource, and any legacy token, fails as `bad_signature` — the id
115
+ * is not in the payload, so a mismatch is indistinguishable from tampering.
116
+ */
117
+ export function verifyCompactShortLivedToken(token, expectedResourceId) {
118
+ if (typeof token !== "string" || !token.includes(".")) {
119
+ return { ok: false, reason: "malformed" };
120
+ }
121
+ const [payloadStr, sig] = token.split(".", 2);
122
+ if (!payloadStr || !sig)
123
+ return { ok: false, reason: "malformed" };
124
+ if (!timingSafeEqualB64(sig, compactSignature(expectedResourceId, payloadStr))) {
125
+ return { ok: false, reason: "bad_signature" };
126
+ }
127
+ let claims;
128
+ try {
129
+ claims = JSON.parse(base64UrlDecode(payloadStr).toString("utf8"));
130
+ }
131
+ catch {
132
+ return { ok: false, reason: "bad_payload" };
133
+ }
134
+ if (typeof claims.e !== "number")
135
+ return { ok: false, reason: "bad_payload" };
136
+ if ((claims.v !== undefined && typeof claims.v !== "string") ||
137
+ (claims.l !== undefined && typeof claims.l !== "string")) {
138
+ return { ok: false, reason: "bad_payload" };
139
+ }
140
+ if (claims.e * 1000 < Date.now())
141
+ return { ok: false, reason: "expired" };
142
+ return { ok: true, viewerEmail: claims.v, agentLabel: claims.l };
143
+ }
97
144
  // ── Realtime subscribe tokens ────────────────────────────────────────────────
98
145
  //
99
146
  // An identity-bearing extension of the same HMAC discipline, used by the hosted
@@ -1,4 +1,5 @@
1
1
  export declare const ANALYTICS_ANONYMOUS_ID_COOKIE_NAME = "an_aid";
2
2
  export declare const ANALYTICS_ANONYMOUS_ID_COOKIE_MAX_AGE_SECONDS: number;
3
+ export declare const ANALYTICS_ANONYMOUS_ID_MAX_LENGTH = 128;
3
4
  export declare function normalizeAnalyticsAnonymousId(value: unknown): string | undefined;
4
5
  export declare function serializeAnalyticsAnonymousIdCookie(value: unknown): string | undefined;
@@ -1,6 +1,6 @@
1
1
  export const ANALYTICS_ANONYMOUS_ID_COOKIE_NAME = "an_aid";
2
2
  export const ANALYTICS_ANONYMOUS_ID_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
3
- const ANALYTICS_ANONYMOUS_ID_MAX_LENGTH = 128;
3
+ export const ANALYTICS_ANONYMOUS_ID_MAX_LENGTH = 128;
4
4
  const ANALYTICS_ANONYMOUS_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
5
5
  export function normalizeAnalyticsAnonymousId(value) {
6
6
  if (typeof value !== "string")
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Whether a visit says where the person came from. The browser uses this to
3
+ * decide which visits to keep as first and last touch, and the server uses the
4
+ * same share-link rules to derive `referral_source`, so the two can't drift.
5
+ */
6
+ export interface AttributionTouch {
7
+ ref?: string;
8
+ via?: string;
9
+ utm_source?: string;
10
+ utm_medium?: string;
11
+ utm_campaign?: string;
12
+ utm_content?: string;
13
+ utm_term?: string;
14
+ gclid?: string;
15
+ msclkid?: string;
16
+ vector_source?: string;
17
+ landing_path?: string;
18
+ landing_referrer?: string;
19
+ site_referrer?: string;
20
+ }
21
+ export declare function shareLandingSource(path: string | undefined): "clip_share" | "plan_share" | undefined;
22
+ export declare function isFirstPartyHost(host: string | undefined): boolean;
23
+ /**
24
+ * Whether a referrer host says where someone came from. Moving between our own
25
+ * apps and the marketing site, a local dev server, or returning from Google
26
+ * sign-in is navigation, not a source. The browser and the server both use
27
+ * this, so capture and `referral_source` agree.
28
+ */
29
+ export declare function isSourceReferrerHost(host: string | undefined): boolean;
30
+ export declare function hasAttributionSource(touch: AttributionTouch | null | undefined): boolean;
@@ -0,0 +1,65 @@
1
+ const TAG_FIELDS = [
2
+ "ref",
3
+ "via",
4
+ "utm_source",
5
+ "utm_medium",
6
+ "utm_campaign",
7
+ "utm_content",
8
+ "utm_term",
9
+ "gclid",
10
+ "msclkid",
11
+ "vector_source",
12
+ ];
13
+ const PLAN_SHARE_PATH_SEGMENTS = [
14
+ "/p/",
15
+ "/plan/",
16
+ "/plans/",
17
+ "/recaps/",
18
+ "/share-plan/",
19
+ ];
20
+ export function shareLandingSource(path) {
21
+ if (!path)
22
+ return undefined;
23
+ if (path.startsWith("/share/"))
24
+ return "clip_share";
25
+ if (PLAN_SHARE_PATH_SEGMENTS.some((segment) => path.includes(segment))) {
26
+ return "plan_share";
27
+ }
28
+ return undefined;
29
+ }
30
+ export function isFirstPartyHost(host) {
31
+ const normalized = host?.trim().toLowerCase().replace(/\.$/, "") ?? "";
32
+ return (normalized === "agent-native.com" ||
33
+ normalized.endsWith(".agent-native.com"));
34
+ }
35
+ // Coming back from Google sign-in sends this referrer. It's a step in signing
36
+ // in, never where someone heard about us.
37
+ const SIGN_IN_HOSTS = new Set(["accounts.google.com"]);
38
+ // A developer's own machine, with or without a port.
39
+ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "0.0.0.0"]);
40
+ /**
41
+ * Whether a referrer host says where someone came from. Moving between our own
42
+ * apps and the marketing site, a local dev server, or returning from Google
43
+ * sign-in is navigation, not a source. The browser and the server both use
44
+ * this, so capture and `referral_source` agree.
45
+ */
46
+ export function isSourceReferrerHost(host) {
47
+ const normalized = host?.trim().toLowerCase().replace(/\.$/, "") ?? "";
48
+ if (!normalized)
49
+ return false;
50
+ const hostname = normalized.startsWith("[")
51
+ ? normalized.slice(0, normalized.indexOf("]") + 1)
52
+ : normalized.split(":")[0];
53
+ return (!isFirstPartyHost(hostname) &&
54
+ !LOCAL_HOSTS.has(hostname) &&
55
+ !SIGN_IN_HOSTS.has(hostname));
56
+ }
57
+ export function hasAttributionSource(touch) {
58
+ if (!touch)
59
+ return false;
60
+ if (TAG_FIELDS.some((field) => !!touch[field]?.trim()))
61
+ return true;
62
+ if (shareLandingSource(touch.landing_path))
63
+ return true;
64
+ return [touch.landing_referrer, touch.site_referrer].some(isSourceReferrerHost);
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.201.1",
3
+ "version": "0.201.2-nightly-20261006010750",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -671,7 +671,7 @@
671
671
  "yaml": "^2.9.0",
672
672
  "yjs": "^13.6.33",
673
673
  "zod": "^4.3.6",
674
- "@agent-native/agentkit": "^0.201.1",
674
+ "@agent-native/agentkit": "^0.201.2-nightly-20261006010750",
675
675
  "@agent-native/recap-cli": "^0.5.62"
676
676
  },
677
677
  "devDependencies": {
@@ -742,7 +742,7 @@
742
742
  "xlsx": "^0.18.5"
743
743
  },
744
744
  "peerDependencies": {
745
- "@agent-native/core-corpus": ">=0.196.0",
745
+ "@agent-native/core-corpus": "0.201.2-nightly-20261006010750",
746
746
  "@ai-sdk/anthropic": ">=3",
747
747
  "@ai-sdk/cohere": ">=3",
748
748
  "@ai-sdk/google": ">=3",