@coreiam/auth 1.0.7 → 1.0.9

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.
@@ -4,7 +4,7 @@ export interface AuthResult {
4
4
  status: number;
5
5
  data: any;
6
6
  }
7
- export declare const getSession: () => Promise<CoreIAMIdentity | null>;
7
+ export declare function getSession(): Promise<CoreIAMIdentity | null>;
8
8
  export declare function loginActionSdk(identifier: string, password: string): Promise<AuthResult>;
9
9
  export declare function registerActionSdk(userData: any): Promise<AuthResult>;
10
10
  export declare function logoutActionSdk(): Promise<AuthResult>;
@@ -1,25 +1,134 @@
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
+ // src/nextjs/actions.ts
2
123
  'use server';
124
+ // 'use server';
3
125
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.getSession = void 0;
126
+ exports.getSession = getSession;
5
127
  exports.loginActionSdk = loginActionSdk;
6
128
  exports.registerActionSdk = registerActionSdk;
7
129
  exports.logoutActionSdk = logoutActionSdk;
8
- const react_1 = require("react");
9
130
  const headers_1 = require("next/headers");
10
131
  const core_1 = require("../core");
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
132
  let _iam = null;
24
133
  function getIam() {
25
134
  if (!_iam)
@@ -71,27 +180,9 @@ async function syncCookiesToNext(response) {
71
180
  }
72
181
  }
73
182
  }
