@ph-cms/client-sdk 0.1.6 → 0.1.7
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 +266 -160
- package/dist/auth/base-provider.d.ts +151 -0
- package/dist/auth/base-provider.js +210 -0
- package/dist/auth/firebase-provider.d.ts +46 -12
- package/dist/auth/firebase-provider.js +54 -42
- package/dist/auth/interfaces.d.ts +25 -2
- package/dist/auth/jwt-utils.d.ts +53 -0
- package/dist/auth/jwt-utils.js +85 -0
- package/dist/auth/local-provider.d.ts +34 -17
- package/dist/auth/local-provider.js +45 -39
- package/dist/client.d.ts +23 -0
- package/dist/client.js +116 -26
- package/dist/context.d.ts +10 -0
- package/dist/context.js +18 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/modules/auth.d.ts +2 -2
- package/dist/modules/auth.js +11 -20
- package/dist/types.d.ts +15 -1
- package/dist/types.js +7 -15
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { AuthProvider } from './interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* Abstract base class that implements the common token-management logic
|
|
4
|
+
* shared by all {@link AuthProvider} implementations (e.g. `LocalAuthProvider`,
|
|
5
|
+
* `FirebaseAuthProvider`).
|
|
6
|
+
*
|
|
7
|
+
* Subclasses **must** implement:
|
|
8
|
+
* - `type` — the discriminator literal (`'LOCAL'` | `'FIREBASE'`).
|
|
9
|
+
* - `getToken()` — provider-specific logic for returning a valid access token
|
|
10
|
+
* (including any fallback strategies such as Firebase ID tokens).
|
|
11
|
+
*
|
|
12
|
+
* Subclasses **may** override:
|
|
13
|
+
* - `logout()` — to perform additional cleanup (e.g. signing out of Firebase).
|
|
14
|
+
* Always call `super.logout()` to ensure in-memory and localStorage tokens
|
|
15
|
+
* are cleared.
|
|
16
|
+
*/
|
|
17
|
+
export declare abstract class BaseAuthProvider implements AuthProvider {
|
|
18
|
+
/** Discriminator that identifies the provider type. */
|
|
19
|
+
abstract readonly type: 'FIREBASE' | 'LOCAL';
|
|
20
|
+
/**
|
|
21
|
+
* Returns the current valid access token.
|
|
22
|
+
*
|
|
23
|
+
* Implementations should check token expiration and attempt a refresh
|
|
24
|
+
* automatically before returning the token. If the token is expired
|
|
25
|
+
* and cannot be refreshed, return `null`.
|
|
26
|
+
*/
|
|
27
|
+
abstract getToken(): Promise<string | null>;
|
|
28
|
+
/** The current PH-CMS access token (JWT), or `null` if not authenticated. */
|
|
29
|
+
protected accessToken: string | null;
|
|
30
|
+
/** The current refresh token, or `null` if not authenticated. */
|
|
31
|
+
protected refreshToken: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Callback invoked when the token is known to be expired and automatic
|
|
34
|
+
* refresh has failed. Typically used to redirect the user to a login page.
|
|
35
|
+
*/
|
|
36
|
+
protected onExpiredCallback: (() => Promise<void>) | null;
|
|
37
|
+
/**
|
|
38
|
+
* Function that performs the actual token refresh against the PH-CMS
|
|
39
|
+
* server. Registered by `PHCMSClient` after construction via
|
|
40
|
+
* {@link setRefreshFn}.
|
|
41
|
+
*/
|
|
42
|
+
protected refreshFn: ((refreshToken: string) => Promise<{
|
|
43
|
+
accessToken: string;
|
|
44
|
+
refreshToken: string;
|
|
45
|
+
}>) | null;
|
|
46
|
+
/**
|
|
47
|
+
* In-flight refresh promise used to de-duplicate concurrent refresh
|
|
48
|
+
* requests triggered by parallel `getToken()` calls.
|
|
49
|
+
*/
|
|
50
|
+
protected refreshPromise: Promise<string | null> | null;
|
|
51
|
+
/**
|
|
52
|
+
* How many milliseconds before actual JWT expiration the token is
|
|
53
|
+
* considered "expired" so the caller can refresh proactively.
|
|
54
|
+
*/
|
|
55
|
+
protected expiryBufferMs: number;
|
|
56
|
+
/**
|
|
57
|
+
* Prefix applied to all `localStorage` keys managed by this provider
|
|
58
|
+
* (e.g. `'ph_cms_'` → keys `ph_cms_access_token`, `ph_cms_refresh_token`).
|
|
59
|
+
*/
|
|
60
|
+
protected readonly storageKeyPrefix: string;
|
|
61
|
+
/**
|
|
62
|
+
* @param storageKeyPrefix Prefix for `localStorage` keys (e.g. `'ph_cms_'`).
|
|
63
|
+
* @param expiryBufferMs How many ms before actual expiration the token is
|
|
64
|
+
* considered expired. Defaults to {@link DEFAULT_EXPIRY_BUFFER_MS}
|
|
65
|
+
* (60 000 ms / 1 minute).
|
|
66
|
+
*/
|
|
67
|
+
constructor(storageKeyPrefix: string, expiryBufferMs?: number);
|
|
68
|
+
/**
|
|
69
|
+
* Returns `true` if the provider currently holds any token (access **or**
|
|
70
|
+
* refresh). This is a synchronous check used to decide whether to attempt
|
|
71
|
+
* API calls that require authentication (e.g. `/auth/me`).
|
|
72
|
+
*/
|
|
73
|
+
hasToken(): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Returns the current refresh token, or `null` if none is stored.
|
|
76
|
+
*/
|
|
77
|
+
getRefreshToken(): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Registers the function that performs the actual token refresh against
|
|
80
|
+
* the server. Called by `PHCMSClient` after construction so that the
|
|
81
|
+
* provider can refresh tokens autonomously inside {@link getToken} without
|
|
82
|
+
* a circular dependency on the client/module layer.
|
|
83
|
+
*
|
|
84
|
+
* @param fn A function that receives the current refresh token and returns
|
|
85
|
+
* a fresh access/refresh token pair.
|
|
86
|
+
*/
|
|
87
|
+
setRefreshFn(fn: (refreshToken: string) => Promise<{
|
|
88
|
+
accessToken: string;
|
|
89
|
+
refreshToken: string;
|
|
90
|
+
}>): void;
|
|
91
|
+
/**
|
|
92
|
+
* Stores a new pair of access/refresh tokens in memory **and**
|
|
93
|
+
* `localStorage` (when running in a browser).
|
|
94
|
+
*
|
|
95
|
+
* @param accessToken The new JWT access token.
|
|
96
|
+
* @param refreshToken The new refresh token.
|
|
97
|
+
*/
|
|
98
|
+
setTokens(accessToken: string, refreshToken: string): void;
|
|
99
|
+
/**
|
|
100
|
+
* Sets a callback to be invoked when the token is known to be expired
|
|
101
|
+
* and automatic refresh has failed.
|
|
102
|
+
*
|
|
103
|
+
* @param callback An async function executed on unrecoverable expiry
|
|
104
|
+
* (e.g. redirect the user to a login page).
|
|
105
|
+
*/
|
|
106
|
+
onTokenExpired(callback: () => Promise<void>): void;
|
|
107
|
+
/**
|
|
108
|
+
* Clears the current session by removing tokens from memory and
|
|
109
|
+
* `localStorage`.
|
|
110
|
+
*
|
|
111
|
+
* Subclasses may override this method to perform additional cleanup
|
|
112
|
+
* (e.g. signing out of Firebase) but **must** call `super.logout()`.
|
|
113
|
+
*/
|
|
114
|
+
logout(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Attempt to refresh the token pair using the registered {@link refreshFn}.
|
|
117
|
+
*
|
|
118
|
+
* If a refresh is already in progress (e.g. from a concurrent
|
|
119
|
+
* `getToken()` call) the existing promise is returned so we don't fire
|
|
120
|
+
* multiple refresh requests simultaneously.
|
|
121
|
+
*
|
|
122
|
+
* @returns The new access token on success, or `null` on failure.
|
|
123
|
+
*/
|
|
124
|
+
protected tryRefresh(): Promise<string | null>;
|
|
125
|
+
/**
|
|
126
|
+
* Executes the actual refresh call via {@link refreshFn}.
|
|
127
|
+
*
|
|
128
|
+
* On success the new tokens are persisted via {@link setTokens}.
|
|
129
|
+
* On failure all tokens are cleared and the {@link onExpiredCallback}
|
|
130
|
+
* is invoked.
|
|
131
|
+
*
|
|
132
|
+
* @returns The new access token on success, or `null` on failure.
|
|
133
|
+
*/
|
|
134
|
+
protected executeRefresh(): Promise<string | null>;
|
|
135
|
+
/**
|
|
136
|
+
* Removes the access and refresh tokens from both in-memory state and
|
|
137
|
+
* `localStorage`.
|
|
138
|
+
*/
|
|
139
|
+
protected clearTokens(): void;
|
|
140
|
+
/**
|
|
141
|
+
* Checks whether the current access token is expired (or will expire
|
|
142
|
+
* within {@link expiryBufferMs}).
|
|
143
|
+
*
|
|
144
|
+
* @returns
|
|
145
|
+
* - `true` — the token is expired or will expire within the buffer.
|
|
146
|
+
* - `false` — the token is still valid beyond the buffer window.
|
|
147
|
+
* - `null` — the token could not be parsed (caller should treat this
|
|
148
|
+
* as potentially valid and let the server decide).
|
|
149
|
+
*/
|
|
150
|
+
protected isCurrentTokenExpired(): boolean | null;
|
|
151
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseAuthProvider = void 0;
|
|
4
|
+
const jwt_utils_1 = require("./jwt-utils");
|
|
5
|
+
/**
|
|
6
|
+
* Buffer time (ms) before actual expiration at which we consider the token
|
|
7
|
+
* "expired" and proactively refresh. Default: 60 seconds.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_EXPIRY_BUFFER_MS = 60000;
|
|
10
|
+
/**
|
|
11
|
+
* Abstract base class that implements the common token-management logic
|
|
12
|
+
* shared by all {@link AuthProvider} implementations (e.g. `LocalAuthProvider`,
|
|
13
|
+
* `FirebaseAuthProvider`).
|
|
14
|
+
*
|
|
15
|
+
* Subclasses **must** implement:
|
|
16
|
+
* - `type` — the discriminator literal (`'LOCAL'` | `'FIREBASE'`).
|
|
17
|
+
* - `getToken()` — provider-specific logic for returning a valid access token
|
|
18
|
+
* (including any fallback strategies such as Firebase ID tokens).
|
|
19
|
+
*
|
|
20
|
+
* Subclasses **may** override:
|
|
21
|
+
* - `logout()` — to perform additional cleanup (e.g. signing out of Firebase).
|
|
22
|
+
* Always call `super.logout()` to ensure in-memory and localStorage tokens
|
|
23
|
+
* are cleared.
|
|
24
|
+
*/
|
|
25
|
+
class BaseAuthProvider {
|
|
26
|
+
// -----------------------------------------------------------------------
|
|
27
|
+
// Constructor
|
|
28
|
+
// -----------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* @param storageKeyPrefix Prefix for `localStorage` keys (e.g. `'ph_cms_'`).
|
|
31
|
+
* @param expiryBufferMs How many ms before actual expiration the token is
|
|
32
|
+
* considered expired. Defaults to {@link DEFAULT_EXPIRY_BUFFER_MS}
|
|
33
|
+
* (60 000 ms / 1 minute).
|
|
34
|
+
*/
|
|
35
|
+
constructor(storageKeyPrefix, expiryBufferMs) {
|
|
36
|
+
// -----------------------------------------------------------------------
|
|
37
|
+
// Protected state — accessible to subclasses
|
|
38
|
+
// -----------------------------------------------------------------------
|
|
39
|
+
/** The current PH-CMS access token (JWT), or `null` if not authenticated. */
|
|
40
|
+
this.accessToken = null;
|
|
41
|
+
/** The current refresh token, or `null` if not authenticated. */
|
|
42
|
+
this.refreshToken = null;
|
|
43
|
+
/**
|
|
44
|
+
* Callback invoked when the token is known to be expired and automatic
|
|
45
|
+
* refresh has failed. Typically used to redirect the user to a login page.
|
|
46
|
+
*/
|
|
47
|
+
this.onExpiredCallback = null;
|
|
48
|
+
/**
|
|
49
|
+
* Function that performs the actual token refresh against the PH-CMS
|
|
50
|
+
* server. Registered by `PHCMSClient` after construction via
|
|
51
|
+
* {@link setRefreshFn}.
|
|
52
|
+
*/
|
|
53
|
+
this.refreshFn = null;
|
|
54
|
+
/**
|
|
55
|
+
* In-flight refresh promise used to de-duplicate concurrent refresh
|
|
56
|
+
* requests triggered by parallel `getToken()` calls.
|
|
57
|
+
*/
|
|
58
|
+
this.refreshPromise = null;
|
|
59
|
+
this.storageKeyPrefix = storageKeyPrefix;
|
|
60
|
+
this.expiryBufferMs = expiryBufferMs ?? DEFAULT_EXPIRY_BUFFER_MS;
|
|
61
|
+
// Hydrate tokens from localStorage when running in a browser context.
|
|
62
|
+
if (typeof window !== 'undefined') {
|
|
63
|
+
this.accessToken = localStorage.getItem(`${this.storageKeyPrefix}access_token`);
|
|
64
|
+
this.refreshToken = localStorage.getItem(`${this.storageKeyPrefix}refresh_token`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// -----------------------------------------------------------------------
|
|
68
|
+
// Concrete AuthProvider methods
|
|
69
|
+
// -----------------------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* Returns `true` if the provider currently holds any token (access **or**
|
|
72
|
+
* refresh). This is a synchronous check used to decide whether to attempt
|
|
73
|
+
* API calls that require authentication (e.g. `/auth/me`).
|
|
74
|
+
*/
|
|
75
|
+
hasToken() {
|
|
76
|
+
return this.accessToken !== null || this.refreshToken !== null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns the current refresh token, or `null` if none is stored.
|
|
80
|
+
*/
|
|
81
|
+
getRefreshToken() {
|
|
82
|
+
return this.refreshToken;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Registers the function that performs the actual token refresh against
|
|
86
|
+
* the server. Called by `PHCMSClient` after construction so that the
|
|
87
|
+
* provider can refresh tokens autonomously inside {@link getToken} without
|
|
88
|
+
* a circular dependency on the client/module layer.
|
|
89
|
+
*
|
|
90
|
+
* @param fn A function that receives the current refresh token and returns
|
|
91
|
+
* a fresh access/refresh token pair.
|
|
92
|
+
*/
|
|
93
|
+
setRefreshFn(fn) {
|
|
94
|
+
this.refreshFn = fn;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stores a new pair of access/refresh tokens in memory **and**
|
|
98
|
+
* `localStorage` (when running in a browser).
|
|
99
|
+
*
|
|
100
|
+
* @param accessToken The new JWT access token.
|
|
101
|
+
* @param refreshToken The new refresh token.
|
|
102
|
+
*/
|
|
103
|
+
setTokens(accessToken, refreshToken) {
|
|
104
|
+
this.accessToken = accessToken;
|
|
105
|
+
this.refreshToken = refreshToken;
|
|
106
|
+
if (typeof window !== 'undefined') {
|
|
107
|
+
localStorage.setItem(`${this.storageKeyPrefix}access_token`, accessToken);
|
|
108
|
+
localStorage.setItem(`${this.storageKeyPrefix}refresh_token`, refreshToken);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sets a callback to be invoked when the token is known to be expired
|
|
113
|
+
* and automatic refresh has failed.
|
|
114
|
+
*
|
|
115
|
+
* @param callback An async function executed on unrecoverable expiry
|
|
116
|
+
* (e.g. redirect the user to a login page).
|
|
117
|
+
*/
|
|
118
|
+
onTokenExpired(callback) {
|
|
119
|
+
this.onExpiredCallback = callback;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Clears the current session by removing tokens from memory and
|
|
123
|
+
* `localStorage`.
|
|
124
|
+
*
|
|
125
|
+
* Subclasses may override this method to perform additional cleanup
|
|
126
|
+
* (e.g. signing out of Firebase) but **must** call `super.logout()`.
|
|
127
|
+
*/
|
|
128
|
+
async logout() {
|
|
129
|
+
this.clearTokens();
|
|
130
|
+
}
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
132
|
+
// Protected helpers — available to subclasses
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* Attempt to refresh the token pair using the registered {@link refreshFn}.
|
|
136
|
+
*
|
|
137
|
+
* If a refresh is already in progress (e.g. from a concurrent
|
|
138
|
+
* `getToken()` call) the existing promise is returned so we don't fire
|
|
139
|
+
* multiple refresh requests simultaneously.
|
|
140
|
+
*
|
|
141
|
+
* @returns The new access token on success, or `null` on failure.
|
|
142
|
+
*/
|
|
143
|
+
async tryRefresh() {
|
|
144
|
+
if (!this.refreshFn || !this.refreshToken) {
|
|
145
|
+
// Cannot refresh — notify the expired callback if set.
|
|
146
|
+
await this.onExpiredCallback?.();
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
// De-duplicate concurrent refreshes.
|
|
150
|
+
if (this.refreshPromise) {
|
|
151
|
+
return this.refreshPromise;
|
|
152
|
+
}
|
|
153
|
+
this.refreshPromise = this.executeRefresh();
|
|
154
|
+
try {
|
|
155
|
+
return await this.refreshPromise;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
this.refreshPromise = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Executes the actual refresh call via {@link refreshFn}.
|
|
163
|
+
*
|
|
164
|
+
* On success the new tokens are persisted via {@link setTokens}.
|
|
165
|
+
* On failure all tokens are cleared and the {@link onExpiredCallback}
|
|
166
|
+
* is invoked.
|
|
167
|
+
*
|
|
168
|
+
* @returns The new access token on success, or `null` on failure.
|
|
169
|
+
*/
|
|
170
|
+
async executeRefresh() {
|
|
171
|
+
try {
|
|
172
|
+
const result = await this.refreshFn(this.refreshToken);
|
|
173
|
+
this.setTokens(result.accessToken, result.refreshToken);
|
|
174
|
+
return result.accessToken;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Refresh failed (e.g. refresh token also expired).
|
|
178
|
+
// Clear tokens and notify.
|
|
179
|
+
this.clearTokens();
|
|
180
|
+
await this.onExpiredCallback?.();
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Removes the access and refresh tokens from both in-memory state and
|
|
186
|
+
* `localStorage`.
|
|
187
|
+
*/
|
|
188
|
+
clearTokens() {
|
|
189
|
+
this.accessToken = null;
|
|
190
|
+
this.refreshToken = null;
|
|
191
|
+
if (typeof window !== 'undefined') {
|
|
192
|
+
localStorage.removeItem(`${this.storageKeyPrefix}access_token`);
|
|
193
|
+
localStorage.removeItem(`${this.storageKeyPrefix}refresh_token`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Checks whether the current access token is expired (or will expire
|
|
198
|
+
* within {@link expiryBufferMs}).
|
|
199
|
+
*
|
|
200
|
+
* @returns
|
|
201
|
+
* - `true` — the token is expired or will expire within the buffer.
|
|
202
|
+
* - `false` — the token is still valid beyond the buffer window.
|
|
203
|
+
* - `null` — the token could not be parsed (caller should treat this
|
|
204
|
+
* as potentially valid and let the server decide).
|
|
205
|
+
*/
|
|
206
|
+
isCurrentTokenExpired() {
|
|
207
|
+
return (0, jwt_utils_1.isTokenExpired)(this.accessToken, this.expiryBufferMs);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
exports.BaseAuthProvider = BaseAuthProvider;
|
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
import type { Auth } from "firebase/auth";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { BaseAuthProvider } from "./base-provider";
|
|
3
|
+
/**
|
|
4
|
+
* Authentication provider that integrates Firebase Authentication with PH-CMS.
|
|
5
|
+
*
|
|
6
|
+
* Uses Firebase as the identity provider while managing PH-CMS-specific
|
|
7
|
+
* access/refresh tokens. When no valid PH-CMS token is available, falls back
|
|
8
|
+
* to the Firebase ID token (useful for the initial token exchange call).
|
|
9
|
+
*
|
|
10
|
+
* Extends {@link BaseAuthProvider} which handles all common token management
|
|
11
|
+
* logic (storage, refresh de-duplication, expiry checks, etc.).
|
|
12
|
+
*/
|
|
13
|
+
export declare class FirebaseAuthProvider extends BaseAuthProvider {
|
|
4
14
|
private readonly auth;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
readonly type: "FIREBASE";
|
|
16
|
+
/**
|
|
17
|
+
* @param auth The Firebase `Auth` instance.
|
|
18
|
+
* @param storageKeyPrefix Prefix for `localStorage` keys.
|
|
19
|
+
* Defaults to `'ph_cms_fb_'`.
|
|
20
|
+
* @param options Optional configuration.
|
|
21
|
+
* @param options.expiryBufferMs How many ms before actual expiration the
|
|
22
|
+
* token is considered expired. Defaults to
|
|
23
|
+
* 60 000 ms (1 minute).
|
|
24
|
+
*/
|
|
25
|
+
constructor(auth: Auth, storageKeyPrefix?: string, options?: {
|
|
26
|
+
/**
|
|
27
|
+
* How many ms before actual expiration the token is considered expired.
|
|
28
|
+
* @default 60_000 (1 minute)
|
|
29
|
+
*/
|
|
30
|
+
expiryBufferMs?: number;
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Returns a valid access token for PH-CMS API calls.
|
|
34
|
+
*
|
|
35
|
+
* 1. If a PH-CMS access token exists and is still valid, return it.
|
|
36
|
+
* 2. If the PH-CMS access token is expired (or will expire within
|
|
37
|
+
* `expiryBufferMs`), attempt an automatic refresh using the stored
|
|
38
|
+
* refresh token.
|
|
39
|
+
* 3. If no PH-CMS token exists at all (or refresh failed), fall back to
|
|
40
|
+
* the Firebase ID token (useful for the initial exchange call or if the
|
|
41
|
+
* server supports Firebase tokens directly).
|
|
42
|
+
*/
|
|
13
43
|
getToken(): Promise<string | null>;
|
|
14
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Clears PH-CMS tokens **and** signs the user out of Firebase.
|
|
46
|
+
*/
|
|
15
47
|
logout(): Promise<void>;
|
|
16
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Returns the current Firebase ID token, or `null` if no user is signed in.
|
|
50
|
+
*/
|
|
17
51
|
getIdToken(): Promise<string | null>;
|
|
18
52
|
}
|
|
@@ -1,58 +1,70 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FirebaseAuthProvider = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const base_provider_1 = require("./base-provider");
|
|
5
|
+
/**
|
|
6
|
+
* Authentication provider that integrates Firebase Authentication with PH-CMS.
|
|
7
|
+
*
|
|
8
|
+
* Uses Firebase as the identity provider while managing PH-CMS-specific
|
|
9
|
+
* access/refresh tokens. When no valid PH-CMS token is available, falls back
|
|
10
|
+
* to the Firebase ID token (useful for the initial token exchange call).
|
|
11
|
+
*
|
|
12
|
+
* Extends {@link BaseAuthProvider} which handles all common token management
|
|
13
|
+
* logic (storage, refresh de-duplication, expiry checks, etc.).
|
|
14
|
+
*/
|
|
15
|
+
class FirebaseAuthProvider extends base_provider_1.BaseAuthProvider {
|
|
16
|
+
/**
|
|
17
|
+
* @param auth The Firebase `Auth` instance.
|
|
18
|
+
* @param storageKeyPrefix Prefix for `localStorage` keys.
|
|
19
|
+
* Defaults to `'ph_cms_fb_'`.
|
|
20
|
+
* @param options Optional configuration.
|
|
21
|
+
* @param options.expiryBufferMs How many ms before actual expiration the
|
|
22
|
+
* token is considered expired. Defaults to
|
|
23
|
+
* 60 000 ms (1 minute).
|
|
24
|
+
*/
|
|
25
|
+
constructor(auth, storageKeyPrefix = 'ph_cms_fb_', options) {
|
|
26
|
+
super(storageKeyPrefix, options?.expiryBufferMs);
|
|
6
27
|
this.auth = auth;
|
|
7
|
-
this.storageKeyPrefix = storageKeyPrefix;
|
|
8
28
|
this.type = 'FIREBASE';
|
|
9
|
-
this.accessToken = null;
|
|
10
|
-
this.refreshToken = null;
|
|
11
|
-
this.onExpiredCallback = null;
|
|
12
|
-
if (typeof window !== 'undefined') {
|
|
13
|
-
this.accessToken = localStorage.getItem(`${this.storageKeyPrefix}access_token`);
|
|
14
|
-
this.refreshToken = localStorage.getItem(`${this.storageKeyPrefix}refresh_token`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
hasToken() {
|
|
18
|
-
return this.accessToken !== null || this.refreshToken !== null;
|
|
19
|
-
}
|
|
20
|
-
setTokens(accessToken, refreshToken) {
|
|
21
|
-
this.accessToken = accessToken;
|
|
22
|
-
this.refreshToken = refreshToken;
|
|
23
|
-
if (typeof window !== 'undefined') {
|
|
24
|
-
localStorage.setItem(`${this.storageKeyPrefix}access_token`, accessToken);
|
|
25
|
-
localStorage.setItem(`${this.storageKeyPrefix}refresh_token`, refreshToken);
|
|
26
|
-
}
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns a valid access token for PH-CMS API calls.
|
|
32
|
+
*
|
|
33
|
+
* 1. If a PH-CMS access token exists and is still valid, return it.
|
|
34
|
+
* 2. If the PH-CMS access token is expired (or will expire within
|
|
35
|
+
* `expiryBufferMs`), attempt an automatic refresh using the stored
|
|
36
|
+
* refresh token.
|
|
37
|
+
* 3. If no PH-CMS token exists at all (or refresh failed), fall back to
|
|
38
|
+
* the Firebase ID token (useful for the initial exchange call or if the
|
|
39
|
+
* server supports Firebase tokens directly).
|
|
40
|
+
*/
|
|
28
41
|
async getToken() {
|
|
29
|
-
// If we have a local access token, use it for PH-CMS API calls.
|
|
30
42
|
if (this.accessToken) {
|
|
31
|
-
|
|
43
|
+
const expired = this.isCurrentTokenExpired();
|
|
44
|
+
if (expired === true) {
|
|
45
|
+
const refreshed = await this.tryRefresh();
|
|
46
|
+
if (refreshed)
|
|
47
|
+
return refreshed;
|
|
48
|
+
// Refresh failed — fall through to Firebase ID token fallback.
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Token is valid (or unparseable — let server decide).
|
|
52
|
+
return this.accessToken;
|
|
53
|
+
}
|
|
32
54
|
}
|
|
33
|
-
// Fallback to Firebase ID token if no
|
|
34
|
-
|
|
35
|
-
// or for the initial exchange call if we want to automate it (though currently it's manual).
|
|
36
|
-
const user = this.auth.currentUser;
|
|
37
|
-
if (!user)
|
|
38
|
-
return null;
|
|
39
|
-
return user.getIdToken();
|
|
40
|
-
}
|
|
41
|
-
onTokenExpired(callback) {
|
|
42
|
-
this.onExpiredCallback = callback;
|
|
55
|
+
// Fallback to Firebase ID token if no valid PH-CMS token is available.
|
|
56
|
+
return this.getIdToken();
|
|
43
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Clears PH-CMS tokens **and** signs the user out of Firebase.
|
|
60
|
+
*/
|
|
44
61
|
async logout() {
|
|
45
|
-
|
|
46
|
-
this.refreshToken = null;
|
|
47
|
-
if (typeof window !== 'undefined') {
|
|
48
|
-
localStorage.removeItem(`${this.storageKeyPrefix}access_token`);
|
|
49
|
-
localStorage.removeItem(`${this.storageKeyPrefix}refresh_token`);
|
|
50
|
-
}
|
|
62
|
+
await super.logout();
|
|
51
63
|
await this.auth.signOut();
|
|
52
64
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Returns the current Firebase ID token, or `null` if no user is signed in.
|
|
67
|
+
*/
|
|
56
68
|
async getIdToken() {
|
|
57
69
|
const user = this.auth.currentUser;
|
|
58
70
|
if (!user)
|
|
@@ -2,7 +2,9 @@ export interface AuthProvider {
|
|
|
2
2
|
type: 'FIREBASE' | 'LOCAL';
|
|
3
3
|
/**
|
|
4
4
|
* Returns the current valid access token.
|
|
5
|
-
*
|
|
5
|
+
* Implementations should check token expiration and attempt a refresh
|
|
6
|
+
* automatically before returning the token. If the token is expired
|
|
7
|
+
* and cannot be refreshed, return `null`.
|
|
6
8
|
*/
|
|
7
9
|
getToken(): Promise<string | null>;
|
|
8
10
|
/**
|
|
@@ -12,7 +14,28 @@ export interface AuthProvider {
|
|
|
12
14
|
*/
|
|
13
15
|
hasToken(): boolean;
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
17
|
+
* Returns the current refresh token, or `null` if none is stored.
|
|
18
|
+
*/
|
|
19
|
+
getRefreshToken(): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Registers the function that performs the actual token refresh against the server.
|
|
22
|
+
* This is called by `PHCMSClient` after construction so that the provider can
|
|
23
|
+
* refresh tokens autonomously inside `getToken()` without a circular dependency
|
|
24
|
+
* on the client/module layer.
|
|
25
|
+
*
|
|
26
|
+
* The function receives the current refresh token and returns a fresh pair.
|
|
27
|
+
*/
|
|
28
|
+
setRefreshFn(fn: (refreshToken: string) => Promise<{
|
|
29
|
+
accessToken: string;
|
|
30
|
+
refreshToken: string;
|
|
31
|
+
}>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Stores a new pair of access/refresh tokens.
|
|
34
|
+
*/
|
|
35
|
+
setTokens(accessToken: string, refreshToken: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Sets a callback to be called when the token is known to be expired by the
|
|
38
|
+
* external world (e.g. 401 response) and automatic refresh has failed.
|
|
16
39
|
*/
|
|
17
40
|
onTokenExpired(callback: () => Promise<void>): void;
|
|
18
41
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight JWT utilities for client-side token inspection.
|
|
3
|
+
*
|
|
4
|
+
* These helpers parse the **payload** of a JWT (the middle segment) using
|
|
5
|
+
* plain base64 decoding. They do NOT verify the signature — that is the
|
|
6
|
+
* server's responsibility. The client only needs to know *when* a token
|
|
7
|
+
* expires so it can proactively refresh before sending an API request.
|
|
8
|
+
*/
|
|
9
|
+
export interface JwtPayload {
|
|
10
|
+
/** Subject (user id / uid) */
|
|
11
|
+
sub?: string;
|
|
12
|
+
/** Internal user id (PH-CMS specific) */
|
|
13
|
+
id?: number;
|
|
14
|
+
/** Issued-at timestamp (seconds since epoch) */
|
|
15
|
+
iat?: number;
|
|
16
|
+
/** Expiration timestamp (seconds since epoch) */
|
|
17
|
+
exp?: number;
|
|
18
|
+
/** Any other claims */
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decode the payload (second segment) of a JWT string.
|
|
23
|
+
*
|
|
24
|
+
* Returns `null` if the token is malformed or cannot be decoded.
|
|
25
|
+
* This function works in both browser and Node.js environments.
|
|
26
|
+
*/
|
|
27
|
+
export declare function decodeJwtPayload(token: string): JwtPayload | null;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the expiration time of a JWT as a Unix timestamp **in milliseconds**.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` if the token is malformed or does not contain an `exp` claim.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getTokenExpirationMs(token: string): number | null;
|
|
34
|
+
/**
|
|
35
|
+
* Check whether a token has expired (or will expire within `bufferMs`).
|
|
36
|
+
*
|
|
37
|
+
* @param token Raw JWT string
|
|
38
|
+
* @param bufferMs Grace period in milliseconds. If the token expires within
|
|
39
|
+
* this window it is considered "expired" so the caller can
|
|
40
|
+
* refresh proactively. Defaults to **60 000 ms (1 minute)**.
|
|
41
|
+
* @returns
|
|
42
|
+
* - `true` — the token is expired or will expire within `bufferMs`
|
|
43
|
+
* - `false` — the token is still valid beyond the buffer window
|
|
44
|
+
* - `null` — the token could not be parsed (treat as expired for safety)
|
|
45
|
+
*/
|
|
46
|
+
export declare function isTokenExpired(token: string, bufferMs?: number): boolean | null;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the number of milliseconds until the token expires.
|
|
49
|
+
*
|
|
50
|
+
* Returns `null` if the token is malformed.
|
|
51
|
+
* Returns a negative value if the token is already expired.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getTokenTTL(token: string): number | null;
|