@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,6 +1,21 @@
1
1
  let trackFn = null;
2
2
  let resolved = false;
3
+ let resolvePromise = null;
3
4
  const ANALYTICS_UTILS_MODULE = '@growth-labs/analytics/utils';
5
+ const AUTH_PROVIDERS = new Set(['google', 'email-code', 'password']);
6
+ const AUTH_FAILURE_REASONS = new Set([
7
+ 'auth_failed',
8
+ 'invalid_credentials',
9
+ 'invalid_code',
10
+ 'invalid_request',
11
+ 'invalid_response',
12
+ 'invalid_token',
13
+ 'missing_auth_flow',
14
+ 'oauth_denied',
15
+ 'request_failed',
16
+ 'send_failed',
17
+ 'token_exchange_failed',
18
+ ]);
4
19
  async function importOptionalModule(modulePath) {
5
20
  try {
6
21
  return (await import(/* @vite-ignore */ modulePath));
@@ -12,18 +27,23 @@ async function importOptionalModule(modulePath) {
12
27
  async function resolveAnalytics() {
13
28
  if (resolved)
14
29
  return;
15
- resolved = true;
16
- const mod = await importOptionalModule(ANALYTICS_UTILS_MODULE);
17
- let candidate;
18
- try {
19
- candidate = mod?.trackServerEvent;
20
- }
21
- catch {
22
- candidate = undefined;
23
- }
24
- if (typeof candidate === 'function') {
25
- trackFn = candidate;
30
+ if (!resolvePromise) {
31
+ resolvePromise = (async () => {
32
+ const mod = await importOptionalModule(ANALYTICS_UTILS_MODULE);
33
+ let candidate;
34
+ try {
35
+ candidate = mod?.trackServerEvent;
36
+ }
37
+ catch {
38
+ candidate = undefined;
39
+ }
40
+ if (typeof candidate === 'function') {
41
+ trackFn = candidate;
42
+ }
43
+ resolved = true;
44
+ })();
26
45
  }
46
+ await resolvePromise;
27
47
  }
28
48
  export async function emitAuthEvent(context, eventName, data) {
29
49
  await resolveAnalytics();
@@ -31,6 +51,32 @@ export async function emitAuthEvent(context, eventName, data) {
31
51
  return;
32
52
  return trackFn(context, eventName, data);
33
53
  }
54
+ export function normalizeAuthProvider(provider) {
55
+ return provider && AUTH_PROVIDERS.has(provider) ? provider : 'unknown';
56
+ }
57
+ export function normalizeAuthFailureReason(reason) {
58
+ if (reason === 'access_denied')
59
+ return 'oauth_denied';
60
+ return reason && AUTH_FAILURE_REASONS.has(reason) ? reason : 'auth_failed';
61
+ }
62
+ export function authSuccessEventName(isNewUser) {
63
+ return isNewUser ? 'signup_completed' : 'login_completed';
64
+ }
65
+ export async function emitAuthSuccessEvent(context, input) {
66
+ return emitAuthEvent(context, authSuccessEventName(input.isNewUser), {
67
+ provider: normalizeAuthProvider(input.provider),
68
+ isNewUser: input.isNewUser,
69
+ auth_path: input.authPath,
70
+ identity_user_id: input.identityUserId,
71
+ });
72
+ }
73
+ export async function emitAuthFailureEvent(context, input) {
74
+ return emitAuthEvent(context, 'auth_failed', {
75
+ provider: normalizeAuthProvider(input.provider),
76
+ reason: normalizeAuthFailureReason(input.reason),
77
+ auth_path: input.authPath,
78
+ });
79
+ }
34
80
  export function analyticsAvailable(context) {
35
81
  void context;
36
82
  return true;
@@ -41,5 +87,6 @@ export async function trackAuthEvent(context, eventName, data = {}) {
41
87
  export function _resetAnalytics() {
42
88
  trackFn = null;
43
89
  resolved = false;
90
+ resolvePromise = null;
44
91
  }
45
92
  //# sourceMappingURL=analytics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"AAEA,IAAI,OAAO,GAAmB,IAAI,CAAA;AAClC,IAAI,QAAQ,GAAG,KAAK,CAAA;AACpB,MAAM,sBAAsB,GAAG,8BAA8B,CAAA;AAE7D,KAAK,UAAU,oBAAoB,CAAC,UAAkB;IACrD,IAAI,CAAC;QACJ,OAAO,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAA4B,CAAA;IAChF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAA;IACZ,CAAC;AACF,CAAC;AAED,KAAK,UAAU,gBAAgB;IAC9B,IAAI,QAAQ;QAAE,OAAM;IACpB,QAAQ,GAAG,IAAI,CAAA;IACf,MAAM,GAAG,GAAG,MAAM,oBAAoB,CAAC,sBAAsB,CAAC,CAAA;IAC9D,IAAI,SAAkB,CAAA;IACtB,IAAI,CAAC;QACJ,SAAS,GAAG,GAAG,EAAE,gBAAgB,CAAA;IAClC,CAAC;IAAC,MAAM,CAAC;QACR,SAAS,GAAG,SAAS,CAAA;IACtB,CAAC;IACD,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;QACrC,OAAO,GAAG,SAAoB,CAAA;IAC/B,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,OAAgB,EAChB,SAAiB,EACjB,IAA6B;IAE7B,MAAM,gBAAgB,EAAE,CAAA;IACxB,IAAI,CAAC,OAAO;QAAE,OAAM;IACpB,OAAO,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAA6C;IAC/E,KAAK,OAAO,CAAA;IACZ,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,OAAgB,EAChB,SAAiB,EACjB,OAAgC,EAAE;IAElC,OAAO,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,UAAU,eAAe;IAC9B,OAAO,GAAG,IAAI,CAAA;IACd,QAAQ,GAAG,KAAK,CAAA;AACjB,CAAC"}
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export { emitAuthEvent, trackAuthEvent } from './analytics.js';
1
+ export { type AuthAnalyticsProvider, type AuthFailureEventInput, type AuthSuccessEventInput, authSuccessEventName, emitAuthEvent, emitAuthFailureEvent, emitAuthSuccessEvent, normalizeAuthFailureReason, normalizeAuthProvider, trackAuthEvent, } from './analytics.js';
2
2
  export { type BridgeMigrationOptions, type BridgeMigrationStatement, buildLegacyIdentitySubjectCanary, buildLegacyIdentitySubjectMigration, extractIdentityId, findLegacyIdentitySubjects, IDENTITY_SUB_FORMAT_DRIFT, IDENTITY_SUBJECT_PREFIX, IdentitySubFormatDriftError, normalizeIdentitySubject, rewriteLegacyIdentitySubjects, } from './identity.js';
3
3
  export { signIdentityCookie, verifyIdentityCookie } from './identity-cookie.js';
4
4
  export { getAuthorizationUrl, getMagicLinkUrl, getPasswordLoginUrl } from './openauth.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EACN,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,mCAAmC,EACnC,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,GAC7B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,oBAAoB,EACpB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,0BAA0B,EAC1B,qBAAqB,EACrB,cAAc,GACd,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACN,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,mCAAmC,EACnC,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,GAC7B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
@@ -1,4 +1,4 @@
1
- export { emitAuthEvent, trackAuthEvent } from './analytics.js';
1
+ export { authSuccessEventName, emitAuthEvent, emitAuthFailureEvent, emitAuthSuccessEvent, normalizeAuthFailureReason, normalizeAuthProvider, trackAuthEvent, } from './analytics.js';
2
2
  export { buildLegacyIdentitySubjectCanary, buildLegacyIdentitySubjectMigration, extractIdentityId, findLegacyIdentitySubjects, IDENTITY_SUB_FORMAT_DRIFT, IDENTITY_SUBJECT_PREFIX, IdentitySubFormatDriftError, normalizeIdentitySubject, rewriteLegacyIdentitySubjects, } from './identity.js';
3
3
  export { signIdentityCookie, verifyIdentityCookie } from './identity-cookie.js';
4
4
  export { getAuthorizationUrl, getMagicLinkUrl, getPasswordLoginUrl } from './openauth.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAGN,gCAAgC,EAChC,mCAAmC,EACnC,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,GAC7B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,oBAAoB,EACpB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,0BAA0B,EAC1B,qBAAqB,EACrB,cAAc,GACd,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAGN,gCAAgC,EAChC,mCAAmC,EACnC,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,GAC7B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,13 @@
1
+ interface CfExecutionContext {
2
+ waitUntil?: (promise: Promise<unknown>) => void;
3
+ }
4
+ interface ContextWithWaitUntil {
5
+ locals?: {
6
+ cfContext?: CfExecutionContext;
7
+ };
8
+ waitUntil?: (promise: Promise<unknown>) => void;
9
+ }
10
+ export type WaitUntilFn = (promise: Promise<unknown>) => void;
11
+ export declare function resolveWaitUntil(context: ContextWithWaitUntil): WaitUntilFn;
12
+ export {};
13
+ //# sourceMappingURL=wait-until.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-until.d.ts","sourceRoot":"","sources":["../../src/utils/wait-until.ts"],"names":[],"mappings":"AAuBA,UAAU,kBAAkB;IAC3B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;CAC/C;AAED,UAAU,oBAAoB;IAC7B,MAAM,CAAC,EAAE;QACR,SAAS,CAAC,EAAE,kBAAkB,CAAA;KAC9B,CAAA;IACD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;CAC/C;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;AAI7D,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,WAAW,CAS3E"}
@@ -0,0 +1,12 @@
1
+ const NO_OP_WAIT_UNTIL = () => undefined;
2
+ export function resolveWaitUntil(context) {
3
+ const cfWaitUntil = context.locals?.cfContext?.waitUntil;
4
+ if (typeof cfWaitUntil === 'function') {
5
+ return cfWaitUntil.bind(context.locals.cfContext);
6
+ }
7
+ if (typeof context.waitUntil === 'function') {
8
+ return context.waitUntil;
9
+ }
10
+ return NO_OP_WAIT_UNTIL;
11
+ }
12
+ //# sourceMappingURL=wait-until.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-until.js","sourceRoot":"","sources":["../../src/utils/wait-until.ts"],"names":[],"mappings":"AAoCA,MAAM,gBAAgB,GAAgB,GAAG,EAAE,CAAC,SAAS,CAAA;AAErD,MAAM,UAAU,gBAAgB,CAAC,OAA6B;IAC7D,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAA;IACxD,IAAI,OAAO,WAAW,KAAK,UAAU,EAAE,CAAC;QACvC,OAAO,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,MAAO,CAAC,SAAS,CAAC,CAAA;IACnD,CAAC;IACD,IAAI,OAAO,OAAO,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QAC7C,OAAO,OAAO,CAAC,SAAS,CAAA;IACzB,CAAC;IACD,OAAO,gBAAgB,CAAA;AACxB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/auth",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -62,7 +62,7 @@
62
62
  "typescript": "^5.8.0",
63
63
  "vite": "^7.3.2",
64
64
  "vitest": "^3.0.0",
65
- "@growth-labs/analytics": "0.4.2"
65
+ "@growth-labs/analytics": "0.5.1"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc",
@@ -29,5 +29,6 @@ export async function POST(context: RouteContext): Promise<Response> {
29
29
  { email, sent: '1', redirect },
30
30
  options.verifyCodePath,
31
31
  { email, redirect },
32
+ 'email-code',
32
33
  )
33
34
  }
@@ -32,5 +32,6 @@ export async function POST(context: RouteContext): Promise<Response> {
32
32
  { reset: 'success' },
33
33
  options.resetPasswordPath,
34
34
  { token },
35
+ 'password',
35
36
  )
36
37
  }
@@ -28,5 +28,6 @@ export async function POST(context: RouteContext): Promise<Response> {
28
28
  { sent: '1' },
29
29
  options.forgotPasswordPath,
30
30
  { email },
31
+ 'password',
31
32
  )
32
33
  }
@@ -1,6 +1,22 @@
1
1
  import { getConfig } from '../../config-store.js'
2
+ import { getRuntimeEnv } from '../../runtime.js'
2
3
  import { type AuthClientCodeResult, type AuthClientOkResult, authClient } from '../../server.js'
3
- import { getCookie, setCookie } from '../../utils/cookies.js'
4
+ import {
5
+ type AuthAnalyticsProvider,
6
+ analyticsAvailable,
7
+ emitAuthFailureEvent,
8
+ emitAuthSuccessEvent,
9
+ } from '../../utils/analytics.js'
10
+ import {
11
+ clearAuthFlowCookies,
12
+ getCookie,
13
+ setCookie,
14
+ setIdentityCookie,
15
+ setTokenCookies,
16
+ } from '../../utils/cookies.js'
17
+ import { createIdentityCookiePayload, signIdentityCookie } from '../../utils/identity-cookie.js'
18
+ import { exchangeCode } from '../../utils/openauth.js'
19
+ import { resolveWaitUntil } from '../../utils/wait-until.js'
4
20
  import { type RouteContext, readForm, redirectLocation, redirectTo } from '../flow.js'
5
21
 
6
22
  const CROSS_SITE_POST_MESSAGE = 'Cross-site POST form submissions are forbidden'
@@ -37,14 +53,16 @@ export function callbackRedirect(
37
53
  errorPath: string,
38
54
  errorParams: Record<string, string | undefined>,
39
55
  provider?: 'password' | 'email-code',
40
- ): Response {
56
+ ): Promise<Response> | Response {
41
57
  if ('error' in result) {
58
+ queueAuthFailure(context, provider, result.error)
42
59
  return routeResult(context, errorPath, { ...errorParams, error: result.error }, result.error)
43
60
  }
44
61
 
45
62
  const options = getConfig()
46
63
  const state = getCookie(context.request, `${options.cookiePrefix}_state`)
47
64
  if (!state) {
65
+ queueAuthFailure(context, provider, 'missing_auth_flow')
48
66
  return routeResult(
49
67
  context,
50
68
  errorPath,
@@ -53,6 +71,10 @@ export function callbackRedirect(
53
71
  )
54
72
  }
55
73
 
74
+ if (wantsJson(context)) {
75
+ return completeJsonAuth(context, result.code, errorPath, errorParams, provider)
76
+ }
77
+
56
78
  const response = mutableHeaders(
57
79
  routeResult(context, options.callbackPath, {
58
80
  code: result.code,
@@ -78,8 +100,10 @@ export function statusRedirect(
78
100
  successParams: Record<string, string | undefined>,
79
101
  errorPath: string,
80
102
  errorParams: Record<string, string | undefined>,
103
+ provider?: AuthAnalyticsProvider,
81
104
  ): Response {
82
105
  if ('error' in result) {
106
+ queueAuthFailure(context, provider, result.error)
83
107
  return routeResult(context, errorPath, { ...errorParams, error: result.error }, result.error)
84
108
  }
85
109
  return routeResult(context, successPath, successParams)
@@ -119,6 +143,80 @@ function mutableHeaders(response: Response): Response {
119
143
  })
120
144
  }
121
145
 
146
+ async function completeJsonAuth(
147
+ context: RouteContext,
148
+ code: string,
149
+ errorPath: string,
150
+ errorParams: Record<string, string | undefined>,
151
+ provider?: 'password' | 'email-code',
152
+ ): Promise<Response> {
153
+ const options = getConfig()
154
+ const codeVerifier = getCookie(context.request, `${options.cookiePrefix}_pkce`)
155
+ if (!codeVerifier) {
156
+ queueAuthFailure(context, provider, 'missing_auth_flow')
157
+ return routeResult(
158
+ context,
159
+ errorPath,
160
+ { ...errorParams, error: 'missing_auth_flow' },
161
+ 'missing_auth_flow',
162
+ )
163
+ }
164
+
165
+ const tokenResponse = await exchangeCode(context.url, code, codeVerifier, provider)
166
+ if (!tokenResponse) {
167
+ queueAuthFailure(context, provider, 'token_exchange_failed')
168
+ return routeResult(context, errorPath, { ...errorParams, error: 'auth_failed' }, 'auth_failed')
169
+ }
170
+
171
+ const redirectPath = getCookie(context.request, `${options.cookiePrefix}_redirect`) || '/'
172
+ const redirect = new URL(redirectPath, context.url.origin).toString()
173
+ const response = mutableHeaders(Response.json({ ok: true, redirect }))
174
+
175
+ setTokenCookies(response, options, tokenResponse.accessToken, tokenResponse.refreshToken)
176
+ clearAuthFlowCookies(response, options)
177
+ await setOptionalIdentityCookie(response, tokenResponse.claims.sub)
178
+ if (analyticsAvailable(context)) {
179
+ resolveWaitUntil(context)(
180
+ emitAuthSuccessEvent(context, {
181
+ provider: tokenResponse.provider,
182
+ isNewUser: tokenResponse.isNewUser,
183
+ authPath: context.url.pathname,
184
+ identityUserId: tokenResponse.claims.sub,
185
+ }),
186
+ )
187
+ }
188
+
189
+ return response
190
+ }
191
+
192
+ function queueAuthFailure(
193
+ context: RouteContext,
194
+ provider: AuthAnalyticsProvider | undefined,
195
+ reason: string,
196
+ ): void {
197
+ if (!analyticsAvailable(context)) return
198
+ resolveWaitUntil(context)(
199
+ emitAuthFailureEvent(context, {
200
+ provider,
201
+ reason,
202
+ authPath: context.url.pathname,
203
+ }),
204
+ )
205
+ }
206
+
207
+ async function setOptionalIdentityCookie(response: Response, subject: string): Promise<void> {
208
+ const secret = getRuntimeEnv().GL_IDENTITY_SECRET
209
+ if (typeof secret !== 'string' || !secret) return
210
+ try {
211
+ setIdentityCookie(
212
+ response,
213
+ await signIdentityCookie(await createIdentityCookiePayload(subject, secret), secret),
214
+ )
215
+ } catch {
216
+ // Keep login working if the optional analytics identity secret is misconfigured.
217
+ }
218
+ }
219
+
122
220
  function authFlowCookieAttrs() {
123
221
  return {
124
222
  path: '/',
@@ -1,6 +1,10 @@
1
1
  import { getConfig } from '../config-store.js'
2
2
  import { getRuntimeEnv } from '../runtime.js'
3
- import { analyticsAvailable, emitAuthEvent } from '../utils/analytics.js'
3
+ import {
4
+ analyticsAvailable,
5
+ emitAuthFailureEvent,
6
+ emitAuthSuccessEvent,
7
+ } from '../utils/analytics.js'
4
8
  import {
5
9
  clearAuthFlowCookies,
6
10
  getCookie,
@@ -10,11 +14,16 @@ import {
10
14
  import { createIdentityCookiePayload, signIdentityCookie } from '../utils/identity-cookie.js'
11
15
  import { exchangeCode } from '../utils/openauth.js'
12
16
  import { verifyState } from '../utils/state.js'
17
+ import { resolveWaitUntil } from '../utils/wait-until.js'
13
18
 
14
19
  interface CallbackContext {
15
20
  request: Request
16
21
  url: URL
17
22
  locals?: Record<string, unknown>
23
+ // Some callers may surface `waitUntil` at the top level (synthetic
24
+ // contexts in tests; future runtimes). The production wiring on Astro v6
25
+ // + @astrojs/cloudflare@13 instead exposes it at
26
+ // `locals.cfContext.waitUntil`, which `resolveWaitUntil` handles below.
18
27
  waitUntil?: (promise: Promise<unknown>) => void
19
28
  }
20
29
 
@@ -23,47 +32,98 @@ function getIdentitySecret(): string | null {
23
32
  return typeof secret === 'string' && secret.length > 0 ? secret : null
24
33
  }
25
34
 
35
+ function wantsJson(request: Request): boolean {
36
+ const accept = request.headers.get('accept') ?? ''
37
+ return accept
38
+ .split(',')
39
+ .some((value) => value.trim().toLowerCase().split(';')[0] === 'application/json')
40
+ }
41
+
42
+ function loginErrorResponse(request: Request, origin: string, loginPath: string): Response {
43
+ const redirect = new URL(`${loginPath}?error=auth_failed`, origin).toString()
44
+ if (wantsJson(request)) {
45
+ return Response.json({ error: 'auth_failed', redirect }, { status: 400 })
46
+ }
47
+ return Response.redirect(redirect, 302)
48
+ }
49
+
50
+ function queueAuthFailure(
51
+ context: CallbackContext,
52
+ provider: string | null,
53
+ reason: string | null,
54
+ ): void {
55
+ if (!analyticsAvailable(context)) return
56
+ const promise = emitAuthFailureEvent(context, {
57
+ provider,
58
+ reason,
59
+ authPath: context.url.pathname,
60
+ })
61
+ resolveWaitUntil(context)(promise)
62
+ }
63
+
26
64
  export async function GET(context: CallbackContext): Promise<Response> {
27
65
  const options = getConfig()
28
66
  const { url, request } = context
29
67
 
30
68
  const code = url.searchParams.get('code')
31
69
  const state = url.searchParams.get('state')
70
+ const error = url.searchParams.get('error')
32
71
 
33
72
  // Verify CSRF state
34
73
  const storedState = getCookie(request, `${options.cookiePrefix}_state`)
35
74
  if (!state || !storedState || !verifyState(state, storedState)) {
75
+ queueAuthFailure(
76
+ context,
77
+ getCookie(request, `${options.cookiePrefix}_provider`),
78
+ 'missing_auth_flow',
79
+ )
36
80
  return new Response('Invalid state parameter', { status: 403 })
37
81
  }
38
82
 
39
83
  // Read PKCE verifier
40
84
  const codeVerifier = getCookie(request, `${options.cookiePrefix}_pkce`)
41
85
  if (!codeVerifier) {
86
+ queueAuthFailure(
87
+ context,
88
+ getCookie(request, `${options.cookiePrefix}_provider`),
89
+ 'missing_auth_flow',
90
+ )
42
91
  return new Response('Missing PKCE verifier', { status: 400 })
43
92
  }
44
93
 
45
94
  const provider = getCookie(request, `${options.cookiePrefix}_provider`)
46
95
  const authPath = getCookie(request, `${options.cookiePrefix}_auth_path`) || url.pathname
47
96
 
97
+ if (error) {
98
+ queueAuthFailure(context, provider, error)
99
+ return loginErrorResponse(request, url.origin, options.loginPath)
100
+ }
101
+
102
+ if (!code) {
103
+ queueAuthFailure(context, provider, 'invalid_request')
104
+ return loginErrorResponse(request, url.origin, options.loginPath)
105
+ }
106
+
48
107
  // Exchange code for tokens
49
108
  const tokenResponse = await exchangeCode(
50
109
  url,
51
- code!,
110
+ code,
52
111
  codeVerifier,
53
112
  provider === 'google' || provider === 'password' || provider === 'email-code'
54
113
  ? provider
55
114
  : undefined,
56
115
  )
57
116
  if (!tokenResponse) {
58
- return Response.redirect(
59
- new URL(`${options.loginPath}?error=auth_failed`, url.origin).toString(),
60
- 302,
61
- )
117
+ queueAuthFailure(context, provider, 'token_exchange_failed')
118
+ return loginErrorResponse(request, url.origin, options.loginPath)
62
119
  }
63
120
 
64
121
  // Build redirect response
65
122
  const redirectPath = getCookie(request, `${options.cookiePrefix}_redirect`) || '/'
66
- const response = Response.redirect(new URL(redirectPath, url.origin).toString(), 302)
123
+ const redirect = new URL(redirectPath, url.origin).toString()
124
+ const response = wantsJson(request)
125
+ ? Response.json({ ok: true, redirect })
126
+ : Response.redirect(redirect, 302)
67
127
 
68
128
  // Mutable copy for setting cookies
69
129
  const mutableResponse = new Response(response.body, {
@@ -89,16 +149,21 @@ export async function GET(context: CallbackContext): Promise<Response> {
89
149
  }
90
150
  }
91
151
 
92
- // Emit analytics event (non-blocking)
152
+ // Emit analytics event (non-blocking). Register the promise via the
153
+ // Cloudflare execution context's `waitUntil` so the dynamic import of
154
+ // `@growth-labs/analytics/utils` and the downstream WAE + D1 writes
155
+ // survive past the 302 response. Without this, the Cloudflare runtime
156
+ // terminates the orphaned promise before the conversion event lands —
157
+ // the root cause of the missing `login_completed` / `signup_completed`
158
+ // rows in `gl_conversion_events` after the Fronts cutover.
93
159
  if (analyticsAvailable(context)) {
94
- const eventName = tokenResponse.isNewUser ? 'signup_completed' : 'login_completed'
95
- const promise = emitAuthEvent(context, eventName, {
160
+ const promise = emitAuthSuccessEvent(context, {
96
161
  provider: tokenResponse.provider,
97
162
  isNewUser: tokenResponse.isNewUser,
98
- auth_path: authPath,
99
- identity_user_id: tokenResponse.claims.sub,
163
+ authPath,
164
+ identityUserId: tokenResponse.claims.sub,
100
165
  })
101
- context.waitUntil?.(promise)
166
+ resolveWaitUntil(context)(promise)
102
167
  }
103
168
 
104
169
  return mutableResponse
@@ -5,6 +5,12 @@ import { createAuthFlow } from '../utils/openauth.js'
5
5
  export interface RouteContext {
6
6
  request: Request
7
7
  url: URL
8
+ locals?: {
9
+ cfContext?: {
10
+ waitUntil?: (promise: Promise<unknown>) => void
11
+ }
12
+ }
13
+ waitUntil?: (promise: Promise<unknown>) => void
8
14
  }
9
15
 
10
16
  export async function withAuthFlowCookies(
@@ -8,10 +8,14 @@ import {
8
8
  getCookie,
9
9
  } from '../utils/cookies.js'
10
10
  import { revokeToken } from '../utils/openauth.js'
11
+ import { resolveWaitUntil } from '../utils/wait-until.js'
11
12
 
12
13
  interface LogoutContext {
13
14
  request: Request
14
15
  url: URL
16
+ // See callback.ts: Astro v6 + @astrojs/cloudflare@13 places the Worker
17
+ // execution context's `waitUntil` at `locals.cfContext.waitUntil`, not
18
+ // at the top level. `resolveWaitUntil` handles both shapes.
15
19
  waitUntil?: (promise: Promise<unknown>) => void
16
20
  locals?: Record<string, unknown>
17
21
  }
@@ -20,6 +24,7 @@ export async function GET(context: LogoutContext): Promise<Response> {
20
24
  const options = getConfig()
21
25
  const refreshToken = getCookie(context.request, `${options.cookiePrefix}_rt`)
22
26
  const clientId = options.clientId || context.url.hostname
27
+ const waitUntil = resolveWaitUntil(context)
23
28
 
24
29
  // Revoke at issuer (non-blocking)
25
30
  if (refreshToken) {
@@ -27,7 +32,7 @@ export async function GET(context: LogoutContext): Promise<Response> {
27
32
  const promise = clientSecret
28
33
  ? revokeToken(refreshToken, clientId, clientSecret)
29
34
  : revokeToken(refreshToken, clientId)
30
- context.waitUntil?.(promise)
35
+ waitUntil(promise)
31
36
  }
32
37
 
33
38
  // Emit auth_logout (non-blocking) when the consumer has @growth-labs/analytics
@@ -37,7 +42,7 @@ export async function GET(context: LogoutContext): Promise<Response> {
37
42
  const promise = emitAuthEvent(context, 'auth_logout', {
38
43
  revoked: refreshToken ? 'true' : 'false',
39
44
  })
40
- context.waitUntil?.(promise)
45
+ waitUntil(promise)
41
46
  }
42
47
 
43
48
  // Build redirect response