@meistrari/auth-nuxt 3.18.0 → 3.19.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.
@@ -1,6 +1,8 @@
1
1
  import { createError, useRuntimeConfig } from "#imports";
2
2
  import { defineEventHandler, deleteCookie, getCookie, setCookie } from "h3";
3
+ import { parseTokenExpiry } from "../../../helpers/token.js";
3
4
  import { createNuxtAuthClient } from "../../../shared.js";
5
+ import { planRefreshFailure, shouldWriteTokens } from "../../utils/refresh-upstream.js";
4
6
  export default defineEventHandler(async (event) => {
5
7
  const config = useRuntimeConfig();
6
8
  const authConfig = config.public.telaAuth;
@@ -12,44 +14,62 @@ export default defineEventHandler(async (event) => {
12
14
  message: "No refresh token found"
13
15
  });
14
16
  }
17
+ const authClient = createNuxtAuthClient(
18
+ authConfig.apiUrl,
19
+ () => null,
20
+ () => refreshToken
21
+ );
22
+ let accessToken;
23
+ let newRefreshToken;
24
+ let user;
25
+ let organization;
26
+ let assurance;
15
27
  try {
16
- const authClient = createNuxtAuthClient(
17
- authConfig.apiUrl,
18
- () => null,
19
- () => refreshToken
20
- );
21
- const { accessToken, refreshToken: newRefreshToken, user, organization, assurance } = await authClient.application.refreshAccessToken(refreshToken);
22
- setCookie(event, "tela-access-token", accessToken, {
23
- secure: !import.meta.dev,
24
- sameSite: "lax",
25
- maxAge: 60 * 15,
26
- // 15 minutes
27
- priority: "high",
28
- path: "/"
29
- });
30
- setCookie(event, "tela-refresh-token", newRefreshToken, {
31
- secure: !import.meta.dev,
32
- sameSite: "lax",
33
- httpOnly: true,
34
- maxAge: 60 * 60 * 24 * 7,
35
- // 7 days
36
- priority: "high",
37
- path: "/"
38
- });
39
- return {
40
- success: true,
41
- user,
42
- organization,
43
- assurance
44
- };
28
+ ({ accessToken, refreshToken: newRefreshToken, user, organization, assurance } = await authClient.application.refreshAccessToken(refreshToken));
45
29
  } catch (error) {
46
- console.error("[Auth Refresh] Token refresh error:", error);
47
- deleteCookie(event, "tela-access-token", { path: "/" });
48
- deleteCookie(event, "tela-refresh-token", { path: "/" });
30
+ const plan = planRefreshFailure(error);
31
+ console.error(`[Auth Refresh] Token refresh error (responding ${plan.statusCode}):`, error);
32
+ if (plan.deleteCookies) {
33
+ deleteCookie(event, "tela-access-token", { path: "/" });
34
+ deleteCookie(event, "tela-refresh-token", { path: "/" });
35
+ }
49
36
  throw createError({
50
- statusCode: 401,
51
- statusMessage: "Unauthorized",
52
- message: "Failed to refresh access token"
37
+ statusCode: plan.statusCode,
38
+ statusMessage: plan.statusMessage,
39
+ message: plan.message
40
+ });
41
+ }
42
+ const currentAccessToken = getCookie(event, "tela-access-token");
43
+ if (!shouldWriteTokens({ accessToken, refreshToken: newRefreshToken, currentAccessToken })) {
44
+ console.error("[Auth Refresh] Upstream returned unusable tokens, refusing to overwrite cookies");
45
+ throw createError({
46
+ statusCode: 503,
47
+ statusMessage: "Service Unavailable",
48
+ message: "REFRESH_UPSTREAM_UNAVAILABLE"
53
49
  });
54
50
  }
51
+ setCookie(event, "tela-access-token", accessToken, {
52
+ secure: !import.meta.dev,
53
+ sameSite: "lax",
54
+ maxAge: 60 * 15,
55
+ // 15 minutes
56
+ priority: "high",
57
+ path: "/"
58
+ });
59
+ setCookie(event, "tela-refresh-token", newRefreshToken, {
60
+ secure: !import.meta.dev,
61
+ sameSite: "lax",
62
+ httpOnly: true,
63
+ maxAge: 60 * 60 * 24 * 7,
64
+ // 7 days
65
+ priority: "high",
66
+ path: "/"
67
+ });
68
+ return {
69
+ success: true,
70
+ user,
71
+ organization,
72
+ assurance,
73
+ accessTokenExpiresAt: parseTokenExpiry(accessToken)
74
+ };
55
75
  });