74
- // FIX: wrapped in React's cache(). This is the generic fix for "why are we
75
- // calling /profile so much" — any number of calls to getSession() within the
76
- // same request/render (direct calls, or nested inside a consumer's own
77
- // currentUser()-style wrapper) now collapse into a single network round trip
78
- // to CoreIAM automatically, for every current and future call site.
79
- //
80
- // Deliberately NOT adding a refresh-retry fallback here (an earlier draft in
81
- // this thread did). Next.js only allows cookies().set()/cookies().delete()
82
- // inside a Server Action or Route Handler — never during a Server Component
83
- // render — and getSession() is called from both (e.g. from a plain
84
- // RootLayout render, and from consumers' currentUser()-style wrappers used
85
- // throughout Server Components). A refresh path that can only sometimes
86
- // persist its new cookies isn't safe: on the render-context call it would
87
- // still redeem the refresh token server-side (rotating/burning it) but never
88
- // deliver the new one to the browser, stranding the client on a now-dead
89
- // refresh token. Refresh belongs solely in middleware.ts, which is the one
90
- // place in the request lifecycle actually allowed to mutate response
91
- // cookies. If middleware already refreshed upstream, this succeeds; if
92
- // middleware's refresh also failed, returning null here is correct, not a
93
- // bug to work around.
94
- exports.getSession = (0, react_1.cache)(async () => {
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!
185
+ async function getSession() {
95
186
  const reqHeaders = await (0, headers_1.headers)();
96
187
  const response = await getIam().proxy('/auth/profile', { method: 'GET' }, reqHeaders);
97
188
  if (!response.ok)
@@ -100,7 +191,7 @@ exports.getSession = (0, react_1.cache)(async () => {
100
191
  if (!coreIamIdentity?.sub)
101
192
  return null;
102
193
  return coreIamIdentity;
103
- });
194
+ }
104
195
  async function loginActionSdk(identifier, password) {
105
196
  const reqHeaders = await (0, headers_1.headers)();
106
197
  try {
@@ -10,7 +10,6 @@ Object.defineProperty(exports, "loginActionSdk", { enumerable: true, get: functi
10
10
  Object.defineProperty(exports, "registerActionSdk", { enumerable: true, get: function () { return actions_1.registerActionSdk; } });
11
11
  Object.defineProperty(exports, "logoutActionSdk", { enumerable: true, get: function () { return actions_1.logoutActionSdk; } });
12
12
  Object.defineProperty(exports, "getSession", { enumerable: true, get: function () { return actions_1.getSession; } });
13
- // FIX: Export the new handleCoreIamAuth function instead of the old middleware
14
13
  var middleware_1 = require("./middleware");
15
14
  Object.defineProperty(exports, "coreIamMiddleware", { enumerable: true, get: function () { return middleware_1.coreIamMiddleware; } });
16
15
  const server_1 = require("next/server");
@@ -18,7 +17,6 @@ const core_1 = require("../core");
18
17
  class NextHandlers {
19
18
  iam;
20
19
  constructor(iam) {
21
- // If no iam client is provided, instantiate one using the env variables
22
20
  this.iam = iam || new core_1.CoreIAM({ apiKey: process.env.COREIAM_API_KEY, baseUrl: process.env.COREIAM_API_URL });
23
21
  }
24
22
  async handleProxy(req, internalPath) {
@@ -4,24 +4,6 @@ exports.coreIamMiddleware = coreIamMiddleware;
4
4
  // src/nextjs/middleware.ts
5
5
  const server_1 = require("next/server");
6
6
  const jwt_decode_1 = require("jwt-decode");
7
- // Module-level: persists across requests as long as this runs in a single,
8
- // long-running Node process (true for a VPS deploy). This lets concurrent
9
- // middleware invocations on THIS process share one in-flight refresh
10
- // instead of each independently redeeming the same (single-use) refresh
11
- // token and knocking each other out with 401 Invalid refresh token.
12
- //
13
- // This lock is per-process by nature — no amount of cleverness here makes
14
- // it coordinate across multiple Node processes/instances, since it's just
15
- // in-memory state. That's fine: it's an optimization (avoids the latency
16
- // and CoreIAM load of firing off N calls that are mostly doomed to fail
17
- // anyway), not the thing that makes rotation actually correct under
18
- // concurrency. That guarantee now lives where all callers ultimately
19
- // converge regardless of how many Qalam or CoreIAM processes exist: a
20
- // conditional (WHERE isRevoked: false) update inside a transaction in
21
- // auth.service.ts's refreshToken(), enforced by Postgres itself. If this
22
- // lock is ever bypassed or scaled beyond one process, refreshes just get a
23
- // little less efficient (a few requests eagerly hit CoreIAM instead of
24
- // waiting on a local promise) — they don't get less correct.
25
7
  let refreshInFlight = null;
26
8
  async function performRefresh(refreshTokenCookie, jwtCookie, gatewayUrl) {
27
9
  try {
@@ -92,21 +74,18 @@ async function coreIamMiddleware(request) {
92
74
  const response = server_1.NextResponse.next({
93
75
  request: { headers: requestHeaders },
94
76
  });
95
- // FIX: flags now match cookie.util.ts exactly (httpOnly, sameSite:
96
- // 'strict', secure in production only, correct maxAge per cookie),
97
- // instead of the placeholder flags used before that file was reviewed.
98
77
  const isProd = process.env.NODE_ENV === 'production';
99
78
  response.cookies.set('jwt', result.jwt, {
100
79
  httpOnly: true,
101
80
  secure: isProd,
102
81
  sameSite: 'strict',
103
- maxAge: 24 * 60 * 60, // seconds — matches the 24h maxAge in cookie.util.ts
82
+ maxAge: 24 * 60 * 60,
104
83
  });
105
84
  response.cookies.set('refreshToken', result.refreshToken, {
106
85
  httpOnly: true,
107
86
  secure: isProd,
108
87
  sameSite: 'strict',
109
- maxAge: 7 * 24 * 60 * 60, // seconds — matches the 7d maxAge in cookie.util.ts
88
+ maxAge: 7 * 24 * 60 * 60,
110
89
  });
111
90
  return { response, isAuthenticated: true };
112
91
  }
@@ -16,9 +16,9 @@ const AuthContext = (0, react_1.createContext)({
16
16
  register: async () => ({ error: 'CoreIAMProvider is not mounted' }),
17
17
  refetchIdentity: async () => { },
18
18
  });
19
- function CoreIAMProvider({ children, initialIdentity = null, redirects = {}, }) {
20
- const [identity, setIdentity] = (0, react_1.useState)(initialIdentity);
21
- const [isLoading, setIsLoading] = (0, react_1.useState)(initialIdentity === null);
19
+ function CoreIAMProvider({ children, initialIdentity, redirects = {}, }) {
20
+ const [identity, setIdentity] = (0, react_1.useState)(initialIdentity ?? null);
21
+ const [isLoading, setIsLoading] = (0, react_1.useState)(initialIdentity === undefined);
22
22
  const router = (0, navigation_1.useRouter)();
23
23
  const refetchIdentity = async () => {
24
24
  try {
@@ -33,7 +33,16 @@ function CoreIAMProvider({ children, initialIdentity = null, redirects = {}, })
33
33
  }
34
34
  };
35
35
  (0, react_1.useEffect)(() => {
36
- if (initialIdentity === null) {
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
+ if (initialIdentity === undefined) {
37
46
  refetchIdentity();
38
47
  }
39
48
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreiam/auth",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "main": "./dist/core/index.js",
5
5
  "types": "./dist/core/index.d.ts",
6
6
  "exports": {