@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.
Files changed (46) hide show
  1. package/README.md +20 -15
  2. package/dist/routes/api/code-request.d.ts.map +1 -1
  3. package/dist/routes/api/code-request.js +1 -1
  4. package/dist/routes/api/code-request.js.map +1 -1
  5. package/dist/routes/api/password-reset-confirm.d.ts.map +1 -1
  6. package/dist/routes/api/password-reset-confirm.js +1 -1
  7. package/dist/routes/api/password-reset-confirm.js.map +1 -1
  8. package/dist/routes/api/password-reset-request.d.ts.map +1 -1
  9. package/dist/routes/api/password-reset-request.js +1 -1
  10. package/dist/routes/api/password-reset-request.js.map +1 -1
  11. package/dist/routes/api/shared.d.ts +3 -2
  12. package/dist/routes/api/shared.d.ts.map +1 -1
  13. package/dist/routes/api/shared.js +61 -2
  14. package/dist/routes/api/shared.js.map +1 -1
  15. package/dist/routes/callback.d.ts.map +1 -1
  16. package/dist/routes/callback.js +53 -9
  17. package/dist/routes/callback.js.map +1 -1
  18. package/dist/routes/flow.d.ts +6 -0
  19. package/dist/routes/flow.d.ts.map +1 -1
  20. package/dist/routes/flow.js.map +1 -1
  21. package/dist/routes/logout.d.ts.map +1 -1
  22. package/dist/routes/logout.js +4 -2
  23. package/dist/routes/logout.js.map +1 -1
  24. package/dist/utils/analytics.d.ts +17 -0
  25. package/dist/utils/analytics.d.ts.map +1 -1
  26. package/dist/utils/analytics.js +58 -11
  27. package/dist/utils/analytics.js.map +1 -1
  28. package/dist/utils/index.d.ts +1 -1
  29. package/dist/utils/index.d.ts.map +1 -1
  30. package/dist/utils/index.js +1 -1
  31. package/dist/utils/index.js.map +1 -1
  32. package/dist/utils/wait-until.d.ts +13 -0
  33. package/dist/utils/wait-until.d.ts.map +1 -0
  34. package/dist/utils/wait-until.js +12 -0
  35. package/dist/utils/wait-until.js.map +1 -0
  36. package/package.json +2 -2
  37. package/src/routes/api/code-request.ts +1 -0
  38. package/src/routes/api/password-reset-confirm.ts +1 -0
  39. package/src/routes/api/password-reset-request.ts +1 -0
  40. package/src/routes/api/shared.ts +100 -2
  41. package/src/routes/callback.ts +78 -13
  42. package/src/routes/flow.ts +6 -0
  43. package/src/routes/logout.ts +7 -2
  44. package/src/utils/analytics.ts +84 -10
  45. package/src/utils/index.ts +12 -1
  46. package/src/utils/wait-until.ts +48 -0
@@ -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
- resolved = true
18
- const mod = await importOptionalModule(ANALYTICS_UTILS_MODULE)
19
- let candidate: unknown
20
- try {
21
- candidate = mod?.trackServerEvent
22
- } catch {
23
- candidate = undefined
24
- }
25
- if (typeof candidate === 'function') {
26
- trackFn = candidate as TrackFn
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
  }
@@ -1,4 +1,15 @@
1
- export { emitAuthEvent, trackAuthEvent } from './analytics.js'
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
+ }