@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.
@@ -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
+ }