najm-auth 2.0.5 → 2.0.6

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/README.md CHANGED
@@ -681,6 +681,9 @@ throw new HttpError(403, 'Insufficient permissions for this action');
681
681
  their session version.
682
682
  - Expired signed sessions recover through authoritative, non-rotating refresh
683
683
  validation; middleware verifies the reissued HMAC before using its claims.
684
+ - Server-side recovery sends only the configured refresh cookie and accepts
685
+ relative or exact same-origin endpoints. URL credentials and any
686
+ scheme/hostname/port change are rejected before the network request.
684
687
  - `verifyAlways` forces that authoritative check on every protected request;
685
688
  the default bounds cached role/status staleness to `session.maxAge`.
686
689
 
@@ -247,4 +247,4 @@ declare class NajmAuthClient {
247
247
  */
248
248
  declare function createAuthClient(config: AuthClientConfig): NajmAuthClient;
249
249
 
250
- export { AuthError as A, type DecodedToken as D, FetchClient as F, type HydrateSession as H, NajmAuthClient as N, type OAuthProvider as O, type RetryConfig as R, type SyncPayload as S, type TabSyncMessage as T, type AuthClientConfig as a, type AuthEventMap as b, createAuthClient as c, type AuthState as d, type AuthUser as e, type AuthEvent as f, type AuthEventHandler as g, type ServerResponse as h, type TokenPair as i, type RequestOptions as j, type OAuthLoginOptions as k };
250
+ export { type AuthClientConfig as A, type DecodedToken as D, FetchClient as F, type HydrateSession as H, NajmAuthClient as N, type OAuthLoginOptions as O, type RequestOptions as R, type SyncPayload as S, type TabSyncMessage as T, AuthError as a, type AuthEvent as b, type AuthEventHandler as c, type AuthEventMap as d, type AuthState as e, type AuthUser as f, type OAuthProvider as g, type RetryConfig as h, type ServerResponse as i, type TokenPair as j, createAuthClient as k };
@@ -139,12 +139,16 @@ async function requestSessionRecovery(options) {
139
139
  if (!isCookieValue(options.refreshCookieValue)) {
140
140
  return { status: "invalid" };
141
141
  }
142
- if (!isSafeRecoveryEndpoint(options.endpoint)) {
142
+ const endpoint = sameOriginRecoveryEndpoint(
143
+ options.endpoint,
144
+ options.requestOrigin
145
+ );
146
+ if (!endpoint) {
143
147
  return { status: "unavailable" };
144
148
  }
145
149
  let response;
146
150
  try {
147
- response = await fetch(options.endpoint, {
151
+ response = await fetch(endpoint, {
148
152
  method: "POST",
149
153
  headers: {
150
154
  Accept: "application/json",
@@ -208,21 +212,24 @@ function isCookieName(value) {
208
212
  }
209
213
  __name(isCookieName, "isCookieName");
210
214
  function isCookieValue(value) {
211
- return value.length > 0 && !/[\r\n;]/.test(value);
215
+ return value.length > 0 && /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/.test(value);
212
216
  }
213
217
  __name(isCookieValue, "isCookieValue");
214
- function isSafeRecoveryEndpoint(value) {
218
+ function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
215
219
  try {
216
- const url = new URL(value);
217
- if (url.username || url.password) return false;
218
- if (url.protocol === "https:") return true;
219
- if (url.protocol !== "http:") return false;
220
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
220
+ const trusted = new URL(requestOrigin);
221
+ if (trusted.protocol !== "https:" && trusted.protocol !== "http:" || trusted.username || trusted.password) {
222
+ return void 0;
223
+ }
224
+ const resolved = new URL(endpoint, trusted.origin);
225
+ if (resolved.username || resolved.password) return void 0;
226
+ if (resolved.origin !== trusted.origin) return void 0;
227
+ return resolved.toString();
221
228
  } catch {
222
- return false;
229
+ return void 0;
223
230
  }
224
231
  }
225
- __name(isSafeRecoveryEndpoint, "isSafeRecoveryEndpoint");
232
+ __name(sameOriginRecoveryEndpoint, "sameOriginRecoveryEndpoint");
226
233
 
227
234
  // src/client/server/withAuthMiddleware.ts
228
235
  function withAuthMiddleware(config) {
@@ -281,6 +288,7 @@ function withAuthMiddleware(config) {
281
288
  const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
282
289
  recovery = await requestSessionRecovery({
283
290
  endpoint,
291
+ requestOrigin: url.origin,
284
292
  refreshCookieName: cookieName,
285
293
  refreshCookieValue: refreshCookie,
286
294
  sessionCookieName,
@@ -1,5 +1,5 @@
1
- import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-B9dGk9MH.js';
2
- export { a as AuthClientConfig, A as AuthError, f as AuthEvent, g as AuthEventHandler, b as AuthEventMap, d as AuthState, e as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, k as OAuthLoginOptions, O as OAuthProvider, j as RequestOptions, R as RetryConfig, h as ServerResponse, i as TokenPair, c as createAuthClient } from '../NajmAuthClient-B9dGk9MH.js';
1
+ import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-0yzFb9CR.js';
2
+ export { A as AuthClientConfig, a as AuthError, b as AuthEvent, c as AuthEventHandler, d as AuthEventMap, e as AuthState, f as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, O as OAuthLoginOptions, g as OAuthProvider, R as RequestOptions, h as RetryConfig, i as ServerResponse, j as TokenPair, k as createAuthClient } from '../NajmAuthClient-0yzFb9CR.js';
3
3
 
4
4
  /**
5
5
  * Decode a JWT token payload without verification.
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, CSSProperties, ReactElement } from 'react';
4
- import { N as NajmAuthClient, H as HydrateSession, d as AuthState, e as AuthUser, A as AuthError, k as OAuthLoginOptions, f as AuthEvent, b as AuthEventMap } from '../../NajmAuthClient-B9dGk9MH.js';
4
+ import { N as NajmAuthClient, H as HydrateSession, e as AuthState, f as AuthUser, a as AuthError, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-0yzFb9CR.js';
5
5
 
6
6
  interface AuthProviderProps {
7
7
  client: NajmAuthClient;
@@ -7,7 +7,7 @@ import { useEffect, useRef } from "react";
7
7
 
8
8
  // src/client/react/context.ts
9
9
  import { createContext, useContext } from "react";
10
- var KEY = /* @__PURE__ */ Symbol.for("najm:auth:client:context");
10
+ var KEY = Symbol.for("najm:auth:client:context");
11
11
  var contextStore = globalThis;
12
12
  function getAuthClientContext() {
13
13
  const existing = contextStore[KEY];
@@ -1,4 +1,4 @@
1
- import { e as AuthUser, F as FetchClient, R as RetryConfig, N as NajmAuthClient } from '../../NajmAuthClient-B9dGk9MH.js';
1
+ import { f as AuthUser, F as FetchClient, N as NajmAuthClient, h as RetryConfig } from '../../NajmAuthClient-0yzFb9CR.js';
2
2
  export { withAuthMiddleware } from '../edge.js';
3
3
  import 'next/server';
4
4
 
@@ -72,7 +72,7 @@ interface ServerSession {
72
72
  interface GetSessionConfig {
73
73
  /**
74
74
  * Base URL for auth endpoints.
75
- * Defaults to `${NEXT_PUBLIC_API_URL || http://localhost:${PORT||3000}}/api`.
75
+ * Defaults to `NEXT_PUBLIC_API_URL` or the same-origin `/api` path.
76
76
  */
77
77
  baseURL?: string;
78
78
  /** Auth route prefix appended to baseURL (default: '/auth'). */
@@ -152,12 +152,16 @@ async function requestSessionRecovery(options) {
152
152
  if (!isCookieValue(options.refreshCookieValue)) {
153
153
  return { status: "invalid" };
154
154
  }
155
- if (!isSafeRecoveryEndpoint(options.endpoint)) {
155
+ const endpoint = sameOriginRecoveryEndpoint(
156
+ options.endpoint,
157
+ options.requestOrigin
158
+ );
159
+ if (!endpoint) {
156
160
  return { status: "unavailable" };
157
161
  }
158
162
  let response;
159
163
  try {
160
- response = await fetch(options.endpoint, {
164
+ response = await fetch(endpoint, {
161
165
  method: "POST",
162
166
  headers: {
163
167
  Accept: "application/json",
@@ -216,17 +220,20 @@ function isCookieName(value) {
216
220
  return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
217
221
  }
218
222
  function isCookieValue(value) {
219
- return value.length > 0 && !/[\r\n;]/.test(value);
223
+ return value.length > 0 && /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/.test(value);
220
224
  }
221
- function isSafeRecoveryEndpoint(value) {
225
+ function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
222
226
  try {
223
- const url = new URL(value);
224
- if (url.username || url.password) return false;
225
- if (url.protocol === "https:") return true;
226
- if (url.protocol !== "http:") return false;
227
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
227
+ const trusted = new URL(requestOrigin);
228
+ if (trusted.protocol !== "https:" && trusted.protocol !== "http:" || trusted.username || trusted.password) {
229
+ return void 0;
230
+ }
231
+ const resolved = new URL(endpoint, trusted.origin);
232
+ if (resolved.username || resolved.password) return void 0;
233
+ if (resolved.origin !== trusted.origin) return void 0;
234
+ return resolved.toString();
228
235
  } catch {
229
- return false;
236
+ return void 0;
230
237
  }
231
238
  }
232
239
  var init_sessionRecovery = __esm({
@@ -238,7 +245,7 @@ var init_sessionRecovery = __esm({
238
245
  __name(readSetCookieValue, "readSetCookieValue");
239
246
  __name(isCookieName, "isCookieName");
240
247
  __name(isCookieValue, "isCookieValue");
241
- __name(isSafeRecoveryEndpoint, "isSafeRecoveryEndpoint");
248
+ __name(sameOriginRecoveryEndpoint, "sameOriginRecoveryEndpoint");
242
249
  }
243
250
  });
244
251
 
@@ -253,8 +260,25 @@ __export(getSession_exports, {
253
260
  function defaultBaseURL() {
254
261
  const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
255
262
  if (explicit) return explicit;
256
- const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
257
- return `${origin.replace(/\/$/, "")}/api`;
263
+ const publicUrl = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL : void 0;
264
+ return publicUrl || "/api";
265
+ }
266
+ function firstForwardedValue(value) {
267
+ const first = value?.split(",")[0]?.trim();
268
+ return first || void 0;
269
+ }
270
+ function requestOriginFromHeaders(headers) {
271
+ const host = firstForwardedValue(headers.get("x-forwarded-host")) ?? firstForwardedValue(headers.get("host"));
272
+ if (!host) return void 0;
273
+ const protocol = firstForwardedValue(headers.get("x-forwarded-proto")) ?? "https";
274
+ if (protocol !== "https" && protocol !== "http") return void 0;
275
+ try {
276
+ const url = new URL(`${protocol}://${host}`);
277
+ if (url.username || url.password) return void 0;
278
+ return url.origin;
279
+ } catch {
280
+ return void 0;
281
+ }
258
282
  }
259
283
  async function getSession(config = {}) {
260
284
  const cookieName = config.cookieName ?? "refreshToken";
@@ -264,11 +288,15 @@ async function getSession(config = {}) {
264
288
  const strict = config.mode === "strict";
265
289
  let sessionCookieValue;
266
290
  let refreshCookieValue;
291
+ let requestOrigin;
267
292
  try {
268
293
  const mod = await import("next/headers");
269
294
  const cookieStore = await mod.cookies();
270
295
  sessionCookieValue = cookieStore.get(sessionCookieName)?.value;
271
296
  refreshCookieValue = cookieStore.get(cookieName)?.value;
297
+ if (typeof mod.headers === "function") {
298
+ requestOrigin = requestOriginFromHeaders(await mod.headers());
299
+ }
272
300
  } catch {
273
301
  if (strict) throw new AuthConfigError("Failed to read cookies from Next.js headers()");
274
302
  return null;
@@ -295,9 +323,14 @@ async function getSession(config = {}) {
295
323
  if (strict) throw new NoSessionError("No recoverable refresh session");
296
324
  return null;
297
325
  }
298
- const endpoint = config.recoveryURL ? new URL(config.recoveryURL, baseURL).toString() : authEndpoint(baseURL, prefix, "/session/recover");
326
+ if (!requestOrigin) {
327
+ if (strict) throw new AuthConfigError("Incoming request origin is unavailable");
328
+ return null;
329
+ }
330
+ const endpoint = config.recoveryURL ? new URL(config.recoveryURL, requestOrigin).toString() : authEndpoint(baseURL, prefix, "/session/recover", requestOrigin);
299
331
  const recovery = await requestSessionRecovery({
300
332
  endpoint,
333
+ requestOrigin,
301
334
  refreshCookieName: cookieName,
302
335
  refreshCookieValue,
303
336
  sessionCookieName,
@@ -359,6 +392,8 @@ var init_getSession = __esm({
359
392
  code = "AUTH_TRANSPORT_ERROR";
360
393
  };
361
394
  __name(defaultBaseURL, "defaultBaseURL");
395
+ __name(firstForwardedValue, "firstForwardedValue");
396
+ __name(requestOriginFromHeaders, "requestOriginFromHeaders");
362
397
  __name(getSession, "getSession");
363
398
  }
364
399
  });
@@ -611,6 +646,7 @@ function withAuthMiddleware(config) {
611
646
  const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
612
647
  recovery = await requestSessionRecovery({
613
648
  endpoint,
649
+ requestOrigin: url.origin,
614
650
  refreshCookieName: cookieName,
615
651
  refreshCookieValue: refreshCookie,
616
652
  sessionCookieName,
@@ -1185,8 +1221,7 @@ function defineAuth(authConfig = {}) {
1185
1221
  retry
1186
1222
  } = authConfig;
1187
1223
  const sessionConfig = {
1188
- baseURL: void 0,
1189
- // resolved at call time from env/defaults
1224
+ baseURL: apiBaseURL,
1190
1225
  authPrefix,
1191
1226
  cookieName,
1192
1227
  sessionCookieName,
package/dist/index.js CHANGED
@@ -10,12 +10,12 @@ import { Err as Err11, plugin } from "najm-core";
10
10
  import { cache } from "najm-cache";
11
11
 
12
12
  // src/auth.tokens.ts
13
- var AUTH_CONFIG = /* @__PURE__ */ Symbol.for("najm:auth:config");
14
- var AUTH_SCHEMA = /* @__PURE__ */ Symbol.for("najm:auth:schema");
15
- var AUTH_USER = /* @__PURE__ */ Symbol.for("najm:auth:user");
16
- var AUTH_ROLE = /* @__PURE__ */ Symbol.for("najm:auth:role");
17
- var AUTH_PERMISSIONS = /* @__PURE__ */ Symbol.for("najm:auth:permissions");
18
- var AUTH_ENCRYPTION_KEY = /* @__PURE__ */ Symbol.for("najm:auth:encryption-key");
13
+ var AUTH_CONFIG = Symbol.for("najm:auth:config");
14
+ var AUTH_SCHEMA = Symbol.for("najm:auth:schema");
15
+ var AUTH_USER = Symbol.for("najm:auth:user");
16
+ var AUTH_ROLE = Symbol.for("najm:auth:role");
17
+ var AUTH_PERMISSIONS = Symbol.for("najm:auth:permissions");
18
+ var AUTH_ENCRYPTION_KEY = Symbol.for("najm:auth:encryption-key");
19
19
 
20
20
  // src/schema/pg.ts
21
21
  import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex } from "drizzle-orm/pg-core";
@@ -3980,9 +3980,9 @@ var revokeTokenDto = z4.object({
3980
3980
  // src/ownership/scopedOwnership.ts
3981
3981
  import { aliasedTable, eq as eq6, getTableColumns, sql as sql4 } from "drizzle-orm";
3982
3982
  var DEFAULT_ADMIN_ROLES = ["admin"];
3983
- var DRIZZLE_NAME = /* @__PURE__ */ Symbol.for("drizzle:Name");
3984
- var DRIZZLE_BASE_NAME = /* @__PURE__ */ Symbol.for("drizzle:BaseName");
3985
- var DRIZZLE_IS_ALIAS = /* @__PURE__ */ Symbol.for("drizzle:IsAlias");
3983
+ var DRIZZLE_NAME = Symbol.for("drizzle:Name");
3984
+ var DRIZZLE_BASE_NAME = Symbol.for("drizzle:BaseName");
3985
+ var DRIZZLE_IS_ALIAS = Symbol.for("drizzle:IsAlias");
3986
3986
  function join2(left, right) {
3987
3987
  const table = right.table;
3988
3988
  if (!table)
@@ -4430,9 +4430,9 @@ __name(configureOwnership, "configureOwnership");
4430
4430
  // src/ownership/ScopeGuard.ts
4431
4431
  import "reflect-metadata";
4432
4432
  import { composeGuards as composeGuards5 } from "najm-guard";
4433
- var ACTION_KEY = /* @__PURE__ */ Symbol.for("najm:guard:action");
4434
- var TOKEN_KEY = /* @__PURE__ */ Symbol.for("najm:guard:token");
4435
- var POLICY_KEY = /* @__PURE__ */ Symbol.for("najm:policy:token");
4433
+ var ACTION_KEY = Symbol.for("najm:guard:action");
4434
+ var TOKEN_KEY = Symbol.for("najm:guard:token");
4435
+ var POLICY_KEY = Symbol.for("najm:policy:token");
4436
4436
  var PERM = {
4437
4437
  list: /* @__PURE__ */ __name((n) => `read:${n}`, "list"),
4438
4438
  read: /* @__PURE__ */ __name((n) => `read:${n}`, "read"),
@@ -4502,7 +4502,7 @@ var __metadata25 = function(k, v) {
4502
4502
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4503
4503
  };
4504
4504
  var _a16;
4505
- var OWNED_META = /* @__PURE__ */ Symbol.for("najm:owned");
4505
+ var OWNED_META = Symbol.for("najm:owned");
4506
4506
  var ScopeContext = class ScopeContext2 {
4507
4507
  static {
4508
4508
  __name(this, "ScopeContext");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [
@@ -67,9 +67,9 @@
67
67
  "@testing-library/react": "^16.3.2",
68
68
  "@types/jsonwebtoken": "^9.0.10",
69
69
  "@types/node": "^25.0.2",
70
- "drizzle-kit": "^0.31.8",
71
- "drizzle-orm": "^0.45.1",
72
- "happy-dom": "^17.6.3",
70
+ "drizzle-kit": "^0.31.10",
71
+ "drizzle-orm": "^0.45.2",
72
+ "happy-dom": "^20.11.1",
73
73
  "tsup": "^8.5.1",
74
74
  "typescript": "^5.9.3"
75
75
  },
@@ -93,8 +93,8 @@
93
93
  "zod": "^4.2.1"
94
94
  },
95
95
  "peerDependencies": {
96
- "drizzle-orm": "^0.45.1",
97
- "hono": "^4.0.0",
96
+ "drizzle-orm": "^0.45.2",
97
+ "hono": "^4.12.18",
98
98
  "react": ">=18",
99
99
  "next": ">=14"
100
100
  },