@agent-native/core 0.214.0 → 0.216.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/action.js +54 -1
  2. package/dist/agent/actions/disconnect-chatgpt-subscription.js +5 -0
  3. package/dist/agent/actions/get-agentkit-capabilities.js +3 -3
  4. package/dist/agent/actions/manage-builder-connection.js +5 -0
  5. package/dist/agent/actions/manage-provider-key-policy.js +5 -0
  6. package/dist/agent/actions/manage-provider-models.js +5 -0
  7. package/dist/agent/actions/manage-service-providers.js +5 -0
  8. package/dist/agent/actions/select-chatgpt-subscription-account.js +5 -0
  9. package/dist/agent/context-xray/actions/context-evict.js +5 -0
  10. package/dist/agent/context-xray/actions/context-pin.js +5 -0
  11. package/dist/agent/context-xray/actions/context-report.js +5 -0
  12. package/dist/agent/context-xray/actions/context-restore.js +5 -0
  13. package/dist/agent/execution-fence.d.ts +1 -0
  14. package/dist/agent/execution-fence.js +5 -0
  15. package/dist/agent/production-agent.d.ts +35 -2
  16. package/dist/agent/production-agent.js +629 -32
  17. package/dist/agent/run-manager.d.ts +2 -0
  18. package/dist/agent/run-manager.js +397 -115
  19. package/dist/agent/run-slot-lock.d.ts +2 -0
  20. package/dist/agent/run-slot-lock.js +6 -0
  21. package/dist/agent/run-store.d.ts +5 -0
  22. package/dist/agent/run-store.js +423 -156
  23. package/dist/agent/thread-data-builder.js +27 -5
  24. package/dist/agent/types.d.ts +6 -12
  25. package/dist/agentkit/capabilities.d.ts +1 -1
  26. package/dist/application-state/store.d.ts +5 -0
  27. package/dist/application-state/store.js +26 -0
  28. package/dist/chat-threads/schema-migrations.js +17 -0
  29. package/dist/chat-threads/store.d.ts +21 -1
  30. package/dist/chat-threads/store.js +223 -41
  31. package/dist/cli/skills-content/turn-into-app-skill.d.ts +1 -1
  32. package/dist/cli/skills-content/turn-into-app-skill.js +20 -18
  33. package/dist/cli/templates-meta.js +14 -0
  34. package/dist/client/analytics.js +167 -17
  35. package/dist/client/chat/agentkit-agent-native.js +79 -3
  36. package/dist/client/chat/agentkit-protocol.js +80 -42
  37. package/dist/client/chat/runtime.d.ts +5 -1
  38. package/dist/client/chat/runtime.js +46 -3
  39. package/dist/client/consent.d.ts +36 -0
  40. package/dist/client/consent.js +283 -0
  41. package/dist/client/review/use-review.d.ts +2 -0
  42. package/dist/client/review/use-review.js +28 -1
  43. package/dist/client/sharing/useResourceAccessGate.d.ts +3 -2
  44. package/dist/client/sharing/useResourceAccessGate.js +9 -1
  45. package/dist/client/use-chat-thread-list.d.ts +2 -0
  46. package/dist/client/use-chat-thread-list.js +12 -2
  47. package/dist/client/use-chat-threads.d.ts +1 -0
  48. package/dist/client/use-chat-threads.js +2 -0
  49. package/dist/collab/awareness.d.ts +2 -2
  50. package/dist/collab/routes.d.ts +2 -2
  51. package/dist/collab/struct-routes.d.ts +1 -1
  52. package/dist/connections/catalog.d.ts +5 -5
  53. package/dist/connections/catalog.js +22 -3
  54. package/dist/data-widgets/index.d.ts +36 -2
  55. package/dist/data-widgets/index.js +94 -6
  56. package/dist/db/migrations.js +4 -1
  57. package/dist/db/schema-snapshot.generated.js +8 -1
  58. package/dist/feature-flags/actions/set-feature-flag.js +5 -0
  59. package/dist/feature-flags/index.d.ts +1 -1
  60. package/dist/feature-flags/index.js +1 -1
  61. package/dist/feature-flags/store.d.ts +12 -0
  62. package/dist/feature-flags/store.js +79 -7
  63. package/dist/file-upload/actions/manage-file-storage.js +5 -0
  64. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  65. package/dist/file-upload/actions/upload-image.js +5 -0
  66. package/dist/history/actions/create-resource-version.js +5 -0
  67. package/dist/history/actions/restore-resource-version.js +5 -0
  68. package/dist/ingestion/index.d.ts +1 -1
  69. package/dist/ingestion/index.js +1 -1
  70. package/dist/ingestion/pptx.d.ts +11 -0
  71. package/dist/ingestion/pptx.js +81 -1
  72. package/dist/integrations/actions/manage-messaging-channel.js +5 -0
  73. package/dist/jobs/scheduler.js +17 -1
  74. package/dist/labs/actions/set-lab.js +5 -0
  75. package/dist/localization/actions/set-localization-preference.js +5 -0
  76. package/dist/observability/actions/save-observability-instruction-update.js +5 -0
  77. package/dist/observability/actions/save-observability-review-summary.js +5 -0
  78. package/dist/progress/routes.d.ts +1 -1
  79. package/dist/provider-api/actions/custom-provider-registration.d.ts +18 -18
  80. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  81. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  82. package/dist/provider-api/index.js +9 -2
  83. package/dist/review/actions/consume-review-feedback.js +5 -0
  84. package/dist/review/actions/create-review-comment.js +5 -0
  85. package/dist/review/actions/delete-review-comment.js +5 -0
  86. package/dist/review/actions/react-to-review-comment.js +5 -0
  87. package/dist/review/actions/reply-review-comment.js +5 -0
  88. package/dist/review/actions/resolve-review-thread.js +5 -0
  89. package/dist/review/actions/send-review-thread-to-agent.js +5 -0
  90. package/dist/review/actions/set-review-status.js +5 -0
  91. package/dist/review/actions/set-review-thread-muted.js +5 -0
  92. package/dist/review/actions/set-review-thread-unread.js +5 -0
  93. package/dist/review/actions/set-review-threads-unread.js +5 -0
  94. package/dist/review/actions/update-review-comment-anchor.js +5 -0
  95. package/dist/review/actions/update-review-comment.js +5 -0
  96. package/dist/review/suggestions/actions.js +60 -21
  97. package/dist/review/suggestions/store.d.ts +2 -0
  98. package/dist/review/suggestions/store.js +28 -0
  99. package/dist/secrets/actions/delete-api-key.js +5 -0
  100. package/dist/secrets/routes.d.ts +6 -6
  101. package/dist/server/agent-chat/context-tools.js +63 -2
  102. package/dist/server/agent-chat-plugin.d.ts +2 -0
  103. package/dist/server/agent-chat-plugin.js +242 -54
  104. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  105. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  106. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  107. package/dist/server/application-state-maintenance.d.ts +2 -0
  108. package/dist/server/application-state-maintenance.js +2 -0
  109. package/dist/server/attribution.d.ts +14 -0
  110. package/dist/server/attribution.js +62 -2
  111. package/dist/server/auth.js +7 -0
  112. package/dist/server/consent-route.d.ts +41 -0
  113. package/dist/server/consent-route.js +102 -0
  114. package/dist/server/first-touch-route.d.ts +39 -0
  115. package/dist/server/first-touch-route.js +115 -0
  116. package/dist/server/index.d.ts +1 -0
  117. package/dist/server/index.js +1 -0
  118. package/dist/server/queued-submission-inbox-drain.d.ts +9 -0
  119. package/dist/server/queued-submission-inbox-drain.js +12 -0
  120. package/dist/server/request-origin.d.ts +6 -0
  121. package/dist/server/request-origin.js +12 -0
  122. package/dist/settings/org-settings.d.ts +2 -1
  123. package/dist/settings/org-settings.js +4 -1
  124. package/dist/settings/store.d.ts +8 -0
  125. package/dist/settings/store.js +14 -5
  126. package/dist/shared/consent.d.ts +31 -0
  127. package/dist/shared/consent.js +145 -0
  128. package/dist/shared/first-touch-persistence.d.ts +31 -0
  129. package/dist/shared/first-touch-persistence.js +53 -0
  130. package/dist/sharing/actions/set-resource-visibility.js +5 -0
  131. package/dist/sharing/actions/share-resource.js +5 -0
  132. package/dist/sharing/actions/unshare-resource.js +5 -0
  133. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -1
  134. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -1
  135. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +36 -80
  136. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +54 -16
  137. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +50 -73
  138. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -1
  139. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -1
  140. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +20 -18
  141. package/dist/triggers/actions/manage-automation.d.ts +1 -1
  142. package/dist/usage/actions/get-usage-metrics.js +27 -4
  143. package/dist/usage/actions/manage-usage-alert.js +5 -0
  144. package/dist/vite/client.js +8 -6
  145. package/dist/workspace-connections/actions/bulk-update-workspace-user-groups.js +5 -0
  146. package/dist/workspace-connections/actions/delete-workspace-user-group.js +5 -0
  147. package/dist/workspace-connections/actions/upsert-workspace-user-group.js +5 -0
  148. package/docs/content/actions-access-control.mdx +8 -8
  149. package/docs/content/actions-defining.mdx +9 -8
  150. package/docs/content/template-design-developers.mdx +9 -9
  151. package/docs/content/template-design-features.mdx +1 -3
  152. package/docs/content/turn-into-app.mdx +4 -0
  153. package/package.json +4 -3
  154. package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -1
  155. package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -1
  156. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +36 -80
  157. package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +54 -16
  158. package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +50 -73
  159. package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -1
  160. package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -1
  161. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +20 -18
