@tumbaland/frontend-core 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,8 @@ export interface ApiClientOptions {
7
7
  export interface ApiRequestOptions extends RequestInit {
8
8
  /** Skip the onUnauthorized callback for this call (e.g. an auth-check that expects 401 as a normal "not logged in" result, not a hard redirect). */
9
9
  skipAuthRedirect?: boolean;
10
+ /** Serialized onto the URL as a query string; undefined/null values are omitted. */
11
+ params?: Record<string, string | number | boolean | undefined | null>;
10
12
  }
11
13
  export declare class ApiError extends Error {
12
14
  status: number;
@@ -18,9 +20,7 @@ export declare class ApiError extends Error {
18
20
  * correlation/session headers, structured errors, and an opt-in 401 →
19
21
  * redirect-to-auth hook, per TECHNICAL_REVIEW.md P1.3. `authService` and
20
22
  * `groupService` don't route through this: they use different auth
21
- * transports (cookie vs. Bearer token) that predate this client. New
22
- * call sites (the many per-front album/finance/etc. service files that
23
- * still hand-roll fetch) can adopt this incrementally.
23
+ * transports (cookie vs. Bearer token) that predate this client.
24
24
  */
25
25
  export declare function createApiClient(options: ApiClientOptions): {
26
26
  request: <T = unknown>(path: string, init?: ApiRequestOptions) => Promise<T>;
package/dist/apiClient.js CHANGED
@@ -1,4 +1,18 @@
1
1
  import { getCorrelationId, getSessionId } from '@tumbaland/components';
2
+ function appendParams(path, params) {
3
+ if (!params)
4
+ return path;
5
+ const searchParams = new URLSearchParams();
6
+ Object.entries(params).forEach(([key, value]) => {
7
+ if (value !== undefined && value !== null) {
8
+ searchParams.append(key, String(value));
9
+ }
10
+ });
11
+ const query = searchParams.toString();
12
+ if (!query)
13
+ return path;
14
+ return `${path}${path.includes('?') ? '&' : '?'}${query}`;
15
+ }
2
16
  export class ApiError extends Error {
3
17
  constructor(status, message, body) {
4
18
  super(message);
@@ -12,14 +26,12 @@ export class ApiError extends Error {
12
26
  * correlation/session headers, structured errors, and an opt-in 401 →
13
27
  * redirect-to-auth hook, per TECHNICAL_REVIEW.md P1.3. `authService` and
14
28
  * `groupService` don't route through this: they use different auth
15
- * transports (cookie vs. Bearer token) that predate this client. New
16
- * call sites (the many per-front album/finance/etc. service files that
17
- * still hand-roll fetch) can adopt this incrementally.
29
+ * transports (cookie vs. Bearer token) that predate this client.
18
30
  */
19
31
  export function createApiClient(options) {
20
32
  async function request(path, init = {}) {
21
- const { skipAuthRedirect, headers, ...rest } = init;
22
- const response = await fetch(`${options.baseUrl()}${path}`, {
33
+ const { skipAuthRedirect, headers, params, ...rest } = init;
34
+ const response = await fetch(`${options.baseUrl()}${appendParams(path, params)}`, {
23
35
  credentials: 'include',
24
36
  ...rest,
25
37
  headers: {
@@ -14,12 +14,14 @@ export interface AuthServiceConfig {
14
14
  * getAuthServiceUrl: () => getGlobalConfig().AUTH_SERVICE_URL!,
15
15
  * getAuthFrontUrl: () => getGlobalConfig().AUTH_FRONT_URL!
16
16
  * });
17
+ *
18
+ * The JWT lives only in an httpOnly `access_token` cookie — it is never
19
+ * readable from JS (no localStorage mirror, no client-side decode). Every
20
+ * call here that needs the session sends `credentials: 'include'` so the
21
+ * browser attaches the cookie automatically; auth state is always read via
22
+ * the network (checkAuth/getCurrentUser), never decoded locally.
17
23
  */
18
24
  export declare function createAuthService(config: AuthServiceConfig): {
19
- /** Sync the JWT from cookie to localStorage. Call once on app start. */
20
- init(): void;
21
- getToken(): string | null;
22
- syncToken(): void;
23
25
  clearAuthCache(): void;
24
26
  /** Verifies the session against auth-service. Cached for 5 minutes. */
25
27
  checkAuth(): Promise<AuthResponse>;
@@ -28,8 +30,6 @@ export declare function createAuthService(config: AuthServiceConfig): {
28
30
  getCurrentUser(): Promise<User | null>;
29
31
  /** Full profile (e.g. firstName/lastName) — only auth-service's /auth/profile has this. */
30
32
  getProfile(): Promise<User | null>;
31
- /** Decodes the local JWT directly — no network call, but can be stale. */
32
- getCurrentUserFromToken(): User | null;
33
33
  isAuthenticated(): Promise<boolean>;
34
34
  };
35
35
  export type AuthService = ReturnType<typeof createAuthService>;
@@ -7,10 +7,6 @@ function authHeaders() {
7
7
  'x-session-id': getSessionId()
8
8
  };
9
9
  }
10
- function getJwtTokenFromCookie(cookieName = 'access_token') {
11
- const match = document.cookie.match(new RegExp('(^| )' + cookieName + '=([^;]+)'));
12
- return match ? match[2] : null;
13
- }
14
10
  /**
15
11
  * Builds a per-front auth service instance. Each of the 7 frontends that
16
12
  * previously hand-rolled their own (six divergent copies, per
@@ -21,27 +17,17 @@ function getJwtTokenFromCookie(cookieName = 'access_token') {
21
17
  * getAuthServiceUrl: () => getGlobalConfig().AUTH_SERVICE_URL!,
22
18
  * getAuthFrontUrl: () => getGlobalConfig().AUTH_FRONT_URL!
23
19
  * });
20
+ *
21
+ * The JWT lives only in an httpOnly `access_token` cookie — it is never
22
+ * readable from JS (no localStorage mirror, no client-side decode). Every
23
+ * call here that needs the session sends `credentials: 'include'` so the
24
+ * browser attaches the cookie automatically; auth state is always read via
25
+ * the network (checkAuth/getCurrentUser), never decoded locally.
24
26
  */
25
27
  export function createAuthService(config) {
26
28
  let authCache = null;
27
29
  let authCacheTime = 0;
28
- function syncJwtToken() {
29
- const token = getJwtTokenFromCookie();
30
- if (token && localStorage.getItem('authToken') !== token) {
31
- localStorage.setItem('authToken', token);
32
- }
33
- }
34
30
  return {
35
- /** Sync the JWT from cookie to localStorage. Call once on app start. */
36
- init() {
37
- syncJwtToken();
38
- },
39
- getToken() {
40
- return localStorage.getItem('authToken');
41
- },
42
- syncToken() {
43
- syncJwtToken();
44
- },
45
31
  clearAuthCache() {
46
32
  authCache = null;
47
33
  authCacheTime = 0;
@@ -93,7 +79,6 @@ export function createAuthService(config) {
93
79
  }
94
80
  const data = await response.json();
95
81
  if (data.success) {
96
- localStorage.removeItem('authToken');
97
82
  authCache = null;
98
83
  authCacheTime = 0;
99
84
  window.location.reload();
@@ -129,31 +114,6 @@ export function createAuthService(config) {
129
114
  return null;
130
115
  }
131
116
  },
132
- /** Decodes the local JWT directly — no network call, but can be stale. */
133
- getCurrentUserFromToken() {
134
- try {
135
- const token = this.getToken();
136
- if (!token)
137
- return null;
138
- const payload = token.split('.')[1];
139
- if (!payload)
140
- return null;
141
- const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
142
- if (decoded.exp && Date.now() >= decoded.exp * 1000) {
143
- return null;
144
- }
145
- return {
146
- id: decoded.sub || decoded.id,
147
- email: decoded.email,
148
- name: decoded.name || decoded.preferred_username,
149
- picture: decoded.picture
150
- };
151
- }
152
- catch (error) {
153
- console.error('Failed to decode JWT token:', error);
154
- return null;
155
- }
156
- },
157
117
  async isAuthenticated() {
158
118
  const result = await this.checkAuth();
159
119
  return result.authenticated;
@@ -1,11 +1,7 @@
1
1
  import { getCorrelationId, getSessionId } from '@tumbaland/components';
2
- function getAuthToken() {
3
- return localStorage.getItem('authToken') || '';
4
- }
5
2
  function groupHeaders() {
6
3
  return {
7
4
  'Content-Type': 'application/json',
8
- Authorization: `Bearer ${getAuthToken()}`,
9
5
  'x-correlation-id': getCorrelationId(),
10
6
  'x-session-id': getSessionId()
11
7
  };
@@ -24,6 +20,7 @@ export function createGroupService(config) {
24
20
  async getUserGroups() {
25
21
  try {
26
22
  const response = await fetch(`${config.getGroupApiUrl()}/api/groups/mine`, {
23
+ credentials: 'include',
27
24
  headers: groupHeaders()
28
25
  });
29
26
  if (!response.ok) {
@@ -43,6 +40,7 @@ export function createGroupService(config) {
43
40
  async getUserGroupIds() {
44
41
  try {
45
42
  const response = await fetch(`${config.getGroupApiUrl()}/api/groups/mine/ids`, {
43
+ credentials: 'include',
46
44
  headers: groupHeaders()
47
45
  });
48
46
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/frontend-core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Shared frontend auth/group/API-client logic for Tumbaland frontends",
5
5
  "author": "Tumbaland",
6
6
  "license": "MIT",