@agent-native/core 0.214.0 → 0.217.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action.js +54 -1
- package/dist/agent/actions/disconnect-chatgpt-subscription.js +5 -0
- package/dist/agent/actions/get-agentkit-capabilities.js +3 -3
- package/dist/agent/actions/manage-builder-connection.js +5 -0
- package/dist/agent/actions/manage-provider-key-policy.js +5 -0
- package/dist/agent/actions/manage-provider-models.js +5 -0
- package/dist/agent/actions/manage-service-providers.js +5 -0
- package/dist/agent/actions/select-chatgpt-subscription-account.js +5 -0
- package/dist/agent/context-xray/actions/context-evict.js +5 -0
- package/dist/agent/context-xray/actions/context-pin.js +5 -0
- package/dist/agent/context-xray/actions/context-report.js +5 -0
- package/dist/agent/context-xray/actions/context-restore.js +5 -0
- package/dist/agent/execution-fence.d.ts +1 -0
- package/dist/agent/execution-fence.js +5 -0
- package/dist/agent/production-agent.d.ts +35 -2
- package/dist/agent/production-agent.js +629 -32
- package/dist/agent/run-manager.d.ts +2 -0
- package/dist/agent/run-manager.js +397 -115
- package/dist/agent/run-slot-lock.d.ts +2 -0
- package/dist/agent/run-slot-lock.js +6 -0
- package/dist/agent/run-store.d.ts +5 -0
- package/dist/agent/run-store.js +423 -156
- package/dist/agent/thread-data-builder.js +27 -5
- package/dist/agent/types.d.ts +6 -12
- package/dist/agentkit/capabilities.d.ts +1 -1
- package/dist/app-config/analytics.js +1 -1
- package/dist/application-state/store.d.ts +5 -0
- package/dist/application-state/store.js +26 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/chat-threads/schema-migrations.js +17 -0
- package/dist/chat-threads/store.d.ts +21 -1
- package/dist/chat-threads/store.js +223 -41
- package/dist/cli/skills-content/turn-into-app-skill.d.ts +1 -1
- package/dist/cli/skills-content/turn-into-app-skill.js +20 -18
- package/dist/cli/templates-meta.js +14 -0
- package/dist/client/analytics-session.d.ts +7 -0
- package/dist/client/analytics-session.js +27 -1
- package/dist/client/analytics.js +276 -31
- package/dist/client/chat/agentkit-agent-native.js +79 -3
- package/dist/client/chat/agentkit-protocol.js +80 -42
- package/dist/client/chat/runtime.d.ts +5 -1
- package/dist/client/chat/runtime.js +46 -3
- package/dist/client/consent.d.ts +48 -0
- package/dist/client/consent.js +413 -0
- package/dist/client/onboarding/index.d.ts +1 -0
- package/dist/client/onboarding/index.js +1 -0
- package/dist/client/onboarding/use-onboarding-checklist.d.ts +23 -0
- package/dist/client/onboarding/use-onboarding-checklist.js +115 -0
- package/dist/client/review/use-review.d.ts +2 -0
- package/dist/client/review/use-review.js +28 -1
- package/dist/client/session-replay.d.ts +1 -1
- package/dist/client/session-replay.js +142 -3
- package/dist/client/sharing/useResourceAccessGate.d.ts +3 -2
- package/dist/client/sharing/useResourceAccessGate.js +9 -1
- package/dist/client/use-chat-thread-list.d.ts +2 -0
- package/dist/client/use-chat-thread-list.js +12 -2
- package/dist/client/use-chat-threads.d.ts +1 -0
- package/dist/client/use-chat-threads.js +2 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/connections/catalog.d.ts +5 -5
- package/dist/connections/catalog.js +22 -3
- package/dist/data-widgets/index.d.ts +36 -2
- package/dist/data-widgets/index.js +94 -6
- package/dist/db/migrations.js +4 -1
- package/dist/db/schema-snapshot.generated.js +8 -1
- package/dist/feature-flags/actions/set-feature-flag.js +5 -0
- package/dist/feature-flags/index.d.ts +1 -1
- package/dist/feature-flags/index.js +1 -1
- package/dist/feature-flags/store.d.ts +12 -0
- package/dist/feature-flags/store.js +79 -7
- package/dist/file-upload/actions/manage-file-storage.js +5 -0
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.js +5 -0
- package/dist/history/actions/create-resource-version.js +5 -0
- package/dist/history/actions/restore-resource-version.js +5 -0
- package/dist/ingestion/index.d.ts +1 -1
- package/dist/ingestion/index.js +1 -1
- package/dist/ingestion/pptx.d.ts +11 -0
- package/dist/ingestion/pptx.js +81 -1
- package/dist/integrations/actions/manage-messaging-channel.js +5 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/jobs/scheduler.js +17 -1
- package/dist/labs/actions/set-lab.js +5 -0
- package/dist/localization/actions/set-localization-preference.js +5 -0
- package/dist/localization/consent-banner-messages.d.ts +16 -0
- package/dist/localization/consent-banner-messages.js +149 -0
- package/dist/localization/core-messages.js +4 -0
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +2 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/actions/save-observability-instruction-update.js +5 -0
- package/dist/observability/actions/save-observability-review-summary.js +5 -0
- package/dist/observability/routes.d.ts +6 -6
- package/dist/onboarding/index.d.ts +2 -1
- package/dist/onboarding/index.js +1 -0
- package/dist/onboarding/plugin.js +55 -46
- package/dist/onboarding/status.d.ts +15 -0
- package/dist/onboarding/status.js +59 -0
- package/dist/onboarding/types.d.ts +12 -0
- package/dist/onboarding/types.js +2 -1
- package/dist/org/workspace-app-access.js +19 -9
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.js +9 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/review/actions/consume-review-feedback.js +5 -0
- package/dist/review/actions/create-review-comment.js +5 -0
- package/dist/review/actions/delete-review-comment.js +5 -0
- package/dist/review/actions/react-to-review-comment.js +5 -0
- package/dist/review/actions/reply-review-comment.js +5 -0
- package/dist/review/actions/resolve-review-thread.js +5 -0
- package/dist/review/actions/send-review-thread-to-agent.js +5 -0
- package/dist/review/actions/set-review-status.js +5 -0
- package/dist/review/actions/set-review-thread-muted.js +5 -0
- package/dist/review/actions/set-review-thread-unread.js +5 -0
- package/dist/review/actions/set-review-threads-unread.js +5 -0
- package/dist/review/actions/update-review-comment-anchor.js +5 -0
- package/dist/review/actions/update-review-comment.js +5 -0
- package/dist/review/suggestions/actions.js +60 -21
- package/dist/review/suggestions/store.d.ts +2 -0
- package/dist/review/suggestions/store.js +28 -0
- package/dist/secrets/actions/delete-api-key.js +5 -0
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-capabilities.js +13 -6
- package/dist/server/agent-chat/context-tools.js +63 -2
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.js +242 -54
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +0 -1
- package/dist/server/analytics.js +81 -20
- package/dist/server/application-state-maintenance.d.ts +2 -0
- package/dist/server/application-state-maintenance.js +2 -0
- package/dist/server/attribution.d.ts +14 -0
- package/dist/server/attribution.js +62 -2
- package/dist/server/auth.js +7 -0
- package/dist/server/builder-api-auth.d.ts +7 -0
- package/dist/server/builder-api-auth.js +24 -0
- package/dist/server/builder-oauth.d.ts +5 -1
- package/dist/server/builder-oauth.js +5 -0
- package/dist/server/consent-route.d.ts +41 -0
- package/dist/server/consent-route.js +102 -0
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/first-touch-route.d.ts +39 -0
- package/dist/server/first-touch-route.js +115 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/queued-submission-inbox-drain.d.ts +9 -0
- package/dist/server/queued-submission-inbox-drain.js +12 -0
- package/dist/server/request-origin.d.ts +6 -0
- package/dist/server/request-origin.js +12 -0
- package/dist/settings/org-settings.d.ts +2 -1
- package/dist/settings/org-settings.js +4 -1
- package/dist/settings/store.d.ts +8 -0
- package/dist/settings/store.js +14 -5
- package/dist/shared/consent.d.ts +33 -0
- package/dist/shared/consent.js +149 -0
- package/dist/shared/first-touch-persistence.d.ts +31 -0
- package/dist/shared/first-touch-persistence.js +53 -0
- package/dist/sharing/actions/set-resource-visibility.js +5 -0
- package/dist/sharing/actions/share-resource.js +5 -0
- package/dist/sharing/actions/unshare-resource.js +5 -0
- package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -1
- package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -1
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +36 -80
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +54 -16
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +50 -73
- package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +20 -18
- package/dist/triggers/actions/manage-automation.d.ts +4 -4
- package/dist/usage/actions/get-usage-metrics.js +27 -4
- package/dist/usage/actions/manage-usage-alert.js +5 -0
- package/dist/vite/client.js +8 -6
- package/dist/workspace-connections/actions/bulk-update-workspace-user-groups.js +5 -0
- package/dist/workspace-connections/actions/delete-workspace-user-group.js +5 -0
- package/dist/workspace-connections/actions/upsert-workspace-user-group.js +5 -0
- package/docs/content/actions-access-control.mdx +8 -8
- package/docs/content/actions-defining.mdx +9 -8
- package/docs/content/deployment-environment-variables.mdx +1 -1
- package/docs/content/environment-variables.mdx +1 -1
- package/docs/content/template-design-developers.mdx +9 -9
- package/docs/content/template-design-features.mdx +1 -3
- package/docs/content/turn-into-app.mdx +4 -0
- package/package.json +6 -3
- package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -1
- package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -1
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +36 -80
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +54 -16
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +50 -73
- package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -1
- package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -1
- package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +20 -18
package/dist/server/analytics.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getAppConfig } from "../app-config/index.js";
|
|
2
|
+
import { CONSENT_REGION_PATH, CONSENT_REGION_TIME_ZONE_LIST, } from "../shared/consent.js";
|
|
2
3
|
import { SYNTHETIC_TRAFFIC_BETA_E2E } from "../shared/test-traffic.js";
|
|
4
|
+
import { publicFrameworkPath } from "./framework-route-prefix.js";
|
|
3
5
|
const AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT = "https://analytics.agent-native.com/track";
|
|
4
6
|
function normalizeMeasurementId(value) {
|
|
5
7
|
const trimmed = value?.trim();
|
|
@@ -54,18 +56,18 @@ export function getAgentNativeAnalyticsConfigScript() {
|
|
|
54
56
|
");</script>",
|
|
55
57
|
].join("");
|
|
56
58
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.replace(/"/g, """)
|
|
61
|
-
.replace(/</g, "<")
|
|
62
|
-
.replace(/>/g, ">");
|
|
63
|
-
}
|
|
64
|
-
export function getHeadScriptTag() {
|
|
59
|
+
// A deployment's widget may set cookies or track, so it waits for
|
|
60
|
+
// performance consent like GA does.
|
|
61
|
+
function getHeadScript() {
|
|
65
62
|
const src = getAppConfig().analytics.headScriptSrc;
|
|
66
63
|
if (!src)
|
|
67
64
|
return null;
|
|
68
|
-
|
|
65
|
+
// "<" inside an inline script could close it early.
|
|
66
|
+
const jsSrc = JSON.stringify(src).replace(/</g, "\\x3c");
|
|
67
|
+
return consentGatedScript("performance", `var agentNativeHeadScript=document.createElement('script');` +
|
|
68
|
+
`agentNativeHeadScript.src=${jsSrc};` +
|
|
69
|
+
`agentNativeHeadScript.setAttribute('data-agent-native-head-script','');` +
|
|
70
|
+
`document.head.appendChild(agentNativeHeadScript);`);
|
|
69
71
|
}
|
|
70
72
|
export function getGaInlineConfigScriptBody(options) {
|
|
71
73
|
const id = getGaMeasurementId();
|
|
@@ -100,47 +102,106 @@ export function getGaInlineConfigScriptBody(options) {
|
|
|
100
102
|
`document.head.appendChild(agentNativeGtagScript);` +
|
|
101
103
|
`}`);
|
|
102
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Queue `body` until the visitor's region and choice allow `category`; the
|
|
107
|
+
* bootstrap after it, or `client/consent.ts` once loaded, runs it. A reject
|
|
108
|
+
* then a fresh accept calls it again, so it loads its tag only once a page.
|
|
109
|
+
*/
|
|
110
|
+
function consentGatedScript(category, body) {
|
|
111
|
+
return `<script>(window.__AN_CONSENT_QUEUE__=window.__AN_CONSENT_QUEUE__||[]).push([${JSON.stringify(category)},(function(){var ran=false;return function(){if(ran)return;ran=true;${body}}})()]);</script>`;
|
|
112
|
+
}
|
|
113
|
+
const GA_FALLBACK_DATA_LAYER = "__AGENT_NATIVE_GA_DATA_LAYER__";
|
|
114
|
+
// gtag reads an `arguments` object from the data layer, never an array.
|
|
115
|
+
const GOOGLE_CONSENT_PUSH = `function push(g){for(var i=0;i<L.length;i++){var l=w[L[i]]=w[L[i]]||[];` +
|
|
116
|
+
`(function(){l.push(arguments)})("consent",g.mode,{analytics_storage:g.p?"granted":"denied",ad_storage:g.t?"granted":"denied",ad_user_data:g.t?"granted":"denied",ad_personalization:g.t?"granted":"denied"})}}`;
|
|
117
|
+
/**
|
|
118
|
+
* Google Consent Mode: before any Google tag loads, tell each data layer
|
|
119
|
+
* what the stored choice allows (nothing, without one), so tags already on
|
|
120
|
+
* the page stop storing and sending when `client/consent.ts` later pushes a
|
|
121
|
+
* reject. Lists the layers there, under `__AN_CONSENT_LAYERS__`.
|
|
122
|
+
*/
|
|
123
|
+
function getGoogleConsentDefaultScript(dataLayers) {
|
|
124
|
+
return (`<script data-agent-native-consent-mode>(function(w,d){var L=${JSON.stringify(dataLayers)};w.__AN_CONSENT_LAYERS__=L;` +
|
|
125
|
+
GOOGLE_CONSENT_PUSH +
|
|
126
|
+
`var m=/(?:^|;\\s*)an_consent=1\\.p([01])\\.t([01])(?:;|$)/.exec(d.cookie||"");` +
|
|
127
|
+
`push({mode:"default",p:!!m&&m[1]==="1",t:!!m&&m[2]==="1"})` +
|
|
128
|
+
`})(window,document);</script>`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Runs the queued tags on pages that never load the app bundle, like the
|
|
132
|
+
* plain sign-in page, and runs them without waiting for it elsewhere: at once
|
|
133
|
+
* for a stored choice or a region already known this session, else after
|
|
134
|
+
* asking the server, with the time zone as fallback. Once
|
|
135
|
+
* `client/consent.ts` replaces the queue it owns what's left, so nothing runs
|
|
136
|
+
* twice. Keep its cookie and region rules in step with that module.
|
|
137
|
+
*/
|
|
138
|
+
function getConsentBootstrapScript() {
|
|
139
|
+
const zones = JSON.stringify(CONSENT_REGION_TIME_ZONE_LIST);
|
|
140
|
+
const regionPath = JSON.stringify(publicFrameworkPath(CONSENT_REGION_PATH));
|
|
141
|
+
return (`<script data-agent-native-consent>(function(w,d){var Q="__AN_CONSENT_QUEUE__",K="an_consent_region";` +
|
|
142
|
+
`function choice(){var m=/(?:^|;\\s*)an_consent=1\\.p([01])\\.t([01])(?:;|$)/.exec(d.cookie||"");return m?{performance:m[1]==="1",targeting:m[2]==="1"}:null}` +
|
|
143
|
+
`function allows(c,k,region){return c?c[k]:region==="none"}` +
|
|
144
|
+
`function run(region){var q=w[Q];if(!Array.isArray(q))return;var c=choice(),keep=[],L=w.__AN_CONSENT_LAYERS__||[];` +
|
|
145
|
+
GOOGLE_CONSENT_PUSH +
|
|
146
|
+
`push({mode:"update",p:allows(c,"performance",region),t:allows(c,"targeting",region)});` +
|
|
147
|
+
`for(var i=0;i<q.length;i++){var e=q[i];if(allows(c,e[0],region)){e[1]()}else{keep.push(e)}}q.length=0;q.push.apply(q,keep)}` +
|
|
148
|
+
`function zone(){try{return ${zones}.indexOf(Intl.DateTimeFormat().resolvedOptions().timeZone)>-1?"consent":"none"}catch(e){return"consent"}}` +
|
|
149
|
+
// coercion-ok: a page without session storage asks the server again.
|
|
150
|
+
`var cached=null;try{cached=w.sessionStorage.getItem(K)}catch(e){}` +
|
|
151
|
+
`if(choice()||cached==="consent"||cached==="none"){run(cached);return}` +
|
|
152
|
+
// An unreachable server falls back to the time zone.
|
|
153
|
+
// coercion-ok: a region it can't save is asked again on the next page.
|
|
154
|
+
`fetch(${regionPath},{credentials:"same-origin"}).then(function(r){return r.ok?r.json():{}}).then(function(b){return b&&(b.region==="consent"||b.region==="none")?b.region:zone()},zone).then(function(region){try{w.sessionStorage.setItem(K,region)}catch(e){}run(region)})` +
|
|
155
|
+
`})(window,document);</script>`);
|
|
156
|
+
}
|
|
103
157
|
function getGaScript() {
|
|
104
158
|
const id = getGaMeasurementId();
|
|
105
159
|
if (!id)
|
|
106
160
|
return null;
|
|
107
161
|
const inlineBody = getGaInlineConfigScriptBody();
|
|
108
|
-
return
|
|
162
|
+
return inlineBody ? consentGatedScript("performance", inlineBody) : null;
|
|
109
163
|
}
|
|
110
164
|
function getGtmGaFallbackScript() {
|
|
111
165
|
const inlineBody = getGaInlineConfigScriptBody({
|
|
112
|
-
dataLayerName:
|
|
166
|
+
dataLayerName: GA_FALLBACK_DATA_LAYER,
|
|
113
167
|
sendPageView: false,
|
|
114
168
|
});
|
|
115
|
-
return inlineBody ?
|
|
169
|
+
return inlineBody ? consentGatedScript("performance", inlineBody) : null;
|
|
116
170
|
}
|
|
171
|
+
// The container carries the ad pixels (Meta, LinkedIn, Reddit, X, Bing,
|
|
172
|
+
// Google Ads, OpenAI) and B2B identification tags alongside GA events, so it
|
|
173
|
+
// all waits for targeting. Its <noscript> iframe can't wait, so there is none.
|
|
117
174
|
function getGtmHeadScript(containerId) {
|
|
118
175
|
const jsId = JSON.stringify(containerId);
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
function getGtmBodyFallback(containerId) {
|
|
122
|
-
const src = encodeURIComponent(containerId);
|
|
123
|
-
return `<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${src}" height="0" width="0" style="display:none;visibility:hidden" title="Google Tag Manager"></iframe></noscript>`;
|
|
176
|
+
return consentGatedScript("targeting", `if(${getSyntheticTrafficBrowserGuard()}){(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer',${jsId});}`);
|
|
124
177
|
}
|
|
125
178
|
function getAnalyticsInjection() {
|
|
126
179
|
const agentNativeAnalytics = getAgentNativeAnalyticsConfigScript();
|
|
127
|
-
const headScript =
|
|
180
|
+
const headScript = getHeadScript();
|
|
128
181
|
const containerId = getGtmContainerId();
|
|
129
182
|
if (containerId) {
|
|
130
183
|
return {
|
|
131
184
|
head: [
|
|
132
185
|
agentNativeAnalytics,
|
|
186
|
+
getGoogleConsentDefaultScript(["dataLayer", GA_FALLBACK_DATA_LAYER]),
|
|
133
187
|
getGtmHeadScript(containerId),
|
|
134
188
|
getGtmGaFallbackScript(),
|
|
135
189
|
headScript,
|
|
190
|
+
getConsentBootstrapScript(),
|
|
136
191
|
]
|
|
137
192
|
.filter(Boolean)
|
|
138
193
|
.join(""),
|
|
139
|
-
body:
|
|
194
|
+
body: "",
|
|
140
195
|
};
|
|
141
196
|
}
|
|
142
197
|
const gaScript = getGaScript();
|
|
143
|
-
const head = [
|
|
198
|
+
const head = [
|
|
199
|
+
agentNativeAnalytics,
|
|
200
|
+
gaScript ? getGoogleConsentDefaultScript(["dataLayer"]) : null,
|
|
201
|
+
gaScript,
|
|
202
|
+
headScript,
|
|
203
|
+
gaScript || headScript ? getConsentBootstrapScript() : null,
|
|
204
|
+
]
|
|
144
205
|
.filter(Boolean)
|
|
145
206
|
.join("");
|
|
146
207
|
return head ? { head, body: "" } : null;
|
|
@@ -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
|
-
|
|
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.
|
package/dist/server/auth.js
CHANGED
|
@@ -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 =
|
|
@@ -16,6 +16,13 @@ export interface BuilderRequestAuthorization {
|
|
|
16
16
|
authorization: string;
|
|
17
17
|
source: "oauth" | "legacy";
|
|
18
18
|
oauthScope?: RemoteMcpScope;
|
|
19
|
+
oauthResource?: "general" | "publish";
|
|
20
|
+
oauthConnectionId?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Space selected during Builder OAuth. This is only a request locator; the
|
|
23
|
+
* provider must verify the token and reject a different apiKey.
|
|
24
|
+
*/
|
|
25
|
+
oauthSelectedPublicKey?: string;
|
|
19
26
|
legacyCredentialKey?: BuilderLegacyCredentialKey;
|
|
20
27
|
legacyPublicKey?: string;
|
|
21
28
|
userId?: string;
|
|
@@ -51,6 +51,22 @@ function isBuilderPublishResource(value) {
|
|
|
51
51
|
!url.search &&
|
|
52
52
|
!url.hash);
|
|
53
53
|
}
|
|
54
|
+
function selectedPublicKeyFromBuilderAccessToken(token) {
|
|
55
|
+
const payload = token.split(".")[1];
|
|
56
|
+
if (!payload)
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
60
|
+
return typeof decoded.org === "string" && decoded.org.trim()
|
|
61
|
+
? decoded.org.trim()
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// coercion-ok: an opaque or malformed optional locator claim is absent;
|
|
66
|
+
// authorization remains the provider's signed-token responsibility.
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
async function resolveBuilderPublishAuthorization(ownerEmail, orgId) {
|
|
55
71
|
const candidates = orgId
|
|
56
72
|
? [{ scope: "org", scopeId: orgId }]
|
|
@@ -108,6 +124,7 @@ async function resolveBuilderPublishAuthorization(ownerEmail, orgId) {
|
|
|
108
124
|
authorization: `Bearer ${match[1]}`,
|
|
109
125
|
source: "oauth",
|
|
110
126
|
oauthScope: candidate.scope,
|
|
127
|
+
oauthResource: "publish",
|
|
111
128
|
};
|
|
112
129
|
}
|
|
113
130
|
if (ownerEmail) {
|
|
@@ -154,6 +171,7 @@ export async function resolveBuilderRequestAuthorization(input = {}) {
|
|
|
154
171
|
throw new ActionContractError(`Builder.io access needs re-authorizing to grant ${input.requiredScope}. Open Settings and authorize Builder.io again.`, {
|
|
155
172
|
errorCode: "builder_oauth_reauthorization_required",
|
|
156
173
|
statusCode: 400,
|
|
174
|
+
details: { reason: "missing_scope", scope: input.requiredScope },
|
|
157
175
|
});
|
|
158
176
|
}
|
|
159
177
|
throw err;
|
|
@@ -168,6 +186,7 @@ export async function resolveBuilderRequestAuthorization(input = {}) {
|
|
|
168
186
|
throw new ActionContractError(`Builder.io access needs re-authorizing to grant ${input.requiredScope}. Open Settings and authorize Builder.io again.`, {
|
|
169
187
|
errorCode: "builder_oauth_reauthorization_required",
|
|
170
188
|
statusCode: 400,
|
|
189
|
+
details: { reason: "missing_scope", scope: input.requiredScope },
|
|
171
190
|
});
|
|
172
191
|
}
|
|
173
192
|
return {
|
|
@@ -175,6 +194,11 @@ export async function resolveBuilderRequestAuthorization(input = {}) {
|
|
|
175
194
|
authorization: `Bearer ${session.accessToken}`,
|
|
176
195
|
source: "oauth",
|
|
177
196
|
oauthScope: session.scope,
|
|
197
|
+
oauthResource: "general",
|
|
198
|
+
...(session.connectionId
|
|
199
|
+
? { oauthConnectionId: session.connectionId }
|
|
200
|
+
: {}),
|
|
201
|
+
oauthSelectedPublicKey: selectedPublicKeyFromBuilderAccessToken(session.accessToken),
|
|
178
202
|
};
|
|
179
203
|
}
|
|
180
204
|
return resolveBuilderLegacyRequestAuthorization(input.legacyCredentialKeys);
|
|
@@ -3,7 +3,9 @@ export declare const BUILDER_OAUTH_ISSUER = "https://mcp.builder.io";
|
|
|
3
3
|
export declare const BUILDER_OAUTH_RESOURCE = "https://api.builder.io";
|
|
4
4
|
export declare const BUILDER_OAUTH_SCOPE = "builder:ai:invoke";
|
|
5
5
|
export declare const BUILDER_ASSETS_WRITE_SCOPE = "builder:assets:write";
|
|
6
|
-
export declare const
|
|
6
|
+
export declare const BUILDER_CONTENT_READ_SCOPE = "builder:content:read";
|
|
7
|
+
export declare const BUILDER_CONTENT_WRITE_SCOPE = "builder:content:write";
|
|
8
|
+
export declare const BUILDER_OAUTH_SCOPES: readonly ["builder:ai:invoke", "builder:agents:run", "builder:browser:connect", "builder:assets:write", "builder:content:read", "builder:content:write", "builder:projects:read", "builder:projects:write", "builder:designsystem:read", "builder:designsystem:write"];
|
|
7
9
|
export type BuilderOAuthPermissionScope = (typeof BUILDER_OAUTH_SCOPES)[number];
|
|
8
10
|
export type BuilderOAuthPendingFlow = {
|
|
9
11
|
codeVerifier: string;
|
|
@@ -43,6 +45,8 @@ export type BuilderOAuthSession = {
|
|
|
43
45
|
expiresAt?: number;
|
|
44
46
|
scopes: string[];
|
|
45
47
|
scope: BuilderOAuthScope;
|
|
48
|
+
/** Opaque identity of the selected stored OAuth credential lane. */
|
|
49
|
+
connectionId?: string;
|
|
46
50
|
};
|
|
47
51
|
export type BuilderOAuthRequestAccess = BuilderOAuthSession & {
|
|
48
52
|
ownerEmail: string;
|
|
@@ -9,11 +9,15 @@ export const BUILDER_OAUTH_ISSUER = "https://mcp.builder.io";
|
|
|
9
9
|
export const BUILDER_OAUTH_RESOURCE = "https://api.builder.io";
|
|
10
10
|
export const BUILDER_OAUTH_SCOPE = "builder:ai:invoke";
|
|
11
11
|
export const BUILDER_ASSETS_WRITE_SCOPE = "builder:assets:write";
|
|
12
|
+
export const BUILDER_CONTENT_READ_SCOPE = "builder:content:read";
|
|
13
|
+
export const BUILDER_CONTENT_WRITE_SCOPE = "builder:content:write";
|
|
12
14
|
export const BUILDER_OAUTH_SCOPES = [
|
|
13
15
|
BUILDER_OAUTH_SCOPE,
|
|
14
16
|
"builder:agents:run",
|
|
15
17
|
"builder:browser:connect",
|
|
16
18
|
BUILDER_ASSETS_WRITE_SCOPE,
|
|
19
|
+
BUILDER_CONTENT_READ_SCOPE,
|
|
20
|
+
BUILDER_CONTENT_WRITE_SCOPE,
|
|
17
21
|
"builder:projects:read",
|
|
18
22
|
"builder:projects:write",
|
|
19
23
|
"builder:designsystem:read",
|
|
@@ -290,6 +294,7 @@ export async function getBuilderOAuthSession(ownerEmail, orgId, requiredScope, a
|
|
|
290
294
|
expiresAt: credentials.tokenExpiresAt,
|
|
291
295
|
scopes,
|
|
292
296
|
scope: options.scope,
|
|
297
|
+
connectionId: options.key,
|
|
293
298
|
};
|
|
294
299
|
}
|
|
295
300
|
if (requiredScope && missingRequiredScope) {
|
|
@@ -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
|
+
}
|
|
@@ -588,8 +588,8 @@ export declare function createPublicRemoteAgentsHandler(discover?: PublicAgentDi
|
|
|
588
588
|
error: string;
|
|
589
589
|
agents?: undefined;
|
|
590
590
|
} | {
|
|
591
|
-
error?: undefined;
|
|
592
591
|
agents: Omit<import("./agent-discovery.js").DiscoveredAgent, "auth" | "kind">[];
|
|
592
|
+
error?: undefined;
|
|
593
593
|
}>>;
|
|
594
594
|
export declare function getBuilderConnectErrorDisposition(error: unknown, connectAttemptId: string | null): "correlated" | "legacy" | null;
|
|
595
595
|
export declare function getBuilderConnectErrorKey(ownerEmail: string, connectAttemptId?: string | null): string;
|
|
@@ -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
|
+
error: string;
|
|
21
|
+
reason?: undefined;
|
|
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
|
+
error?: undefined;
|
|
36
|
+
reason?: undefined;
|
|
37
|
+
stored: true;
|
|
38
|
+
firstTouch: FirstTouchAttribution;
|
|
39
|
+
}>>;
|