@@ -13,6 +13,10 @@
13
13
  * acquisition; last touch answers "what brought them to sign up?" and credits
14
14
  * the post, video, or person that converted them.
15
15
  *
16
+ * First touch also has a durable copy, `an_fts`, that our server sets for a
17
+ * year (see `first-touch-route.ts`). Signup reads both first-touch cookies and
18
+ * keeps the one `preferFirstTouch` picks.
19
+ *
16
20
  * This module is intentionally pure and dependency-free so it is trivially
17
21
  * unit-testable and can never throw into the signup path. The single hard rule:
18
22
  * parsing untrusted cookie input must NEVER throw — every accessor is defensive.
@@ -80,11 +84,21 @@ export declare const LAST_TOUCH_COOKIE_NAME = "an_lt";
80
84
  export declare function parseCookieHeader(cookieHeader: string | null | undefined): Record<string, string>;
81
85
  export declare function decodeFirstTouchValue(value: string | null | undefined): FirstTouchAttribution | null;
82
86
  export declare function decodeLastTouchValue(value: string | null | undefined): LastTouchAttribution | null;
87
+ /**
88
+ * Decode the year-long `an_fts` cookie. A value from another format version
89
+ * is unreadable here, so it holds no touch.
90
+ */
91
+ export declare function decodeFirstTouchServerValue(value: string | null | undefined): FirstTouchAttribution | null;
83
92
  /**
84
93
  * Read the `an_ft` first-touch attribution out of a raw `Cookie:` header.
85
94
  * Returns `null` when the cookie is absent or unparseable. Never throws.
86
95
  */
87
96
  export declare function readFirstTouchAttribution(cookieHeader: string | null | undefined): FirstTouchAttribution | null;
