@aneuhold/core-ts-api-lib 3.0.39 → 3.0.41

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,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 🔖 [3.0.41] (2026-07-23)
9
+
10
+ ### ✅ Added
11
+
12
+ - Added `APIService.setOnAuthExpired()` and `GCloudAPIService.setOnAuthExpired()` to register a callback that fires when a session cannot be recovered from a 401, so consumers can clear stored tokens and prompt re-login.
13
+ - Added the `OnAuthExpiredCallback` and `OnTokensRefreshedCallback` types to `AuthRefreshToken.ts`.
14
+
15
+ ### 🩹 Fixed
16
+
17
+ - Fixed 401 handling in `GCloudAPIService` so a request with no refresh token, a failed token refresh, or a post-refresh retry that is still a 401 all report expired auth instead of silently returning the unauthorized response.
18
+
19
+ ## 🔖 [3.0.40] (2026-06-19)
20
+
21
+ ### 🏗️ Changed
22
+
23
+ - Updated `@aneuhold/core-ts-db-lib` to `^5.0.10` and `@aneuhold/core-ts-lib` to `^2.4.6`.
24
+
8
25
  ## 🔖 [3.0.39] (2026-06-12)
9
26
 
10
27
  ### 🏗️ Changed
@@ -433,6 +450,8 @@ No direct code changes; version bump for compatibility.
433
450
  - Updated workflow permissions to allow repository write access
434
451
 
435
452
  <!-- Link References -->
453
+ [3.0.41]: https://github.com/aneuhold/ts-libs/compare/core-ts-api-lib-v3.0.40...core-ts-api-lib-v3.0.41
454
+ [3.0.40]: https://github.com/aneuhold/ts-libs/compare/core-ts-api-lib-v3.0.39...core-ts-api-lib-v3.0.40
436
455
  [3.0.39]: https://github.com/aneuhold/ts-libs/compare/core-ts-api-lib-v3.0.38...core-ts-api-lib-v3.0.39
437
456
  [3.0.38]: https://github.com/aneuhold/ts-libs/compare/core-ts-api-lib-v3.0.37...core-ts-api-lib-v3.0.38
438
457
  [3.0.37]: https://github.com/aneuhold/ts-libs/compare/core-ts-api-lib-v3.0.36...core-ts-api-lib-v3.0.37
@@ -1,6 +1,7 @@
1
1
  import type { AdminInput, AdminOutput } from '../../types/Admin.js';
2
2
  import type { APIResponse } from '../../types/APIResponse.js';
3
3
  import type { AuthDeleteAccountOutput } from '../../types/AuthDeleteAccount.js';
4
+ import type { OnAuthExpiredCallback, OnTokensRefreshedCallback } from '../../types/AuthRefreshToken.js';
4
5
  import type { AuthValidateUserInput, AuthValidateUserOutput } from '../../types/AuthValidateUser.js';
5
6
  import type { ProjectDashboardInput, ProjectDashboardOutput } from '../../types/project/dashboard/ProjectDashboard.js';
6
7
  import type { ProjectWorkoutPrimaryInput, ProjectWorkoutPrimaryOutput } from '../../types/project/workout/ProjectWorkout.js';
