Skip to content

Commit 01ae82c

Browse files
committed
fix!: first party mode config
1 parent bcd2cfb commit 01ae82c

37 files changed

Lines changed: 1267 additions & 716 deletions

FIRST_PARTY.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Some Path A scripts also define `autoInject` on their proxy config to set SDK en
4444
- **Rybbit**: `analyticsHost``/_scripts/p/app.rybbit.io/api`
4545
- **Databuddy**: `apiUrl``/_scripts/p/basket.databuddy.cc`
4646

47-
Auto-inject respects per-script `firstParty: false` opt-out (see "Per-script opt-out" below).
47+
Auto-inject respects per-script `reverseProxyIntercept: false` opt-out (see "Per-script opt-out" below).
4848

4949
### SDK-specific post-processing (`postProcess` on ProxyConfig)
5050
Some SDKs have quirks that require targeted regex patches after AST rewriting. These are defined as `postProcess` functions directly on each script's `ProxyConfig`:
@@ -61,34 +61,34 @@ The only exception is `googleAdsense` which sets `proxy: 'googleAnalytics'` to s
6161

6262
## Per-script opt-out
6363

64-
Scripts can opt out of first-party mode at two levels:
64+
Scripts can opt out of first-party mode at three levels:
6565

66-
### Registry level (`proxy: false`)
67-
Set `proxy: false` in `registry.ts` for scripts that should never be proxied. Used for scripts that require fingerprinting for core functionality:
66+
### Registry level (no `reverseProxyIntercept` capability)
67+
Scripts without the `reverseProxyIntercept` capability in `registry.ts` are never proxied. Used for scripts that require fingerprinting for core functionality:
6868
- **Stripe**, **PayPal**: Fraud detection requires real client IP and browser fingerprints
6969
- **Google reCAPTCHA**: Bot detection requires real fingerprints
7070
- **Google Sign-In**: Auth integrity requires direct connection
7171

7272
These scripts also have `scriptBundling: false` to prevent AST rewriting.
7373

74-
### Config level (`firstParty: false` in registry config)
74+
### Config level (`reverseProxyIntercept: false` in registry config)
7575
Users can opt out per-script in `nuxt.config.ts`:
7676

7777
```
78-
scripts.registry.plausibleAnalytics = { domain: 'mysite.com', firstParty: false }
78+
scripts.registry.plausibleAnalytics = { domain: 'mysite.com', reverseProxyIntercept: false }
7979
```
8080

8181
Or in tuple form with scriptOptions:
8282
```
83-
scripts.registry.plausibleAnalytics = [{ domain: 'mysite.com' }, { firstParty: false }]
83+
scripts.registry.plausibleAnalytics = [{ domain: 'mysite.com' }, { reverseProxyIntercept: false }]
8484
```
8585

8686
This skips domain registration, auto-inject, and AST rewriting for that script. Important for scripts with `autoInject` (Plausible, PostHog, Umami, Rybbit, Databuddy) since `autoInject` runs at module setup before transforms.
8787

88-
### Composable level (`scriptOptions: { firstParty: false }`)
88+
### Composable level (`scriptOptions: { reverseProxyIntercept: false }`)
8989
```ts
9090
useScriptPlausibleAnalytics({
91-
scriptOptions: { firstParty: false }
91+
scriptOptions: { reverseProxyIntercept: false }
9292
})
9393
```
9494

