@agent-native/core 0.172.6 → 0.172.9
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/changelog/2026-08-24-chat-draft-preservation.md +5 -0
- package/corpus/templates/clips/changelog/2026-08-24-muted-microphone-start-guidance.md +5 -0
- package/corpus/templates/clips/desktop/src/app.tsx +4 -24
- package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +3 -20
- package/corpus/templates/clips/desktop/src/styles.css +0 -20
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/org/workspace-app-links.d.ts +1 -0
- package/dist/client/org/workspace-app-links.js +18 -7
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/attribution.d.ts +33 -3
- package/dist/server/attribution.js +24 -2
- package/dist/server/auth.js +20 -5
- package/dist/server/better-auth-instance.d.ts +54 -2
- package/dist/server/better-auth-instance.js +94 -46
- package/dist/server/google-oauth.d.ts +9 -0
- package/dist/server/google-oauth.js +7 -5
- package/dist/server/identity-sso.js +4 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/tracking/providers.js +3 -0
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -1087,8 +1087,6 @@ export function App({
|
|
|
1087
1087
|
);
|
|
1088
1088
|
const [micOn, setMicOn] = useState<boolean>(() => loadBool(MIC_ON_KEY, true));
|
|
1089
1089
|
const [micOffConfirmOpen, setMicOffConfirmOpen] = useState(false);
|
|
1090
|
-
const pendingStartOptionsRef =
|
|
1091
|
-
useRef<Parameters<typeof handleStartRecording>[0]>(undefined);
|
|
1092
1090
|
const [systemAudioOn, setSystemAudioOn] = useState<boolean>(() =>
|
|
1093
1091
|
loadBool(SYSTEM_AUDIO_KEY, true),
|
|
1094
1092
|
);
|
|
@@ -3591,16 +3589,14 @@ export function App({
|
|
|
3591
3589
|
}, []);
|
|
3592
3590
|
|
|
3593
3591
|
// Gates every start-recording gesture (button, global shortcut, permission
|
|
3594
|
-
// retry) on the mic toggle. When the mic is off we
|
|
3595
|
-
//
|
|
3596
|
-
//
|
|
3597
|
-
// activation handleStartRecording needs, same as the direct gesture would.
|
|
3592
|
+
// retry) on the mic toggle. When the mic is off we show the informational
|
|
3593
|
+
// mic-off screen and wait for the user to go back and change that setting
|
|
3594
|
+
// before the actual getDisplayMedia/getUserMedia call runs.
|
|
3598
3595
|
function beginRecording(
|
|
3599
3596
|
options?: Parameters<typeof handleStartRecording>[0],
|
|
3600
3597
|
beginOptions?: { revealPopoverIfMicOff?: boolean },
|
|
3601
3598
|
) {
|
|
3602
3599
|
if (!micOn) {
|
|
3603
|
-
pendingStartOptionsRef.current = options;
|
|
3604
3600
|
if (beginOptions?.revealPopoverIfMicOff) {
|
|
3605
3601
|
invoke("show_popover").catch(() => {});
|
|
3606
3602
|
}
|
|
@@ -3611,21 +3607,9 @@ export function App({
|
|
|
3611
3607
|
}
|
|
3612
3608
|
|
|
3613
3609
|
function closeMicOffConfirmation() {
|
|
3614
|
-
pendingStartOptionsRef.current = undefined;
|
|
3615
3610
|
setMicOffConfirmOpen(false);
|
|
3616
3611
|
}
|
|
3617
3612
|
|
|
3618
|
-
function unmuteFromConfirmation() {
|
|
3619
|
-
setMicOn(true);
|
|
3620
|
-
closeMicOffConfirmation();
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
function continueWithoutMic() {
|
|
3624
|
-
const options = pendingStartOptionsRef.current;
|
|
3625
|
-
closeMicOffConfirmation();
|
|
3626
|
-
void handleStartRecording(options);
|
|
3627
|
-
}
|
|
3628
|
-
|
|
3629
3613
|
recordShortcutHandlerRef.current = () => {
|
|
3630
3614
|
if (recorder) {
|
|
3631
3615
|
emit("clips:recorder-stop").catch(() => {});
|
|
@@ -4319,11 +4303,7 @@ export function App({
|
|
|
4319
4303
|
return (
|
|
4320
4304
|
<div className="app app-recorder" ref={appRef}>
|
|
4321
4305
|
{micOffConfirmOpen ? (
|
|
4322
|
-
<MicOffConfirmation
|
|
4323
|
-
onBack={closeMicOffConfirmation}
|
|
4324
|
-
onUnmute={unmuteFromConfirmation}
|
|
4325
|
-
onContinue={continueWithoutMic}
|
|
4326
|
-
/>
|
|
4306
|
+
<MicOffConfirmation onBack={closeMicOffConfirmation} />
|
|
4327
4307
|
) : null}
|
|
4328
4308
|
|
|
4329
4309
|
<div
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { IconArrowLeft, IconMicrophone2 } from "@tabler/icons-react";
|
|
2
2
|
|
|
3
|
-
export function MicOffConfirmation({
|
|
4
|
-
onBack,
|
|
5
|
-
onUnmute,
|
|
6
|
-
onContinue,
|
|
7
|
-
}: {
|
|
8
|
-
onBack: () => void;
|
|
9
|
-
onUnmute: () => void;
|
|
10
|
-
onContinue: () => void;
|
|
11
|
-
}) {
|
|
3
|
+
export function MicOffConfirmation({ onBack }: { onBack: () => void }) {
|
|
12
4
|
return (
|
|
13
5
|
<section className="mic-off-confirmation" aria-labelledby="mic-off-title">
|
|
14
6
|
<header className="mic-off-confirmation-header">
|
|
@@ -29,19 +21,10 @@ export function MicOffConfirmation({
|
|
|
29
21
|
</div>
|
|
30
22
|
<h2 id="mic-off-title">Your mic is muted</h2>
|
|
31
23
|
<p>
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
You can change your microphone setting and then come back to start
|
|
25
|
+
recording.
|
|
34
26
|
</p>
|
|
35
27
|
</div>
|
|
36
|
-
|
|
37
|
-
<footer className="mic-off-confirmation-actions">
|
|
38
|
-
<button type="button" className="secondary" onClick={onUnmute}>
|
|
39
|
-
Unmute
|
|
40
|
-
</button>
|
|
41
|
-
<button type="button" className="primary" onClick={onContinue}>
|
|
42
|
-
Continue
|
|
43
|
-
</button>
|
|
44
|
-
</footer>
|
|
45
28
|
</section>
|
|
46
29
|
);
|
|
47
30
|
}
|
|
@@ -2177,26 +2177,6 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
2177
2177
|
line-height: 1.55;
|
|
2178
2178
|
}
|
|
2179
2179
|
|
|
2180
|
-
.mic-off-confirmation-actions {
|
|
2181
|
-
display: grid;
|
|
2182
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2183
|
-
gap: 10px;
|
|
2184
|
-
padding: 16px 18px 18px;
|
|
2185
|
-
border-top: 1px solid var(--border);
|
|
2186
|
-
background: var(--surface);
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
.mic-off-confirmation-actions .primary,
|
|
2190
|
-
.mic-off-confirmation-actions .secondary {
|
|
2191
|
-
width: auto;
|
|
2192
|
-
min-width: 0;
|
|
2193
|
-
height: 42px;
|
|
2194
|
-
padding: 0 12px;
|
|
2195
|
-
border-radius: var(--radius);
|
|
2196
|
-
font-size: 13px;
|
|
2197
|
-
white-space: nowrap;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
2180
|
/* ------------------------------------------------------------------------- */
|
|
2201
2181
|
/* Alert dialog */
|
|
2202
2182
|
/* ------------------------------------------------------------------------- */
|
|
@@ -1458,7 +1458,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
1458
1458
|
// Cleared on the next message send.
|
|
1459
1459
|
const [composerError, setComposerError] = useState(null);
|
|
1460
1460
|
const [composerText, setComposerText] = useState("");
|
|
1461
|
-
const composerDraftScope =
|
|
1461
|
+
const composerDraftScope = tabId || threadId;
|
|
1462
1462
|
const initialComposerText = useMemo(() => readAssistantChatComposerDraft(composerDraftScope), [composerDraftScope]);
|
|
1463
1463
|
const handleComposerTextChange = useCallback((text) => {
|
|
1464
1464
|
setComposerText(text);
|
|
@@ -20,6 +20,7 @@ export declare function isWorkspaceAppEnvironment(env?: RuntimeEnv): boolean;
|
|
|
20
20
|
export declare function dispatchOverviewHref(apps: OrgSwitcherAppLink[], env?: RuntimeEnv): string;
|
|
21
21
|
export declare function dispatchAppsHref(apps: OrgSwitcherAppLink[], env?: RuntimeEnv): string;
|
|
22
22
|
export declare function visibleOrgAppLinks(apps: OrgSwitcherAppLink[], max?: number): VisibleOrgAppLinks;
|
|
23
|
+
export declare function workspaceAppFetchUrls(env: RuntimeEnv): string[];
|
|
23
24
|
export interface UseOrgSwitcherAppLinksResult {
|
|
24
25
|
apps: OrgSwitcherAppLink[];
|
|
25
26
|
isWorkspace: boolean;
|
|
@@ -203,7 +203,7 @@ export function visibleOrgAppLinks(apps, max = ORG_SWITCHER_MAX_APP_LINKS) {
|
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
205
|
function initialWorkspaceLinks(env) {
|
|
206
|
-
return
|
|
206
|
+
return [
|
|
207
207
|
{
|
|
208
208
|
id: DISPATCH_ID,
|
|
209
209
|
name: "Dispatch",
|
|
@@ -213,20 +213,31 @@ function initialWorkspaceLinks(env) {
|
|
|
213
213
|
isDispatch: true,
|
|
214
214
|
status: "ready",
|
|
215
215
|
},
|
|
216
|
-
]
|
|
216
|
+
];
|
|
217
217
|
}
|
|
218
|
-
function workspaceAppFetchUrls(env) {
|
|
219
|
-
const urls = [
|
|
218
|
+
export function workspaceAppFetchUrls(env) {
|
|
219
|
+
const urls = [
|
|
220
|
+
"/_agent-native/actions/list-workspace-apps?includeAgentCards=false",
|
|
221
|
+
];
|
|
220
222
|
const gatewayUrl = envString(env, "VITE_WORKSPACE_GATEWAY_URL");
|
|
221
223
|
if (gatewayUrl) {
|
|
222
224
|
try {
|
|
223
|
-
|
|
225
|
+
const gateway = new URL(gatewayUrl);
|
|
226
|
+
const localGateway = [
|
|
227
|
+
"localhost",
|
|
228
|
+
"127.0.0.1",
|
|
229
|
+
"0.0.0.0",
|
|
230
|
+
"::1",
|
|
231
|
+
].includes(gateway.hostname.replace(/^\[|\]$/g, "").toLowerCase());
|
|
232
|
+
if (localGateway) {
|
|
233
|
+
urls.push(new URL("/_workspace/apps", `${stripTrailingSlash(gatewayUrl)}/`).toString());
|
|
234
|
+
}
|
|
224
235
|
}
|
|
225
236
|
catch {
|
|
226
|
-
//
|
|
237
|
+
// coercion-ok: malformed gateway URL leaves the authenticated action as
|
|
238
|
+
// the only hosted source.
|
|
227
239
|
}
|
|
228
240
|
}
|
|
229
|
-
urls.push("/_agent-native/actions/list-workspace-apps?includeAgentCards=false");
|
|
230
241
|
return urls;
|
|
231
242
|
}
|
|
232
243
|
export function useOrgSwitcherAppLinks(enabled) {
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
states: {
|
|
66
67
|
clientId: number;
|
|
67
68
|
state: string;
|
|
68
69
|
}[];
|
|
69
|
-
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
+
error?: undefined;
|
|
80
81
|
users: {
|
|
81
82
|
clientId: number;
|
|
82
83
|
lastSeen: number;
|
|
83
84
|
}[];
|
|
84
|
-
error?: undefined;
|
|
85
85
|
}>>;
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
ok?: undefined;
|
|
17
16
|
error: string;
|
|
17
|
+
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
+
error?: undefined;
|
|
44
45
|
summary: import("./types.js").TraceSummary;
|
|
45
46
|
spans: import("./types.js").TraceSpan[];
|
|
46
47
|
id?: undefined;
|
|
47
|
-
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
+
error?: undefined;
|
|
50
51
|
summary?: undefined;
|
|
51
52
|
spans?: undefined;
|
|
52
53
|
id: string;
|
|
53
|
-
error?: undefined;
|
|
54
54
|
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
59
59
|
error: any;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
+
error?: undefined;
|
|
62
63
|
summary?: undefined;
|
|
63
64
|
spans?: undefined;
|
|
64
65
|
id?: undefined;
|
|
65
|
-
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
37
37
|
ok?: undefined;
|
|
38
38
|
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
44
|
ok?: undefined;
|
|
45
45
|
error: string;
|
|
46
46
|
removed?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
|
|
|
58
58
|
error: string;
|
|
59
59
|
note?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -35,6 +35,21 @@ export interface FirstTouchAttribution {
|
|
|
35
35
|
/** ISO timestamp of when the visitor first landed. */
|
|
36
36
|
landed_at?: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Which flow created the account, as opposed to which credential it uses.
|
|
40
|
+
*
|
|
41
|
+
* Better Auth fires its `user.create.after` hook on every `user` row insert,
|
|
42
|
+
* but only some inserts are a person signing up in a browser. Every emitted
|
|
43
|
+
* `signup` event names its origin so a campaign report can select the ones
|
|
44
|
+
* that are acquisitions instead of counting row inserts.
|
|
45
|
+
*/
|
|
46
|
+
export type SignupOrigin =
|
|
47
|
+
/** A person completed a signup form or magic link in a browser. */
|
|
48
|
+
"browser_signup"
|
|
49
|
+
/** The framework's Google OAuth callback owns this event. */
|
|
50
|
+
| "google_oauth"
|
|
51
|
+
/** Federated SSO provisioning an identity into a sibling app. */
|
|
52
|
+
| "sso_jit";
|
|
38
53
|
/**
|
|
39
54
|
* Request-scoped browser attribution captured at the signup boundary.
|
|
40
55
|
* Better Auth may create the user in a later async callback where the
|
|
@@ -103,8 +118,15 @@ export declare function signupAttributionFromCookieHeader(cookieHeader: string |
|
|
|
103
118
|
* Capture all browser attribution needed by the server-side signup event.
|
|
104
119
|
* Keep this as one boundary helper so every signup entry point carries the
|
|
105
120
|
* same values into Better Auth's user-create hook.
|
|
121
|
+
*
|
|
122
|
+
* Returns `undefined` when the request carried neither cookie. A browser that
|
|
123
|
+
* ran our client script always has `an_ft`, so "no cookies at all" means no
|
|
124
|
+
* browser — a server-side backfill or provisioning call. Reporting that as
|
|
125
|
+
* `referral_source: "direct"` is the coercion that made this metric unusable:
|
|
126
|
+
* it renders "we never saw a visitor" identical to "a visitor arrived with no
|
|
127
|
+
* campaign", and only the second one is direct traffic.
|
|
106
128
|
*/
|
|
107
|
-
export declare function signupAttributionContextFromCookieHeader(cookieHeader: string | null | undefined): SignupAttributionContext;
|
|
129
|
+
export declare function signupAttributionContextFromCookieHeader(cookieHeader: string | null | undefined): SignupAttributionContext | undefined;
|
|
108
130
|
/** Serialize the bounded attribution handoff for a Better Auth request. */
|
|
109
131
|
export declare function encodeSignupAttributionContext(context: SignupAttributionContext): string;
|
|
110
132
|
/**
|
|
@@ -112,7 +134,15 @@ export declare function encodeSignupAttributionContext(context: SignupAttributio
|
|
|
112
134
|
* attribution, so the caller can safely fall back to the request cookie.
|
|
113
135
|
*/
|
|
114
136
|
export declare function decodeSignupAttributionContext(value: string | null | undefined): SignupAttributionContext | undefined;
|
|
115
|
-
/**
|
|
116
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Stamp the explicit handoff onto a Better Auth request/API header set.
|
|
139
|
+
*
|
|
140
|
+
* This is the only writer of the handoff header, and it always writes: with a
|
|
141
|
+
* context it sets ours, without one it deletes whatever was there. The header
|
|
142
|
+
* is unsigned and outranks the request cookie when the hook reads it, so an
|
|
143
|
+
* inbound copy from the public internet is an attacker-supplied `anonymous_id`
|
|
144
|
+
* and campaign for someone else's signup row. Never merge — replace.
|
|
145
|
+
*/
|
|
146
|
+
export declare function addSignupAttributionHeader(headers: HeadersInit | undefined, context: SignupAttributionContext | undefined): Headers;
|
|
117
147
|
/** Read the explicit handoff from Better Auth's request context headers. */
|
|
118
148
|
export declare function signupAttributionContextFromHeaders(headers: Headers | null | undefined): SignupAttributionContext | undefined;
|
|
@@ -185,11 +185,21 @@ export function signupAttributionFromCookieHeader(cookieHeader) {
|
|
|
185
185
|
* Capture all browser attribution needed by the server-side signup event.
|
|
186
186
|
* Keep this as one boundary helper so every signup entry point carries the
|
|
187
187
|
* same values into Better Auth's user-create hook.
|
|
188
|
+
*
|
|
189
|
+
* Returns `undefined` when the request carried neither cookie. A browser that
|
|
190
|
+
* ran our client script always has `an_ft`, so "no cookies at all" means no
|
|
191
|
+
* browser — a server-side backfill or provisioning call. Reporting that as
|
|
192
|
+
* `referral_source: "direct"` is the coercion that made this metric unusable:
|
|
193
|
+
* it renders "we never saw a visitor" identical to "a visitor arrived with no
|
|
194
|
+
* campaign", and only the second one is direct traffic.
|
|
188
195
|
*/
|
|
189
196
|
export function signupAttributionContextFromCookieHeader(cookieHeader) {
|
|
197
|
+
const firstTouch = readFirstTouchAttribution(cookieHeader);
|
|
190
198
|
const anonymousId = readAnalyticsAnonymousId(cookieHeader);
|
|
199
|
+
if (!firstTouch && !anonymousId)
|
|
200
|
+
return undefined;
|
|
191
201
|
return {
|
|
192
|
-
attribution:
|
|
202
|
+
attribution: deriveSignupAttribution(firstTouch),
|
|
193
203
|
...(anonymousId ? { anonymousId } : {}),
|
|
194
204
|
};
|
|
195
205
|
}
|
|
@@ -236,9 +246,21 @@ export function decodeSignupAttributionContext(value) {
|
|
|
236
246
|
return undefined;
|
|
237
247
|
}
|
|
238
248
|
}
|
|
239
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Stamp the explicit handoff onto a Better Auth request/API header set.
|
|
251
|
+
*
|
|
252
|
+
* This is the only writer of the handoff header, and it always writes: with a
|
|
253
|
+
* context it sets ours, without one it deletes whatever was there. The header
|
|
254
|
+
* is unsigned and outranks the request cookie when the hook reads it, so an
|
|
255
|
+
* inbound copy from the public internet is an attacker-supplied `anonymous_id`
|
|
256
|
+
* and campaign for someone else's signup row. Never merge — replace.
|
|
257
|
+
*/
|
|
240
258
|
export function addSignupAttributionHeader(headers, context) {
|
|
241
259
|
const result = new Headers(headers);
|
|
260
|
+
if (!context) {
|
|
261
|
+
result.delete(SIGNUP_ATTRIBUTION_HEADER_NAME);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
242
264
|
result.set(SIGNUP_ATTRIBUTION_HEADER_NAME, encodeSignupAttributionContext(context));
|
|
243
265
|
return result;
|
|
244
266
|
}
|
package/dist/server/auth.js
CHANGED
|
@@ -61,7 +61,7 @@ import { getAppProductionUrl } from "./app-url.js";
|
|
|
61
61
|
import { addSignupAttributionHeader, readAnalyticsAnonymousId, readFirstTouchAttribution, signupAttributionContextFromCookieHeader, signupAttributionFromCookieHeader, } from "./attribution.js";
|
|
62
62
|
import { getAuthLoginMode } from "./auth-login-mode.js";
|
|
63
63
|
import { injectBetaOptOutPersistence } from "./beta-opt-out-html.js";
|
|
64
|
-
import { createBetterAuthSessionForEmail, ensureGoogleAuthIdentity, getAuthSecret, getBetterAuth, getBetterAuthSync, isDeployPreview, } from "./better-auth-instance.js";
|
|
64
|
+
import { createBetterAuthSessionForEmail, ensureGoogleAuthIdentity, getBetterAuthUserIdForEmail, getAuthSecret, getBetterAuth, getBetterAuthSync, isDeployPreview, } from "./better-auth-instance.js";
|
|
65
65
|
import { BUILDER_CONNECT_PARAM, BUILDER_RELAY_PATH, BUILDER_RELAY_STATE_PARAM, verifyBuilderConnectTokenAndGetOwner, verifyBuilderPreviewRelayStateForCallback, } from "./builder-browser.js";
|
|
66
66
|
import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
|
|
67
67
|
import { getAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
|
|
@@ -93,6 +93,13 @@ function withSignupAttributionContext(cookieHeader, fn) {
|
|
|
93
93
|
signupAttribution,
|
|
94
94
|
}, fn);
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Replace the internal attribution handoff on a request bound for Better
|
|
98
|
+
* Auth. Call this on every such request, including the ones with no
|
|
99
|
+
* attribution to add — the header is unsigned and outranks the request cookie
|
|
100
|
+
* inside the user-create hook, so an inbound copy is a stranger writing the
|
|
101
|
+
* `anonymous_id` and campaign of somebody else's signup.
|
|
102
|
+
*/
|
|
96
103
|
function requestWithSignupAttribution(request, signupAttribution) {
|
|
97
104
|
return new Request(request, {
|
|
98
105
|
headers: addSignupAttributionHeader(request.headers, signupAttribution),
|
|
@@ -3357,10 +3364,20 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3357
3364
|
mobile,
|
|
3358
3365
|
trackSignup: {
|
|
3359
3366
|
authProvider: "google",
|
|
3360
|
-
|
|
3367
|
+
// The signup event joins to `referrer_user` in the virality
|
|
3368
|
+
// panels, which is always a Better Auth id — the Google profile
|
|
3369
|
+
// id that used to go here joined to nothing. Only looked up when
|
|
3370
|
+
// the event will actually be emitted.
|
|
3371
|
+
authUserId: isNewGoogleUser
|
|
3372
|
+
? await getBetterAuthUserIdForEmail(email)
|
|
3373
|
+
: undefined,
|
|
3361
3374
|
name: typeof user.name === "string" ? user.name : undefined,
|
|
3362
3375
|
attribution: signupAttribution,
|
|
3363
3376
|
signupAnonymousId,
|
|
3377
|
+
// `ensureGoogleAuthIdentity` above already wrote the canonical
|
|
3378
|
+
// row, so this callback is the only thing that still knows
|
|
3379
|
+
// whether the person is new.
|
|
3380
|
+
isNewUser: isNewGoogleUser,
|
|
3364
3381
|
},
|
|
3365
3382
|
});
|
|
3366
3383
|
logGoogleOAuthDebug(event, "callback-session-created", {
|
|
@@ -3859,9 +3876,7 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3859
3876
|
// validation error.
|
|
3860
3877
|
}
|
|
3861
3878
|
}
|
|
3862
|
-
|
|
3863
|
-
requestForAuth = requestWithSignupAttribution(requestForAuth, signupAttribution);
|
|
3864
|
-
}
|
|
3879
|
+
requestForAuth = requestWithSignupAttribution(requestForAuth, signupAttribution);
|
|
3865
3880
|
let response;
|
|
3866
3881
|
try {
|
|
3867
3882
|
response = await (isEmailSignup
|
|
@@ -6,12 +6,62 @@
|
|
|
6
6
|
* implementation detail behind those interfaces.
|
|
7
7
|
*/
|
|
8
8
|
import { type BetterAuthOptions } from "better-auth";
|
|
9
|
+
import { type SignupOrigin } from "./attribution.js";
|
|
9
10
|
export { getAuthLoginMode, resolveAuthLoginMode, resolveAuthLoginModeFromReadiness, type AuthLoginMode, } from "./auth-login-mode.js";
|
|
10
11
|
export declare function hasBetterAuthUserEmail(email: string): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* The canonical Better Auth user id for an email, or `undefined` when there
|
|
14
|
+
* is no row (or the adapter is unavailable).
|
|
15
|
+
*
|
|
16
|
+
* Signup events must carry this id, not the identity provider's subject id:
|
|
17
|
+
* the virality panels self-join `auth_user_id` against `referrer_user`, and
|
|
18
|
+
* `referrer_user` is always a Better Auth id, so a Google profile id in that
|
|
19
|
+
* column silently joins to nothing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getBetterAuthUserIdForEmail(email: string): Promise<string | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* The endpoint context Better Auth (1.6.x) hands to `user.create.after`. It is
|
|
24
|
+
* `null` for any row created through `internalAdapter` outside an endpoint —
|
|
25
|
+
* see `emitSignupEventForCreatedUser`.
|
|
26
|
+
*/
|
|
27
|
+
export interface BetterAuthUserCreateContext {
|
|
28
|
+
headers?: Headers | null;
|
|
29
|
+
request?: {
|
|
30
|
+
headers?: Headers | null;
|
|
31
|
+
url?: string;
|
|
32
|
+
} | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Emit the `signup` event for a freshly created Better Auth `user` row — but
|
|
36
|
+
* only when that row is an actual person signing up.
|
|
37
|
+
*
|
|
38
|
+
* Better Auth runs its create hook on every `user` insert. Only inserts made
|
|
39
|
+
* through one of its HTTP endpoints carry the originating request; everything
|
|
40
|
+
* that reaches the row through `internalAdapter` directly gets a null context,
|
|
41
|
+
* because `getCurrentAuthContext()` throws outside an endpoint. Two production
|
|
42
|
+
* paths do exactly that, and neither is an acquisition:
|
|
43
|
+
*
|
|
44
|
+
* - `ensureCanonicalUserForLegacySession` backfills a canonical row for
|
|
45
|
+
* someone who signed up long ago, on an ordinary authenticated request.
|
|
46
|
+
* - `ensureGoogleAuthIdentity` provisions the canonical row during the Google
|
|
47
|
+
* callback, whose own attributed event `createOAuthSession` emits.
|
|
48
|
+
*
|
|
49
|
+
* Emitting for those is what broke campaign reporting: they arrived with no
|
|
50
|
+
* browser context and were recorded as `referral_source: "direct"`, so ~94% of
|
|
51
|
+
* `better-auth` signups were unattributable rows that were never signups, and
|
|
52
|
+
* one person provisioned across sibling apps counted as a dozen. A row insert
|
|
53
|
+
* is not an acquisition, so a context-free insert emits nothing at all.
|
|
54
|
+
*/
|
|
55
|
+
export declare function emitSignupEventForCreatedUser(user: {
|
|
56
|
+
id?: string;
|
|
57
|
+
email?: string;
|
|
58
|
+
name?: string | null;
|
|
59
|
+
}, context?: BetterAuthUserCreateContext | null): Promise<void>;
|
|
11
60
|
/** Return whether the canonical user has a verified Google account link. */
|
|
12
61
|
export declare function hasGoogleAuthIdentity(email: string): Promise<boolean | undefined>;
|
|
13
|
-
export declare function trackSignupEvent({ authProvider, authUserId, email, name, attribution, anonymousId, }: {
|
|
62
|
+
export declare function trackSignupEvent({ authProvider, origin, authUserId, email, name, attribution, anonymousId, }: {
|
|
14
63
|
authProvider: string;
|
|
64
|
+
origin: SignupOrigin;
|
|
15
65
|
authUserId?: string;
|
|
16
66
|
email: string;
|
|
17
67
|
name?: string | null;
|
|
@@ -20,7 +70,9 @@ export declare function trackSignupEvent({ authProvider, authUserId, email, name
|
|
|
20
70
|
* cookie (see `server/attribution.ts`). Snake_case keys such as
|
|
21
71
|
* `referral_source`, `referrer_user`, and the UTM passthrough are merged
|
|
22
72
|
* into the `signup` event so we can measure where new users came from.
|
|
23
|
-
* `undefined` values are dropped; a missing object is a clean no-op
|
|
73
|
+
* `undefined` values are dropped; a missing object is a clean no-op — and
|
|
74
|
+
* it must stay a no-op rather than defaulting to `direct`, so a signup we
|
|
75
|
+
* could not attribute stays visibly different from an unattributed visit.
|
|
24
76
|
*/
|
|
25
77
|
attribution?: Record<string, string | undefined>;
|
|
26
78
|
anonymousId?: string;
|
|
@@ -56,6 +56,95 @@ export async function hasBetterAuthUserEmail(email) {
|
|
|
56
56
|
.catch(() => null);
|
|
57
57
|
return !!existing?.user?.email;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The canonical Better Auth user id for an email, or `undefined` when there
|
|
61
|
+
* is no row (or the adapter is unavailable).
|
|
62
|
+
*
|
|
63
|
+
* Signup events must carry this id, not the identity provider's subject id:
|
|
64
|
+
* the virality panels self-join `auth_user_id` against `referrer_user`, and
|
|
65
|
+
* `referrer_user` is always a Better Auth id, so a Google profile id in that
|
|
66
|
+
* column silently joins to nothing.
|
|
67
|
+
*/
|
|
68
|
+
export async function getBetterAuthUserIdForEmail(email) {
|
|
69
|
+
const adapter = await getBetterAuthInternalAdapter();
|
|
70
|
+
if (!adapter)
|
|
71
|
+
return undefined;
|
|
72
|
+
try {
|
|
73
|
+
const existing = await adapter.findUserByEmail(email.trim().toLowerCase(), {
|
|
74
|
+
includeAccounts: false,
|
|
75
|
+
});
|
|
76
|
+
return existing?.user?.id || undefined;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// coercion-ok: this is analytics enrichment on the sign-in path. Failing
|
|
80
|
+
// the sign-in over it would be worse, and the only consequence is one
|
|
81
|
+
// event missing `auth_user_id` — never a wrong id. Logged, not swallowed.
|
|
82
|
+
console.error("[auth] failed to resolve the canonical user id for a signup event", error);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Emit the `signup` event for a freshly created Better Auth `user` row — but
|
|
88
|
+
* only when that row is an actual person signing up.
|
|
89
|
+
*
|
|
90
|
+
* Better Auth runs its create hook on every `user` insert. Only inserts made
|
|
91
|
+
* through one of its HTTP endpoints carry the originating request; everything
|
|
92
|
+
* that reaches the row through `internalAdapter` directly gets a null context,
|
|
93
|
+
* because `getCurrentAuthContext()` throws outside an endpoint. Two production
|
|
94
|
+
* paths do exactly that, and neither is an acquisition:
|
|
95
|
+
*
|
|
96
|
+
* - `ensureCanonicalUserForLegacySession` backfills a canonical row for
|
|
97
|
+
* someone who signed up long ago, on an ordinary authenticated request.
|
|
98
|
+
* - `ensureGoogleAuthIdentity` provisions the canonical row during the Google
|
|
99
|
+
* callback, whose own attributed event `createOAuthSession` emits.
|
|
100
|
+
*
|
|
101
|
+
* Emitting for those is what broke campaign reporting: they arrived with no
|
|
102
|
+
* browser context and were recorded as `referral_source: "direct"`, so ~94% of
|
|
103
|
+
* `better-auth` signups were unattributable rows that were never signups, and
|
|
104
|
+
* one person provisioned across sibling apps counted as a dozen. A row insert
|
|
105
|
+
* is not an acquisition, so a context-free insert emits nothing at all.
|
|
106
|
+
*/
|
|
107
|
+
export async function emitSignupEventForCreatedUser(user, context) {
|
|
108
|
+
const email = user?.email;
|
|
109
|
+
if (!email)
|
|
110
|
+
return;
|
|
111
|
+
const requestHeaders = context?.headers ?? context?.request?.headers ?? null;
|
|
112
|
+
if (!requestHeaders)
|
|
113
|
+
return;
|
|
114
|
+
const scoped = hasContinuationLocalRequestContext()
|
|
115
|
+
? getRequestContext()
|
|
116
|
+
: undefined;
|
|
117
|
+
let attribution;
|
|
118
|
+
let anonymousId;
|
|
119
|
+
try {
|
|
120
|
+
const browser =
|
|
121
|
+
// The signed magic-link token is the only source that survives the link
|
|
122
|
+
// being opened in a different browser than the one that requested it.
|
|
123
|
+
(context?.request?.url?.includes("newUserCallbackURL")
|
|
124
|
+
? readMagicLinkSignupAttribution(context.request.url, getAuthSecret())
|
|
125
|
+
: undefined) ??
|
|
126
|
+
scoped?.signupAttribution ??
|
|
127
|
+
signupAttributionContextFromHeaders(requestHeaders) ??
|
|
128
|
+
signupAttributionContextFromCookieHeader(requestHeaders.get("cookie"));
|
|
129
|
+
attribution = browser?.attribution;
|
|
130
|
+
anonymousId = browser?.anonymousId;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
// Analytics must never block signup, but a parse failure is not "this
|
|
134
|
+
// visitor came direct" — leave both unset so the event shows the gap
|
|
135
|
+
// instead of inventing a source for it.
|
|
136
|
+
console.error("[auth] failed to derive signup attribution", err);
|
|
137
|
+
}
|
|
138
|
+
await trackSignupEvent({
|
|
139
|
+
authProvider: "better-auth",
|
|
140
|
+
origin: scoped?.signupOrigin ?? "browser_signup",
|
|
141
|
+
authUserId: user.id,
|
|
142
|
+
email,
|
|
143
|
+
name: user.name,
|
|
144
|
+
attribution,
|
|
145
|
+
anonymousId,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
59
148
|
/** Return whether the canonical user has a verified Google account link. */
|
|
60
149
|
export async function hasGoogleAuthIdentity(email) {
|
|
61
150
|
const adapter = await getBetterAuthInternalAdapter();
|
|
@@ -67,7 +156,7 @@ export async function hasGoogleAuthIdentity(email) {
|
|
|
67
156
|
return (existing?.accounts.some((account) => account.providerId === "google") ??
|
|
68
157
|
false);
|
|
69
158
|
}
|
|
70
|
-
export async function trackSignupEvent({ authProvider, authUserId, email, name, attribution, anonymousId, }) {
|
|
159
|
+
export async function trackSignupEvent({ authProvider, origin, authUserId, email, name, attribution, anonymousId, }) {
|
|
71
160
|
identify(email, {
|
|
72
161
|
email,
|
|
73
162
|
name: name ?? undefined,
|
|
@@ -84,6 +173,7 @@ export async function trackSignupEvent({ authProvider, authUserId, email, name,
|
|
|
84
173
|
track("signup", {
|
|
85
174
|
...resolveSignupTrackingProperties(),
|
|
86
175
|
auth_provider: authProvider,
|
|
176
|
+
signup_origin: origin,
|
|
87
177
|
...(authUserId ? { auth_user_id: authUserId } : {}),
|
|
88
178
|
...cleanAttribution,
|
|
89
179
|
}, { userId: email, ...(anonymousId ? { anonymousId } : {}) });
|
|
@@ -1064,54 +1154,12 @@ async function createBetterAuthInstance(config) {
|
|
|
1064
1154
|
// signups the callback request's headers), which is where the
|
|
1065
1155
|
// browser's `an_ft` first-touch cookie rides in.
|
|
1066
1156
|
context) => {
|
|
1067
|
-
// When a newly-created user's email has pending org invitations
|
|
1068
|
-
// (common when someone is invited *before* they've signed up),
|
|
1069
|
-
// auto-accept them so the user lands in the org on their very
|
|
1070
|
-
// first page load instead of a blank-slate workspace.
|
|
1071
1157
|
const email = user?.email;
|
|
1072
1158
|
if (!email)
|
|
1073
1159
|
return;
|
|
1074
|
-
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
let attribution;
|
|
1078
|
-
let anonymousId;
|
|
1079
|
-
try {
|
|
1080
|
-
const cookieHeader = context?.headers?.get("cookie") ??
|
|
1081
|
-
context?.request?.headers?.get("cookie") ??
|
|
1082
|
-
null;
|
|
1083
|
-
const scopedSignupAttribution = hasContinuationLocalRequestContext()
|
|
1084
|
-
? getRequestContext()?.signupAttribution
|
|
1085
|
-
: undefined;
|
|
1086
|
-
const requestSignupAttribution = signupAttributionContextFromCookieHeader(cookieHeader);
|
|
1087
|
-
const headerSignupAttribution = signupAttributionContextFromHeaders(context?.headers) ??
|
|
1088
|
-
signupAttributionContextFromHeaders(context?.request?.headers);
|
|
1089
|
-
const magicLinkAttribution = context?.request?.url?.includes("newUserCallbackURL")
|
|
1090
|
-
? readMagicLinkSignupAttribution(context.request.url, getAuthSecret())
|
|
1091
|
-
: undefined;
|
|
1092
|
-
attribution =
|
|
1093
|
-
magicLinkAttribution?.attribution ??
|
|
1094
|
-
scopedSignupAttribution?.attribution ??
|
|
1095
|
-
headerSignupAttribution?.attribution ??
|
|
1096
|
-
requestSignupAttribution.attribution;
|
|
1097
|
-
anonymousId =
|
|
1098
|
-
magicLinkAttribution?.anonymousId ??
|
|
1099
|
-
scopedSignupAttribution?.anonymousId ??
|
|
1100
|
-
headerSignupAttribution?.anonymousId ??
|
|
1101
|
-
requestSignupAttribution.anonymousId;
|
|
1102
|
-
}
|
|
1103
|
-
catch (err) {
|
|
1104
|
-
console.error("[auth] failed to derive signup attribution", err);
|
|
1105
|
-
attribution = undefined;
|
|
1106
|
-
}
|
|
1107
|
-
await trackSignupEvent({
|
|
1108
|
-
authProvider: "better-auth",
|
|
1109
|
-
authUserId: user.id,
|
|
1110
|
-
email,
|
|
1111
|
-
name: user.name,
|
|
1112
|
-
attribution,
|
|
1113
|
-
anonymousId,
|
|
1114
|
-
});
|
|
1160
|
+
await emitSignupEventForCreatedUser(user, context);
|
|
1161
|
+
// Invitations and domain auto-join are about the row existing, not
|
|
1162
|
+
// about how it got here, so they run for backfills too.
|
|
1115
1163
|
try {
|
|
1116
1164
|
await acceptPendingInvitationsForEmail(email);
|
|
1117
1165
|
}
|
|
@@ -192,6 +192,15 @@ export declare function createOAuthSession(event: H3Event, email: string, opts:
|
|
|
192
192
|
name?: string | null;
|
|
193
193
|
attribution?: Record<string, string | undefined>;
|
|
194
194
|
signupAnonymousId?: string;
|
|
195
|
+
/**
|
|
196
|
+
* Whether this callback created the account, decided by the caller at
|
|
197
|
+
* the moment it created it. Callers that provision the canonical user
|
|
198
|
+
* before getting here MUST pass this: the `hasBetterAuthUserEmail`
|
|
199
|
+
* probe below then reads the row they just wrote and concludes the
|
|
200
|
+
* person is an existing user, which silently deleted the only
|
|
201
|
+
* attributed signup event Google sign-in produces.
|
|
202
|
+
*/
|
|
203
|
+
isNewUser?: boolean;
|
|
195
204
|
};
|
|
196
205
|
}): Promise<OAuthSessionResult>;
|
|
197
206
|
/**
|
|
@@ -562,11 +562,12 @@ export async function createOAuthSession(event, email, opts) {
|
|
|
562
562
|
let shouldTrackSignup = false;
|
|
563
563
|
if (!opts.hasProductionSession || needsDeepLink) {
|
|
564
564
|
if (opts.trackSignup && !opts.hasProductionSession) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
565
|
+
shouldTrackSignup =
|
|
566
|
+
opts.trackSignup.isNewUser ??
|
|
567
|
+
(await Promise.all([
|
|
568
|
+
hasLegacySessionForEmail(email).catch(() => true),
|
|
569
|
+
hasBetterAuthUserEmail(email).catch(() => true),
|
|
570
|
+
]).then(([hasLegacySession, hasUser]) => !hasLegacySession && !hasUser));
|
|
570
571
|
}
|
|
571
572
|
sessionToken = crypto.randomBytes(32).toString("hex");
|
|
572
573
|
await addSession(sessionToken, email);
|
|
@@ -578,6 +579,7 @@ export async function createOAuthSession(event, email, opts) {
|
|
|
578
579
|
readAnalyticsAnonymousId(getHeader(event, "cookie") ?? null);
|
|
579
580
|
await trackSignupEvent({
|
|
580
581
|
authProvider: opts.trackSignup.authProvider,
|
|
582
|
+
origin: "google_oauth",
|
|
581
583
|
authUserId: opts.trackSignup.authUserId,
|
|
582
584
|
email,
|
|
583
585
|
name: opts.trackSignup.name,
|
|
@@ -474,6 +474,10 @@ export async function handleIdentitySso(event, subpath) {
|
|
|
474
474
|
? runWithRequestContext({
|
|
475
475
|
...(getRequestContext() ?? {}),
|
|
476
476
|
signupAttribution,
|
|
477
|
+
// This person already signed up somewhere; we are provisioning
|
|
478
|
+
// them into this app. Counting it as an acquisition is how one
|
|
479
|
+
// human became a dozen "signups" across sibling apps.
|
|
480
|
+
signupOrigin: "sso_jit",
|
|
477
481
|
}, linkIdentity)
|
|
478
482
|
: linkIdentity());
|
|
479
483
|
}
|
|
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
expiresAt?: undefined;
|
|
27
27
|
ttlSeconds?: undefined;
|
|
28
28
|
} | {
|
|
29
|
+
error?: undefined;
|
|
29
30
|
token: string;
|
|
30
31
|
expiresAt: string;
|
|
31
32
|
ttlSeconds: number;
|
|
32
|
-
error?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -115,6 +115,12 @@ export interface RequestContext {
|
|
|
115
115
|
* async user-create hook. Analytics-only; never used for authorization.
|
|
116
116
|
*/
|
|
117
117
|
signupAttribution?: SignupAttributionContext;
|
|
118
|
+
/**
|
|
119
|
+
* Which flow is creating a user row on this request. Set by callers that
|
|
120
|
+
* provision an identity rather than acquire a new person, so the signup
|
|
121
|
+
* event they trigger says so instead of impersonating a browser signup.
|
|
122
|
+
*/
|
|
123
|
+
signupOrigin?: import("./attribution.js").SignupOrigin;
|
|
118
124
|
/** Canonical client surface for analytics attribution. */
|
|
119
125
|
clientPlatform?: import("../shared/analytics-platform.js").AnalyticsClientPlatform;
|
|
120
126
|
/**
|
|
@@ -324,6 +324,9 @@ function createWebhookProvider(url, authHeader) {
|
|
|
324
324
|
event: event.name,
|
|
325
325
|
properties: event.properties,
|
|
326
326
|
userId: event.userId,
|
|
327
|
+
// Without this a webhook consumer cannot join a signup back to the
|
|
328
|
+
// anonymous pageviews that preceded it — the whole point of the id.
|
|
329
|
+
anonymousId: event.anonymousId,
|
|
327
330
|
sessionId: event.sessionId,
|
|
328
331
|
timestamp: event.timestamp,
|
|
329
332
|
}), extra);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.172.
|
|
3
|
+
"version": "0.172.9",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|