@caffeinebounce/identity 0.12.1
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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.d.mts +778 -0
- package/dist/index.d.ts +778 -0
- package/dist/index.js +5847 -0
- package/dist/index.mjs +5795 -0
- package/dist/server.d.mts +110 -0
- package/dist/server.d.ts +110 -0
- package/dist/server.js +295 -0
- package/dist/server.mjs +277 -0
- package/package.json +84 -0
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
export { generateSecureToken, getClientIP, getGeolocationFromIP, hashString } from '@caffeinebounce/shared-utils';
|
|
3
|
+
|
|
4
|
+
// src/handlers/callback.ts
|
|
5
|
+
var POST_AUTH_HOOK_ERROR_MESSAGE = "Authentication completed, but setup failed. Please try again.";
|
|
6
|
+
var DEFAULT_LINKING_ACCOUNT_ERROR_MESSAGE = "This account is already connected to another account. Each external account can only be linked to one account.";
|
|
7
|
+
var EMAIL_OTP_TYPES = [
|
|
8
|
+
"signup",
|
|
9
|
+
"invite",
|
|
10
|
+
"magiclink",
|
|
11
|
+
"recovery",
|
|
12
|
+
"email_change",
|
|
13
|
+
"email"
|
|
14
|
+
];
|
|
15
|
+
function getSafeRedirectPath(candidate, fallback) {
|
|
16
|
+
if (candidate?.startsWith("/") && !candidate.startsWith("//")) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
function getEmailOtpType(value) {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return EMAIL_OTP_TYPES.includes(value) ? value : null;
|
|
26
|
+
}
|
|
27
|
+
function isDefaultLinkingFlow({
|
|
28
|
+
redirectPath
|
|
29
|
+
}) {
|
|
30
|
+
return redirectPath.includes("/profile") || redirectPath.includes("/settings");
|
|
31
|
+
}
|
|
32
|
+
function isAlreadyLinkedAccountError(message) {
|
|
33
|
+
return message.includes("already linked") || message.includes("identity already exists") || message.includes("already registered");
|
|
34
|
+
}
|
|
35
|
+
function getDefaultLinkingErrorMessage(message) {
|
|
36
|
+
return isAlreadyLinkedAccountError(message) ? DEFAULT_LINKING_ACCOUNT_ERROR_MESSAGE : message;
|
|
37
|
+
}
|
|
38
|
+
async function createLinkingErrorRedirect({
|
|
39
|
+
request,
|
|
40
|
+
origin,
|
|
41
|
+
redirectPath,
|
|
42
|
+
error,
|
|
43
|
+
errorDescription,
|
|
44
|
+
message,
|
|
45
|
+
source,
|
|
46
|
+
isLinkingFlow,
|
|
47
|
+
resolveLinkingErrorMessage
|
|
48
|
+
}) {
|
|
49
|
+
const linkingFlowContext = { request, origin, redirectPath };
|
|
50
|
+
if (!isLinkingFlow(linkingFlowContext)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const defaultMessage = getDefaultLinkingErrorMessage(message);
|
|
54
|
+
const userMessage = await resolveLinkingErrorMessage?.({
|
|
55
|
+
...linkingFlowContext,
|
|
56
|
+
error,
|
|
57
|
+
errorDescription,
|
|
58
|
+
message,
|
|
59
|
+
defaultMessage,
|
|
60
|
+
source
|
|
61
|
+
}) ?? defaultMessage;
|
|
62
|
+
const nextUrl = new URL(redirectPath, origin);
|
|
63
|
+
nextUrl.searchParams.set("link_error", userMessage);
|
|
64
|
+
return NextResponse.redirect(nextUrl.toString());
|
|
65
|
+
}
|
|
66
|
+
async function runPostAuthHook({
|
|
67
|
+
postAuthHook,
|
|
68
|
+
postAuthHookErrorMode,
|
|
69
|
+
supabase,
|
|
70
|
+
user,
|
|
71
|
+
request,
|
|
72
|
+
origin,
|
|
73
|
+
redirectPath,
|
|
74
|
+
flow,
|
|
75
|
+
otpType,
|
|
76
|
+
signInPath
|
|
77
|
+
}) {
|
|
78
|
+
if (!postAuthHook) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await postAuthHook({
|
|
83
|
+
supabase,
|
|
84
|
+
user,
|
|
85
|
+
request,
|
|
86
|
+
origin,
|
|
87
|
+
redirectPath,
|
|
88
|
+
flow,
|
|
89
|
+
otpType
|
|
90
|
+
});
|
|
91
|
+
return null;
|
|
92
|
+
} catch {
|
|
93
|
+
if (postAuthHookErrorMode === "ignore") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return NextResponse.redirect(
|
|
97
|
+
`${origin}${signInPath}?error=${encodeURIComponent(POST_AUTH_HOOK_ERROR_MESSAGE)}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function createRedirectResponse(target, origin) {
|
|
102
|
+
if (!target) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (target instanceof Response) {
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
if (target instanceof URL) {
|
|
109
|
+
return NextResponse.redirect(target.toString());
|
|
110
|
+
}
|
|
111
|
+
if (target.startsWith("/") && !target.startsWith("//")) {
|
|
112
|
+
return NextResponse.redirect(`${origin}${target}`);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
async function redirectAfterSuccessfulAuth({
|
|
117
|
+
supabase,
|
|
118
|
+
request,
|
|
119
|
+
origin,
|
|
120
|
+
redirectPath,
|
|
121
|
+
flow,
|
|
122
|
+
otpType,
|
|
123
|
+
postAuthHook,
|
|
124
|
+
postAuthHookErrorMode,
|
|
125
|
+
signInPath,
|
|
126
|
+
resolveSuccessRedirect
|
|
127
|
+
}) {
|
|
128
|
+
const {
|
|
129
|
+
data: { user }
|
|
130
|
+
} = await supabase.auth.getUser();
|
|
131
|
+
if (!user) {
|
|
132
|
+
return NextResponse.redirect(`${origin}${redirectPath}`);
|
|
133
|
+
}
|
|
134
|
+
const postAuthHookRedirect = await runPostAuthHook({
|
|
135
|
+
postAuthHook,
|
|
136
|
+
postAuthHookErrorMode,
|
|
137
|
+
supabase,
|
|
138
|
+
user,
|
|
139
|
+
request,
|
|
140
|
+
origin,
|
|
141
|
+
redirectPath,
|
|
142
|
+
flow,
|
|
143
|
+
otpType,
|
|
144
|
+
signInPath
|
|
145
|
+
});
|
|
146
|
+
if (postAuthHookRedirect) {
|
|
147
|
+
return postAuthHookRedirect;
|
|
148
|
+
}
|
|
149
|
+
const customRedirect = await resolveSuccessRedirect?.({
|
|
150
|
+
supabase,
|
|
151
|
+
user,
|
|
152
|
+
request,
|
|
153
|
+
origin,
|
|
154
|
+
redirectPath,
|
|
155
|
+
flow,
|
|
156
|
+
otpType
|
|
157
|
+
});
|
|
158
|
+
const customRedirectResponse = createRedirectResponse(customRedirect, origin);
|
|
159
|
+
if (customRedirectResponse) {
|
|
160
|
+
return customRedirectResponse;
|
|
161
|
+
}
|
|
162
|
+
return NextResponse.redirect(`${origin}${redirectPath}`);
|
|
163
|
+
}
|
|
164
|
+
function createAuthCallbackHandler({
|
|
165
|
+
createClient,
|
|
166
|
+
defaultRedirect = "/dashboard",
|
|
167
|
+
signInPath = "/signin",
|
|
168
|
+
postAuthHook,
|
|
169
|
+
postAuthHookErrorMode = "block",
|
|
170
|
+
resolveSuccessRedirect,
|
|
171
|
+
isLinkingFlow = isDefaultLinkingFlow,
|
|
172
|
+
resolveLinkingErrorMessage
|
|
173
|
+
}) {
|
|
174
|
+
return async function GET(request) {
|
|
175
|
+
const requestUrl = new URL(request.url);
|
|
176
|
+
const code = requestUrl.searchParams.get("code");
|
|
177
|
+
const tokenHash = requestUrl.searchParams.get("token_hash");
|
|
178
|
+
const rawOtpType = requestUrl.searchParams.get("type");
|
|
179
|
+
const otpType = getEmailOtpType(rawOtpType);
|
|
180
|
+
const next = getSafeRedirectPath(
|
|
181
|
+
requestUrl.searchParams.get("next") ?? requestUrl.searchParams.get("redirect_to"),
|
|
182
|
+
defaultRedirect
|
|
183
|
+
);
|
|
184
|
+
const origin = requestUrl.origin;
|
|
185
|
+
const error = requestUrl.searchParams.get("error");
|
|
186
|
+
const errorDescription = requestUrl.searchParams.get("error_description");
|
|
187
|
+
if (error) {
|
|
188
|
+
const message = errorDescription || error;
|
|
189
|
+
const linkingErrorRedirect = await createLinkingErrorRedirect({
|
|
190
|
+
request,
|
|
191
|
+
origin,
|
|
192
|
+
redirectPath: next,
|
|
193
|
+
error,
|
|
194
|
+
errorDescription,
|
|
195
|
+
message,
|
|
196
|
+
source: "oauth_error",
|
|
197
|
+
isLinkingFlow,
|
|
198
|
+
resolveLinkingErrorMessage
|
|
199
|
+
});
|
|
200
|
+
if (linkingErrorRedirect) {
|
|
201
|
+
return linkingErrorRedirect;
|
|
202
|
+
}
|
|
203
|
+
return NextResponse.redirect(
|
|
204
|
+
`${origin}${signInPath}?error=${encodeURIComponent(message)}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (code) {
|
|
208
|
+
const supabase = await createClient();
|
|
209
|
+
const { error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
|
|
210
|
+
if (!exchangeError) {
|
|
211
|
+
return redirectAfterSuccessfulAuth({
|
|
212
|
+
supabase,
|
|
213
|
+
request,
|
|
214
|
+
origin,
|
|
215
|
+
redirectPath: next,
|
|
216
|
+
flow: "oauth",
|
|
217
|
+
postAuthHook,
|
|
218
|
+
postAuthHookErrorMode,
|
|
219
|
+
signInPath,
|
|
220
|
+
resolveSuccessRedirect
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const errorMessage = exchangeError.message;
|
|
224
|
+
const linkingErrorRedirect = await createLinkingErrorRedirect({
|
|
225
|
+
request,
|
|
226
|
+
origin,
|
|
227
|
+
redirectPath: next,
|
|
228
|
+
error: exchangeError.name || "code_exchange",
|
|
229
|
+
errorDescription: null,
|
|
230
|
+
message: errorMessage,
|
|
231
|
+
source: "code_exchange",
|
|
232
|
+
isLinkingFlow,
|
|
233
|
+
resolveLinkingErrorMessage
|
|
234
|
+
});
|
|
235
|
+
if (linkingErrorRedirect) {
|
|
236
|
+
return linkingErrorRedirect;
|
|
237
|
+
}
|
|
238
|
+
return NextResponse.redirect(
|
|
239
|
+
`${origin}${signInPath}?error=${encodeURIComponent(errorMessage)}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (tokenHash || rawOtpType) {
|
|
243
|
+
if (!tokenHash || !otpType) {
|
|
244
|
+
return NextResponse.redirect(
|
|
245
|
+
`${origin}${signInPath}?error=${encodeURIComponent("Invalid verification link")}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const supabase = await createClient();
|
|
249
|
+
const { error: verifyError } = await supabase.auth.verifyOtp({
|
|
250
|
+
token_hash: tokenHash,
|
|
251
|
+
type: otpType
|
|
252
|
+
});
|
|
253
|
+
if (!verifyError) {
|
|
254
|
+
return redirectAfterSuccessfulAuth({
|
|
255
|
+
supabase,
|
|
256
|
+
request,
|
|
257
|
+
origin,
|
|
258
|
+
redirectPath: next,
|
|
259
|
+
flow: "otp",
|
|
260
|
+
otpType,
|
|
261
|
+
postAuthHook,
|
|
262
|
+
postAuthHookErrorMode,
|
|
263
|
+
signInPath,
|
|
264
|
+
resolveSuccessRedirect
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return NextResponse.redirect(
|
|
268
|
+
`${origin}${signInPath}?error=${encodeURIComponent(verifyError.message)}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return NextResponse.redirect(
|
|
272
|
+
`${origin}${signInPath}?error=No authorization code received`
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { createAuthCallbackHandler };
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@caffeinebounce/identity",
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"description": "Authentication components and handlers for Caffeine Bounce projects",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./server": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/server.d.mts",
|
|
22
|
+
"default": "./dist/server.mjs"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/server.d.ts",
|
|
26
|
+
"default": "./dist/server.js"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"test": "yarn run -T vitest run",
|
|
37
|
+
"test:watch": "yarn run -T vitest",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"lint": "biome check src/"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/caffeinebounce/shared-packages.git",
|
|
49
|
+
"directory": "packages/identity"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"next": ">=14.0.0",
|
|
53
|
+
"react": "^18 || ^19",
|
|
54
|
+
"react-dom": "^18 || ^19"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"next": {
|
|
58
|
+
"optional": false
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@caffeinebounce/logger": "^0.10.0",
|
|
63
|
+
"@caffeinebounce/shared-utils": "^0.7.136",
|
|
64
|
+
"@caffeinebounce/ui": "^0.62.2",
|
|
65
|
+
"@supabase/ssr": "^0.8.0",
|
|
66
|
+
"@supabase/supabase-js": "^2.49.4",
|
|
67
|
+
"input-otp": "^1.4.2",
|
|
68
|
+
"lucide-react": "^0.577.0",
|
|
69
|
+
"sonner": "^2.0.7",
|
|
70
|
+
"validator": "^13.15.26"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "^25.5.0",
|
|
74
|
+
"@types/react": "^19.2.14",
|
|
75
|
+
"@types/react-dom": "^19.2.3",
|
|
76
|
+
"@types/validator": "^13.15.10",
|
|
77
|
+
"next": "^16.2.6",
|
|
78
|
+
"react": "^19.2.4",
|
|
79
|
+
"react-dom": "^19.2.4",
|
|
80
|
+
"tsup": "^8.5.1",
|
|
81
|
+
"typescript": "^5.9.3"
|
|
82
|
+
},
|
|
83
|
+
"license": "MIT"
|
|
84
|
+
}
|