@coreiam/auth 1.0.9 → 1.0.10

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.
@@ -1,127 +1,6 @@
1
1
  "use strict";
2
- // 'use server';
3
- // import { cache } from 'react';
4
- // import { cookies, headers } from 'next/headers';
5
- // import { CoreIAM, CoreIAMIdentity } from '../core';
6
- // export interface AuthResult {
7
- // ok: boolean;
8
- // status: number;
9
- // data: any;
10
- // }
11
- // // FIX: lazy singleton instead of `const iam = new CoreIAM();` at module
12
- // // scope. The constructor throws if COREIAM_API_KEY/COREIAM_API_URL are
13
- // // missing — fine on the server, where those env vars exist — but a
14
- // // module-level throw happens the instant the module is evaluated, before
15
- // // any function in it is even called. If this module's 'use server'
16
- // // boundary is ever not recognized correctly by the bundler (see the note
17
- // // on the browser error below), the real module gets included in the client
18
- // // bundle instead of being swapped for action-reference stubs, and that
19
- // // throw fires in the browser instead, where those env vars are never
20
- // // defined. Making this lazy doesn't fix a lost 'use server' boundary by
21
- // // itself, but it does mean import alone can never crash — only an actual
22
- // // invocation can, which is strictly better either way.
23
- // let _iam: CoreIAM | null = null;
24
- // function getIam(): CoreIAM {
25
- // if (!_iam) _iam = new CoreIAM();
26
- // return _iam;
27
- // }
28
- // async function syncCookiesToNext(response: Response) {
29
- // const cookieStore = await cookies();
30
- // const setCookies = response.headers.getSetCookie();
31
- // for (const cookieStr of setCookies) {
32
- // const parts = cookieStr.split(';');
33
- // const [nameValue, ...attributes] = parts;
34
- // const [name, ...valueParts] = nameValue.split('=');
35
- // const cookieOptions: any = {};
36
- // let isDeleting = false;
37
- // attributes.forEach(attr => {
38
- // const [attrName, attrValue] = attr.trim().split('=');
39
- // const lowerName = attrName?.toLowerCase();
40
- // if (lowerName === 'httponly') cookieOptions.httpOnly = true;
41
- // if (lowerName === 'secure') cookieOptions.secure = true;
42
- // if (lowerName === 'path') cookieOptions.path = attrValue || '/';
43
- // if (lowerName === 'max-age') {
44
- // const maxAge = parseInt(attrValue, 10);
45
- // if (maxAge <= 0) isDeleting = true;
46
- // else cookieOptions.maxAge = maxAge;
47
- // }
48
- // if (lowerName === 'expires') {
49
- // const expiryDate = new Date(attrValue);
50
- // if (expiryDate.getTime() < Date.now()) isDeleting = true;
51
- // else cookieOptions.expires = expiryDate;
52
- // }
53
- // if (lowerName === 'samesite') cookieOptions.sameSite = attrValue?.toLowerCase() || 'lax';
54
- // });
55
- // const cookieName = name.trim();
56
- // const cookieValue = valueParts.join('=').trim();
57
- // if (isDeleting || cookieValue === '') {
58
- // cookieStore.delete(cookieName);
59
- // } else {
60
- // cookieStore.set(cookieName, cookieValue, cookieOptions);
61
- // }
62
- // }
63
- // }
64
- // // FIX: wrapped in React's cache(). This is the generic fix for "why are we
65
- // // calling /profile so much" — any number of calls to getSession() within the
66
- // // same request/render (direct calls, or nested inside a consumer's own
67
- // // currentUser()-style wrapper) now collapse into a single network round trip
68
- // // to CoreIAM automatically, for every current and future call site.
69
- // //
70
- // // Deliberately NOT adding a refresh-retry fallback here (an earlier draft in
71
- // // this thread did). Next.js only allows cookies().set()/cookies().delete()
72
- // // inside a Server Action or Route Handler — never during a Server Component
73
- // // render — and getSession() is called from both (e.g. from a plain
74
- // // RootLayout render, and from consumers' currentUser()-style wrappers used
75
- // // throughout Server Components). A refresh path that can only sometimes
76
- // // persist its new cookies isn't safe: on the render-context call it would
77
- // // still redeem the refresh token server-side (rotating/burning it) but never
78
- // // deliver the new one to the browser, stranding the client on a now-dead
79
- // // refresh token. Refresh belongs solely in middleware.ts, which is the one
80
- // // place in the request lifecycle actually allowed to mutate response
81
- // // cookies. If middleware already refreshed upstream, this succeeds; if
82
- // // middleware's refresh also failed, returning null here is correct, not a
83
- // // bug to work around.
84
- // export const getSession = cache(async (): Promise<CoreIAMIdentity | null> => {
85
- // const reqHeaders = await headers();
86
- // const response = await getIam().proxy('/auth/profile', { method: 'GET' }, reqHeaders);
87
- // if (!response.ok) return null;
88
- // const coreIamIdentity = await response.json();
89
- // if (!coreIamIdentity?.sub) return null;
90
- // return coreIamIdentity as CoreIAMIdentity;
91
- // });
92
- // export async function loginActionSdk(identifier: string, password: string): Promise<AuthResult> {
93
- // const reqHeaders = await headers();
94
- // try {
95
- // const response = await getIam().loginWithCsrfProtection(identifier, password, reqHeaders);
96
- // await syncCookiesToNext(response);
97
- // return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
98
- // } catch (error) {
99
- // return { ok: false, status: 500, data: { error: error instanceof Error ? error.message : 'Login failed' } };
100
- // }
101
- // }
102
- // export async function registerActionSdk(userData: any): Promise<AuthResult> {
103
- // const reqHeaders = await headers();
104
- // try {
105
- // const response = await getIam().registerWithCsrfProtection(userData, reqHeaders);
106
- // await syncCookiesToNext(response);
107
- // return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
108
- // } catch (error) {
109
- // return { ok: false, status: 500, data: { error: error instanceof Error ? error.message : 'Registration failed' } };
110
- // }
111
- // }
112
- // export async function logoutActionSdk(): Promise<AuthResult> {
113
- // const reqHeaders = await headers();
114
- // try {
115
- // const response = await getIam().logout(reqHeaders);
116
- // await syncCookiesToNext(response);
117
- // return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
118
- // } catch (error) {
119
- // return { ok: false, status: 500, data: { error: 'Logout failed' } };
120
- // }
121
- // }
122
2
  // src/nextjs/actions.ts
