@growth-labs/auth 0.7.2 → 0.7.4
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/README.md +20 -15
- package/dist/routes/api/code-request.d.ts.map +1 -1
- package/dist/routes/api/code-request.js +1 -1
- package/dist/routes/api/code-request.js.map +1 -1
- package/dist/routes/api/password-reset-confirm.d.ts.map +1 -1
- package/dist/routes/api/password-reset-confirm.js +1 -1
- package/dist/routes/api/password-reset-confirm.js.map +1 -1
- package/dist/routes/api/password-reset-request.d.ts.map +1 -1
- package/dist/routes/api/password-reset-request.js +1 -1
- package/dist/routes/api/password-reset-request.js.map +1 -1
- package/dist/routes/api/shared.d.ts +3 -2
- package/dist/routes/api/shared.d.ts.map +1 -1
- package/dist/routes/api/shared.js +61 -2
- package/dist/routes/api/shared.js.map +1 -1
- package/dist/routes/callback.d.ts.map +1 -1
- package/dist/routes/callback.js +53 -9
- package/dist/routes/callback.js.map +1 -1
- package/dist/routes/flow.d.ts +6 -0
- package/dist/routes/flow.d.ts.map +1 -1
- package/dist/routes/flow.js.map +1 -1
- package/dist/routes/logout.d.ts.map +1 -1
- package/dist/routes/logout.js +4 -2
- package/dist/routes/logout.js.map +1 -1
- package/dist/utils/analytics.d.ts +17 -0
- package/dist/utils/analytics.d.ts.map +1 -1
- package/dist/utils/analytics.js +58 -11
- package/dist/utils/analytics.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/wait-until.d.ts +13 -0
- package/dist/utils/wait-until.d.ts.map +1 -0
- package/dist/utils/wait-until.js +12 -0
- package/dist/utils/wait-until.js.map +1 -0
- package/package.json +2 -2
- package/src/routes/api/code-request.ts +1 -0
- package/src/routes/api/password-reset-confirm.ts +1 -0
- package/src/routes/api/password-reset-request.ts +1 -0
- package/src/routes/api/shared.ts +100 -2
- package/src/routes/callback.ts +78 -13
- package/src/routes/flow.ts +6 -0
- package/src/routes/logout.ts +7 -2
- package/src/utils/analytics.ts +84 -10
- package/src/utils/index.ts +12 -1
- package/src/utils/wait-until.ts +48 -0
package/src/utils/analytics.ts
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
type TrackFn = (context: unknown, event: string, props?: Record<string, unknown>) => Promise<void>
|
|
2
2
|
|
|
3
|
+
export type AuthAnalyticsProvider = 'google' | 'email-code' | 'password' | 'unknown'
|
|
4
|
+
|
|
5
|
+
export interface AuthSuccessEventInput {
|
|
6
|
+
provider: AuthAnalyticsProvider | string | null | undefined
|
|
7
|
+
isNewUser: boolean
|
|
8
|
+
authPath: string
|
|
9
|
+
identityUserId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AuthFailureEventInput {
|
|
13
|
+
provider: AuthAnalyticsProvider | string | null | undefined
|
|
14
|
+
reason: string | null | undefined
|
|
15
|
+
authPath: string
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
let trackFn: TrackFn | null = null
|
|
4
19
|
let resolved = false
|
|
20
|
+
let resolvePromise: Promise<void> | null = null
|
|
5
21
|
const ANALYTICS_UTILS_MODULE = '@growth-labs/analytics/utils'
|
|
22
|
+
const AUTH_PROVIDERS = new Set(['google', 'email-code', 'password'])
|
|
23
|
+
const AUTH_FAILURE_REASONS = new Set([
|
|
24
|
+
'auth_failed',
|
|
25
|
+
'invalid_credentials',
|
|
26
|
+
'invalid_code',
|
|
27
|
+
'invalid_request',
|
|
28
|
+
'invalid_response',
|
|
29
|
+
'invalid_token',
|
|
30
|
+
'missing_auth_flow',
|
|
31
|
+
'oauth_denied',
|
|
32
|
+
'request_failed',
|
|
33
|
+
'send_failed',
|
|
34
|
+
'token_exchange_failed',
|
|
35
|
+
])
|
|
6
36
|
|
|
7
37
|
async function importOptionalModule(modulePath: string): Promise<Record<string, unknown> | null> {
|
|
8
38
|
try {
|
|
@@ -14,17 +44,22 @@ async function importOptionalModule(modulePath: string): Promise<Record<string,
|
|
|
14
44
|
|
|
15
45
|
async function resolveAnalytics(): Promise<void> {
|
|
16
46
|
if (resolved) return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
if (!resolvePromise) {
|
|
48
|
+
resolvePromise = (async () => {
|
|
49
|
+
const mod = await importOptionalModule(ANALYTICS_UTILS_MODULE)
|
|
50
|
+
let candidate: unknown
|
|
51
|
+
try {
|
|
52
|
+
candidate = mod?.trackServerEvent
|
|
53
|
+
} catch {
|
|
54
|
+
candidate = undefined
|
|
55
|
+
}
|
|
56
|
+
if (typeof candidate === 'function') {
|
|
57
|
+
trackFn = candidate as TrackFn
|
|
58
|
+
}
|
|
59
|
+
resolved = true
|
|
60
|
+
})()
|
|
27
61
|
}
|
|
62
|
+
await resolvePromise
|
|
28
63
|
}
|
|
29
64
|
|
|
30
65
|
export async function emitAuthEvent(
|
|
@@ -37,6 +72,44 @@ export async function emitAuthEvent(
|
|
|
37
72
|
return trackFn(context, eventName, data)
|
|
38
73
|
}
|
|
39
74
|
|
|
75
|
+
export function normalizeAuthProvider(
|
|
76
|
+
provider: AuthAnalyticsProvider | string | null | undefined,
|
|
77
|
+
): AuthAnalyticsProvider {
|
|
78
|
+
return provider && AUTH_PROVIDERS.has(provider) ? (provider as AuthAnalyticsProvider) : 'unknown'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeAuthFailureReason(reason: string | null | undefined): string {
|
|
82
|
+
if (reason === 'access_denied') return 'oauth_denied'
|
|
83
|
+
return reason && AUTH_FAILURE_REASONS.has(reason) ? reason : 'auth_failed'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function authSuccessEventName(isNewUser: boolean): 'signup_completed' | 'login_completed' {
|
|
87
|
+
return isNewUser ? 'signup_completed' : 'login_completed'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function emitAuthSuccessEvent(
|
|
91
|
+
context: unknown,
|
|
92
|
+
input: AuthSuccessEventInput,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
return emitAuthEvent(context, authSuccessEventName(input.isNewUser), {
|
|
95
|
+
provider: normalizeAuthProvider(input.provider),
|
|
96
|
+
isNewUser: input.isNewUser,
|
|
97
|
+
auth_path: input.authPath,
|
|
98
|
+
identity_user_id: input.identityUserId,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function emitAuthFailureEvent(
|
|
103
|
+
context: unknown,
|
|
104
|
+
input: AuthFailureEventInput,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
return emitAuthEvent(context, 'auth_failed', {
|
|
107
|
+
provider: normalizeAuthProvider(input.provider),
|
|
108
|
+
reason: normalizeAuthFailureReason(input.reason),
|
|
109
|
+
auth_path: input.authPath,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
40
113
|
export function analyticsAvailable(context: { locals?: Record<string, unknown> }): boolean {
|
|
41
114
|
void context
|
|
42
115
|
return true
|
|
@@ -53,4 +126,5 @@ export async function trackAuthEvent(
|
|
|
53
126
|
export function _resetAnalytics(): void {
|
|
54
127
|
trackFn = null
|
|
55
128
|
resolved = false
|
|
129
|
+
resolvePromise = null
|
|
56
130
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
type AuthAnalyticsProvider,
|
|
3
|
+
type AuthFailureEventInput,
|
|
4
|
+
type AuthSuccessEventInput,
|
|
5
|
+
authSuccessEventName,
|
|
6
|
+
emitAuthEvent,
|
|
7
|
+
emitAuthFailureEvent,
|
|
8
|
+
emitAuthSuccessEvent,
|
|
9
|
+
normalizeAuthFailureReason,
|
|
10
|
+
normalizeAuthProvider,
|
|
11
|
+
trackAuthEvent,
|
|
12
|
+
} from './analytics.js'
|
|
2
13
|
export {
|
|
3
14
|
type BridgeMigrationOptions,
|
|
4
15
|
type BridgeMigrationStatement,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Resolve the Cloudflare Worker execution context's `waitUntil` from a
|
|
2
|
+
// request context.
|
|
3
|
+
//
|
|
4
|
+
// Astro v6 + @astrojs/cloudflare@13 places the Worker execution context on
|
|
5
|
+
// `context.locals.cfContext`, NOT directly on the APIContext — so the
|
|
6
|
+
// previously-used `context.waitUntil?.(promise)` was a silent no-op for
|
|
7
|
+
// every injected auth route, orphaning the analytics-emit promise so the
|
|
8
|
+
// dynamic import of `@growth-labs/analytics/utils` (and the WAE + D1 writes
|
|
9
|
+
// it triggers) was terminated before completing. Manifested in production
|
|
10
|
+
// as `login_completed` / `signup_completed` / `auth_logout` rows never
|
|
11
|
+
// landing in `gl_conversion_events`.
|
|
12
|
+
//
|
|
13
|
+
// The analytics package's own `runtime.ts` already uses this pattern;
|
|
14
|
+
// keeping the resolution shape identical guarantees both packages register
|
|
15
|
+
// promises against the same execution context.
|
|
16
|
+
//
|
|
17
|
+
// Fallback order, mirroring the runtime expectations of consumers:
|
|
18
|
+
// 1. `context.locals.cfContext.waitUntil` — Astro v6 + cloudflare adapter.
|
|
19
|
+
// 2. `context.waitUntil` — direct callers passing a synthetic context
|
|
20
|
+
// (e.g., tests, or future runtimes that surface `waitUntil` at the top
|
|
21
|
+
// level).
|
|
22
|
+
// 3. A no-op — so emit calls degrade silently when no execution context
|
|
23
|
+
// is reachable (dev environments without a Worker, unit tests).
|
|
24
|
+
interface CfExecutionContext {
|
|
25
|
+
waitUntil?: (promise: Promise<unknown>) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ContextWithWaitUntil {
|
|
29
|
+
locals?: {
|
|
30
|
+
cfContext?: CfExecutionContext
|
|
31
|
+
}
|
|
32
|
+
waitUntil?: (promise: Promise<unknown>) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type WaitUntilFn = (promise: Promise<unknown>) => void
|
|
36
|
+
|
|
37
|
+
const NO_OP_WAIT_UNTIL: WaitUntilFn = () => undefined
|
|
38
|
+
|
|
39
|
+
export function resolveWaitUntil(context: ContextWithWaitUntil): WaitUntilFn {
|
|
40
|
+
const cfWaitUntil = context.locals?.cfContext?.waitUntil
|
|
41
|
+
if (typeof cfWaitUntil === 'function') {
|
|
42
|
+
return cfWaitUntil.bind(context.locals!.cfContext)
|
|
43
|
+
}
|
|
44
|
+
if (typeof context.waitUntil === 'function') {
|
|
45
|
+
return context.waitUntil
|
|
46
|
+
}
|
|
47
|
+
return NO_OP_WAIT_UNTIL
|
|
48
|
+
}
|