@@ -45,7 +46,16 @@ export default class APIService {
45
46
  *
46
47
  * @param callback - The callback receiving the new accessToken and refreshTokenString.
47
48
  */
48
- static setOnTokensRefreshed(callback: ((accessToken: string, refreshTokenString: string) => void) | null): void;
49
+ static setOnTokensRefreshed(callback: OnTokensRefreshedCallback | null): void;
50
+ /**
51
+ * Registers a callback that is invoked when the session cannot be recovered
52
+ * on a 401 (no refresh token, refresh failed, or the retry is still 401). The
53
+ * stored tokens are cleared before the callback fires, so use this to clear
54
+ * the consumer's own copy of them and prompt the user to log in again.
55
+ *
56
+ * @param callback - The callback invoked when auth has expired.
57
+ */
58
+ static setOnAuthExpired(callback: OnAuthExpiredCallback | null): void;
49
59
  /**
50
60
  * Calls the dashboard API and returns the result.
51
61
  *
@@ -1 +1 @@
1
- {"version":3,"file":"API.service.d.ts","sourceRoot":"","sources":["../../../src/services/APIService/API.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAChF,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,+CAA+C,CAAC;AAGvD;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B;;;;;;OAMG;WACU,YAAY,CACvB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;OAGG;WACU,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAItD;;;OAGG;WACU,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,uBAAuB,CAAC,CAAC;IAI3E;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1C;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIjD;;;;;OAKG;IACH,MAAM,CAAC,oBAAoB,CACzB,QAAQ,EAAE,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAC3E,IAAI;IAIP;;;;OAIG;WACU,gBAAgB,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;;OAIG;WACU,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IAI/E;;;;OAIG;WACU,cAAc,CACzB,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,WAAW,CAAC,2BAA2B,CAAC,CAAC;IAIpD;;OAEG;IACH,MAAM,CAAC,gBAAgB,IAAI,MAAM;IAIjC;;;;OAIG;IACH,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAInC;;OAEG;IACH,MAAM,CAAC,gBAAgB,IAAI,MAAM;CAGlC"}
1
+ {"version":3,"file":"API.service.d.ts","sourceRoot":"","sources":["../../../src/services/APIService/API.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAChF,OAAO,KAAK,EACV,qBAAqB,EACrB,yBAAyB,EAC1B,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,+CAA+C,CAAC;AAGvD;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B;;;;;;OAMG;WACU,YAAY,CACvB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;OAGG;WACU,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAItD;;;OAGG;WACU,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,uBAAuB,CAAC,CAAC;IAI3E;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1C;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIjD;;;;;OAKG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,GAAG,IAAI;IAI7E;;;;;;;OAOG;IACH,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,GAAG,IAAI;IAIrE;;;;OAIG;WACU,gBAAgB,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;;OAIG;WACU,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IAI/E;;;;OAIG;WACU,cAAc,CACzB,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,WAAW,CAAC,2BAA2B,CAAC,CAAC;IAIpD;;OAEG;IACH,MAAM,CAAC,gBAAgB,IAAI,MAAM;IAIjC;;;;OAIG;IACH,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAInC;;OAEG;IACH,MAAM,CAAC,gBAAgB,IAAI,MAAM;CAGlC"}
@@ -53,6 +53,17 @@ export default class APIService {
53
53
  static setOnTokensRefreshed(callback) {
54
54
  GCloudAPIService.setOnTokensRefreshed(callback);
55
55
  }
56
+ /**
57
+ * Registers a callback that is invoked when the session cannot be recovered
58
+ * on a 401 (no refresh token, refresh failed, or the retry is still 401). The
59
+ * stored tokens are cleared before the callback fires, so use this to clear
60
+ * the consumer's own copy of them and prompt the user to log in again.
61
+ *
62
+ * @param callback - The callback invoked when auth has expired.
63
+ */
64
+ static setOnAuthExpired(callback) {
65
+ GCloudAPIService.setOnAuthExpired(callback);
66
+ }
56
67
  /**
57
68
  * Calls the dashboard API and returns the result.
58
69
  *
@@ -1 +1 @@
1
- {"version":3,"file":"API.service.js","sourceRoot":"","sources":["../../../src/services/APIService/API.service.ts"],"names":[],"mappings":"AAeA,OAAO,gBAAgB,MAAM,0CAA0C,CAAC;AAExE;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,YAAY,CACvB,KAA4B;QAE5B,OAAO,gBAAgB,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM;QACjB,OAAO,gBAAgB,CAAC,UAAU,EAAE,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,aAAa;QACxB,OAAO,gBAAgB,CAAC,iBAAiB,EAAE,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,KAAa;QACjC,gBAAgB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAAC,KAAa;QACxC,gBAAgB,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,oBAAoB,CACzB,QAA4E;QAE5E,gBAAgB,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAC3B,KAA4B;QAE5B,OAAO,gBAAgB,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,KAAiB;QACzC,OAAO,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc,CACzB,KAAiC;QAEjC,OAAO,gBAAgB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,gBAAgB;QACrB,OAAO,gBAAgB,CAAC,MAAM,EAAE,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,SAAS,CAAC,GAAW;QAC1B,gBAAgB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,gBAAgB;QACrB,OAAO,gBAAgB,CAAC,UAAU,CAAC;IACrC,CAAC;CACF"}
1
+ {"version":3,"file":"API.service.js","sourceRoot":"","sources":["../../../src/services/APIService/API.service.ts"],"names":[],"mappings":"AAmBA,OAAO,gBAAgB,MAAM,0CAA0C,CAAC;AAExE;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,YAAY,CACvB,KAA4B;QAE5B,OAAO,gBAAgB,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM;QACjB,OAAO,gBAAgB,CAAC,UAAU,EAAE,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,aAAa;QACxB,OAAO,gBAAgB,CAAC,iBAAiB,EAAE,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,KAAa;QACjC,gBAAgB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAAC,KAAa;QACxC,gBAAgB,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAA0C;QACpE,gBAAgB,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,gBAAgB,CAAC,QAAsC;QAC5D,gBAAgB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAC3B,KAA4B;QAE5B,OAAO,gBAAgB,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,KAAiB;QACzC,OAAO,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc,CACzB,KAAiC;QAEjC,OAAO,gBAAgB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,gBAAgB;QACrB,OAAO,gBAAgB,CAAC,MAAM,EAAE,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,SAAS,CAAC,GAAW;QAC1B,gBAAgB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,gBAAgB;QACrB,OAAO,gBAAgB,CAAC,UAAU,CAAC;IACrC,CAAC;CACF"}
@@ -1,6 +1,10 @@
1
1
  import type { AdminInput, AdminOutput } from '../../types/Admin.js';
2
2
  import type { APIResponse } from '../../types/APIResponse.js';
3
3
  import type { AuthDeleteAccountOutput } from '../../types/AuthDeleteAccount.js';
4
+ import type {
5
+ OnAuthExpiredCallback,
6
+ OnTokensRefreshedCallback
7
+ } from '../../types/AuthRefreshToken.js';
4
8
  import type {
5
9
  AuthValidateUserInput,
6
10
  AuthValidateUserOutput
@@ -73,12 +77,22 @@ export default class APIService {
73
77
  *
74
78
  * @param callback - The callback receiving the new accessToken and refreshTokenString.
75
79
  */
76
- static setOnTokensRefreshed(
77
- callback: ((accessToken: string, refreshTokenString: string) => void) | null
78
- ): void {
80
+ static setOnTokensRefreshed(callback: OnTokensRefreshedCallback | null): void {
79
81
  GCloudAPIService.setOnTokensRefreshed(callback);
80
82
  }
81
83
 
84
+ /**
85
+ * Registers a callback that is invoked when the session cannot be recovered
86
+ * on a 401 (no refresh token, refresh failed, or the retry is still 401). The
87
+ * stored tokens are cleared before the callback fires, so use this to clear
88
+ * the consumer's own copy of them and prompt the user to log in again.
89
+ *
90
+ * @param callback - The callback invoked when auth has expired.
91
+ */
92
+ static setOnAuthExpired(callback: OnAuthExpiredCallback | null): void {
93
+ GCloudAPIService.setOnAuthExpired(callback);
94
+ }
95
+
82
96
  /**
83
97
  * Calls the dashboard API and returns the result.
84
98
  *
@@ -1,14 +1,10 @@
1
1
  import type { AdminInput, AdminOutput } from '../../types/Admin.js';
2
2
  import type { APIResponse } from '../../types/APIResponse.js';
3
3
  import type { AuthDeleteAccountOutput } from '../../types/AuthDeleteAccount.js';
4
+ import type { OnAuthExpiredCallback, OnTokensRefreshedCallback } from '../../types/AuthRefreshToken.js';
4
5
  import type { AuthValidateUserInput, AuthValidateUserOutput } from '../../types/AuthValidateUser.js';
5
6
  import type { ProjectDashboardInput, ProjectDashboardOutput } from '../../types/project/dashboard/ProjectDashboard.js';
6
7
  import type { ProjectWorkoutPrimaryInput, ProjectWorkoutPrimaryOutput } from '../../types/project/workout/ProjectWorkout.js';
7
- /**
8
- * Callback invoked after tokens are successfully refreshed. The frontend
9
- * should use this to persist the new tokens (e.g. to localStorage).
10
- */
11
- type OnTokensRefreshedCallback = (accessToken: string, refreshTokenString: string) => void;
12
8
  /**
13
9
  * A service for interacting with the Google Cloud API service for personal projects.
14
10
  */
@@ -48,6 +44,15 @@ export default class GCloudAPIService {
48
44
  * @param callback - The callback receiving the new accessToken and refreshTokenString.
49
45
  */
50
46
  static setOnTokensRefreshed(callback: OnTokensRefreshedCallback | null): void;
47
+ /**
48
+ * Registers a callback that is invoked when the session cannot be recovered
49
+ * on a 401 (no refresh token, refresh failed, or the post-refresh retry is
50
+ * still 401). The stored tokens are cleared before the callback fires, so use
51
+ * this to clear the consumer's own copy of them and prompt re-login.
52
+ *
53
+ * @param callback - The callback invoked when auth has expired.
54
+ */
55
+ static setOnAuthExpired(callback: OnAuthExpiredCallback | null): void;
51
56
  /**
52
57
  * Calls the auth validateUser endpoint.
53
58
  *
@@ -83,5 +88,4 @@ export default class GCloudAPIService {
83
88
  */
84
89
  static projectWorkout(input: ProjectWorkoutPrimaryInput): Promise<APIResponse<ProjectWorkoutPrimaryOutput>>;
85
90
  }
86
- export {};
87
91
  //# sourceMappingURL=GCloudAPI.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GCloudAPI.service.d.ts","sourceRoot":"","sources":["../../../src/services/GCloudAPIService/GCloudAPI.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAEV,uBAAuB,EACxB,MAAM,kCAAkC,CAAC;AAK1C,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,+CAA+C,CAAC;AAEvD;;;GAGG;AACH,KAAK,yBAAyB,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3F;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAgB;;IACnC,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAmC;IAcrE;;;;OAIG;IACH,MAAM,CAAC,MAAM,IAAI,MAAM;IAIvB;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIhC;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1C;;;;;OAKG;IACH,MAAM,CAAC,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIjD;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,GAAG,IAAI;IAI7E;;;;OAIG;WACU,gBAAgB,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;OAGG;WACU,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAa1D;;;OAGG;WACU,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,uBAAuB,CAAC,CAAC;IAO/E;;;;OAIG;WACU,gBAAgB,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;;OAIG;WACU,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IAIxE;;;;OAIG;WACU,cAAc,CACzB,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,WAAW,CAAC,2BAA2B,CAAC,CAAC;CA6IrD"}
1
+ {"version":3,"file":"GCloudAPI.service.d.ts","sourceRoot":"","sources":["../../../src/services/GCloudAPIService/GCloudAPI.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAEV,uBAAuB,EACxB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAGV,qBAAqB,EACrB,yBAAyB,EAC1B,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,+CAA+C,CAAC;AAEvD;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAgB;;IACnC,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAmC;IAkBrE;;;;OAIG;IACH,MAAM,CAAC,MAAM,IAAI,MAAM;IAIvB;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIhC;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1C;;;;;OAKG;IACH,MAAM,CAAC,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIjD;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,GAAG,IAAI;IAI7E;;;;;;;OAOG;IACH,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,GAAG,IAAI;IAIrE;;;;OAIG;WACU,gBAAgB,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;OAGG;WACU,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAa1D;;;OAGG;WACU,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,uBAAuB,CAAC,CAAC;IAO/E;;;;OAIG;WACU,gBAAgB,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAI/C;;;;OAIG;WACU,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IAIxE;;;;OAIG;WACU,cAAc,CACzB,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,WAAW,CAAC,2BAA2B,CAAC,CAAC;CAyKrD"}
@@ -12,6 +12,8 @@ export default class GCloudAPIService {
12
12
  static #accessToken;
13
13
  static #refreshTokenString;
14
14
  static #onTokensRefreshed = null;
15
+ static #onAuthExpired = null;
16
+ static #refreshPromise = null;
15
17
  /**
16
18
  * Gets the current URL of the Google Cloud API.
17
19
  *
@@ -55,6 +57,17 @@ export default class GCloudAPIService {
55
57
  static setOnTokensRefreshed(callback) {
56
58
  this.#onTokensRefreshed = callback;
57
59
  }
60
+ /**
61
+ * Registers a callback that is invoked when the session cannot be recovered
62
+ * on a 401 (no refresh token, refresh failed, or the post-refresh retry is
63
+ * still 401). The stored tokens are cleared before the callback fires, so use
64
+ * this to clear the consumer's own copy of them and prompt re-login.
65
+ *
66
+ * @param callback - The callback invoked when auth has expired.
67
+ */
68
+ static setOnAuthExpired(callback) {
69
+ this.#onAuthExpired = callback;
70
+ }
58
71
  /**
59
72
  * Calls the auth validateUser endpoint.
60
73
  *
@@ -110,28 +123,55 @@ export default class GCloudAPIService {
110
123
  /**
111
124
  * Makes a call to the API. On a 401 response, automatically attempts to
112
125
  * refresh the access token using the stored refresh token. If refresh
113
- * succeeds, the original request is retried once.
126
+ * succeeds, the original request is retried once. When the session cannot be
127
+ * recovered, the stored tokens are cleared and the {@link #onAuthExpired}
128
+ * callback fires.
114
129
  *
115
130
  * @param urlPath - The path to the endpoint.
116
131
  * @param input - The input to the endpoint.
117
132
  */
118
133
  static async #call(urlPath, input) {
119
- const { response, decoded } = await this.#fetchAndDecode(urlPath, input);
120
- if (response.status === 401 && this.#refreshTokenString) {
121
- const refreshed = await this.#tryRefreshTokens();
122
- if (refreshed) {
123
- const retry = await this.#fetchAndDecode(urlPath, input);
124
- return retry.decoded;
125
- }
134
+ let { response, decoded } = await this.#fetchAndDecode(urlPath, input);
135
+ // On a 401, try once to recover by refreshing the tokens and retrying.
136
+ // #tryRefreshTokens returns false when there is no refresh token to use.
137
+ if (response.status === 401 && (await this.#tryRefreshTokens())) {
138
+ // Fancy destructure into pre-existing variables
139
+ ({ response, decoded } = await this.#fetchAndDecode(urlPath, input));
140
+ }
141
+ // Still a 401 after any recovery attempt means the session cannot be
142
+ // recovered, so drop the dead tokens and notify the frontend to prompt
143
+ // re-login.
144
+ if (response.status === 401) {
145
+ this.#accessToken = undefined;
146
+ this.#refreshTokenString = undefined;
147
+ this.#onAuthExpired?.();
126
148
  }
127
149
  return decoded;
128
150
  }
129
151
  /**
130
- * Attempts to refresh the access token using the stored refresh token.
131
- * On success, updates the stored tokens and notifies via the
132
- * {@link #onTokensRefreshed} callback.
152
+ * Attempts to refresh the access token, sharing one in-flight refresh across
153
+ * concurrent callers. Refresh tokens rotate server-side, so parallel
154
+ * refreshes sent with the same token would invalidate each other and drop the
155
+ * session.
133
156
  */
134
157
  static async #tryRefreshTokens() {
158
+ // Set the refreshPromise only if it is currently null, otherwise we continue.
159
+ this.#refreshPromise ??= this.#performTokenRefresh();
160
+ try {
161
+ return await this.#refreshPromise;
162
+ }
163
+ finally {
164
+ // Cleared once settled so the next 401 starts a fresh attempt rather than
165
+ // reusing a stale result.
166
+ this.#refreshPromise = null;
167
+ }
168
+ }
169
+ /**
170
+ * Exchanges the stored refresh token for a new token pair. On success,
171
+ * updates the stored tokens and notifies via the {@link #onTokensRefreshed}
172
+ * callback.
173
+ */
174
+ static async #performTokenRefresh() {
135
175
  if (!this.#refreshTokenString) {
136
176
  return false;
137
177
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GCloudAPI.service.js","sourceRoot":"","sources":["../../../src/services/GCloudAPIService/GCloudAPI.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"GCloudAPI.service.js","sourceRoot":"","sources":["../../../src/services/GCloudAPIService/GCloudAPI.service.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,9 @@ import type {
7
7
  } from '../../types/AuthDeleteAccount.js';
8
8
  import type {
9
9
  AuthRefreshTokenInput,
10
- AuthRefreshTokenOutput
10
+ AuthRefreshTokenOutput,
11
+ OnAuthExpiredCallback,
12
+ OnTokensRefreshedCallback
11
13
  } from '../../types/AuthRefreshToken.js';
12
14
  import type {
13
15
  AuthValidateUserInput,
@@ -22,12 +24,6 @@ import type {
22
24
  ProjectWorkoutPrimaryOutput
23
25
  } from '../../types/project/workout/ProjectWorkout.js';
24
26
 
25
- /**
26
- * Callback invoked after tokens are successfully refreshed. The frontend
27
- * should use this to persist the new tokens (e.g. to localStorage).
28
- */
29
- type OnTokensRefreshedCallback = (accessToken: string, refreshTokenString: string) => void;
30
-
31
27
  /**
32
28
  * A service for interacting with the Google Cloud API service for personal projects.
33
29
  */
@@ -46,6 +42,10 @@ export default class GCloudAPIService {
46
42
 
47
43
  static #onTokensRefreshed: OnTokensRefreshedCallback | null = null;
48
44
 
45
+ static #onAuthExpired: OnAuthExpiredCallback | null = null;
46
+
47
+ static #refreshPromise: Promise<boolean> | null = null;
48
+
49
49
  /**
50
50
  * Gets the current URL of the Google Cloud API.
51
51
  *
@@ -94,6 +94,18 @@ export default class GCloudAPIService {
94
94
  this.#onTokensRefreshed = callback;
95
95
  }
96
96
 
97
+ /**
98
+ * Registers a callback that is invoked when the session cannot be recovered
99
+ * on a 401 (no refresh token, refresh failed, or the post-refresh retry is
100
+ * still 401). The stored tokens are cleared before the callback fires, so use
101
+ * this to clear the consumer's own copy of them and prompt re-login.
102
+ *
103
+ * @param callback - The callback invoked when auth has expired.
104
+ */
105
+ static setOnAuthExpired(callback: OnAuthExpiredCallback | null): void {
106
+ this.#onAuthExpired = callback;
107
+ }
108
+
97
109
  /**
98
110
  * Calls the auth validateUser endpoint.
99
111
  *
@@ -170,7 +182,9 @@ export default class GCloudAPIService {
170
182
  /**
171
183
  * Makes a call to the API. On a 401 response, automatically attempts to
172
184
  * refresh the access token using the stored refresh token. If refresh
173
- * succeeds, the original request is retried once.
185
+ * succeeds, the original request is retried once. When the session cannot be
186
+ * recovered, the stored tokens are cleared and the {@link #onAuthExpired}
187
+ * callback fires.
174
188
  *
175
189
  * @param urlPath - The path to the endpoint.
176
190
  * @param input - The input to the endpoint.
@@ -179,25 +193,51 @@ export default class GCloudAPIService {
179
193
  urlPath: string,
180
194
  input: TInput
181
195
  ): Promise<APIResponse<TOutput>> {
182
- const { response, decoded } = await this.#fetchAndDecode<TInput, TOutput>(urlPath, input);
196
+ let { response, decoded } = await this.#fetchAndDecode<TInput, TOutput>(urlPath, input);
183
197
 
184
- if (response.status === 401 && this.#refreshTokenString) {
185
- const refreshed = await this.#tryRefreshTokens();
186
- if (refreshed) {
187
- const retry = await this.#fetchAndDecode<TInput, TOutput>(urlPath, input);
188
- return retry.decoded;
189
- }
198
+ // On a 401, try once to recover by refreshing the tokens and retrying.
199
+ // #tryRefreshTokens returns false when there is no refresh token to use.
200
+ if (response.status === 401 && (await this.#tryRefreshTokens())) {
201
+ // Fancy destructure into pre-existing variables
202
+ ({ response, decoded } = await this.#fetchAndDecode<TInput, TOutput>(urlPath, input));
203
+ }
204
+
205
+ // Still a 401 after any recovery attempt means the session cannot be
206
+ // recovered, so drop the dead tokens and notify the frontend to prompt
207
+ // re-login.
208
+ if (response.status === 401) {
209
+ this.#accessToken = undefined;
210
+ this.#refreshTokenString = undefined;
211
+ this.#onAuthExpired?.();
190
212
  }
191
213
 
192
214
  return decoded;
193
215
  }
194
216
 
195
217
  /**
196
- * Attempts to refresh the access token using the stored refresh token.
197
- * On success, updates the stored tokens and notifies via the
198
- * {@link #onTokensRefreshed} callback.
218
+ * Attempts to refresh the access token, sharing one in-flight refresh across
219
+ * concurrent callers. Refresh tokens rotate server-side, so parallel
220
+ * refreshes sent with the same token would invalidate each other and drop the
221
+ * session.
199
222
  */
200
223
  static async #tryRefreshTokens(): Promise<boolean> {
224
+ // Set the refreshPromise only if it is currently null, otherwise we continue.
225
+ this.#refreshPromise ??= this.#performTokenRefresh();
226
+ try {
227
+ return await this.#refreshPromise;
228
+ } finally {
229
+ // Cleared once settled so the next 401 starts a fresh attempt rather than
230
+ // reusing a stale result.
231
+ this.#refreshPromise = null;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Exchanges the stored refresh token for a new token pair. On success,
237
+ * updates the stored tokens and notifies via the {@link #onTokensRefreshed}
238
+ * callback.
239
+ */
240
+ static async #performTokenRefresh(): Promise<boolean> {
201
241
  if (!this.#refreshTokenString) {
202
242
  return false;
203
243
  }
@@ -14,4 +14,13 @@ export interface AuthRefreshTokenOutput {
14
14
  /** New raw refresh token string (rotation — old one is deleted). */
15
15
  refreshTokenString: string;
16
16
  }
17
+ /**
18
+ * Callback invoked after tokens are successfully refreshed.
19
+ */
20
+ export type OnTokensRefreshedCallback = (accessToken: string, refreshTokenString: string) => void;
21
+ /**
22
+ * Callback invoked when a session can no longer be recovered because the access
23
+ * token is expired and the refresh token is missing or invalid.
24
+ */
25
+ export type OnAuthExpiredCallback = () => void;
17
26
  //# sourceMappingURL=AuthRefreshToken.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthRefreshToken.d.ts","sourceRoot":"","sources":["../../src/types/AuthRefreshToken.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,+DAA+D;IAC/D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAC;CAC5B"}
1
+ {"version":3,"file":"AuthRefreshToken.d.ts","sourceRoot":"","sources":["../../src/types/AuthRefreshToken.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,+DAA+D;IAC/D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC;AAElG;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC"}
@@ -15,3 +15,14 @@ export interface AuthRefreshTokenOutput {
15
15
  /** New raw refresh token string (rotation — old one is deleted). */
16
16
  refreshTokenString: string;
17
17
  }
18
+
19
+ /**
20
+ * Callback invoked after tokens are successfully refreshed.
21
+ */
22
+ export type OnTokensRefreshedCallback = (accessToken: string, refreshTokenString: string) => void;
23
+
24
+ /**
25
+ * Callback invoked when a session can no longer be recovered because the access
26
+ * token is expired and the refresh token is missing or invalid.
27
+ */
28
+ export type OnAuthExpiredCallback = () => void;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@aneuhold/core-ts-api-lib",
3
3
  "author": "Anton G. Neuhold Jr.",
4
4
  "license": "MIT",
5
- "version": "3.0.39",
5
+ "version": "3.0.41",
6
6
  "description": "A library for interacting with the backend and defining the backend API for personal projects.",
7
7
  "packageManager": "pnpm@10.25.0",
8
8
  "type": "module",
@@ -72,12 +72,12 @@
72
72
  "TypeScript"
73
73
  ],
74
74
  "dependencies": {
75
- "@aneuhold/core-ts-db-lib": "^5.0.9",
76
- "@aneuhold/core-ts-lib": "^2.4.5",
75
+ "@aneuhold/core-ts-db-lib": "^5.0.10",
76
+ "@aneuhold/core-ts-lib": "^2.4.6",
77
77
  "bson": "^7.0.0"
78
78
  },
79
79
  "devDependencies": {
80
- "@aneuhold/local-npm-registry": "^0.2.33",
80
+ "@aneuhold/local-npm-registry": "^0.2.34",
81
81
  "@aneuhold/main-scripts": "^2.9.5",
82
82
  "@types/node": "^25.0.2",
83
83
  "eslint": "^9.39.2",