@agent-native/core 0.172.4 → 0.172.8
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/corpus/templates/factory/AGENTS.md +5 -3
- package/corpus/templates/factory/actions/babysit-pull-request.ts +6 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +2 -2
- package/corpus/templates/factory/actions/save-factory-automation.ts +0 -5
- package/corpus/templates/factory/changelog/2026-08-24-slack-checking-works-for-teammate-created-factories.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-24-teammates-can-edit-and-run-factory-jobs.md +6 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +8 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +1 -9
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -1
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +20 -0
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/settings/SettingsPanel.js +80 -33
- package/dist/jobs/run-now.js +2 -2
- 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/request-context.d.ts +6 -0
- package/dist/tracking/providers.js +3 -0
- package/dist/vite/client.js +5 -2
- package/docs/content/docs-components.mdx +20 -1
- package/docs/content/template-calendar-agent.mdx +63 -26
- package/docs/content/template-calendar-developers.mdx +165 -40
- package/docs/content/template-calendar-features.mdx +276 -0
- package/docs/content/template-calendar-integrations.mdx +78 -0
- package/docs/content/template-calendar.mdx +56 -123
- package/package.json +3 -3
- package/docs/content/template-calendar-booking-links.mdx +0 -73
- package/docs/content/template-calendar-scheduling.mdx +0 -128
package/dist/jobs/run-now.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AGENT_CHAT_BACKGROUND_RUN_FIELD, dispatchPathTargetsNetlifyBackgroundFunction, resolveAgentChatProcessRunDispatchPath, } from "../agent/durable-background.js";
|
|
2
|
-
import {
|
|
2
|
+
import { canQueueAutomationRunNow, } from "../automations/service.js";
|
|
3
3
|
import { isLocalDatabase } from "../db/client.js";
|
|
4
4
|
import { organizationResourceOwner, resourceGetByPath, } from "../resources/store.js";
|
|
5
5
|
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
@@ -77,7 +77,7 @@ export async function queueAutomationRunNow(input) {
|
|
|
77
77
|
statusCode: 404,
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
-
if (!(await
|
|
80
|
+
if (!(await canQueueAutomationRunNow(input, resource, input.scope))) {
|
|
81
81
|
throw Object.assign(new Error("Only the automation's creator or an organization admin can run it."), { statusCode: 403 });
|
|
82
82
|
}
|
|
83
83
|
const { body } = parseJobResource(resource.content);
|
|
@@ -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
|
}
|
|
@@ -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/dist/vite/client.js
CHANGED
|
@@ -1680,6 +1680,10 @@ function virtualModuleIdFromRuntimeUrl(runtimeUrl) {
|
|
|
1680
1680
|
}
|
|
1681
1681
|
async function loadMountedEmbedRuntimeModule(server, runtimeUrl) {
|
|
1682
1682
|
const virtualId = virtualModuleIdFromRuntimeUrl(runtimeUrl);
|
|
1683
|
+
// transform import to encode virtual modules in vite as these imports don't work in the browser
|
|
1684
|
+
const result = await server.transformRequest(virtualId ?? runtimeUrl);
|
|
1685
|
+
if (typeof result?.code === "string")
|
|
1686
|
+
return result.code;
|
|
1683
1687
|
if (virtualId) {
|
|
1684
1688
|
const loaded = await server.pluginContainer?.load?.(virtualId);
|
|
1685
1689
|
if (typeof loaded === "string")
|
|
@@ -1687,8 +1691,7 @@ async function loadMountedEmbedRuntimeModule(server, runtimeUrl) {
|
|
|
1687
1691
|
if (loaded && typeof loaded.code === "string")
|
|
1688
1692
|
return loaded.code;
|
|
1689
1693
|
}
|
|
1690
|
-
|
|
1691
|
-
return result?.code ?? null;
|
|
1694
|
+
return null;
|
|
1692
1695
|
}
|
|
1693
1696
|
function serveMountedEmbedRuntimeModule(server, req, res, base) {
|
|
1694
1697
|
if (req.method !== "GET" && req.method !== "HEAD")
|
|
@@ -197,6 +197,7 @@ Each card is a `###` heading (optionally linked) followed by the description. Ca
|
|
|
197
197
|
| `plug` | integrations, connected tools |
|
|
198
198
|
| `network` | cross-app or distributed communication |
|
|
199
199
|
| `terminal` | CLI, scripts, terminal-driven workflows |
|
|
200
|
+
| `calendar` | scheduling, dates, time-based features |
|
|
200
201
|
|
|
201
202
|
The grid defaults to 3 columns. Set `columns` (1-4) on the opening tag to override it, e.g. `<Cards columns={2}>`. Below 640px the grid always collapses to a single column regardless of `columns`.
|
|
202
203
|
|
|
@@ -337,7 +338,25 @@ Define one action. The agent calls it as a tool, the UI calls it through a React
|
|
|
337
338
|
</Comparison>
|
|
338
339
|
```
|
|
339
340
|
|
|
340
|
-
Add more `###` sections to get 3 or 4 columns (max 4). The red/green label accent
|
|
341
|
+
Add more `###` sections to get 3 or 4 columns (max 4). The red/green label accent applies automatically to headings that exactly match `Before`, `Old`, `Without` (red) or `After`, `New`, `With` (green).
|
|
342
|
+
|
|
343
|
+
For any other pairing, an explicit `:color:` prefix sets a column's accent directly, the same shortcode idiom `Cards` uses for `:icon:`:
|
|
344
|
+
|
|
345
|
+
```mdx
|
|
346
|
+
<Comparison>
|
|
347
|
+
|
|
348
|
+
### :blue: You ask
|
|
349
|
+
|
|
350
|
+
"Am I free Thursday afternoon?"
|
|
351
|
+
|
|
352
|
+
### :green: What happens
|
|
353
|
+
|
|
354
|
+
The agent answers in chat. Nothing on screen changes.
|
|
355
|
+
|
|
356
|
+
</Comparison>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`color` accepts `blue`, `green`, `red`, or `yellow`, matching `Badge`'s color naming. Each routes through a real theme token (the site's brand blue, or the same semantic red/yellow/green used elsewhere) rather than a raw color, so it stays correct in both themes. An explicit `color` always wins over the Before/After heuristic.
|
|
341
360
|
|
|
342
361
|
---
|
|
343
362
|
|