97
+ /**
98
+ * The `an_fts` cookie value for a first touch, dropping the lowest-priority
99
+ * fields until `name=value` fits its budget.
100
+ */
101
+ export declare function encodeFirstTouchServerCookieValue(touch: FirstTouchAttribution): string;
88
102
  /**
89
103
  * Read the `an_lt` last-touch attribution out of a raw `Cookie:` header.
90
104
  * Returns `null` when the cookie is absent or unparseable. Never throws.
@@ -1,6 +1,7 @@
1
1
  import { ANALYTICS_ANONYMOUS_ID_COOKIE_NAME, ANALYTICS_ANONYMOUS_ID_MAX_LENGTH, normalizeAnalyticsAnonymousId, } from "../shared/analytics-anonymous-id.js";
2
2
  import { ANALYTICS_SESSION_ID_COOKIE_NAME, normalizeAnalyticsSessionId, } from "../shared/analytics-session-id.js";
3
3
  import { isSourceReferrerHost, shareLandingSource, } from "../shared/attribution-source.js";
4
+ import { FIRST_TOUCH_SERVER_COOKIE_MAX_BYTES, FIRST_TOUCH_SERVER_COOKIE_NAME, FIRST_TOUCH_SERVER_COOKIE_VERSION, preferFirstTouch, } from "../shared/first-touch-persistence.js";
4
5
  export const SIGNUP_ATTRIBUTION_HEADER_NAME = "x-agent-native-signup-attribution";
5
6
  const SIGNUP_ATTRIBUTION_HEADER_MAX_LENGTH = 4096;
6
7
  export const FIRST_TOUCH_COOKIE_NAME = "an_ft";
@@ -70,7 +71,14 @@ export function decodeFirstTouchValue(value) {
70
71
  export function decodeLastTouchValue(value) {
71
72
  return decodeAttributionCookieValue(value, LAST_TOUCH_STRING_FIELDS);
72
73
  }
73
- function decodeAttributionCookieValue(value, fields) {
74
+ /**
75
+ * Decode the year-long `an_fts` cookie. A value from another format version
76
+ * is unreadable here, so it holds no touch.
77
+ */
78
+ export function decodeFirstTouchServerValue(value) {
79
+ return decodeAttributionCookieValue(value, STRING_FIELDS, FIRST_TOUCH_SERVER_COOKIE_VERSION);
80
+ }
81
+ function decodeAttributionCookieValue(value, fields, version) {
74
82
  if (!value || typeof value !== "string")
75
83
  return null;
76
84
  let decoded = value;
@@ -91,6 +99,8 @@ function decodeAttributionCookieValue(value, fields) {
91
99
  return null;
92
100
  }
93
101
  const source = parsed;
102
+ if (version !== undefined && source.v !== version)
103
+ return null;
94
104
  const result = {};
95
105
  let any = false;
96
106
  for (const field of fields) {
@@ -109,12 +119,62 @@ function decodeAttributionCookieValue(value, fields) {
109
119
  export function readFirstTouchAttribution(cookieHeader) {
110
120
  try {
111
121
  const cookies = parseCookieHeader(cookieHeader);
112
- return decodeFirstTouchValue(cookies[FIRST_TOUCH_COOKIE_NAME]);
122
+ return preferFirstTouch(decodeFirstTouchServerValue(cookies[FIRST_TOUCH_SERVER_COOKIE_NAME]), decodeFirstTouchValue(cookies[FIRST_TOUCH_COOKIE_NAME]));
113
123
  }
114
124
  catch {
115
125
  return null;
116
126
  }
117
127
  }
128
+ // `landed_at` sets the cookie's expiry and identifies the visit, so it is
129
+ // never the field dropped to fit.
130
+ const FIRST_TOUCH_SERVER_FIELD_PRIORITY = [
131
+ "landed_at",
132
+ "gclid",
133
+ "msclkid",
134
+ "vector_source",
135
+ "utm_source",
136
+ "utm_medium",
137
+ "utm_campaign",
138
+ "ref",
139
+ "via",
140
+ "utm_content",
141
+ "utm_term",
142
+ "landing_path",
143
+ "landing_referrer",
144
+ "site_referrer",
145
+ "site_landing_path",
146
+ ];
147
+ /**
148
+ * The `an_fts` cookie value for a first touch, dropping the lowest-priority
149
+ * fields until `name=value` fits its budget.
150
+ */
151
+ export function encodeFirstTouchServerCookieValue(touch) {
152
+ const compact = {
153
+ v: FIRST_TOUCH_SERVER_COOKIE_VERSION,
154
+ };
155
+ let truncated = touch.capture_truncated === "1";
156
+ const encode = (value) => encodeURIComponent(JSON.stringify(value));
157
+ const fits = (value) => FIRST_TOUCH_SERVER_COOKIE_NAME.length + 1 + encode(value).length <=
158
+ FIRST_TOUCH_SERVER_COOKIE_MAX_BYTES;
159
+ // Room for the marker, so adding it later can't push the value over.
160
+ const withMarker = (value) => ({
161
+ ...value,
162
+ capture_truncated: "1",
163
+ });
164
+ for (const field of FIRST_TOUCH_SERVER_FIELD_PRIORITY) {
165
+ const value = touch[field];
166
+ if (!value)
167
+ continue;
168
+ const candidate = { ...compact, [field]: value };
169
+ if (fits(withMarker(candidate))) {
170
+ compact[field] = value;
171
+ }
172
+ else {
173
+ truncated = true;
174
+ }
175
+ }
176
+ return encode(truncated ? withMarker(compact) : compact);
177
+ }
118
178
  /**
119
179
  * Read the `an_lt` last-touch attribution out of a raw `Cookie:` header.
120
180
  * Returns `null` when the cookie is absent or unparseable. Never throws.
@@ -5,14 +5,18 @@ import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
5
5
  import { newOrgSelection, ORG_SELECTION_COOKIE, } from "../org/request-org-cache.js";
6
6
  import { isWorkspaceAppAccessUnavailable, workspaceAppAccessUnavailableBody, } from "../org/workspace-app-access-outcome.js";
7
7
  import { checkWorkspaceAppAccessForRequest } from "../org/workspace-app-access-request.js";
8
+ import { CONSENT_PATH, CONSENT_REGION_PATH } from "../shared/consent.js";
8
9
  import { EMBED_SESSION_COOKIE, EMBED_START_PATH, EMBED_TARGET_HEADER, isMcpDirectoryWidgetCapabilityScope, } from "../shared/embed-auth.js";
9
10
  import { FIRST_RUN_ONBOARDING_COOKIE, FIRST_RUN_ONBOARDING_MAX_AGE, } from "../shared/first-run-onboarding.js";
11
+ import { FIRST_TOUCH_SERVER_PATH } from "../shared/first-touch-persistence.js";
10
12
  import { isGoogleProfileImageUrl } from "../shared/google-profile-image.js";
11
13
  import { EMBED_TRANSPLANT_HEADER, isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
14
+ import { createConsentHandler, createConsentRegionHandler, } from "./consent-route.js";
12
15
  import { readDevActionDiscoveryFile } from "./dev-action-discovery.js";
13
16
  import { devLoopbackAuthHint } from "./dev-origin-hint.js";
14
17
  import { EMAIL_AUTH_LINK_LANDING_PATH, emailAuthLinkFields, emailAuthLinkLandingPage, emailAuthVerificationPath, emailAuthVerificationUrl, } from "./email-auth-links.js";
15
18
  import { isEmbedCapabilityScope, revokeEmbedSessionsForOwners, requestHasEmbedAuthMarker, resolveEmbedSessionCookieOwners, resolveEmbedSessionFromRequest, verifyEmbedSessionToken, } from "./embed-session.js";
19
+ import { createFirstTouchHandler } from "./first-touch-route.js";
16
20
  import { getPublicFrameworkPathname } from "./framework-request-context.js";
17
21
  import { FRAMEWORK_INTERNAL_ROUTE_PREFIX, canonicalFrameworkPathname, getFrameworkRoutePrefix, publicFrameworkPath, } from "./framework-route-prefix.js";
18
22
  import { isHttpsRequest } from "./https-request.js";
@@ -5547,6 +5551,9 @@ export async function autoMountAuth(app, options = {}) {
5547
5551
  const workspaceAppAudience = resolveWorkspaceAppAudience(options);
5548
5552
  const workspaceAppRouteAccess = resolveWorkspaceAppRouteAccess(options);
5549
5553
  mountAuthCorsMiddleware(app);
5554
+ app.use(FIRST_TOUCH_SERVER_PATH, createFirstTouchHandler());
5555
+ app.use(CONSENT_REGION_PATH, createConsentRegionHandler());
5556
+ app.use(CONSENT_PATH, createConsentHandler());
5550
5557
  if (options.getSession) {
5551
5558
  customGetSession = options.getSession;
5552
5559
  trustCustomEmailVerification =
@@ -0,0 +1,41 @@
1
+ import type { H3Event } from "h3";
2
+ import { type ConsentCategory } from "../shared/consent.js";
3
+ /**
4
+ * The visitor's country from the host's geo header, if it sent one. The
5
+ * headers a CDN always overwrites come first, so a visitor's own `x-country`
6
+ * can't outrank them.
7
+ */
8
+ export declare function requestCountry(event: H3Event): string | undefined;
9
+ /**
10
+ * A stored choice applies everywhere; without one, the country decides
11
+ * whether storing `category` cookies requires an opt-in.
12
+ */
13
+ export declare function requestAllowsConsentCategory(event: H3Event, category: ConsentCategory): boolean;
14
+ /**
15
+ * `GET /_agent-native/auth/consent-region`: `consent` when the visitor's
16
+ * country gets the banner, `none` when it doesn't, `unknown` when the host
17
+ * sent no country. The SSR shell is cached for everyone, so the browser asks
18
+ * here instead of reading it from the page.
19
+ */
20
+ export declare function createConsentRegionHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, {
21
+ error: string;
22
+ region?: undefined;
23
+ } | {
24
+ error?: undefined;
25
+ region: string;
26
+ }>;
27
+ /**
28
+ * `POST /_agent-native/auth/consent` with `{ choice: "1.p1.t0" }`: write the
29
+ * visitor's choice from the server. Safari keeps a cookie page JavaScript
30
+ * wrote for seven days at most, and builder.io asks again only after a year.
31
+ * Uses the same domain as the browser's copy, so it replaces that copy.
32
+ */
33
+ export declare function createConsentHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
34
+ error: string;
35
+ stored?: undefined;
36
+ choice?: undefined;
37
+ } | {
38
+ error?: undefined;
39
+ stored: boolean;
40
+ choice: any;
41
+ }>>;
@@ -0,0 +1,102 @@
1
+ import { appendResponseHeader, defineEventHandler, getHeader, getMethod, setResponseHeader, setResponseStatus, } from "h3";
2
+ import { CONSENT_COOKIE_LIFETIME_SECONDS, CONSENT_COOKIE_NAME, consentCookieDomain, isConsentRegionCountry, parseConsent, serializeConsent, } from "../shared/consent.js";
3
+ import { parseCookieHeader } from "./attribution.js";
4
+ import { readBodyWithSizeLimit } from "./h3-helpers.js";
5
+ import { getForwardedRequestOrigin, getForwardedRequestURL, isSameOriginPost, } from "./request-origin.js";
6
+ const MAX_CONSENT_BODY_BYTES = 256;
7
+ /**
8
+ * The visitor's country from the host's geo header, if it sent one. The
9
+ * headers a CDN always overwrites come first, so a visitor's own `x-country`
10
+ * can't outrank them.
11
+ */
12
+ export function requestCountry(event) {
13
+ return (netlifyGeoCountry(event) ??
14
+ getHeader(event, "cf-ipcountry") ??
15
+ getHeader(event, "x-vercel-ip-country") ??
16
+ getHeader(event, "x-country"));
17
+ }
18
+ function netlifyGeoCountry(event) {
19
+ const geo = getHeader(event, "x-nf-geo");
20
+ if (!geo)
21
+ return undefined;
22
+ try {
23
+ const parsed = JSON.parse(Buffer.from(geo, "base64").toString("utf8"));
24
+ const code = parsed?.country?.code;
25
+ return typeof code === "string" ? code : undefined;
26
+ }
27
+ catch {
28
+ // coercion-ok: an unreadable geo header is an unknown country; the next
29
+ // header, or the browser's time zone, decides.
30
+ return undefined;
31
+ }
32
+ }
33
+ /**
34
+ * A stored choice applies everywhere; without one, the country decides
35
+ * whether storing `category` cookies requires an opt-in.
36
+ */
37
+ export function requestAllowsConsentCategory(event, category) {
38
+ const consent = parseConsent(parseCookieHeader(getHeader(event, "cookie"))[CONSENT_COOKIE_NAME]);
39
+ if (consent)
40
+ return consent[category];
41
+ return !isConsentRegionCountry(requestCountry(event));
42
+ }
43
+ /**
44
+ * `GET /_agent-native/auth/consent-region`: `consent` when the visitor's
45
+ * country gets the banner, `none` when it doesn't, `unknown` when the host
46
+ * sent no country. The SSR shell is cached for everyone, so the browser asks
47
+ * here instead of reading it from the page.
48
+ */
49
+ export function createConsentRegionHandler() {
50
+ return defineEventHandler((event) => {
51
+ setResponseHeader(event, "cache-control", "no-store");
52
+ if (getMethod(event) !== "GET") {
53
+ setResponseStatus(event, 405);
54
+ return { error: "Method not allowed" };
55
+ }
56
+ const country = requestCountry(event);
57
+ if (!country)
58
+ return { region: "unknown" };
59
+ return { region: isConsentRegionCountry(country) ? "consent" : "none" };
60
+ });
61
+ }
62
+ /**
63
+ * `POST /_agent-native/auth/consent` with `{ choice: "1.p1.t0" }`: write the
64
+ * visitor's choice from the server. Safari keeps a cookie page JavaScript
65
+ * wrote for seven days at most, and builder.io asks again only after a year.
66
+ * Uses the same domain as the browser's copy, so it replaces that copy.
67
+ */
68
+ export function createConsentHandler() {
69
+ return defineEventHandler(async (event) => {
70
+ setResponseHeader(event, "cache-control", "no-store");
71
+ if (getMethod(event) !== "POST") {
72
+ setResponseStatus(event, 405);
73
+ return { error: "Method not allowed" };
74
+ }
75
+ if (!isSameOriginPost(event)) {
76
+ setResponseStatus(event, 403);
77
+ return { error: "Cross-origin request" };
78
+ }
79
+ let choice;
80
+ try {
81
+ const body = await readBodyWithSizeLimit(event, MAX_CONSENT_BODY_BYTES);
82
+ choice = parseConsent(body?.choice);
83
+ }
84
+ catch (error) {
85
+ if (error.statusCode === 413) {
86
+ return { error: "Body too large" };
87
+ }
88
+ setResponseStatus(event, 400);
89
+ return { error: "Invalid JSON" };
90
+ }
91
+ if (!choice) {
92
+ setResponseStatus(event, 400);
93
+ return { error: "Missing choice" };
94
+ }
95
+ const domain = consentCookieDomain(getForwardedRequestURL(event).hostname);
96
+ const secure = getForwardedRequestOrigin(event).startsWith("https:");
97
+ appendResponseHeader(event, "set-cookie", `${CONSENT_COOKIE_NAME}=${serializeConsent(choice)}; ` +
98
+ `Max-Age=${CONSENT_COOKIE_LIFETIME_SECONDS}; Path=/; SameSite=Lax` +
99
+ `${domain ? `; Domain=${domain}` : ""}${secure ? "; Secure" : ""}`);
100
+ return { stored: true, choice };
101
+ });
102
+ }
@@ -0,0 +1,39 @@
1
+ import { type FirstTouchAttribution } from "./attribution.js";
2
+ export type FirstTouchSyncResult = {
3
+ stored: true;
4
+ firstTouch: FirstTouchAttribution;
5
+ } | {
6
+ stored: false;
7
+ reason: "consent_required" | "expired";
8
+ };
9
+ /**
10
+ * `POST /_agent-native/auth/attribution` with `{ firstTouch }`: keep the
11
+ * browser's first touch in the year-long `an_fts` cookie. Anyone may call it,
12
+ * signed in or not, but only from this origin. The value already in the
13
+ * cookie wins unless `preferFirstTouch` picks the new one, so a slow request
14
+ * from another tab can't replace a better first touch it never saw. Where the
15
+ * banner applies and the visitor hasn't accepted targeting cookies, it stores
16
+ * nothing and clears every attribution cookie. Elsewhere it also writes the
17
+ * browser's `an_ft` and `an_lt` again, so Safari keeps them their 30 days.
18
+ */
19
+ export declare function createFirstTouchHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
20
+ reason?: undefined;
21
+ error: string;
22
+ stored?: undefined;
23
+ firstTouch?: undefined;
24
+ } | {
25
+ error?: undefined;
26
+ stored: false;
27
+ reason: "consent_required";
28
+ firstTouch?: undefined;
29
+ } | {
30
+ error?: undefined;
31
+ stored: false;
32
+ reason: "expired";
33
+ firstTouch?: undefined;
34
+ } | {
35
+ reason?: undefined;
36
+ error?: undefined;
37
+ stored: true;
38
+ firstTouch: FirstTouchAttribution;
39
+ }>>;
@@ -0,0 +1,115 @@
1
+ import { appendResponseHeader, defineEventHandler, getHeader, getMethod, setResponseHeader, setResponseStatus, } from "h3";
2
+ import { FIRST_TOUCH_SERVER_COOKIE_NAME, firstTouchServerExpiry, preferFirstTouch, } from "../shared/first-touch-persistence.js";
3
+ import { FIRST_TOUCH_COOKIE_NAME, LAST_TOUCH_COOKIE_NAME, decodeFirstTouchServerValue, decodeFirstTouchValue, decodeLastTouchValue, encodeFirstTouchServerCookieValue, parseCookieHeader, } from "./attribution.js";
4
+ import { requestAllowsConsentCategory } from "./consent-route.js";
5
+ import { readBodyWithSizeLimit } from "./h3-helpers.js";
6
+ import { getForwardedRequestOrigin, isSameOriginPost, } from "./request-origin.js";
7
+ const MAX_BODY_BYTES = 8 * 1024;
8
+ /** How long the browser's own `an_ft` and `an_lt` copies last. */
9
+ const BROWSER_COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
10
+ const BROWSER_COOKIES = [
11
+ { name: FIRST_TOUCH_COOKIE_NAME, decode: decodeFirstTouchValue },
12
+ { name: LAST_TOUCH_COOKIE_NAME, decode: decodeLastTouchValue },
13
+ ];
14
+ // What `encodeURIComponent` emits, so a value echoed into Set-Cookie can't
15
+ // carry an attribute or a header break.
16
+ const ENCODED_COOKIE_VALUE = /^[A-Za-z0-9\-_.!~*'()%]{1,1500}$/;
17
+ function cookieAttributes(event) {
18
+ const secure = getForwardedRequestOrigin(event).startsWith("https:");
19
+ return `Path=/; SameSite=Lax${secure ? "; Secure" : ""}`;
20
+ }
21
+ function clearAttributionCookies(event) {
22
+ for (const name of [
23
+ FIRST_TOUCH_SERVER_COOKIE_NAME,
24
+ FIRST_TOUCH_COOKIE_NAME,
25
+ LAST_TOUCH_COOKIE_NAME,
26
+ ]) {
27
+ appendResponseHeader(event, "set-cookie", `${name}=; Max-Age=0; ${cookieAttributes(event)}`);
28
+ }
29
+ }
30
+ /**
31
+ * Safari keeps a cookie page JavaScript wrote for seven days at most, so write
32
+ * the browser's own `an_ft` and `an_lt` again from here, for their full 30
33
+ * days. Only values that decode as touches are echoed back.
34
+ */
35
+ function resetBrowserCookies(event) {
36
+ const cookies = parseCookieHeader(getHeader(event, "cookie"));
37
+ for (const { name, decode } of BROWSER_COOKIES) {
38
+ const value = cookies[name];
39
+ if (!value || !ENCODED_COOKIE_VALUE.test(value) || !decode(value)) {
40
+ continue;
41
+ }
42
+ appendResponseHeader(event, "set-cookie", `${name}=${value}; Max-Age=${BROWSER_COOKIE_MAX_AGE_SECONDS}; ${cookieAttributes(event)}`);
43
+ }
44
+ }
45
+ function readCandidate(body) {
46
+ const firstTouch = body?.firstTouch;
47
+ if (!firstTouch || typeof firstTouch !== "object")
48
+ return null;
49
+ return decodeFirstTouchValue(encodeURIComponent(JSON.stringify(firstTouch)));
50
+ }
51
+ /** A touch more than a year old can't be kept, or win against one that can. */
52
+ function unexpired(touch) {
53
+ return touch && firstTouchServerExpiry(touch) ? touch : null;
54
+ }
55
+ /**
56
+ * `POST /_agent-native/auth/attribution` with `{ firstTouch }`: keep the
57
+ * browser's first touch in the year-long `an_fts` cookie. Anyone may call it,
58
+ * signed in or not, but only from this origin. The value already in the
59
+ * cookie wins unless `preferFirstTouch` picks the new one, so a slow request
60
+ * from another tab can't replace a better first touch it never saw. Where the
61
+ * banner applies and the visitor hasn't accepted targeting cookies, it stores
62
+ * nothing and clears every attribution cookie. Elsewhere it also writes the
63
+ * browser's `an_ft` and `an_lt` again, so Safari keeps them their 30 days.
64
+ */
65
+ export function createFirstTouchHandler() {
66
+ return defineEventHandler(async (event) => {
67
+ setResponseHeader(event, "cache-control", "no-store");
68
+ if (getMethod(event) !== "POST") {
69
+ setResponseStatus(event, 405);
70
+ return { error: "Method not allowed" };
71
+ }
72
+ if (!isSameOriginPost(event)) {
73
+ setResponseStatus(event, 403);
74
+ return { error: "Cross-origin request" };
75
+ }
76
+ let body;
77
+ try {
78
+ body = await readBodyWithSizeLimit(event, MAX_BODY_BYTES);
79
+ }
80
+ catch (error) {
81
+ if (error.statusCode === 413) {
82
+ return { error: "Body too large" };
83
+ }
84
+ setResponseStatus(event, 400);
85
+ return { error: "Invalid JSON" };
86
+ }
87
+ if (!requestAllowsConsentCategory(event, "targeting")) {
88
+ clearAttributionCookies(event);
89
+ return {
90
+ stored: false,
91
+ reason: "consent_required",
92
+ };
93
+ }
94
+ const candidate = readCandidate(body);
95
+ if (!candidate) {
96
+ setResponseStatus(event, 400);
97
+ return { error: "Missing firstTouch" };
98
+ }
99
+ resetBrowserCookies(event);
100
+ const existing = decodeFirstTouchServerValue(parseCookieHeader(getHeader(event, "cookie"))[FIRST_TOUCH_SERVER_COOKIE_NAME]);
101
+ const firstTouch = preferFirstTouch(unexpired(existing), unexpired(candidate));
102
+ const expires = firstTouch && firstTouchServerExpiry(firstTouch);
103
+ if (!firstTouch || !expires) {
104
+ return {
105
+ stored: false,
106
+ reason: "expired",
107
+ };
108
+ }
109
+ if (firstTouch !== existing) {
110
+ appendResponseHeader(event, "set-cookie", `${FIRST_TOUCH_SERVER_COOKIE_NAME}=${encodeFirstTouchServerCookieValue(firstTouch)}; ` +
111
+ `Expires=${expires.toUTCString()}; ${cookieAttributes(event)}`);
112
+ }
113
+ return { stored: true, firstTouch };
114
+ });
115
+ }
@@ -127,3 +127,4 @@ export { AGENT_READABLE_RESOURCE_PAYLOAD_TYPE, AGENT_READABLE_RESOURCE_SCRIPT_TY
127
127
  export { registerObservabilityProvider, type ObservabilityMeterProvider, type ObservabilityProvider, type ObservabilityTracerProvider, } from "../observability/otel-provider.js";
128
128
  export type NitroPluginDef = (nitroApp: any) => void | Promise<void>;
129
129
  export declare function defineNitroPlugin(def: NitroPluginDef): NitroPluginDef;
130
+ export { appStateListKeysByKeySuffixPrefix } from "./application-state-maintenance.js";
@@ -122,3 +122,4 @@ export { registerObservabilityProvider, } from "../observability/otel-provider.j
122
122
  export function defineNitroPlugin(def) {
123
123
  return def;
124
124
  }
125
+ export { appStateListKeysByKeySuffixPrefix } from "./application-state-maintenance.js";
@@ -0,0 +1,9 @@
1
+ export declare function scheduleQueuedSubmissionInboxDrain(input: {
2
+ threadId: string;
3
+ turnContinues: boolean;
4
+ waitUntil?: (promise: Promise<unknown>) => void;
5
+ drain: (input: {
6
+ threadId: string;
7
+ }) => Promise<unknown>;
8
+ onError: (error: unknown) => void;
9
+ }): void;
@@ -0,0 +1,12 @@
1
+ import { runWithoutExecutionFence } from "../agent/execution-fence.js";
2
+ export function scheduleQueuedSubmissionInboxDrain(input) {
3
+ if (input.turnContinues)
4
+ return;
5
+ const drain = Promise.resolve()
6
+ .then(() => runWithoutExecutionFence(() => input.drain({ threadId: input.threadId })))
7
+ .catch(input.onError);
8
+ if (input.waitUntil)
9
+ input.waitUntil(drain);
10
+ else
11
+ void drain;
12
+ }
@@ -4,4 +4,10 @@ export declare function getForwardedRequestOrigin(event: H3Event): string;
4
4
  export declare function getForwardedRequestURL(event: H3Event): URL;
5
5
  export declare function getForwardedRequestHostname(event: H3Event): string;
6
6
  export declare function getForwardedRequestHostnameFromHeaders(headers: Headers | IncomingHttpHeaders, remoteAddress?: string): string;
7
+ /**
8
+ * Stricter than `isSameOriginRequest`, for a POST that writes cookies: a
9
+ * request with neither header, a navigation, or a desktop shell's origin
10
+ * can't prove it came from this site's pages.
11
+ */
12
+ export declare function isSameOriginPost(event: H3Event): boolean;
7
13
  export declare function isSameOriginRequest(event: H3Event): boolean;
@@ -95,6 +95,18 @@ export function getForwardedRequestHostnameFromHeaders(headers, remoteAddress) {
95
95
  function isLoopbackHost(host) {
96
96
  return host.startsWith("localhost:") || host.startsWith("127.0.0.1:");
97
97
  }
98
+ /**
99
+ * Stricter than `isSameOriginRequest`, for a POST that writes cookies: a
100
+ * request with neither header, a navigation, or a desktop shell's origin
101
+ * can't prove it came from this site's pages.
102
+ */
103
+ export function isSameOriginPost(event) {
104
+ const fetchSite = getRequestHeader(event, "sec-fetch-site");
105
+ if (fetchSite)
106
+ return fetchSite === "same-origin";
107
+ const origin = getRequestHeader(event, "origin");
108
+ return !!origin && origin === getForwardedRequestOrigin(event);
109
+ }
98
110
  export function isSameOriginRequest(event) {
99
111
  const fetchSite = getRequestHeader(event, "sec-fetch-site");
100
112
  if (fetchSite)
@@ -1,5 +1,6 @@
1
- import { type StoreWriteOptions, type StoreReadOptions } from "./store.js";
1
+ import { type SettingEntry, type StoreWriteOptions, type StoreReadOptions } from "./store.js";
2
2
  export declare function getOrgSetting(orgId: string, key: string, options?: StoreReadOptions): Promise<Record<string, unknown> | null>;
3
+ export declare function getOrgSettingEntry(orgId: string, key: string, options?: StoreReadOptions): Promise<SettingEntry>;
3
4
  export declare function putOrgSetting(orgId: string, key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
4
5
  export declare function mutateOrgSetting(orgId: string, key: string, updater: (current: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>, options?: StoreWriteOptions): Promise<Record<string, unknown>>;
5
6
  export declare function deleteOrgSetting(orgId: string, key: string, options?: StoreWriteOptions): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingsByPrefix, listSettingsByPrefix, } from "./store.js";
1
+ import { getSetting, getSettingEntry, mutateSetting, putSetting, deleteSetting, deleteSettingsByPrefix, listSettingsByPrefix, } from "./store.js";
2
2
  function orgKey(orgId, key) {
3
3
  return `o:${orgId}:${key}`;
4
4
  }
@@ -6,6 +6,9 @@ const ORG_PREFIX_RE = /^o:([^:]+):(.+)$/;
6
6
  export async function getOrgSetting(orgId, key, options) {
7
7
  return getSetting(orgKey(orgId, key), options);
8
8
  }
9
+ export async function getOrgSettingEntry(orgId, key, options) {
10
+ return getSettingEntry(orgKey(orgId, key), options);
11
+ }
9
12
  export async function putOrgSetting(orgId, key, value, options) {
10
13
  return putSetting(orgKey(orgId, key), value, options);
11
14
  }
@@ -9,6 +9,14 @@ export interface StoreReadOptions {
9
9
  transaction?: DbExec;
10
10
  }
11
11
  export declare function getSetting(key: string, options?: StoreReadOptions): Promise<Record<string, unknown> | null>;
12
+ export type SettingEntry = {
13
+ found: false;
14
+ } | {
15
+ found: true;
16
+ value: unknown;
17
+ };
18
+ /** Like `getSetting`, but a stored JSON `null` is found, not missing. */
19
+ export declare function getSettingEntry(key: string, options?: StoreReadOptions): Promise<SettingEntry>;
12
20
  export declare function getSettings(keys: readonly string[], options?: StoreReadOptions): Promise<Map<string, Record<string, unknown> | null>>;
13
21
  export interface StoreWriteOptions {
14
22
  requestSource?: string;
@@ -63,12 +63,10 @@ export const settingsStore = defineStore({
63
63
  export function ensureTable() {
64
64
  return settingsStore.ready();
65
65
  }
66
- export async function getSetting(key, options) {
66
+ async function readSettingRaw(key, options) {
67
67
  const cache = options?.transaction ? null : requestSettingsCache();
68
- if (!options?.bypassCache && cache?.has(key)) {
69
- const cached = cache.get(key);
70
- return cached == null ? null : JSON.parse(cached);
71
- }
68
+ if (!options?.bypassCache && cache?.has(key))
69
+ return cache.get(key) ?? null;
72
70
  if (!options?.transaction)
73
71
  await ensureTable();
74
72
  const client = options?.transaction ?? getDbExec();
@@ -80,8 +78,19 @@ export async function getSetting(key, options) {
80
78
  const raw = rows.length === 0 ? null : rows[0].value;
81
79
  if (!options?.bypassCache)
82
80
  cache?.set(key, raw);
81
+ return raw;
82
+ }
83
+ export async function getSetting(key, options) {
84
+ const raw = await readSettingRaw(key, options);
83
85
  return raw == null ? null : JSON.parse(raw);
84
86
  }
87
+ /** Like `getSetting`, but a stored JSON `null` is found, not missing. */
88
+ export async function getSettingEntry(key, options) {
89
+ const raw = await readSettingRaw(key, options);
90
+ return raw == null
91
+ ? { found: false }
92
+ : { found: true, value: JSON.parse(raw) };
93
+ }
85
94
  const SETTINGS_IN_LIST_CHUNK_SIZE = 500;
86
95
  function parseSettingValue(key, raw) {
87
96
  try {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Cookie consent for the regions that need it. The regions and categories
3
+ * follow builder.io's consent banner: visitors in these countries opt in, and
4
+ * every other visitor is tracked as before, with no banner.
5
+ */
6
+ export declare function isConsentRegionCountry(code: string | null | undefined): boolean;
7
+ export declare function isConsentRegionTimeZone(timeZone: string | null | undefined): boolean;
8
+ /**
9
+ * What the banner asks about. `performance` covers analytics (Google
10
+ * Analytics, Amplitude); `targeting` covers advertising and attribution (Tag
11
+ * Manager's ad tags, the referral attribution cookies). Strictly necessary
12
+ * cookies need no consent.
13
+ */
14
+ export type ConsentCategory = "performance" | "targeting";
15
+ export type ConsentChoice = Record<ConsentCategory, boolean>;
16
+ /** `consent` shows the banner; `none` tracks as before. */
17
+ export type ConsentRegion = "consent" | "none";
18
+ export declare const CONSENT_COOKIE_NAME = "an_consent";
19
+ /** Asked again a year later, like builder.io. */
20
+ export declare const CONSENT_COOKIE_LIFETIME_SECONDS: number;
21
+ export declare const CONSENT_REGION_PATH = "/_agent-native/auth/consent-region";
22
+ export declare const CONSENT_PATH = "/_agent-native/auth/consent";
23
+ /**
24
+ * One choice covers the site and every app on agent-native.com. The browser
25
+ * and the server must agree, or the two copies become two cookies.
26
+ */
27
+ export declare function consentCookieDomain(hostname: string): string | null;
28
+ /** `1.p1.t0`: version, then one flag per category. */
29
+ export declare function serializeConsent(choice: ConsentChoice): string;
30
+ /** The stored choice, or `null` when there is none this version can read. */
31
+ export declare function parseConsent(value: string | null | undefined): ConsentChoice | null;