@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/dist/utils/analytics.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/utils/index.js
CHANGED
|
@@ -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';
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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.
|
|
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.
|
|
65
|
+
"@growth-labs/analytics": "0.5.1"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc",
|
package/src/routes/api/shared.ts
CHANGED
|
@@ -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 {
|
|
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: '/',
|
package/src/routes/callback.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { getConfig } from '../config-store.js'
|
|
2
2
|
import { getRuntimeEnv } from '../runtime.js'
|
|
3
|
-
import {
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
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
|
|
95
|
-
const promise = emitAuthEvent(context, eventName, {
|
|
160
|
+
const promise = emitAuthSuccessEvent(context, {
|
|
96
161
|
provider: tokenResponse.provider,
|
|
97
162
|
isNewUser: tokenResponse.isNewUser,
|
|
98
|
-
|
|
99
|
-
|
|
163
|
+
authPath,
|
|
164
|
+
identityUserId: tokenResponse.claims.sub,
|
|
100
165
|
})
|
|
101
|
-
context
|
|
166
|
+
resolveWaitUntil(context)(promise)
|
|
102
167
|
}
|
|
103
168
|
|
|
104
169
|
return mutableResponse
|
package/src/routes/flow.ts
CHANGED
|
@@ -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(
|
package/src/routes/logout.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
45
|
+
waitUntil(promise)
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
// Build redirect response
|