@@ -102,7 +102,7 @@ This only affects AST rewriting (the transform plugin skips proxy rewrites for t
102102

103103
For npm-mode scripts (no download), define `autoInject` to configure the SDK's endpoint field.
104104

105-
For scripts that need fingerprinting (payments, CAPTCHA, auth), set `proxy: false` and `scriptBundling: false` in the registry entry.
105+
For scripts that need fingerprinting (payments, CAPTCHA, auth), omit the `reverseProxyIntercept` capability and set `scriptBundling: false` in the registry entry.
106106

107107
## Privacy presets
108108

@@ -167,8 +167,8 @@ The server handler extracts the target domain directly from the proxy path (`/_s
167167

168168
### Two-phase setup, configs built once
169169
`module.ts` calls two functions:
170-
1. `setupFirstParty(config, resolvePath)`resolves config, builds all proxy configs once, registers proxy handler. Returns `FirstPartyConfig` with pre-built `proxyConfigs` map.
171-
2. `finalizeFirstParty({...})` (in `modules:done`) — uses the pre-built configs to collect domain privacy mappings, apply auto-injects, register intercept plugin, populate runtimeConfig. Respects per-entry `firstParty: false` opt-out.
170+
1. `setupFirstParty(config, resolvePath)`registers the proxy handler unconditionally (handler rejects unknown domains at runtime). Returns `FirstPartyConfig`.
171+
2. In `modules:done`: resolves capabilities for each configured script via `resolveCapabilities()`, then calls `finalizeFirstParty({...})` which builds proxy configs from the registry, collects domain privacy mappings, applies auto-injects, registers the intercept plugin, and populates runtimeConfig. Respects per-entry `reverseProxyIntercept: false` opt-out.
172172

173173
The transform plugin receives the pre-built `proxyConfigs` map via options — direct lookup per-script, no rebuilding.
174174

client/app.vue

Lines changed: 172 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,44 @@ function privacyLevelBg(level: string) {
204204
return 'bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-400'
205205
}
206206
207+
// Capability visualization helpers
208+
const capabilityDefs = [
209+
{ key: 'bundle', label: 'Bundle', icon: 'carbon:archive', desc: 'Downloaded at build time, served from your domain' },
210+
{ key: 'reverseProxyIntercept', label: 'Proxy', icon: 'carbon:security', desc: 'Collection requests routed through your server' },
211+
{ key: 'partytown', label: 'Partytown', icon: 'carbon:container-software', desc: 'Can run in a web worker via Partytown' },
212+
] as const
213+
214+
type CapState = 'active' | 'available' | 'off'
215+
216+
function capState(script: any, capKey: string): CapState {
217+
const supported = script.capabilities?.[capKey]
218+
if (!supported) return 'off'
219+
const active = script.defaultCapability?.[capKey]
220+
return active ? 'active' : 'available'
221+
}
222+
223+
function capStateClass(state: CapState): string {
224+
if (state === 'active')
225+
return 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/25'
226+
if (state === 'available')
227+
return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20 border-dashed'
228+
return 'opacity-20 border-transparent'
229+
}
230+
231+
function capStateLabel(state: CapState): string {
232+
if (state === 'active') return 'Active by default'
233+
if (state === 'available') return 'Supported (opt-in)'
234+
return 'Not supported'
235+
}
236+
237+
function capSummary(script: any): string {
238+
const parts: string[] = []
239+
if (script.capabilities?.bundle) parts.push('bundle')
240+
if (script.capabilities?.reverseProxyIntercept) parts.push('proxy')
241+
if (script.capabilities?.partytown) parts.push('partytown')
242+
return parts.length ? parts.join(' · ') : 'none'
243+
}
244+
207245
function mechanismLabel(m: string) {
208246
return m === 'bundle-rewrite-intercept' ? 'Bundle + Rewrite' : 'Config Injection'
209247
}
@@ -476,7 +514,7 @@ function scriptLogo(script: any) {
476514
First-Party Mode is not enabled
477515
</p>
478516
<p class="text-sm opacity-60">
479-
Enable in nuxt.config: <code class="px-1.5 py-0.5 n-bg-active rounded text-xs font-mono">scripts: { firstParty: true }</code>
517+
Proxy is auto-enabled when scripts with proxy capabilities are configured. Ensure <code class="px-1.5 py-0.5 n-bg-active rounded text-xs font-mono">scripts: { proxy: false }</code> is not set.
480518
</p>
481519
<NLink href="https://scripts.nuxt.com/docs/guides/first-party" target="_blank" class="text-sm opacity-70 underline mt-2">
482520
View Documentation
@@ -686,54 +724,160 @@ function scriptLogo(script: any) {
686724
</div>
687725

688726
<!-- Registry Tab -->
689-
<div v-else-if="tab === 'registry'" class="h-full relative max-h-full p-4">
727+
<div v-else-if="tab === 'registry'" class="h-full relative max-h-full p-4 overflow-y-auto">
690728
<div v-if="!scriptRegistry?.length" class="text-sm opacity-50">
691729
No registry scripts available.
692730
</div>
693-
<div class="space-y-4">
694-
<div v-for="(script, index) in scriptRegistry" :key="index" class="rounded-lg p-4 hover:n-bg-active transition-colors">
695-
<div class="flex items-start justify-between">
696-
<div class="flex items-center gap-3">
731+
732+
<!-- Capability Legend -->
733+
<div class="flex items-center gap-4 mb-4 px-1">
734+
<div class="flex items-center gap-1.5 text-[10px]">
735+
<span class="inline-block w-2 h-2 rounded-full bg-emerald-500" />
736+
<span class="opacity-50">Active by default</span>
737+
</div>
738+
<div class="flex items-center gap-1.5 text-[10px]">
739+
<span class="inline-block w-2 h-2 rounded-full border border-dashed border-amber-500" />
740+
<span class="opacity-50">Supported (opt-in)</span>
741+
</div>
742+
<div class="flex items-center gap-1.5 text-[10px]">
743+
<span class="inline-block w-2 h-2 rounded-full bg-gray-400/20" />
744+
<span class="opacity-50">Not supported</span>
745+
</div>
746+
</div>
747+
748+
<div class="space-y-2">
749+
<div
750+
v-for="(script, index) in scriptRegistry"
751+
:key="index"
752+
class="rounded-lg border n-border-base overflow-hidden hover:border-emerald-500/30 transition-colors group"
753+
>
754+
<div class="px-4 py-3 flex items-center gap-3">
755+
<!-- Logo -->
756+
<div class="w-7 h-7 flex items-center justify-center flex-shrink-0">
697757
<img
698758
v-if="script.logo && typeof script.logo === 'string' && script.logo.startsWith('http')"
699-
class="max-w-8 h-8"
759+
class="max-w-7 h-7"
700760
:src="typeof script.logo === 'object' ? script.logo.dark || script.logo.light : script.logo"
701761
alt="Script logo"
702762
>
703763
<div
704764
v-else-if="script.logo"
705-
class="max-w-8 h-8 flex items-center"
765+
class="max-w-7 h-7 flex items-center [&>svg]:max-h-7 [&>svg]:max-w-7"
706766
v-html="typeof script.logo === 'object' ? (script.logo.dark || script.logo.light) : script.logo"
707767
/>
708-
<div v-else class="max-w-8 h-8 flex items-center">
709-
<NIcon icon="carbon:script" class="text-blue-500 dark:text-blue-300 text-2xl" />
768+
<NIcon v-else icon="carbon:script" class="text-lg opacity-40" />
769+
</div>
770+
771+
<!-- Name + Category -->
772+
<div class="flex-1 min-w-0">
773+
<div class="flex items-center gap-2">
774+
<span class="font-semibold text-sm truncate">{{ script.label }}</span>
775+
<span class="text-[10px] px-1.5 py-0.5 rounded n-bg-active opacity-50 flex-shrink-0">{{ script.category }}</span>
710776
</div>
711-
<div>
712-
<h3 class="font-semibold text-lg">{{ script.label }}</h3>
713-
<p class="text-sm opacity-60">{{ script.description }}</p>
714-
<div class="flex items-center gap-2 mt-2">
715-
<span class="text-xs px-2 py-1 bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300 rounded">
716-
{{ script.category }}
717-
</span>
718-
<span v-if="script.src" class="text-xs opacity-40">{{ script.src }}</span>
719-
</div>
777+
<div v-if="script.src && script.src !== false" class="text-[10px] font-mono opacity-30 truncate mt-0.5">
778+
{{ script.src }}
779+
</div>
780+
<div v-else-if="script.src === false" class="text-[10px] font-mono opacity-30 mt-0.5">
781+
npm (no script download)
720782
</div>
721783
</div>
784+
785+
<!-- Capability Indicators -->
786+
<div class="flex items-center gap-1 flex-shrink-0">
787+
<VTooltip v-for="cap in capabilityDefs" :key="cap.key">
788+
<div
789+
class="w-7 h-7 rounded-md border flex items-center justify-center transition-all"
790+
:class="capStateClass(capState(script, cap.key))"
791+
>
792+
<NIcon :icon="cap.icon" class="text-sm" />
793+
</div>
794+
<template #popper>
795+
<div class="text-xs">
796+
<div class="font-semibold">{{ cap.label }}</div>
797+
<div class="opacity-70 mt-0.5">{{ capStateLabel(capState(script, cap.key)) }}</div>
798+
<div class="opacity-50 mt-1 max-w-48">{{ cap.desc }}</div>
799+
</div>
800+
</template>
801+
</VTooltip>
802+
</div>
803+
804+
<!-- Docs link -->
722805
<a
723806
:href="`https://scripts.nuxt.com/scripts/${script.label.toLowerCase().replace(/ /g, '-')}`"
724807
target="_blank"
725-
class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline flex-shrink-0"
808+
class="opacity-0 group-hover:opacity-60 transition-opacity ml-1 flex-shrink-0"
726809
>
727-
View docs
810+
<NIcon icon="carbon:launch" class="text-sm" />
728811
</a>
729812
</div>
730-
<div class="mt-3">
731-
<OCodeBlock
732-
:code="`const script = use${script.label.replace(/\s+/g, '')}()`"
733-
lang="typescript"
734-
class="text-sm rounded"
735-
/>
736-
</div>
813+
</div>
814+
</div>
815+
816+
<!-- Capability Matrix -->
817+
<div v-if="scriptRegistry.length > 3" class="mt-6 rounded-lg border n-border-base overflow-hidden">
818+
<div class="px-4 py-2.5 border-b n-border-base n-bg-active">
819+
<h4 class="text-xs font-medium uppercase tracking-wider opacity-50 flex items-center gap-1.5">
820+
<NIcon icon="carbon:data-table" class="text-sm" />
821+
Capability Matrix
822+
</h4>
823+
</div>
824+
<div class="overflow-x-auto">
825+
<table class="w-full text-xs">
826+
<thead>
827+
<tr class="border-b n-border-base">
828+
<th class="px-4 py-2 text-left font-medium opacity-40">Script</th>
829+
<th
830+
v-for="cap in capabilityDefs"
831+
:key="cap.key"
832+
class="px-3 py-2 text-center font-medium opacity-40 whitespace-nowrap"
833+
>
834+
<VTooltip>
835+
<span class="flex items-center justify-center gap-1">
836+
<NIcon :icon="cap.icon" class="text-xs" />
837+
{{ cap.label }}
838+
</span>
839+
<template #popper>
840+
<span class="text-xs">{{ cap.desc }}</span>
841+
</template>
842+
</VTooltip>
843+
</th>
844+
</tr>
845+
</thead>
846+
<tbody>
847+
<tr
848+
v-for="script in scriptRegistry"
849+
:key="script.registryKey || script.label"
850+
class="border-b n-border-base last:border-b-0 hover:n-bg-active transition-colors"
851+
>
852+
<td class="px-4 py-2 font-medium whitespace-nowrap">
853+
<div class="flex items-center gap-2">
854+
<div
855+
v-if="script.logo"
856+
class="w-4 h-4 flex items-center [&>svg]:max-h-4 [&>svg]:max-w-4"
857+
v-html="typeof script.logo === 'object' ? (script.logo.dark || script.logo.light) : script.logo"
858+
/>
859+
<NIcon v-else icon="carbon:script" class="text-xs opacity-40" />
860+
{{ script.label }}
861+
</div>
862+
</td>
863+
<td v-for="cap in capabilityDefs" :key="cap.key" class="px-3 py-2 text-center">
864+
<VTooltip>
865+
<div
866+
class="w-3 h-3 rounded-full mx-auto transition-colors"
867+
:class="{
868+
'bg-emerald-500': capState(script, cap.key) === 'active',
869+
'border-2 border-dashed border-amber-500/60': capState(script, cap.key) === 'available',
870+
'bg-gray-400/15': capState(script, cap.key) === 'off',
871+
}"
872+
/>
873+
<template #popper>
874+
<span class="text-xs">{{ capStateLabel(capState(script, cap.key)) }}</span>
875+
</template>
876+
</VTooltip>
877+
</td>
878+
</tr>
879+
</tbody>
880+
</table>
737881
</div>
738882
</div>
739883
</div>

0 commit comments

Comments
 (0)