@devopsplaybook.io/common-web 0.2.1-beta.3.d89a040 → 0.3.0-beta.4.2c7f218

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/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project are documented here.
5
5
  This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
  and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-09-28
9
+
10
+ ### Added
11
+
12
+ - Automatically renew authenticated sessions before their JWT expires.
13
+
8
14
  ## [0.2.1] - 2026-09-28
9
15
 
10
16
  ### Fixed
package/README.md CHANGED
@@ -50,6 +50,10 @@ required. The `playground/` directory demonstrates a consuming app.
50
50
  The canonical auth storage key is `auth_token`. During this compatibility
51
51
  release, `AUTH_TOKEN` and `AUTH_TOKEN_KEY` are read and migrated to the
52
52
  canonical key. Tokens remain in `localStorage`, matching existing applications.
53
+ When an authenticated token has entered the final 20% of its lifetime,
54
+ `AuthService` renews it through `POST /api/users/session/refresh` before
55
+ returning it from `getToken()` or `getAuthHeader()`. This extends active
56
+ sessions without requiring users to sign in again.
53
57
 
54
58
  ## Tokens and app-shell styles
55
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devopsplaybook.io/common-web",
3
- "version": "0.2.1-beta.3.d89a040",
3
+ "version": "0.3.0-beta.4.2c7f218",
4
4
  "description": "Shared Nuxt layer for DevOpsPlaybook web applications",
5
5
  "license": "ISC",
6
6
  "engines": {
@@ -1,8 +1,15 @@
1
+ import axios from "axios";
1
2
  import { jwtDecode } from "jwt-decode";
3
+ import Config from "./Config";
2
4
 
3
5
  export const AUTH_TOKEN_KEY = "auth_token";
4
6
  const LEGACY_TOKEN_KEYS = ["AUTH_TOKEN", "AUTH_TOKEN_KEY"] as const;
5
7
 
8
+ interface AuthTokenClaims {
9
+ exp?: number;
10
+ iat?: number;
11
+ }
12
+
6
13
  export interface AuthHeaders {
7
14
  headers?: {
8
15
  Authorization: string;
@@ -10,6 +17,10 @@ export interface AuthHeaders {
10
17
  }
11
18
 
12
19
  export class AuthService {
20
+ private static renewal:
21
+ | { token: string; promise: Promise<string | null> }
22
+ | null = null;
23
+
13
24
  static async isAuthenticated(): Promise<boolean> {
14
25
  return Boolean(await AuthService.getToken());
15
26
  }
@@ -40,12 +51,22 @@ export class AuthService {
40
51
 
41
52
  if (!token) return null;
42
53
 
43
- const decoded = jwtDecode<{ exp?: number }>(token);
44
- if (typeof decoded.exp === "number" && decoded.exp <= Date.now() / 1000) {
54
+ const decoded = jwtDecode<AuthTokenClaims>(token);
55
+ const now = Date.now() / 1000;
56
+ if (typeof decoded.exp === "number" && decoded.exp <= now) {
45
57
  await AuthService.removeToken();
46
58
  return null;
47
59
  }
48
60
 
61
+ if (
62
+ typeof decoded.exp === "number" &&
63
+ typeof decoded.iat === "number" &&
64
+ decoded.exp > decoded.iat &&
65
+ decoded.exp - now <= (decoded.exp - decoded.iat) * 0.2
66
+ ) {
67
+ return AuthService.renewToken(token);
68
+ }
69
+
49
70
  return token;
50
71
  }
51
72
 
@@ -55,4 +76,43 @@ export class AuthService {
55
76
  ? { headers: { Authorization: `Bearer ${token}` } }
56
77
  : {};
57
78
  }
79
+
80
+ private static async renewToken(token: string): Promise<string | null> {
81
+ const existing = AuthService.renewal;
82
+ if (existing?.token === token) {
83
+ return existing.promise;
84
+ }
85
+
86
+ const promise = AuthService.requestTokenRenewal(token);
87
+ AuthService.renewal = { token, promise };
88
+ try {
89
+ return await promise;
90
+ } finally {
91
+ if (AuthService.renewal?.promise === promise) {
92
+ AuthService.renewal = null;
93
+ }
94
+ }
95
+ }
96
+
97
+ private static async requestTokenRenewal(
98
+ token: string,
99
+ ): Promise<string | null> {
100
+ const config = await Config.get();
101
+ const response = await axios.post<{ token?: unknown }>(
102
+ `${config.SERVER_URL}/users/session/refresh`,
103
+ {},
104
+ { headers: { Authorization: `Bearer ${token}` } },
105
+ );
106
+ const renewedToken = response.data?.token;
107
+ if (typeof renewedToken !== "string" || renewedToken.length === 0) {
108
+ throw new Error("Session renewal response did not include a token");
109
+ }
110
+
111
+ if (localStorage.getItem(AUTH_TOKEN_KEY) !== token) {
112
+ return localStorage.getItem(AUTH_TOKEN_KEY);
113
+ }
114
+
115
+ await AuthService.saveToken(renewedToken);
116
+ return renewedToken;
117
+ }
58
118
  }