@auth-gate/react 0.1.0

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,316 @@
1
+ import * as hono_hono_base from 'hono/hono-base';
2
+ import * as hono_utils_http_status from 'hono/utils/http-status';
3
+ import * as hono_types from 'hono/types';
4
+ import { AuthGateConfig } from '@auth-gate/core';
5
+
6
+ /**
7
+ * Configuration for {@link createAuthRoutes}.
8
+ * Extends {@link AuthGateConfig} with the app's public URL.
9
+ */
10
+ interface AuthRoutesOptions extends AuthGateConfig {
11
+ /** Your app's public URL (e.g. `"http://localhost:5173"`). Used to build callback URLs. */
12
+ appUrl: string;
13
+ }
14
+ /**
15
+ * Create a Hono router with all AuthGate auth endpoints.
16
+ *
17
+ * Mount the returned router at `/api/auth` (or any prefix you prefer):
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { Hono } from "hono";
22
+ * import { createAuthRoutes } from "@auth-gate/react/server";
23
+ *
24
+ * const app = new Hono();
25
+ * const auth = createAuthRoutes({
26
+ * apiKey: process.env.AUTHGATE_API_KEY!,
27
+ * projectId: process.env.AUTHGATE_PROJECT_ID!,
28
+ * baseUrl: process.env.AUTHGATE_URL!,
29
+ * sessionSecret: process.env.SESSION_SECRET!,
30
+ * appUrl: "http://localhost:5173",
31
+ * });
32
+ *
33
+ * app.route("/api/auth", auth);
34
+ * ```
35
+ *
36
+ * @param options - AuthGate config plus `appUrl`.
37
+ * @returns A Hono router instance with all auth routes.
38
+ */
39
+ declare function createAuthRoutes(options: AuthRoutesOptions): hono_hono_base.HonoBase<hono_types.BlankEnv, {
40
+ "/:provider/login": {
41
+ $get: {
42
+ input: {
43
+ param: {
44
+ provider: string;
45
+ };
46
+ };
47
+ output: {
48
+ error: string;
49
+ };
50
+ outputFormat: "json";
51
+ status: 400;
52
+ } | {
53
+ input: {
54
+ param: {
55
+ provider: string;
56
+ };
57
+ };
58
+ output: undefined;
59
+ outputFormat: "redirect";
60
+ status: 302;
61
+ };
62
+ };
63
+ } & {
64
+ "/callback": {
65
+ $get: {
66
+ input: {};
67
+ output: undefined;
68
+ outputFormat: "redirect";
69
+ status: 302;
70
+ };
71
+ };
72
+ } & {
73
+ "/email/signup": {
74
+ $post: {
75
+ input: {};
76
+ output: {};
77
+ outputFormat: "json";
78
+ status: 400;
79
+ } | {
80
+ input: {};
81
+ output: {
82
+ error: string;
83
+ };
84
+ outputFormat: "json";
85
+ status: 401;
86
+ } | {
87
+ input: {};
88
+ output: {
89
+ success: true;
90
+ redirect: string;
91
+ };
92
+ outputFormat: "json";
93
+ status: hono_utils_http_status.ContentfulStatusCode;
94
+ };
95
+ };
96
+ } & {
97
+ "/email/signin": {
98
+ $post: {
99
+ input: {};
100
+ output: {};
101
+ outputFormat: "json";
102
+ status: 400;
103
+ } | {
104
+ input: {};
105
+ output: {
106
+ mfa_required: true;
107
+ mfa_challenge: any;
108
+ mfa_methods: any;
109
+ };
110
+ outputFormat: "json";
111
+ status: hono_utils_http_status.ContentfulStatusCode;
112
+ } | {
113
+ input: {};
114
+ output: {
115
+ error: string;
116
+ };
117
+ outputFormat: "json";
118
+ status: 401;
119
+ } | {
120
+ input: {};
121
+ output: {
122
+ success: true;
123
+ redirect: string;
124
+ };
125
+ outputFormat: "json";
126
+ status: hono_utils_http_status.ContentfulStatusCode;
127
+ };
128
+ };
129
+ } & {
130
+ "/email/forgot-password": {
131
+ $post: {
132
+ input: {};
133
+ output: {};
134
+ outputFormat: "json";
135
+ status: 400;
136
+ } | {
137
+ input: {};
138
+ output: {
139
+ success: true;
140
+ };
141
+ outputFormat: "json";
142
+ status: hono_utils_http_status.ContentfulStatusCode;
143
+ };
144
+ };
145
+ } & {
146
+ "/email/reset-password": {
147
+ $post: {
148
+ input: {};
149
+ output: {};
150
+ outputFormat: "json";
151
+ status: 400;
152
+ } | {
153
+ input: {};
154
+ output: {
155
+ success: true;
156
+ redirect: string;
157
+ };
158
+ outputFormat: "json";
159
+ status: hono_utils_http_status.ContentfulStatusCode;
160
+ };
161
+ };
162
+ } & {
163
+ "/email/verify-code": {
164
+ $post: {
165
+ input: {};
166
+ output: {};
167
+ outputFormat: "json";
168
+ status: 400;
169
+ } | {
170
+ input: {};
171
+ output: {
172
+ success: true;
173
+ email_verified: true;
174
+ };
175
+ outputFormat: "json";
176
+ status: hono_utils_http_status.ContentfulStatusCode;
177
+ };
178
+ };
179
+ } & {
180
+ "/magic-link/send": {
181
+ $post: {
182
+ input: {};
183
+ output: {};
184
+ outputFormat: "json";
185
+ status: 400;
186
+ } | {
187
+ input: {};
188
+ output: {
189
+ success: true;
190
+ };
191
+ outputFormat: "json";
192
+ status: hono_utils_http_status.ContentfulStatusCode;
193
+ };
194
+ };
195
+ } & {
196
+ "/sms/send-code": {
197
+ $post: {
198
+ input: {};
199
+ output: {};
200
+ outputFormat: "json";
201
+ status: 400;
202
+ } | {
203
+ input: {};
204
+ output: {
205
+ success: true;
206
+ };
207
+ outputFormat: "json";
208
+ status: hono_utils_http_status.ContentfulStatusCode;
209
+ };
210
+ };
211
+ } & {
212
+ "/sms/verify-code": {
213
+ $post: {
214
+ input: {};
215
+ output: {};
216
+ outputFormat: "json";
217
+ status: 400;
218
+ } | {
219
+ input: {};
220
+ output: {
221
+ error: string;
222
+ };
223
+ outputFormat: "json";
224
+ status: 401;
225
+ } | {
226
+ input: {};
227
+ output: {
228
+ success: true;
229
+ redirect: string;
230
+ };
231
+ outputFormat: "json";
232
+ status: hono_utils_http_status.ContentfulStatusCode;
233
+ };
234
+ };
235
+ } & {
236
+ "/me": {
237
+ $get: {
238
+ input: {};
239
+ output: {
240
+ user: null;
241
+ };
242
+ outputFormat: "json";
243
+ status: hono_utils_http_status.ContentfulStatusCode;
244
+ } | {
245
+ input: {};
246
+ output: {
247
+ user: {
248
+ id: string;
249
+ email: string | null;
250
+ phone: string | null;
251
+ name: string | null;
252
+ picture: string | null;
253
+ provider: string;
254
+ };
255
+ };
256
+ outputFormat: "json";
257
+ status: hono_utils_http_status.ContentfulStatusCode;
258
+ };
259
+ };
260
+ } & {
261
+ "/mfa/verify": {
262
+ $post: {
263
+ input: {};
264
+ output: {};
265
+ outputFormat: "json";
266
+ status: 400;
267
+ } | {
268
+ input: {};
269
+ output: {
270
+ error: string;
271
+ };
272
+ outputFormat: "json";
273
+ status: 401;
274
+ } | {
275
+ input: {};
276
+ output: {
277
+ success: true;
278
+ redirect: string;
279
+ };
280
+ outputFormat: "json";
281
+ status: hono_utils_http_status.ContentfulStatusCode;
282
+ };
283
+ };
284
+ } & {
285
+ "/refresh": {
286
+ $post: {
287
+ input: {};
288
+ output: {
289
+ error: string;
290
+ };
291
+ outputFormat: "json";
292
+ status: 401;
293
+ } | {
294
+ input: {};
295
+ output: {
296
+ success: true;
297
+ token: string;
298
+ };
299
+ outputFormat: "json";
300
+ status: hono_utils_http_status.ContentfulStatusCode;
301
+ };
302
+ };
303
+ } & {
304
+ "/logout": {
305
+ $post: {
306
+ input: {};
307
+ output: {
308
+ success: true;
309
+ };
310
+ outputFormat: "json";
311
+ status: hono_utils_http_status.ContentfulStatusCode;
312
+ };
313
+ };
314
+ }, "/", "/logout">;
315
+
316
+ export { type AuthRoutesOptions, createAuthRoutes };
@@ -0,0 +1,326 @@
1
+ // src/server.ts
2
+ import { Hono } from "hono";
3
+ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
4
+ import {
5
+ createAuthGateClient,
6
+ SUPPORTED_PROVIDERS
7
+ } from "@auth-gate/core";
8
+ function createAuthRoutes(options) {
9
+ const client = createAuthGateClient(options);
10
+ const appUrl = options.appUrl;
11
+ async function setSessionCookie(c, user) {
12
+ const encrypted = await client.encryptSession(user);
13
+ const opts = client.getSessionCookieOptions();
14
+ setCookie(c, client.cookieName, encrypted, {
15
+ httpOnly: opts.httpOnly,
16
+ secure: opts.secure,
17
+ sameSite: opts.sameSite === "lax" ? "Lax" : opts.sameSite === "strict" ? "Strict" : "None",
18
+ maxAge: opts.maxAge,
19
+ path: opts.path
20
+ });
21
+ }
22
+ async function getSessionUser(c) {
23
+ const cookie = getCookie(c, client.cookieName);
24
+ if (!cookie) return null;
25
+ return client.decryptSession(cookie);
26
+ }
27
+ const routes = new Hono().get("/:provider/login", async (c) => {
28
+ const provider = c.req.param("provider");
29
+ if (!SUPPORTED_PROVIDERS.includes(provider)) {
30
+ return c.json({ error: "Unsupported provider" }, 400);
31
+ }
32
+ const { state, nonce } = await client.createState();
33
+ const nonceOpts = client.getNonceCookieOptions();
34
+ setCookie(c, client.nonceCookieName, nonce, {
35
+ httpOnly: nonceOpts.httpOnly,
36
+ secure: nonceOpts.secure,
37
+ sameSite: nonceOpts.sameSite === "lax" ? "Lax" : nonceOpts.sameSite === "strict" ? "Strict" : "None",
38
+ maxAge: nonceOpts.maxAge,
39
+ path: nonceOpts.path
40
+ });
41
+ const callbackUrl = `${appUrl}${client.callbackPath}`;
42
+ const authUrl = new URL(
43
+ client.getOAuthUrl(provider, callbackUrl)
44
+ );
45
+ authUrl.searchParams.set("state", state);
46
+ return c.redirect(authUrl.toString());
47
+ }).get("/callback", async (c) => {
48
+ var _a;
49
+ const token = c.req.query("token");
50
+ const state = c.req.query("state");
51
+ const mfaChallenge = c.req.query("mfa_challenge");
52
+ const mfaMethods = c.req.query("mfa_methods");
53
+ const mfaSetupRequired = c.req.query("mfa_setup_required");
54
+ const refreshTokenParam = c.req.query("refresh_token");
55
+ if (mfaChallenge) {
56
+ const mfaUrl = new URL("/mfa-verify", appUrl);
57
+ mfaUrl.searchParams.set("mfa_challenge", mfaChallenge);
58
+ if (mfaMethods) mfaUrl.searchParams.set("mfa_methods", mfaMethods);
59
+ return c.redirect(mfaUrl.toString());
60
+ }
61
+ if (!token) {
62
+ return c.redirect(`${appUrl}/login?error=missing_token`);
63
+ }
64
+ if (state) {
65
+ const nonce = (_a = getCookie(c, client.nonceCookieName)) != null ? _a : "";
66
+ deleteCookie(c, client.nonceCookieName, { path: "/" });
67
+ if (!await client.verifyState(state, nonce)) {
68
+ return c.redirect(`${appUrl}/login?error=invalid_state`);
69
+ }
70
+ }
71
+ const result = await client.verifyToken(token);
72
+ if (!result.valid || !result.user) {
73
+ return c.redirect(`${appUrl}/login?error=invalid_token`);
74
+ }
75
+ await setSessionCookie(c, result.user);
76
+ if (refreshTokenParam) {
77
+ setCookie(c, client.refreshTokenCookieName, refreshTokenParam, {
78
+ httpOnly: true,
79
+ secure: process.env.NODE_ENV === "production",
80
+ sameSite: "Lax",
81
+ maxAge: client.sessionMaxAge,
82
+ path: "/"
83
+ });
84
+ }
85
+ if (mfaSetupRequired) {
86
+ return c.redirect(`${appUrl}/mfa-setup`);
87
+ }
88
+ return c.redirect(`${appUrl}/dashboard`);
89
+ }).post("/email/signup", async (c) => {
90
+ const body = await c.req.json();
91
+ const { email, password, name } = body;
92
+ if (!email || !password) {
93
+ return c.json({ error: "Missing email or password" }, 400);
94
+ }
95
+ const response = await client.emailSignup({
96
+ email,
97
+ password,
98
+ name,
99
+ callbackUrl: `${appUrl}${client.callbackPath}`
100
+ });
101
+ if (!response.ok) {
102
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
103
+ return c.json(error, response.status);
104
+ }
105
+ const data = await response.json().catch(() => ({ error: "Request failed" }));
106
+ const verified = await client.verifyToken(data.token);
107
+ if (!verified.valid || !verified.user) {
108
+ return c.json({ error: "Token verification failed" }, 401);
109
+ }
110
+ await setSessionCookie(c, verified.user);
111
+ return c.json({ success: true, redirect: "/dashboard" });
112
+ }).post("/email/signin", async (c) => {
113
+ const body = await c.req.json();
114
+ const { email, password } = body;
115
+ if (!email || !password) {
116
+ return c.json({ error: "Missing email or password" }, 400);
117
+ }
118
+ const response = await client.emailSignin({
119
+ email,
120
+ password,
121
+ callbackUrl: `${appUrl}${client.callbackPath}`
122
+ });
123
+ if (!response.ok) {
124
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
125
+ return c.json(error, response.status);
126
+ }
127
+ const data = await response.json().catch(() => ({ error: "Request failed" }));
128
+ if (data.mfa_required) {
129
+ return c.json({
130
+ mfa_required: true,
131
+ mfa_challenge: data.mfa_challenge,
132
+ mfa_methods: data.mfa_methods
133
+ });
134
+ }
135
+ const verified = await client.verifyToken(data.token);
136
+ if (!verified.valid || !verified.user) {
137
+ return c.json({ error: "Token verification failed" }, 401);
138
+ }
139
+ await setSessionCookie(c, verified.user);
140
+ if (data.refresh_token) {
141
+ setCookie(c, client.refreshTokenCookieName, data.refresh_token, {
142
+ httpOnly: true,
143
+ secure: process.env.NODE_ENV === "production",
144
+ sameSite: "Lax",
145
+ maxAge: client.sessionMaxAge,
146
+ path: "/"
147
+ });
148
+ }
149
+ return c.json({
150
+ success: true,
151
+ redirect: data.mfa_setup_required ? "/mfa-setup" : "/dashboard"
152
+ });
153
+ }).post("/email/forgot-password", async (c) => {
154
+ const body = await c.req.json();
155
+ const { email } = body;
156
+ if (!email) {
157
+ return c.json({ error: "Missing email" }, 400);
158
+ }
159
+ const response = await client.emailForgotPassword({
160
+ email,
161
+ callbackUrl: `${appUrl}/reset-password`
162
+ });
163
+ if (!response.ok) {
164
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
165
+ return c.json(error, response.status);
166
+ }
167
+ return c.json({ success: true });
168
+ }).post("/email/reset-password", async (c) => {
169
+ const body = await c.req.json();
170
+ const { token, password } = body;
171
+ if (!token || !password) {
172
+ return c.json({ error: "Missing token or password" }, 400);
173
+ }
174
+ const response = await client.emailResetPassword({ token, password });
175
+ if (!response.ok) {
176
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
177
+ return c.json(error, response.status);
178
+ }
179
+ const data = await response.json().catch(() => ({ error: "Request failed" }));
180
+ if (data.token) {
181
+ const verified = await client.verifyToken(data.token);
182
+ if (verified.valid && verified.user) {
183
+ await setSessionCookie(c, verified.user);
184
+ return c.json({ success: true, redirect: "/dashboard" });
185
+ }
186
+ }
187
+ return c.json({ success: true, redirect: "/login" });
188
+ }).post("/email/verify-code", async (c) => {
189
+ const body = await c.req.json();
190
+ const { email, code } = body;
191
+ if (!email || !code) {
192
+ return c.json({ error: "Missing email or code" }, 400);
193
+ }
194
+ const response = await client.emailVerifyCode({ email, code });
195
+ if (!response.ok) {
196
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
197
+ return c.json(error, response.status);
198
+ }
199
+ return c.json({ success: true, email_verified: true });
200
+ }).post("/magic-link/send", async (c) => {
201
+ const body = await c.req.json();
202
+ const { email } = body;
203
+ if (!email) {
204
+ return c.json({ error: "Missing email" }, 400);
205
+ }
206
+ const response = await client.magicLinkSend({
207
+ email,
208
+ callbackUrl: `${appUrl}${client.callbackPath}`
209
+ });
210
+ if (!response.ok) {
211
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
212
+ return c.json(error, response.status);
213
+ }
214
+ return c.json({ success: true });
215
+ }).post("/sms/send-code", async (c) => {
216
+ const body = await c.req.json();
217
+ const { phone } = body;
218
+ if (!phone) {
219
+ return c.json({ error: "Missing phone number" }, 400);
220
+ }
221
+ const response = await client.smsSendCode({
222
+ phone,
223
+ callbackUrl: `${appUrl}${client.callbackPath}`
224
+ });
225
+ if (!response.ok) {
226
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
227
+ return c.json(error, response.status);
228
+ }
229
+ return c.json({ success: true });
230
+ }).post("/sms/verify-code", async (c) => {
231
+ const body = await c.req.json();
232
+ const { phone, code } = body;
233
+ if (!phone || !code) {
234
+ return c.json({ error: "Missing phone or code" }, 400);
235
+ }
236
+ const response = await client.smsVerifyCode({
237
+ phone,
238
+ code,
239
+ callbackUrl: `${appUrl}${client.callbackPath}`
240
+ });
241
+ if (!response.ok) {
242
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
243
+ return c.json(error, response.status);
244
+ }
245
+ const data = await response.json().catch(() => ({ error: "Request failed" }));
246
+ const verified = await client.verifyToken(data.token);
247
+ if (!verified.valid || !verified.user) {
248
+ return c.json({ error: "Token verification failed" }, 401);
249
+ }
250
+ await setSessionCookie(c, verified.user);
251
+ return c.json({ success: true, redirect: "/dashboard" });
252
+ }).get("/me", async (c) => {
253
+ const user = await getSessionUser(c);
254
+ if (!user) {
255
+ return c.json({ user: null });
256
+ }
257
+ const refreshCookie = getCookie(c, client.refreshTokenCookieName);
258
+ if (refreshCookie) {
259
+ const result = await client.refreshToken(refreshCookie);
260
+ if (!result) {
261
+ deleteCookie(c, client.cookieName, { path: "/" });
262
+ deleteCookie(c, client.refreshTokenCookieName, { path: "/" });
263
+ return c.json({ user: null });
264
+ }
265
+ const verified = await client.verifyToken(result.token);
266
+ if (verified.valid && verified.user) {
267
+ await setSessionCookie(c, verified.user);
268
+ return c.json({ user: verified.user });
269
+ }
270
+ }
271
+ return c.json({ user });
272
+ }).post("/mfa/verify", async (c) => {
273
+ const body = await c.req.json();
274
+ const { challenge, code, method } = body;
275
+ if (!challenge || !code || !method) {
276
+ return c.json({ error: "Missing challenge, code, or method" }, 400);
277
+ }
278
+ const response = await client.mfaVerify({ challenge, code, method });
279
+ if (!response.ok) {
280
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
281
+ return c.json(error, response.status);
282
+ }
283
+ const data = await response.json();
284
+ const verified = await client.verifyToken(data.token);
285
+ if (!verified.valid || !verified.user) {
286
+ return c.json({ error: "Token verification failed" }, 401);
287
+ }
288
+ await setSessionCookie(c, verified.user);
289
+ if (data.refresh_token) {
290
+ setCookie(c, client.refreshTokenCookieName, data.refresh_token, {
291
+ httpOnly: true,
292
+ secure: process.env.NODE_ENV === "production",
293
+ sameSite: "Lax",
294
+ maxAge: client.sessionMaxAge,
295
+ path: "/"
296
+ });
297
+ }
298
+ return c.json({ success: true, redirect: "/dashboard" });
299
+ }).post("/refresh", async (c) => {
300
+ const refreshCookie = getCookie(c, client.refreshTokenCookieName);
301
+ if (!refreshCookie) {
302
+ return c.json({ error: "No refresh token" }, 401);
303
+ }
304
+ const result = await client.refreshToken(refreshCookie);
305
+ if (!result) {
306
+ return c.json({ error: "Invalid refresh token" }, 401);
307
+ }
308
+ const verified = await client.verifyToken(result.token);
309
+ if (verified.valid && verified.user) {
310
+ await setSessionCookie(c, verified.user);
311
+ }
312
+ return c.json({ success: true, token: result.token });
313
+ }).post("/logout", async (c) => {
314
+ const refreshCookie = getCookie(c, client.refreshTokenCookieName);
315
+ if (refreshCookie) {
316
+ await client.revokeSession(refreshCookie);
317
+ deleteCookie(c, client.refreshTokenCookieName, { path: "/" });
318
+ }
319
+ deleteCookie(c, client.cookieName, { path: "/" });
320
+ return c.json({ success: true });
321
+ });
322
+ return routes;
323
+ }
324
+ export {
325
+ createAuthRoutes
326
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@auth-gate/react",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.mjs",
9
+ "require": "./dist/index.cjs"
10
+ },
11
+ "./server": {
12
+ "types": "./dist/server.d.ts",
13
+ "import": "./dist/server.mjs",
14
+ "require": "./dist/server.cjs"
15
+ }
16
+ },
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.mjs",
19
+ "types": "./dist/index.d.ts",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "dependencies": {
24
+ "@auth-gate/core": "0.1.0"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18",
28
+ "hono": ">=4"
29
+ },
30
+ "devDependencies": {
31
+ "react": "^19.0.0",
32
+ "@types/react": "^19",
33
+ "hono": "^4.0.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "typecheck": "tsc --noEmit"
40
+ }
41
+ }