123
- 'use server';
124
- // 'use server';
3
+ // REMOVE 'use server' from here.
125
4
  Object.defineProperty(exports, "__esModule", { value: true });
126
5
  exports.getSession = getSession;
127
6
  exports.loginActionSdk = loginActionSdk;
@@ -180,8 +59,7 @@ async function syncCookiesToNext(response) {
180
59
  }
181
60
  }
182
61
  }
183
- // FIX: MUST be `export async function`, NOT `export const`.
184
- // If it's a const, Next.js ignores 'use server' and bundles this file in the client!
62
+ // This is called by Server Components (e.g., layout.tsx)
185
63
  async function getSession() {
186
64
  const reqHeaders = await (0, headers_1.headers)();
187
65
  const response = await getIam().proxy('/auth/profile', { method: 'GET' }, reqHeaders);
@@ -192,6 +70,8 @@ async function getSession() {
192
70
  return null;
193
71
  return coreIamIdentity;
194
72
  }
73
+ // These are no longer called by the SDK's provider.tsx,
74
+ // but can still be used by Qalam's Server Actions if needed.
195
75
  async function loginActionSdk(identifier, password) {
196
76
  const reqHeaders = await (0, headers_1.headers)();
197
77
  try {
@@ -1,22 +1,15 @@
1
1
  import React from 'react';
2
2
  import type { CoreIAMIdentity } from '../core';
3
3
  export interface CoreIAMRedirects {
4
- /** Where to send the user after a successful login, if no callbackUrl is passed to login(). */
5
4
  afterLogin?: string;
6
- /** Where to send the user after logout. */
7
5
  afterLogout?: string;
8
6
  }
9
7
  interface LoginResult {
10
8
  success?: boolean;
11
9
  error?: string;
10
+ twoFactor?: boolean;
12
11
  }
13
12
  interface AuthContextType {
14
- /**
15
- * Generic CoreIAM identity (sub/email/roles/tenantId) — NOT an app-shaped
16
- * user. This SDK has no knowledge of any consumer's local user model
17
- * (roles, profile fields, etc). Consumers that need an enriched user
18
- * object should build their own layer on top of this, keyed off `sub`.
19
- */
20
13
  identity: CoreIAMIdentity | null;
21
14
  isLoading: boolean;
22
15
  isAuthenticated: boolean;
@@ -30,9 +23,7 @@ interface AuthContextType {
30
23
  }
31
24
  export declare function CoreIAMProvider({ children, initialIdentity, redirects, }: {
32
25
  children: React.ReactNode;
33
- /** Pass the result of a server-side getSession() call to skip the client fetch on first paint. */
34
26
  initialIdentity?: CoreIAMIdentity | null;
35
- /** Where to redirect after login/logout. Omit for no redirect (caller handles navigation itself). */
36
27
  redirects?: CoreIAMRedirects;
37
28
  }): import("react/jsx-runtime").JSX.Element;
38
29
  export declare function useAuth(): AuthContextType;
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ // src/nextjs/provider.tsx
2
3
  'use client';
3
4
  Object.defineProperty(exports, "__esModule", { value: true });
4
5
  exports.CoreIAMProvider = CoreIAMProvider;
@@ -6,7 +7,6 @@ exports.useAuth = useAuth;
6
7
  const jsx_runtime_1 = require("react/jsx-runtime");
7
8
  const react_1 = require("react");
8
9
  const navigation_1 = require("next/navigation");
9
- const actions_1 = require("./actions");
10
10
  const AuthContext = (0, react_1.createContext)({
11
11
  identity: null,
12
12
  isLoading: true,
@@ -20,10 +20,11 @@ function CoreIAMProvider({ children, initialIdentity, redirects = {}, }) {
20
20
  const [identity, setIdentity] = (0, react_1.useState)(initialIdentity ?? null);
21
21
  const [isLoading, setIsLoading] = (0, react_1.useState)(initialIdentity === undefined);
22
22
  const router = (0, navigation_1.useRouter)();
23
- const refetchIdentity = async () => {
23
+ const refetchIdentity = (0, react_1.useCallback)(async () => {
24
24
  try {
25
- const result = await (0, actions_1.getSession)();
26
- setIdentity(result);
25
+ // Call the Next.js API route, which securely calls CoreIAM
26
+ const res = await fetch('/api/auth/session', { credentials: 'include' });
27
+ setIdentity(res.ok ? await res.json() : null);
27
28
  }
28
29
  catch {
29
30
  setIdentity(null);
@@ -31,33 +32,40 @@ function CoreIAMProvider({ children, initialIdentity, redirects = {}, }) {
31
32
  finally {
32
33
  setIsLoading(false);
33
34
  }
34
- };
35
+ }, []);
35
36
  (0, react_1.useEffect)(() => {
36
- // FIX: was `initialIdentity === null`, which can't tell "SSR ran and
37
- // confirmed you're logged out" (a correct, final null) apart from "no
38
- // SSR value was ever provided" (a real undefined). Since layout.tsx
39
- // always calls getSession() and passes its result — even when that
40
- // result legitimately is null — every logged-out page view was
41
- // treated as "no SSR value, fetch again," firing a second, wholly
42
- // redundant /auth/profile call for something SSR had already
43
- // determined. undefined (the prop genuinely omitted) is the only case
44
- // that should trigger this fallback fetch.
45
37
  if (initialIdentity === undefined) {
46
38
  refetchIdentity();
47
39
  }
48
40
  else {
49
41
  setIsLoading(false);
50
42
  }
51
- // Intentionally only on mount — initialIdentity is a one-time SSR seed,
52
- // not something we want to re-run this effect for if it changes identity.
53
43
  // eslint-disable-next-line react-hooks/exhaustive-deps
54
44
  }, []);
55
45
  const login = async (values, callbackUrl) => {
56
46
  setIsLoading(true);
57
47
  try {
58
- const result = await (0, actions_1.loginActionSdk)(values.email, values.password);
59
- if (!result.ok) {
60
- return { error: result.data?.message || result.data?.error || 'Invalid credentials' };
48
+ // Fetch CSRF token first
49
+ const csrfRes = await fetch('/api/auth/csrf', { credentials: 'include' });
50
+ if (!csrfRes.ok)
51
+ return { error: 'Failed to get CSRF token' };
52
+ const { csrfToken } = await csrfRes.json();
53
+ // Call login API route
54
+ const loginRes = await fetch('/api/auth/login', {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'x-csrf-token': csrfToken,
59
+ },
60
+ credentials: 'include',
61
+ body: JSON.stringify(values),
62
+ });
63
+ const data = await loginRes.json();
64
+ if (!loginRes.ok) {
65
+ return { error: data.message || data.error || 'Invalid credentials' };
66
+ }
67
+ if (data.twoFactor) {
68
+ return { twoFactor: true };
61
69
  }
62
70
  await refetchIdentity();
63
71
  const target = callbackUrl || redirects.afterLogin;
@@ -74,7 +82,10 @@ function CoreIAMProvider({ children, initialIdentity, redirects = {}, }) {
74
82
  };
75
83
  const logout = async () => {
76
84
  try {
77
- await (0, actions_1.logoutActionSdk)();
85
+ await fetch('/api/auth/logout', {
86
+ method: 'POST',
87
+ credentials: 'include',
88
+ });
78
89
  }
79
90
  finally {
80
91
  setIdentity(null);
@@ -85,14 +96,29 @@ function CoreIAMProvider({ children, initialIdentity, redirects = {}, }) {
85
96
  const register = async (userData) => {
86
97
  setIsLoading(true);
87
98
  try {
88
- const result = await (0, actions_1.registerActionSdk)(userData);
89
- if (!result.ok) {
90
- return { error: result.data?.message || result.data?.error || 'Registration failed' };
99
+ const csrfRes = await fetch('/api/auth/csrf', { credentials: 'include' });
100
+ if (!csrfRes.ok)
101
+ return { error: 'Failed to get CSRF token' };
102
+ const { csrfToken } = await csrfRes.json();
103
+ const registerRes = await fetch('/api/auth/register', {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'x-csrf-token': csrfToken,
108
+ },
109
+ credentials: 'include',
110
+ body: JSON.stringify(userData),
111
+ });
112
+ if (!registerRes.ok) {
113
+ const error = await registerRes.json();
114
+ return { error: error.message || error.error || 'Registration failed' };
91
115
  }
92
- // Registration on the backend doesn't auto-issue a session — log in
93
- // immediately after, same as the flow this replaces did.
116
+ // Auto-login after registration
94
117
  return await login({ email: userData.email, password: userData.password });
95
118
  }
119
+ catch {
120
+ return { error: 'Something went wrong' };
121
+ }
96
122
  finally {
97
123
  setIsLoading(false);
98
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreiam/auth",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "main": "./dist/core/index.js",
5
5
  "types": "./dist/core/index.d.ts",
6
6
  "exports": {