@@ -0,0 +1,28 @@
1
+ /** How the `/auth/refresh` route should respond to an upstream failure */
2
+ export interface RefreshFailurePlan {
3
+ statusCode: number;
4
+ statusMessage: string;
5
+ message: string;
6
+ /** Only a definitive auth failure may destroy the session cookies */
7
+ deleteCookies: boolean;
8
+ }
9
+ /**
10
+ * Maps an upstream refresh failure to the HTTP response the route should
11
+ * send, so the browser can distinguish "session revoked" (401, cookies
12
+ * deleted) from "API briefly down" (429/503, cookies kept).
13
+ */
14
+ export declare function planRefreshFailure(error: unknown): RefreshFailurePlan;
15
+ /**
16
+ * Guards cookie writes after a successful upstream refresh
17
+ *
18
+ * Rejects empty, undecodable, and already-expired tokens, plus access
19
+ * tokens strictly older than the one currently in the cookie — a delayed
20
+ * response must not overwrite a newer rotation. Grace-window reissues
21
+ * return the same token (equal `exp`), which stays writable.
22
+ */
23
+ export declare function shouldWriteTokens(input: {
24
+ accessToken: string | null | undefined;
25
+ refreshToken: string | null | undefined;
26
+ currentAccessToken?: string | null;
27
+ nowMs?: number;
28
+ }): boolean;
@@ -0,0 +1,46 @@
1
+ import { RefreshTokenExpiredError } from "@meistrari/auth-core";
2
+ import { classifyRefreshStatus, extractHttpStatus } from "../../helpers/refresh-policy.js";
3
+ import { parseTokenExpiry } from "../../helpers/token.js";
4
+ export function planRefreshFailure(error) {
5
+ const status = extractHttpStatus(error);
6
+ const isAuthFailure = error instanceof RefreshTokenExpiredError || error?.code === "REFRESH_TOKEN_EXPIRED" || status !== void 0 && classifyRefreshStatus(status) === "auth";
7
+ if (isAuthFailure) {
8
+ return {
9
+ statusCode: 401,
10
+ statusMessage: "Unauthorized",
11
+ message: "REFRESH_TOKEN_EXPIRED",
12
+ deleteCookies: true
13
+ };
14
+ }
15
+ if (status === 429) {
16
+ return {
17
+ statusCode: 429,
18
+ statusMessage: "Too Many Requests",
19
+ message: "REFRESH_RATE_LIMITED",
20
+ deleteCookies: false
21
+ };
22
+ }
23
+ return {
24
+ statusCode: 503,
25
+ statusMessage: "Service Unavailable",
26
+ message: "REFRESH_UPSTREAM_UNAVAILABLE",
27
+ deleteCookies: false
28
+ };
29
+ }
30
+ export function shouldWriteTokens(input) {
31
+ const { accessToken, refreshToken, currentAccessToken, nowMs = Date.now() } = input;
32
+ if (!accessToken || !refreshToken) {
33
+ return false;
34
+ }
35
+ const expiry = parseTokenExpiry(accessToken);
36
+ if (expiry === null || expiry <= nowMs) {
37
+ return false;
38
+ }
39
+ if (currentAccessToken) {
40
+ const currentExpiry = parseTokenExpiry(currentAccessToken);
41
+ if (currentExpiry !== null && expiry < currentExpiry) {
42
+ return false;
43
+ }
44
+ }
45
+ return true;
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -36,11 +36,11 @@
36
36
  "docs": "nuxt-module-build prepare && typedoc"
37
37
  },
38
38
  "dependencies": {
39
- "@meistrari/auth-core": "1.28.0",
39
+ "@meistrari/auth-core": "1.29.0",
40
40
  "jose": "6.1.3"
41
41
  },
42
42
  "peerDependencies": {
43
- "nuxt": "^3.0.0 || ^4.0.0",
43
+ "nuxt": "^3.10.0 || ^4.0.0",
44
44
  "vue": "^3.0.0"
45
45
  },
46
46
  "devDependencies": {