@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.
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * Lightweight JWT utilities for client-side token inspection.
4
+ *
5
+ * These helpers parse the **payload** of a JWT (the middle segment) using
6
+ * plain base64 decoding. They do NOT verify the signature — that is the
7
+ * server's responsibility. The client only needs to know *when* a token
8
+ * expires so it can proactively refresh before sending an API request.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.decodeJwtPayload = decodeJwtPayload;
12
+ exports.getTokenExpirationMs = getTokenExpirationMs;
13
+ exports.isTokenExpired = isTokenExpired;
14
+ exports.getTokenTTL = getTokenTTL;
15
+ /**
16
+ * Decode the payload (second segment) of a JWT string.
17
+ *
18
+ * Returns `null` if the token is malformed or cannot be decoded.
19
+ * This function works in both browser and Node.js environments.
20
+ */
21
+ function decodeJwtPayload(token) {
22
+ try {
23
+ const parts = token.split('.');
24
+ if (parts.length !== 3)
25
+ return null;
26
+ const base64Url = parts[1];
27
+ // Convert base64url → base64
28
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
29
+ let jsonStr;
30
+ if (typeof atob === 'function') {
31
+ // Browser (and Node >= 16 with global atob)
32
+ jsonStr = atob(base64);
33
+ }
34
+ else {
35
+ // Fallback for older Node.js environments
36
+ jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
37
+ }
38
+ return JSON.parse(jsonStr);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /**
45
+ * Returns the expiration time of a JWT as a Unix timestamp **in milliseconds**.
46
+ *
47
+ * Returns `null` if the token is malformed or does not contain an `exp` claim.
48
+ */
49
+ function getTokenExpirationMs(token) {
50
+ const payload = decodeJwtPayload(token);
51
+ if (!payload || typeof payload.exp !== 'number')
52
+ return null;
53
+ // JWT `exp` is in seconds; convert to milliseconds.
54
+ return payload.exp * 1000;
55
+ }
56
+ /**
57
+ * Check whether a token has expired (or will expire within `bufferMs`).
58
+ *
59
+ * @param token Raw JWT string
60
+ * @param bufferMs Grace period in milliseconds. If the token expires within
61
+ * this window it is considered "expired" so the caller can
62
+ * refresh proactively. Defaults to **60 000 ms (1 minute)**.
63
+ * @returns
64
+ * - `true` — the token is expired or will expire within `bufferMs`
65
+ * - `false` — the token is still valid beyond the buffer window
66
+ * - `null` — the token could not be parsed (treat as expired for safety)
67
+ */
68
+ function isTokenExpired(token, bufferMs = 60000) {
69
+ const expiresAtMs = getTokenExpirationMs(token);
70
+ if (expiresAtMs === null)
71
+ return null;
72
+ return Date.now() >= expiresAtMs - bufferMs;
73
+ }
74
+ /**
75
+ * Returns the number of milliseconds until the token expires.
76
+ *
77
+ * Returns `null` if the token is malformed.
78
+ * Returns a negative value if the token is already expired.
79
+ */
80
+ function getTokenTTL(token) {
81
+ const expiresAtMs = getTokenExpirationMs(token);
82
+ if (expiresAtMs === null)
83
+ return null;
84
+ return expiresAtMs - Date.now();
85
+ }
@@ -1,19 +1,36 @@
1
- import { AuthProvider } from "./interfaces";
2
- export declare class LocalAuthProvider implements AuthProvider {
3
- private readonly storageKeyPrefix;
4
- private readonly refreshFn?;
5
- readonly type = "LOCAL";
6
- private accessToken;
7
- private refreshToken;
8
- private onExpiredCallback;
9
- constructor(storageKeyPrefix?: string, refreshFn?: ((refreshToken: string) => Promise<{
10
- accessToken: string;
11
- refreshToken: string;
12
- }>) | undefined);
13
- setTokens(accessToken: string, refreshToken: string): void;
14
- hasToken(): boolean;
1
+ import { BaseAuthProvider } from './base-provider';
2
+ /**
3
+ * Authentication provider that stores PH-CMS tokens in `localStorage`.
4
+ *
5
+ * This is the default provider for applications that use email/password
6
+ * or any non-Firebase authentication flow.
7
+ *
8
+ * Token lifecycle:
9
+ * 1. On `getToken()`, the stored access token's `exp` claim is checked.
10
+ * 2. If the token is expired (or will expire within `expiryBufferMs`),
11
+ * an automatic refresh is attempted via the registered `refreshFn`.
12
+ * 3. If refresh fails, tokens are cleared and the `onExpiredCallback` is
13
+ * invoked so the UI layer can redirect to a login page.
14
+ */
15
+ export declare class LocalAuthProvider extends BaseAuthProvider {
16
+ readonly type: "LOCAL";
17
+ constructor(storageKeyPrefix?: string, options?: {
18
+ /**
19
+ * How many ms before actual expiration the token is considered expired.
20
+ * @default 60_000 (1 minute)
21
+ */
22
+ expiryBufferMs?: number;
23
+ });
24
+ /**
25
+ * Returns a valid access token.
26
+ *
27
+ * Before returning the stored access token this method checks its `exp`
28
+ * claim. If the token is expired (or will expire within `expiryBufferMs`)
29
+ * **and** a refresh function has been registered via `setRefreshFn()`,
30
+ * it automatically refreshes the token pair.
31
+ *
32
+ * Concurrent callers share the same in-flight refresh promise so that
33
+ * multiple parallel requests don't each trigger their own refresh.
34
+ */
15
35
  getToken(): Promise<string | null>;
16
- onTokenExpired(callback: () => Promise<void>): void;
17
- logout(): Promise<void>;
18
- getRefreshToken(): string | null;
19
36
  }
@@ -1,49 +1,55 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalAuthProvider = void 0;
4
- class LocalAuthProvider {
5
- constructor(storageKeyPrefix = 'ph_cms_', refreshFn) {
6
- this.storageKeyPrefix = storageKeyPrefix;
7
- this.refreshFn = refreshFn;
4
+ const base_provider_1 = require("./base-provider");
5
+ /**
6
+ * Authentication provider that stores PH-CMS tokens in `localStorage`.
7
+ *
8
+ * This is the default provider for applications that use email/password
9
+ * or any non-Firebase authentication flow.
10
+ *
11
+ * Token lifecycle:
12
+ * 1. On `getToken()`, the stored access token's `exp` claim is checked.
13
+ * 2. If the token is expired (or will expire within `expiryBufferMs`),
14
+ * an automatic refresh is attempted via the registered `refreshFn`.
15
+ * 3. If refresh fails, tokens are cleared and the `onExpiredCallback` is
16
+ * invoked so the UI layer can redirect to a login page.
17
+ */
18
+ class LocalAuthProvider extends base_provider_1.BaseAuthProvider {
19
+ constructor(storageKeyPrefix = 'ph_cms_', options) {
20
+ super(storageKeyPrefix, options?.expiryBufferMs);
8
21
  this.type = 'LOCAL';
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
- setTokens(accessToken, refreshToken) {
18
- this.accessToken = accessToken;
19
- this.refreshToken = refreshToken;
20
- if (typeof window !== 'undefined') {
21
- localStorage.setItem(`${this.storageKeyPrefix}access_token`, accessToken);
22
- localStorage.setItem(`${this.storageKeyPrefix}refresh_token`, refreshToken);
23
- }
24
- }
25
- hasToken() {
26
- return this.accessToken !== null || this.refreshToken !== null;
27
22
  }
23
+ /**
24
+ * Returns a valid access token.
25
+ *
26
+ * Before returning the stored access token this method checks its `exp`
27
+ * claim. If the token is expired (or will expire within `expiryBufferMs`)
28
+ * **and** a refresh function has been registered via `setRefreshFn()`,
29
+ * it automatically refreshes the token pair.
30
+ *
31
+ * Concurrent callers share the same in-flight refresh promise so that
32
+ * multiple parallel requests don't each trigger their own refresh.
33
+ */
28
34
  async getToken() {
29
- // Ideally check expiration here using jwt-decode, but for now return what we have.
30
- // If it's expired, the API will return 401, triggering the interceptor which calls onTokenExpired.
31
- return this.accessToken;
32
- }
33
- onTokenExpired(callback) {
34
- this.onExpiredCallback = callback;
35
- }
36
- async logout() {
37
- this.accessToken = null;
38
- this.refreshToken = null;
39
- if (typeof window !== 'undefined') {
40
- localStorage.removeItem(`${this.storageKeyPrefix}access_token`);
41
- localStorage.removeItem(`${this.storageKeyPrefix}refresh_token`);
35
+ // Fast path: no access token at all.
36
+ if (!this.accessToken)
37
+ return null;
38
+ // Check expiration.
39
+ const expired = this.isCurrentTokenExpired();
40
+ // `expired === null` means the token could not be parsed — treat as
41
+ // potentially valid and let the server decide (it might still be fine
42
+ // if the format changed, or the server will 401 and the interceptor
43
+ // will handle it).
44
+ if (expired === true) {
45
+ const refreshed = await this.tryRefresh();
46
+ if (refreshed)
47
+ return refreshed;
48
+ // Refresh failed — return null so the request goes out without a
49
+ // token (will result in 401 which the interceptor can handle).
50
+ return null;
42
51
  }
43
- }
44
- // Helper for the client to trigger refresh manually if needed
45
- getRefreshToken() {
46
- return this.refreshToken;
52
+ return this.accessToken;
47
53
  }
48
54
  }
49
55
  exports.LocalAuthProvider = LocalAuthProvider;
package/dist/client.d.ts CHANGED
@@ -19,7 +19,30 @@ export declare class PHCMSClient {
19
19
  readonly channel: ChannelModule;
20
20
  readonly terms: TermsModule;
21
21
  readonly media: MediaModule;
22
+ /**
23
+ * Whether a token refresh is currently in flight (used by the 401
24
+ * interceptor to de-duplicate concurrent refresh attempts).
25
+ */
26
+ private isRefreshing;
27
+ /**
28
+ * Queue of requests waiting for the current refresh to complete.
29
+ * Each entry will be resolved/rejected once the single in-flight
30
+ * refresh call finishes.
31
+ */
32
+ private refreshQueue;
22
33
  /** Exposes the auth provider so UI layers can check token state synchronously. */
23
34
  get authProvider(): AuthProvider | undefined;
24
35
  constructor(config: PHCMSClientConfig);
36
+ /**
37
+ * If a refresh is already in flight, queue the caller. Otherwise kick
38
+ * off a fresh refresh request and resolve all queued callers when it
39
+ * completes.
40
+ *
41
+ * This method reuses the **provider's** `tryRefresh()` path (via
42
+ * `getToken()` → proactive refresh) when possible. However since we
43
+ * arrive here because the access token already failed server-side
44
+ * validation (401), we perform an explicit refresh using the raw
45
+ * axios instance with `_skipAuth` to avoid circular `getToken()` calls.
46
+ */
47
+ private coordinatedRefresh;
25
48
  }
package/dist/client.js CHANGED
@@ -18,6 +18,17 @@ class PHCMSClient {
18
18
  }
19
19
  constructor(config) {
20
20
  this.config = config;
21
+ /**
22
+ * Whether a token refresh is currently in flight (used by the 401
23
+ * interceptor to de-duplicate concurrent refresh attempts).
24
+ */
25
+ this.isRefreshing = false;
26
+ /**
27
+ * Queue of requests waiting for the current refresh to complete.
28
+ * Each entry will be resolved/rejected once the single in-flight
29
+ * refresh call finishes.
30
+ */
31
+ this.refreshQueue = [];
21
32
  const normalizedApiPrefix = `/${(config.apiPrefix || '/api').replace(/^\/+|\/+$/g, '')}`;
22
33
  this.axiosInstance = axios_1.default.create({
23
34
  baseURL: config.baseURL,
@@ -26,8 +37,33 @@ class PHCMSClient {
26
37
  'Content-Type': 'application/json',
27
38
  },
28
39
  });
29
- // Request Interceptor: Add Auth Token
40
+ // Initialize Modules (before interceptors so the refresh wiring
41
+ // can reference `this.auth`).
42
+ this.auth = new auth_1.AuthModule(this.axiosInstance, config.auth);
43
+ this.content = new content_1.ContentModule(this.axiosInstance);
44
+ this.channel = new channel_1.ChannelModule(this.axiosInstance);
45
+ this.terms = new terms_1.TermsModule(this.axiosInstance);
46
+ this.media = new media_1.MediaModule(this.axiosInstance);
47
+ // Wire the refresh function into the auth provider so it can
48
+ // proactively refresh tokens inside `getToken()` without needing
49
+ // a direct reference to the AuthModule / axios instance.
50
+ //
51
+ // The `_skipAuth` flag ensures the refresh request itself does NOT
52
+ // pass through the "attach auth token" logic in the request
53
+ // interceptor, avoiding a recursive `getToken()` call.
54
+ if (config.auth) {
55
+ config.auth.setRefreshFn(async (refreshToken) => {
56
+ const response = await this.axiosInstance.post('/api/auth/refresh', { refreshToken }, { _skipAuth: true });
57
+ // The response interceptor unwraps `response.data`, so at
58
+ // this point `response` is already the data payload.
59
+ return response;
60
+ });
61
+ }
62
+ // ---------------------------------------------------------------
63
+ // Request Interceptor: Rewrite API prefix & attach auth token
64
+ // ---------------------------------------------------------------
30
65
  this.axiosInstance.interceptors.request.use(async (reqConfig) => {
66
+ // --- URL rewriting ---
31
67
  if (typeof reqConfig.url === 'string') {
32
68
  if (reqConfig.url === '/api') {
33
69
  reqConfig.url = normalizedApiPrefix;
@@ -36,7 +72,9 @@ class PHCMSClient {
36
72
  reqConfig.url = `${normalizedApiPrefix}${reqConfig.url.slice('/api'.length)}`;
37
73
  }
38
74
  }
39
- if (this.config.auth) {
75
+ // --- Auth header ---
76
+ // Skip auth for requests explicitly flagged (e.g. refresh calls)
77
+ if (!reqConfig._skipAuth && this.config.auth) {
40
78
  const token = await this.config.auth.getToken();
41
79
  if (token) {
42
80
  reqConfig.headers.Authorization = `Bearer ${token}`;
@@ -44,37 +82,89 @@ class PHCMSClient {
44
82
  }
45
83
  return reqConfig;
46
84
  });
47
- // Response Interceptor: Unwrap Data & Handle Errors
85
+ // ---------------------------------------------------------------
86
+ // Response Interceptor: Unwrap data, handle errors, 401 auto-retry
87
+ // ---------------------------------------------------------------
48
88
  this.axiosInstance.interceptors.response.use((response) => {
49
- // Return the data directly, stripping Axios wrapper
89
+ // Return the data directly, stripping the Axios wrapper.
50
90
  return response.data;
51
91
  }, async (error) => {
52
- if (error.response) {
53
- // Server responded with a status code outside 2xx
54
- const { status, data } = error.response;
55
- const message = data?.message || error.message;
56
- // Handle 401 Token Expiry if provider supports it
57
- if (status === 401 && this.config.auth) {
58
- // Logic to handle token refresh could be here or triggered by the app.
59
- // For now just throw.
92
+ if (!error.response) {
93
+ if (error.request) {
94
+ throw new errors_1.PHCMSError('No response received from server');
60
95
  }
61
- throw new errors_1.ApiError(message, status, data);
62
- }
63
- else if (error.request) {
64
- // Request made but no response
65
- throw new errors_1.PHCMSError('No response received from server');
66
- }
67
- else {
68
- // Something happened in setting up the request
69
96
  throw new errors_1.PHCMSError(error.message);
70
97
  }
98
+ const { status, data } = error.response;
99
+ const message = data?.message || error.message;
100
+ const originalRequest = error.config;
101
+ // ----------------------------------------------------------
102
+ // 401 handling: attempt one transparent token refresh
103
+ // ----------------------------------------------------------
104
+ if (status === 401 &&
105
+ this.config.auth &&
106
+ !originalRequest._retry &&
107
+ !originalRequest._skipAuth // don't retry refresh requests
108
+ ) {
109
+ originalRequest._retry = true;
110
+ const refreshToken = this.config.auth.getRefreshToken();
111
+ if (refreshToken) {
112
+ try {
113
+ const newToken = await this.coordinatedRefresh(refreshToken);
114
+ if (newToken) {
115
+ // Re-issue the original request with the fresh token.
116
+ originalRequest.headers.Authorization = `Bearer ${newToken}`;
117
+ return this.axiosInstance(originalRequest);
118
+ }
119
+ }
120
+ catch {
121
+ // Refresh itself failed — fall through to throw ApiError.
122
+ }
123
+ }
124
+ }
125
+ throw new errors_1.ApiError(message, status, data);
71
126
  });
72
- // Initialize Modules
73
- this.auth = new auth_1.AuthModule(this.axiosInstance, config.auth);
74
- this.content = new content_1.ContentModule(this.axiosInstance);
75
- this.channel = new channel_1.ChannelModule(this.axiosInstance);
76
- this.terms = new terms_1.TermsModule(this.axiosInstance);
77
- this.media = new media_1.MediaModule(this.axiosInstance);
127
+ }
128
+ // -------------------------------------------------------------------
129
+ // Private: coordinated token refresh (de-duplicates concurrent 401s)
130
+ // -------------------------------------------------------------------
131
+ /**
132
+ * If a refresh is already in flight, queue the caller. Otherwise kick
133
+ * off a fresh refresh request and resolve all queued callers when it
134
+ * completes.
135
+ *
136
+ * This method reuses the **provider's** `tryRefresh()` path (via
137
+ * `getToken()` → proactive refresh) when possible. However since we
138
+ * arrive here because the access token already failed server-side
139
+ * validation (401), we perform an explicit refresh using the raw
140
+ * axios instance with `_skipAuth` to avoid circular `getToken()` calls.
141
+ */
142
+ async coordinatedRefresh(refreshToken) {
143
+ if (this.isRefreshing) {
144
+ // Another request already triggered a refresh — wait for it.
145
+ return new Promise((resolve, reject) => {
146
+ this.refreshQueue.push({ resolve, reject });
147
+ });
148
+ }
149
+ this.isRefreshing = true;
150
+ try {
151
+ const result = await this.axiosInstance.post('/api/auth/refresh', { refreshToken }, { _skipAuth: true });
152
+ // Store the new tokens in the provider.
153
+ this.config.auth.setTokens(result.accessToken, result.refreshToken);
154
+ // Resolve all queued callers.
155
+ this.refreshQueue.forEach((q) => q.resolve(result.accessToken));
156
+ this.refreshQueue = [];
157
+ return result.accessToken;
158
+ }
159
+ catch (err) {
160
+ // Notify all queued callers of the failure.
161
+ this.refreshQueue.forEach((q) => q.reject(err));
162
+ this.refreshQueue = [];
163
+ return null;
164
+ }
165
+ finally {
166
+ this.isRefreshing = false;
167
+ }
78
168
  }
79
169
  }
80
170
  exports.PHCMSClient = PHCMSClient;
package/dist/context.d.ts CHANGED
@@ -2,11 +2,21 @@ import { UserDto } from '@ph-cms/api-contract';
2
2
  import { QueryClient } from '@tanstack/react-query';
3
3
  import React, { ReactNode } from 'react';
4
4
  import { PHCMSClient } from './client';
5
+ export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
5
6
  export interface PHCMSContextType {
6
7
  client: PHCMSClient;
7
8
  user: UserDto | null;
8
9
  isAuthenticated: boolean;
9
10
  isLoading: boolean;
11
+ /**
12
+ * Fine-grained authentication status.
13
+ * - `'loading'`: Token exists, `/auth/me` is being fetched.
14
+ * - `'authenticated'`: User profile successfully loaded.
15
+ * - `'unauthenticated'`: No token, or token validation failed.
16
+ */
17
+ authStatus: AuthStatus;
18
+ /** Whether the auth provider currently holds any token (access or refresh). */
19
+ hasToken: boolean;
10
20
  /** Manually trigger a refetch of the current user profile. */
11
21
  refreshUser: () => Promise<void>;
12
22
  }
package/dist/context.js CHANGED
@@ -76,13 +76,24 @@ const PHCMSInternalProvider = ({ client, children }) => {
76
76
  }, 2000); // Check every 2s as a fallback
77
77
  return () => clearInterval(interval);
78
78
  }, [client.authProvider, hasToken]);
79
- const value = (0, react_1.useMemo)(() => ({
80
- client,
81
- user: user || null,
82
- isAuthenticated: !!user && !isError && hasToken,
83
- isLoading: hasToken ? isLoading : false,
84
- refreshUser,
85
- }), [client, user, isError, isLoading, hasToken, refreshUser]);
79
+ const value = (0, react_1.useMemo)(() => {
80
+ const authStatus = (() => {
81
+ if (hasToken && isLoading)
82
+ return 'loading';
83
+ if (!!user && !isError && hasToken)
84
+ return 'authenticated';
85
+ return 'unauthenticated';
86
+ })();
87
+ return {
88
+ client,
89
+ user: user || null,
90
+ isAuthenticated: authStatus === 'authenticated',
91
+ isLoading: authStatus === 'loading',
92
+ authStatus,
93
+ hasToken,
94
+ refreshUser,
95
+ };
96
+ }, [client, user, isError, isLoading, hasToken, refreshUser]);
86
97
  return react_1.default.createElement(PHCMSContext.Provider, { value: value }, children);
87
98
  };
88
99
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ export * from './auth/base-provider';
1
2
  export * from './auth/firebase-provider';
2
3
  export * from './auth/interfaces';
4
+ export * from './auth/jwt-utils';
3
5
  export * from './auth/local-provider';
4
6
  export * from './client';
5
7
  export * from './errors';
package/dist/index.js CHANGED
@@ -14,8 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./auth/base-provider"), exports);
17
18
  __exportStar(require("./auth/firebase-provider"), exports);
18
19
  __exportStar(require("./auth/interfaces"), exports);
20
+ __exportStar(require("./auth/jwt-utils"), exports);
19
21
  __exportStar(require("./auth/local-provider"), exports);
20
22
  __exportStar(require("./client"), exports);
21
23
  __exportStar(require("./errors"), exports);
@@ -1,12 +1,12 @@
1
+ import { AuthResponse, FirebaseExchangeRequest, LoginRequest, RegisterRequest, UserDto } from "@ph-cms/api-contract";
1
2
  import { AxiosInstance } from "axios";
2
3
  import { AuthProvider } from "../auth/interfaces";
3
- import { LoginRequest, RegisterRequest, AuthResponse, UserDto, FirebaseExchangeRequest } from "@ph-cms/api-contract";
4
4
  export declare class AuthModule {
5
5
  private client;
6
6
  private provider?;
7
7
  constructor(client: AxiosInstance, provider?: AuthProvider | undefined);
8
8
  /**
9
- * Logs in the user and updates the AuthProvider if it's a LocalAuthProvider.
9
+ * Logs in the user and updates the AuthProvider tokens automatically.
10
10
  */
11
11
  login(data: LoginRequest): Promise<AuthResponse>;
12
12
  /**
@@ -9,7 +9,7 @@ class AuthModule {
9
9
  this.provider = provider;
10
10
  }
11
11
  /**
12
- * Logs in the user and updates the AuthProvider if it's a LocalAuthProvider.
12
+ * Logs in the user and updates the AuthProvider tokens automatically.
13
13
  */
14
14
  async login(data) {
15
15
  const validation = api_contract_1.LoginSchema.safeParse(data);
@@ -17,17 +17,8 @@ class AuthModule {
17
17
  throw new errors_1.ValidationError("Invalid login data", validation.error.errors);
18
18
  }
19
19
  const response = await this.client.post('/api/auth/login', data);
20
- // If using Local Provider, update tokens automatically
21
- if (this.provider && (this.provider.type === 'LOCAL' || this.provider.type === 'FIREBASE')) {
22
- // We need to cast to LocalAuthProvider to access setTokens,
23
- // but strictly speaking we shouldn't assume implementation details here.
24
- // However, for this SDK, it's a practical coupling.
25
- // Or we assume the user handles it.
26
- // Let's assume the user handles it via the response for now,
27
- // OR we can try to update it if the method exists.
28
- if ('setTokens' in this.provider) {
29
- this.provider.setTokens(response.accessToken, response.refreshToken);
30
- }
20
+ if (this.provider) {
21
+ this.provider.setTokens(response.accessToken, response.refreshToken);
31
22
  }
32
23
  return response;
33
24
  }
@@ -40,8 +31,7 @@ class AuthModule {
40
31
  throw new errors_1.ValidationError("Invalid Firebase token data", validation.error.errors);
41
32
  }
42
33
  const response = await this.client.post('/api/auth/firebase/exchange', data);
43
- // If using a Provider, update tokens automatically
44
- if (this.provider && ('setTokens' in this.provider)) {
34
+ if (this.provider) {
45
35
  this.provider.setTokens(response.accessToken, response.refreshToken);
46
36
  }
47
37
  return response;
@@ -51,23 +41,24 @@ class AuthModule {
51
41
  if (!validation.success) {
52
42
  throw new errors_1.ValidationError("Invalid register data", validation.error.errors);
53
43
  }
54
- return this.client.post('/api/auth/register', data);
44
+ const response = await this.client.post('/api/auth/register', data);
45
+ if (this.provider) {
46
+ this.provider.setTokens(response.accessToken, response.refreshToken);
47
+ }
48
+ return response;
55
49
  }
56
50
  async me() {
57
51
  return this.client.get('/api/auth/me');
58
52
  }
59
53
  async refresh(refreshToken) {
60
- // Validate refreshToken string? It's just a string.
61
54
  return this.client.post('/api/auth/refresh', { refreshToken });
62
55
  }
63
56
  async logout() {
64
57
  if (this.provider) {
65
58
  await this.provider.logout();
66
59
  }
67
- // Also call API endpoint if needed?
68
- // Usually client-side logout just clears token.
69
- // But if we want to invalidate on server (blacklist), we call endpoint.
70
- // Routes has /api/auth/logout.
60
+ // Call the server endpoint to invalidate the session if applicable.
61
+ // JWT-based auth is stateless, but the server may implement a blacklist.
71
62
  await this.client.post('/api/auth/logout').catch(() => { }); // Ignore error on logout
72
63
  }
73
64
  }
package/dist/types.d.ts CHANGED
@@ -1 +1,15 @@
1
- export * from '@ph-cms/api-contract';
1
+ export type { AuthProvider } from './auth/interfaces';
2
+ export type { JwtPayload } from './auth/jwt-utils';
3
+ export type { PHCMSClientConfig } from './client';
4
+ export type { AuthStatus, PHCMSContextType, PHCMSProviderProps } from './context';
5
+ export type { FirebaseAuthSyncProps, UseFirebaseAuthSyncOptions, UseFirebaseAuthSyncReturn } from './hooks/useFirebaseAuthSync';
6
+ export type { AuthResponse, FirebaseExchangeRequest, LoginRequest, RefreshTokenRequest, RegisterRequest } from '@ph-cms/api-contract';
7
+ export type { UserDto } from '@ph-cms/api-contract';
8
+ export type { ChannelDto, CheckHierarchyQuery, CreateChannelDto, ListChannelQuery, PagedChannelListResponse } from '@ph-cms/api-contract';
9
+ export type { ContentDto, ContentMediaDto, CreateContentRequest, ListContentQuery, PagedContentListResponse, UpdateContentRequest } from '@ph-cms/api-contract';
10
+ export type { HierarchySetDto } from '@ph-cms/api-contract';
11
+ export type { PermissionPolicySetDto } from '@ph-cms/api-contract';
12
+ export type { ListTermsQuery, PagedTermListResponse, TermDto } from '@ph-cms/api-contract';
13
+ export type { BoundsQuery, GeoJSON } from '@ph-cms/api-contract';
14
+ export type { MediaUploadTicketBatchRequest, MediaUploadTicketBatchResponse, MediaUploadTicketRequest, MediaUploadTicketResponse } from '@ph-cms/api-contract';
15
+ export type { PagedResponse } from '@ph-cms/api-contract';
package/dist/types.js CHANGED
@@ -1,17 +1,9 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
2
+ // =============================================================================
3
+ // PH-CMS Client SDK Centralized Type Exports
4
+ //
5
+ // This file re-exports all types used across the SDK so that consumers can
6
+ // import them from a single entry point (`@ph-cms/client-sdk`) without needing
7
+ // to dig into `@ph-cms/api-contract` (which ships as a pre-built dependency).
8
+ // =============================================================================
16
9
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("@ph-cms/api-contract"), exports);