@apptimate/core-lib 1.1.0 → 1.3.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.
@@ -0,0 +1,155 @@
1
+ import { cookie } from "../constants/storageKeys";
2
+ import { decode } from "./commonService";
3
+
4
+ const HOP_BY_HOP_HEADERS = new Set([
5
+ "connection",
6
+ "content-length",
7
+ "content-encoding",
8
+ "keep-alive",
9
+ "proxy-authenticate",
10
+ "proxy-authorization",
11
+ "te",
12
+ "trailer",
13
+ "transfer-encoding",
14
+ "upgrade",
15
+ ]);
16
+
17
+ function parseCookieHeader(header: string | null): Record<string, string> {
18
+ if (!header) {
19
+ return {};
20
+ }
21
+
22
+ return header.split(";").reduce<Record<string, string>>((acc, part) => {
23
+ const [rawName, ...valueParts] = part.trim().split("=");
24
+ if (!rawName) {
25
+ return acc;
26
+ }
27
+
28
+ acc[rawName] = decodeURIComponent(valueParts.join("=") || "");
29
+ return acc;
30
+ }, {});
31
+ }
32
+
33
+ function getCookieValue(cookies: Record<string, string>, name: string): string | null {
34
+ return cookies[name] ?? null;
35
+ }
36
+
37
+ function getAccessToken(cookies: Record<string, string>): string | null {
38
+ const cookieNames = [
39
+ cookie.access_token.encrypted ? cookie.access_token.secretName : cookie.access_token.name,
40
+ cookie.access_token.name,
41
+ cookie.access_token.secretName,
42
+ ];
43
+
44
+ for (const name of cookieNames) {
45
+ const value = getCookieValue(cookies, name);
46
+ if (!value) {
47
+ continue;
48
+ }
49
+
50
+ return cookie.access_token.encrypted ? decode(value) : value;
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ function buildCookieHeader(name: string, options?: { httpOnly?: boolean }): string {
57
+ const parts = [
58
+ `${name}=`,
59
+ "Path=/",
60
+ "Max-Age=0",
61
+ "Expires=Thu, 01 Jan 1970 00:00:00 GMT",
62
+ "SameSite=Lax",
63
+ ];
64
+
65
+ if (options?.httpOnly) {
66
+ parts.push("HttpOnly");
67
+ }
68
+
69
+ if (process.env.NODE_ENV === "production") {
70
+ parts.push("Secure");
71
+ }
72
+
73
+ return parts.join("; ");
74
+ }
75
+
76
+ export function createSessionLogoutResponse(): Response {
77
+ const headers = new Headers({ "content-type": "application/json" });
78
+ headers.append("set-cookie", buildCookieHeader(cookie.access_token.name, { httpOnly: true }));
79
+ headers.append("set-cookie", buildCookieHeader(cookie.access_token.secretName, { httpOnly: true }));
80
+ headers.append("set-cookie", buildCookieHeader("selected_organization_id"));
81
+
82
+ return new Response(JSON.stringify({ cleared: true }), {
83
+ status: 200,
84
+ headers,
85
+ });
86
+ }
87
+
88
+ export async function forwardApiProxyRequest(request: Request, pathSegments: string[]): Promise<Response> {
89
+ const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL;
90
+
91
+ if (!apiBaseUrl) {
92
+ return new Response(JSON.stringify({
93
+ is_success: false,
94
+ message: "NEXT_PUBLIC_API_URL is not configured.",
95
+ result: null,
96
+ system_code: "api_proxy_not_configured",
97
+ }), {
98
+ status: 500,
99
+ headers: { "content-type": "application/json" },
100
+ });
101
+ }
102
+
103
+ const incomingUrl = new URL(request.url);
104
+ const upstreamPath = pathSegments.join("/");
105
+ const upstreamUrl = new URL(upstreamPath, `${apiBaseUrl.replace(/\/$/, "")}/`);
106
+ upstreamUrl.search = incomingUrl.search;
107
+
108
+ const cookies = parseCookieHeader(request.headers.get("cookie"));
109
+ const token = getAccessToken(cookies);
110
+ const selectedOrganizationId = getCookieValue(cookies, "selected_organization_id");
111
+
112
+ const headers = new Headers();
113
+ const accept = request.headers.get("accept");
114
+ const contentType = request.headers.get("content-type");
115
+ const organizationId = request.headers.get("x-organization-id") ?? selectedOrganizationId;
116
+
117
+ if (accept) {
118
+ headers.set("accept", accept);
119
+ }
120
+
121
+ if (contentType) {
122
+ headers.set("content-type", contentType);
123
+ }
124
+
125
+ if (organizationId) {
126
+ headers.set("x-organization-id", organizationId);
127
+ }
128
+
129
+ if (token) {
130
+ headers.set("authorization", `Bearer ${token}`);
131
+ }
132
+
133
+ const hasBody = !["GET", "HEAD"].includes(request.method.toUpperCase());
134
+ const body = hasBody ? await request.arrayBuffer() : undefined;
135
+
136
+ const upstreamResponse = await fetch(upstreamUrl.toString(), {
137
+ method: request.method,
138
+ headers,
139
+ body,
140
+ cache: "no-store",
141
+ redirect: "manual",
142
+ });
143
+
144
+ const responseHeaders = new Headers();
145
+ upstreamResponse.headers.forEach((value, key) => {
146
+ if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
147
+ responseHeaders.set(key, value);
148
+ }
149
+ });
150
+
151
+ return new Response(upstreamResponse.body, {
152
+ status: upstreamResponse.status,
153
+ headers: responseHeaders,
154
+ });
155
+ }
@@ -1,19 +1,21 @@
1
1
  import { IApiResponse } from "../common/interfaces/ICommon";
2
- import Cookies from 'js-cookie';
3
- import { cookie } from '../constants/storageKeys';
4
- import { decrypt, replacePlaceholders } from './commonService';
5
2
 
6
3
  /**
7
4
  * Handles 401 Unauthorized responses globally.
8
- * Clears the auth cookie and localStorage, then redirects to the login page.
5
+ * Clears the server-managed session and app-owned local state, then redirects
6
+ * to the login page.
9
7
  */
10
8
  export function handleUnauthorized(): void {
11
- // Clear auth cookie explicitly to avoid clearing cookies from other apps on localhost
12
- const cookieName = replacePlaceholders(
13
- cookie.access_token.encrypted ? cookie.access_token.secretName : cookie.access_token.name,
14
- {}
15
- );
16
- Cookies.remove(cookieName, { path: '/' });
9
+ // Best-effort server-side cookie clear for HttpOnly auth cookies.
10
+ if (typeof window !== 'undefined') {
11
+ fetch('/api/session/logout', {
12
+ method: 'POST',
13
+ credentials: 'same-origin',
14
+ keepalive: true,
15
+ }).catch(() => {
16
+ // Ignore logout cleanup failures and proceed with client-side reset.
17
+ });
18
+ }
17
19
 
18
20
  // Clear only app-owned localStorage keys instead of wiping the entire origin
19
21
  if (typeof window !== 'undefined') {
@@ -49,6 +51,25 @@ export interface ClientResponse<T = any> {
49
51
  responseData: IApiResponse<T>;
50
52
  }
51
53
 
54
+ function buildBrowserProxyUrl(rawUrl: string): string {
55
+ if (typeof window === 'undefined') {
56
+ return rawUrl;
57
+ }
58
+
59
+ try {
60
+ const parsedUrl = new URL(rawUrl, window.location.origin);
61
+
62
+ if (parsedUrl.origin === window.location.origin) {
63
+ return parsedUrl.toString();
64
+ }
65
+
66
+ const proxyPath = parsedUrl.pathname.replace(/^\/+/, '');
67
+ return `/api/proxy/${proxyPath}${parsedUrl.search}`;
68
+ } catch {
69
+ return rawUrl;
70
+ }
71
+ }
72
+
52
73
  export async function sendRequest<T = any>({ url, method, data, params, headers, signal }: RequestOptions): Promise<ClientResponse<T>> {
53
74
  const defaultHeaders: Record<string, string> = {
54
75
  'Accept': 'application/json',
@@ -69,24 +90,10 @@ export async function sendRequest<T = any>({ url, method, data, params, headers,
69
90
  }
70
91
  }
71
92
 
72
- // Automatically add Authorization header on the client side
93
+ // Client-side requests are routed through a same-origin proxy so the
94
+ // browser never needs direct access to the bearer token.
73
95
  if (typeof window !== 'undefined') {
74
- const name = replacePlaceholders(
75
- cookie.access_token.encrypted ? cookie.access_token.secretName : cookie.access_token.name,
76
- {}
77
- );
78
- const tokenValue = Cookies.get(name);
79
-
80
- if (tokenValue) {
81
- try {
82
- const token = cookie.access_token.encrypted ? decrypt(tokenValue) : tokenValue;
83
- if (token) {
84
- defaultHeaders['Authorization'] = `Bearer ${token}`;
85
- }
86
- } catch (error) {
87
- console.error("Failed to process auth token:", error);
88
- }
89
- }
96
+ finalUrl = buildBrowserProxyUrl(finalUrl);
90
97
 
91
98
  // Automatically inject selected organization ID into every request
92
99
  try {