@salesforce/webapp-experimental 1.80.1 → 1.82.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.
Files changed (52) hide show
  1. package/dist/api/clients.js +72 -77
  2. package/dist/api/index.d.ts +4 -4
  3. package/dist/api/index.d.ts.map +1 -1
  4. package/dist/api/index.js +13 -11
  5. package/dist/api/utils/accounts.js +16 -30
  6. package/dist/api/utils/records.js +21 -20
  7. package/dist/api/utils/user.js +20 -28
  8. package/dist/app/index.d.ts +4 -4
  9. package/dist/app/index.d.ts.map +1 -1
  10. package/dist/app/index.js +7 -7
  11. package/dist/app/manifest.js +23 -37
  12. package/dist/app/org.js +45 -58
  13. package/dist/design/index.js +12 -19
  14. package/dist/design/interactions/interactionsController.d.ts +1 -6
  15. package/dist/design/interactions/interactionsController.d.ts.map +1 -1
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +25 -10
  19. package/dist/package.json.js +4 -0
  20. package/dist/proxy/handler.d.ts +2 -7
  21. package/dist/proxy/handler.d.ts.map +1 -1
  22. package/dist/proxy/handler.js +462 -523
  23. package/dist/proxy/index.d.ts +2 -2
  24. package/dist/proxy/index.d.ts.map +1 -1
  25. package/dist/proxy/index.js +7 -6
  26. package/dist/proxy/livePreviewScript.js +11 -26
  27. package/dist/proxy/routing.d.ts +1 -6
  28. package/dist/proxy/routing.d.ts.map +1 -1
  29. package/dist/proxy/routing.js +73 -103
  30. package/package.json +7 -6
  31. package/dist/api/clients.test.d.ts +0 -7
  32. package/dist/api/clients.test.d.ts.map +0 -1
  33. package/dist/api/clients.test.js +0 -167
  34. package/dist/api/graphql-operations-types.js +0 -44
  35. package/dist/api/utils/records.test.d.ts +0 -7
  36. package/dist/api/utils/records.test.d.ts.map +0 -1
  37. package/dist/api/utils/records.test.js +0 -190
  38. package/dist/api/utils/user.test.d.ts +0 -7
  39. package/dist/api/utils/user.test.d.ts.map +0 -1
  40. package/dist/api/utils/user.test.js +0 -108
  41. package/dist/design/interactions/communicationManager.js +0 -108
  42. package/dist/design/interactions/componentMatcher.js +0 -80
  43. package/dist/design/interactions/editableManager.js +0 -95
  44. package/dist/design/interactions/eventHandlers.js +0 -125
  45. package/dist/design/interactions/index.js +0 -47
  46. package/dist/design/interactions/interactionsController.js +0 -135
  47. package/dist/design/interactions/styleManager.js +0 -97
  48. package/dist/design/interactions/utils/cssUtils.js +0 -72
  49. package/dist/design/interactions/utils/sourceUtils.js +0 -99
  50. package/dist/proxy/livePreviewScript.test.d.ts +0 -7
  51. package/dist/proxy/livePreviewScript.test.d.ts.map +0 -1
  52. package/dist/proxy/livePreviewScript.test.js +0 -96
@@ -1,564 +1,503 @@
1
- /**
2
- * Copyright (c) 2026, Salesforce, Inc.,
3
- * All rights reserved.
4
- * For full license text, see the LICENSE.txt file
5
- */
6
- import { createRequire } from "node:module";
7
- import { getLivePreviewScriptContent, LIVE_PREVIEW_SCRIPT_MARKER } from "./livePreviewScript.js";
1
+ import { LIVE_PREVIEW_SCRIPT_MARKER, getLivePreviewScriptContent } from "./livePreviewScript.js";
8
2
  import { applyTrailingSlash, matchRoute } from "./routing.js";
9
- import { refreshOrgAuth } from "../app/index.js";
10
- const require = createRequire(import.meta.url);
11
- const { version: proxyVersion } = require("../../package.json");
12
- /**
13
- * URL parameter to trigger health check response
14
- */
15
- export const WEBAPP_HEALTH_CHECK_PARAM = "sfProxyHealthCheck";
16
- /**
17
- * Response header indicating the proxy is active
18
- */
19
- export const WEBAPP_PROXY_HEADER = "X-Salesforce-WebApp-Proxy";
20
- /**
21
- * Endpoint for Lightning Out frontdoor URL generation
22
- */
3
+ import { version } from "../package.json.js";
4
+ import { refreshOrgAuth } from "../app/org.js";
5
+ import "node:fs/promises";
6
+ const WEBAPP_HEALTH_CHECK_PARAM = "sfProxyHealthCheck";
7
+ const WEBAPP_PROXY_HEADER = "X-Salesforce-WebApp-Proxy";
23
8
  const LIGHTNING_OUT_SINGLE_ACCESS_PATH = "/services/oauth2/singleaccess";
24
9
  const SALESFORCE_API_PREFIXES = ["/services/", "/lwr/apex/"];
25
10
  const AUTH_FAILED_RESPONSE = {
26
- error: "AUTHENTICATION_FAILED",
27
- message: "Authentication failed. Please re-authenticate to your Salesforce org.",
28
- status: 401,
11
+ error: "AUTHENTICATION_FAILED",
12
+ message: "Authentication failed. Please re-authenticate to your Salesforce org.",
13
+ status: 401
29
14
  };
30
- /**
31
- * Thrown when a Salesforce authentication operation fails
32
- * (e.g. token refresh failure, missing credentials).
33
- * Caught in request handlers to return a 401 response.
34
- */
35
15
  class AuthenticationError extends Error {
36
- constructor(message) {
37
- super(message);
38
- this.name = "AuthenticationError";
39
- }
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "AuthenticationError";
19
+ }
40
20
  }
41
- /**
42
- * Handles all proxy routing and forwarding for WebApps
43
- */
44
21
  class WebAppProxyHandler {
45
- manifest;
46
- orgInfo;
47
- target;
48
- basePath;
49
- options;
50
- startTime = Date.now();
51
- constructor(manifest, orgInfo, target, basePath, options) {
52
- this.manifest = manifest;
53
- this.orgInfo = orgInfo;
54
- this.target = target;
55
- this.basePath = basePath;
56
- this.options = options;
22
+ constructor(manifest, orgInfo, target, basePath, options) {
23
+ this.manifest = manifest;
24
+ this.orgInfo = orgInfo;
25
+ this.target = target;
26
+ this.basePath = basePath;
27
+ this.options = options;
28
+ }
29
+ startTime = Date.now();
30
+ async handle(req, res, next) {
31
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
32
+ if (url.searchParams.get(WEBAPP_HEALTH_CHECK_PARAM) === "true") {
33
+ this.handleHealthCheck(res);
34
+ return;
57
35
  }
58
- async handle(req, res, next) {
59
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
60
- // Health check via URL parameter - respond immediately
61
- if (url.searchParams.get(WEBAPP_HEALTH_CHECK_PARAM) === "true") {
62
- this.handleHealthCheck(res);
63
- return;
64
- }
65
- let pathname = url.pathname;
66
- if (pathname === "/__lo/frontdoor") {
67
- await this.handleLightningOutFrontdoor(req, res, url);
68
- return;
69
- }
36
+ let pathname = url.pathname;
37
+ if (pathname === "/__lo/frontdoor") {
38
+ await this.handleLightningOutFrontdoor(req, res, url);
39
+ return;
40
+ }
41
+ if (this.options?.debug) {
42
+ console.log(`[webapps-proxy] ${req.method} ${pathname}`);
43
+ }
44
+ pathname = applyTrailingSlash(pathname, this.manifest.routing?.trailingSlash);
45
+ const match = matchRoute(
46
+ pathname,
47
+ this.basePath,
48
+ this.manifest.routing?.rewrites,
49
+ this.manifest.routing?.redirects
50
+ );
51
+ if (match) {
52
+ if (match.type === "api") {
53
+ await this.handleSalesforceApi(req, res);
54
+ return;
55
+ }
56
+ if (match.type === "gql") {
57
+ await this.handleGraphQL(req, res);
58
+ return;
59
+ }
60
+ if (match.type === "redirect" && match.target && match.statusCode) {
61
+ this.handleRedirect(res, match.target, match.statusCode);
62
+ return;
63
+ }
64
+ if (match.type === "rewrite" && match.target) {
65
+ url.pathname = `/${match.target}`.replace(/\/+/g, "/");
66
+ req.url = url.pathname + url.search;
70
67
  if (this.options?.debug) {
71
- console.log(`[webapps-proxy] ${req.method} ${pathname}`);
72
- }
73
- pathname = applyTrailingSlash(pathname, this.manifest.routing?.trailingSlash);
74
- const match = matchRoute(pathname, this.basePath, this.manifest.routing?.rewrites, this.manifest.routing?.redirects);
75
- if (match) {
76
- if (match.type === "api") {
77
- await this.handleSalesforceApi(req, res);
78
- return;
79
- }
80
- if (match.type === "gql") {
81
- await this.handleGraphQL(req, res);
82
- return;
83
- }
84
- if (match.type === "redirect" && match.target && match.statusCode) {
85
- this.handleRedirect(res, match.target, match.statusCode);
86
- return;
87
- }
88
- if (match.type === "rewrite" && match.target) {
89
- url.pathname = `/${match.target}`.replace(/\/+/g, "/");
90
- req.url = url.pathname + url.search;
91
- if (this.options?.debug) {
92
- console.log(`[webapps-proxy] Rewrite to ${req.url}`);
93
- }
94
- // After rewrite, let Vite handle the rewritten request (or forward to dev server if no next)
95
- if (next) {
96
- next();
97
- return;
98
- }
99
- await this.forwardToDevServer(req, res);
100
- return;
101
- }
102
- if (match.type === "file-upload") {
103
- console.log("[webapps-proxy] file-upload match found → handleFileUpload");
104
- await this.handleFileUpload(req, res, url);
105
- return;
106
- }
68
+ console.log(`[webapps-proxy] Rewrite to ${req.url}`);
107
69
  }
108
- // Check if this is a Salesforce API request that needs proxying
109
- if (SALESFORCE_API_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
110
- await this.handleSalesforceApi(req, res);
111
- return;
112
- }
113
- // For all other requests (static assets, HMR, HTML), let Vite handle them directly
114
- // Note: In Vite middleware mode, HTML script injection is handled by
115
- // the Vite plugin's transformIndexHtml hook, not here.
116
70
  if (next) {
117
- next();
118
- }
119
- else {
120
- await this.forwardToDevServer(req, res);
71
+ next();
72
+ return;
121
73
  }
74
+ await this.forwardToDevServer(req, res);
75
+ return;
76
+ }
77
+ if (match.type === "file-upload") {
78
+ console.log("[webapps-proxy] file-upload match found → handleFileUpload");
79
+ await this.handleFileUpload(req, res, url);
80
+ return;
81
+ }
122
82
  }
123
- async forwardToDevServer(req, res) {
124
- try {
125
- const baseUrl = this.target ?? `http://${req.headers.host ?? "localhost"}`;
126
- const url = new URL(req.url ?? "/", baseUrl);
127
- if (this.options?.debug) {
128
- console.log(`[webapps-proxy] Forwarding to dev server: ${url.href}`);
129
- }
130
- const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : undefined;
131
- const response = await fetch(url.href, {
132
- method: req.method,
133
- headers: getFilteredHeaders(req.headers),
134
- body: body,
135
- });
136
- await this.sendResponse(res, response);
137
- }
138
- catch (error) {
139
- console.error("[webapps-proxy] Dev server request failed:", error);
140
- this.sendAuthOrGatewayError(res, error, "Failed to forward request to dev server");
141
- }
83
+ if (SALESFORCE_API_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
84
+ await this.handleSalesforceApi(req, res);
85
+ return;
142
86
  }
143
- handleRedirect(res, location, statusCode) {
144
- res.writeHead(statusCode, { Location: location });
145
- res.end();
87
+ if (next) {
88
+ next();
89
+ } else {
90
+ await this.forwardToDevServer(req, res);
146
91
  }
147
- handleHealthCheck(res) {
148
- const response = {
149
- proxyName: "@salesforce/webapp-experimental",
150
- proxyVersion,
151
- port: this.target ? new URL(this.target).port : undefined,
152
- org: this.orgInfo?.orgAlias ?? this.orgInfo?.username,
153
- apiVersion: this.orgInfo?.apiVersion,
154
- webappName: this.manifest.name,
155
- webappFolderPath: this.basePath,
156
- uptime: Date.now() - this.startTime,
157
- timestamp: new Date().toISOString(),
158
- debugMode: this.options?.debug ?? false,
159
- };
160
- res.writeHead(200, {
161
- "Content-Type": "application/json",
162
- [WEBAPP_PROXY_HEADER]: "true",
163
- });
164
- res.end(JSON.stringify(response, null, 2));
92
+ }
93
+ async forwardToDevServer(req, res) {
94
+ try {
95
+ const baseUrl = this.target ?? `http://${req.headers.host ?? "localhost"}`;
96
+ const url = new URL(req.url ?? "/", baseUrl);
97
+ if (this.options?.debug) {
98
+ console.log(`[webapps-proxy] Forwarding to dev server: ${url.href}`);
99
+ }
100
+ const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : void 0;
101
+ const response = await fetch(url.href, {
102
+ method: req.method,
103
+ headers: getFilteredHeaders(req.headers),
104
+ body
105
+ });
106
+ await this.sendResponse(res, response);
107
+ } catch (error) {
108
+ console.error("[webapps-proxy] Dev server request failed:", error);
109
+ this.sendAuthOrGatewayError(res, error, "Failed to forward request to dev server");
165
110
  }
166
- sendNoOrgError(res) {
167
- res.writeHead(401, { "Content-Type": "application/json" });
168
- res.end(JSON.stringify({
169
- error: "NO_ORG_FOUND",
170
- message: "No default Salesforce org found. Run 'sf org login web --set-default' to authenticate.",
171
- }));
111
+ }
112
+ handleRedirect(res, location, statusCode) {
113
+ res.writeHead(statusCode, { Location: location });
114
+ res.end();
115
+ }
116
+ handleHealthCheck(res) {
117
+ const response = {
118
+ proxyName: "@salesforce/webapp-experimental",
119
+ proxyVersion: version,
120
+ port: this.target ? new URL(this.target).port : void 0,
121
+ org: this.orgInfo?.orgAlias ?? this.orgInfo?.username,
122
+ apiVersion: this.orgInfo?.apiVersion,
123
+ webappName: this.manifest.name,
124
+ webappFolderPath: this.basePath,
125
+ uptime: Date.now() - this.startTime,
126
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
127
+ debugMode: this.options?.debug ?? false
128
+ };
129
+ res.writeHead(200, {
130
+ "Content-Type": "application/json",
131
+ [WEBAPP_PROXY_HEADER]: "true"
132
+ });
133
+ res.end(JSON.stringify(response, null, 2));
134
+ }
135
+ sendNoOrgError(res) {
136
+ res.writeHead(401, { "Content-Type": "application/json" });
137
+ res.end(
138
+ JSON.stringify({
139
+ error: "NO_ORG_FOUND",
140
+ message: "No default Salesforce org found. Run 'sf org login web --set-default' to authenticate."
141
+ })
142
+ );
143
+ }
144
+ sendJson(res, statusCode, body) {
145
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
146
+ res.end(JSON.stringify(body));
147
+ }
148
+ sendAuthOrGatewayError(res, error, gatewayMessage) {
149
+ if (error instanceof AuthenticationError) {
150
+ this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
151
+ } else {
152
+ this.sendJson(res, 502, {
153
+ error: "GATEWAY_ERROR",
154
+ message: gatewayMessage
155
+ });
172
156
  }
173
- sendJson(res, statusCode, body) {
174
- res.writeHead(statusCode, { "Content-Type": "application/json" });
175
- res.end(JSON.stringify(body));
157
+ }
158
+ async handleLightningOutFrontdoor(req, res, _url) {
159
+ try {
160
+ if (!this.orgInfo) {
161
+ this.sendNoOrgError(res);
162
+ return;
163
+ }
164
+ if (req.method && req.method !== "GET") {
165
+ this.sendJson(res, 405, { error: "METHOD_NOT_ALLOWED", message: "Use GET" });
166
+ return;
167
+ }
168
+ const { rawInstanceUrl, accessToken, orgId } = this.orgInfo;
169
+ const frontdoorUrl = await this.fetchLightningOutFrontdoorUrl(rawInstanceUrl, accessToken);
170
+ if (!frontdoorUrl) {
171
+ this.sendJson(res, 502, {
172
+ error: "FRONTDOOR_FAILED",
173
+ message: "No frontdoor URL returned from Salesforce"
174
+ });
175
+ return;
176
+ }
177
+ this.sendJson(res, 200, {
178
+ frontdoorUrl,
179
+ instanceUrl: rawInstanceUrl,
180
+ orgId
181
+ });
182
+ } catch (error) {
183
+ console.error("[webapps-proxy] Frontdoor request failed:", error);
184
+ this.sendJson(res, 502, {
185
+ error: "GATEWAY_ERROR",
186
+ message: "Failed to generate frontdoor URL"
187
+ });
176
188
  }
177
- sendAuthOrGatewayError(res, error, gatewayMessage) {
178
- if (error instanceof AuthenticationError) {
179
- this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
180
- }
181
- else {
182
- this.sendJson(res, 502, {
183
- error: "GATEWAY_ERROR",
184
- message: gatewayMessage,
185
- });
189
+ }
190
+ async fetchLightningOutFrontdoorUrl(rawInstanceUrl, accessToken) {
191
+ let baseUrl = rawInstanceUrl.replace(/\/$/, "");
192
+ let response = await fetch(`${baseUrl}${LIGHTNING_OUT_SINGLE_ACCESS_PATH}`, {
193
+ method: "POST",
194
+ headers: {
195
+ Authorization: `Bearer ${accessToken}`,
196
+ "Content-Type": "application/x-www-form-urlencoded"
197
+ }
198
+ });
199
+ if (response.status === 401 || response.status === 403) {
200
+ const updatedOrgInfo = await this.refreshToken();
201
+ if (!updatedOrgInfo) {
202
+ throw new AuthenticationError("Failed to refresh token");
203
+ }
204
+ baseUrl = updatedOrgInfo.rawInstanceUrl.replace(/\/$/, "");
205
+ response = await fetch(`${baseUrl}${LIGHTNING_OUT_SINGLE_ACCESS_PATH}`, {
206
+ method: "POST",
207
+ headers: {
208
+ Authorization: `Bearer ${updatedOrgInfo.accessToken}`,
209
+ "Content-Type": "application/x-www-form-urlencoded"
186
210
  }
211
+ });
187
212
  }
188
- async handleLightningOutFrontdoor(req, res, _url) {
189
- try {
190
- if (!this.orgInfo) {
191
- this.sendNoOrgError(res);
192
- return;
193
- }
194
- if (req.method && req.method !== "GET") {
195
- this.sendJson(res, 405, { error: "METHOD_NOT_ALLOWED", message: "Use GET" });
196
- return;
197
- }
198
- const { rawInstanceUrl, accessToken, orgId } = this.orgInfo;
199
- const frontdoorUrl = await this.fetchLightningOutFrontdoorUrl(rawInstanceUrl, accessToken);
200
- if (!frontdoorUrl) {
201
- this.sendJson(res, 502, {
202
- error: "FRONTDOOR_FAILED",
203
- message: "No frontdoor URL returned from Salesforce",
204
- });
205
- return;
206
- }
207
- this.sendJson(res, 200, {
208
- frontdoorUrl,
209
- instanceUrl: rawInstanceUrl,
210
- orgId,
211
- });
212
- }
213
- catch (error) {
214
- console.error("[webapps-proxy] Frontdoor request failed:", error);
215
- this.sendJson(res, 502, {
216
- error: "GATEWAY_ERROR",
217
- message: "Failed to generate frontdoor URL",
218
- });
219
- }
213
+ if (!response.ok) {
214
+ const errorBody = await response.text();
215
+ throw new Error(`Frontdoor exchange failed: ${response.status} ${errorBody}`);
220
216
  }
221
- async fetchLightningOutFrontdoorUrl(rawInstanceUrl, accessToken) {
222
- let baseUrl = rawInstanceUrl.replace(/\/$/, "");
223
- let response = await fetch(`${baseUrl}${LIGHTNING_OUT_SINGLE_ACCESS_PATH}`, {
224
- method: "POST",
225
- headers: {
226
- Authorization: `Bearer ${accessToken}`,
227
- "Content-Type": "application/x-www-form-urlencoded",
228
- },
229
- });
230
- if (response.status === 401 || response.status === 403) {
231
- const updatedOrgInfo = await this.refreshToken();
232
- if (!updatedOrgInfo) {
233
- throw new AuthenticationError("Failed to refresh token");
234
- }
235
- baseUrl = updatedOrgInfo.rawInstanceUrl.replace(/\/$/, "");
236
- response = await fetch(`${baseUrl}${LIGHTNING_OUT_SINGLE_ACCESS_PATH}`, {
237
- method: "POST",
238
- headers: {
239
- Authorization: `Bearer ${updatedOrgInfo.accessToken}`,
240
- "Content-Type": "application/x-www-form-urlencoded",
241
- },
242
- });
217
+ const data = await response.json();
218
+ return data.frontdoor_uri ?? null;
219
+ }
220
+ async handleSalesforceApi(req, res) {
221
+ try {
222
+ if (!this.orgInfo) {
223
+ this.sendNoOrgError(res);
224
+ return;
225
+ }
226
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
227
+ let pathIndex = url.pathname.indexOf("/lwr/apex/v");
228
+ if (pathIndex === -1) {
229
+ pathIndex = url.pathname.indexOf("/services/data/v");
230
+ }
231
+ const apiPath = url.pathname.substring(pathIndex);
232
+ let targetUrl = `${this.orgInfo.instanceUrl}${apiPath}${url.search}`;
233
+ if (this.options?.debug) {
234
+ console.log(`[webapps-proxy] Forwarding to Salesforce: ${targetUrl}`);
235
+ }
236
+ const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : void 0;
237
+ let response = await fetch(targetUrl, {
238
+ method: req.method,
239
+ headers: {
240
+ ...getFilteredHeaders(req.headers),
241
+ Cookie: `sid=${this.orgInfo.accessToken}`,
242
+ Accept: req.headers.accept ?? "application/json",
243
+ // necessary for Apex requests, for which SessionUtil.validateSessionUsage won't accept OAuth token as `sid` cookie
244
+ Authorization: `Bearer ${this.orgInfo.accessToken}`
245
+ },
246
+ body
247
+ });
248
+ if (response.status === 401 || response.status === 403) {
249
+ console.warn(`[webapps-proxy] Received ${response.status}, refreshing token...`);
250
+ const updatedOrgInfo = await this.refreshToken();
251
+ if (updatedOrgInfo === void 0) {
252
+ console.error("[webapps-proxy] Failed to refresh token - authentication error");
253
+ this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
254
+ return;
243
255
  }
244
- if (!response.ok) {
245
- const errorBody = await response.text();
246
- throw new Error(`Frontdoor exchange failed: ${response.status} ${errorBody}`);
256
+ if (this.options?.debug) {
257
+ console.log("[webapps-proxy] Token refreshed, retrying request");
247
258
  }
248
- const data = (await response.json());
249
- return data.frontdoor_uri ?? null;
259
+ targetUrl = `${updatedOrgInfo.instanceUrl}${url.pathname}${url.search}`;
260
+ response = await fetch(targetUrl, {
261
+ method: req.method,
262
+ headers: {
263
+ ...getFilteredHeaders(req.headers),
264
+ Cookie: `sid=${updatedOrgInfo.accessToken}`,
265
+ Accept: req.headers.accept ?? "application/json"
266
+ },
267
+ body
268
+ });
269
+ }
270
+ await this.sendResponse(res, response);
271
+ } catch (error) {
272
+ console.error("[webapps-proxy] Salesforce API request failed:", error);
273
+ this.sendAuthOrGatewayError(res, error, "Failed to forward request to Salesforce");
250
274
  }
251
- async handleSalesforceApi(req, res) {
252
- try {
253
- if (!this.orgInfo) {
254
- this.sendNoOrgError(res);
255
- return;
256
- }
257
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
258
- let pathIndex = url.pathname.indexOf("/lwr/apex/v");
259
- if (pathIndex === -1) {
260
- pathIndex = url.pathname.indexOf("/services/data/v");
261
- }
262
- const apiPath = url.pathname.substring(pathIndex);
263
- let targetUrl = `${this.orgInfo.instanceUrl}${apiPath}${url.search}`;
264
- if (this.options?.debug) {
265
- console.log(`[webapps-proxy] Forwarding to Salesforce: ${targetUrl}`);
266
- }
267
- // Buffer the request body before sending. This allows us to retry requests
268
- // with the same body in case of authentication failures (403).
269
- // For GET/HEAD requests, body is undefined.
270
- const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : undefined;
271
- let response = await fetch(targetUrl, {
272
- method: req.method,
273
- headers: {
274
- ...getFilteredHeaders(req.headers),
275
- Cookie: `sid=${this.orgInfo.accessToken}`,
276
- Accept: req.headers.accept ?? "application/json",
277
- // necessary for Apex requests, for which SessionUtil.validateSessionUsage won't accept OAuth token as `sid` cookie
278
- Authorization: `Bearer ${this.orgInfo.accessToken}`,
279
- },
280
- body: body,
281
- });
282
- if (response.status === 401 || response.status === 403) {
283
- console.warn(`[webapps-proxy] Received ${response.status}, refreshing token...`);
284
- const updatedOrgInfo = await this.refreshToken();
285
- if (updatedOrgInfo === undefined) {
286
- console.error("[webapps-proxy] Failed to refresh token - authentication error");
287
- this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
288
- return;
289
- }
290
- if (this.options?.debug) {
291
- console.log("[webapps-proxy] Token refreshed, retrying request");
292
- }
293
- // Update target URL with refreshed org info (instance URL may have changed)
294
- targetUrl = `${updatedOrgInfo.instanceUrl}${url.pathname}${url.search}`;
295
- // Retry with the same buffered body
296
- response = await fetch(targetUrl, {
297
- method: req.method,
298
- headers: {
299
- ...getFilteredHeaders(req.headers),
300
- Cookie: `sid=${updatedOrgInfo.accessToken}`,
301
- Accept: req.headers.accept ?? "application/json",
302
- },
303
- body: body,
304
- });
305
- }
306
- await this.sendResponse(res, response);
307
- }
308
- catch (error) {
309
- console.error("[webapps-proxy] Salesforce API request failed:", error);
310
- this.sendAuthOrGatewayError(res, error, "Failed to forward request to Salesforce");
311
- }
275
+ }
276
+ async refreshToken() {
277
+ if (!this.orgInfo) {
278
+ return void 0;
312
279
  }
313
- async refreshToken() {
314
- if (!this.orgInfo) {
315
- return undefined;
316
- }
317
- // Use orgAlias if available, otherwise fall back to username
318
- // This handles cases where orgInfo was created without an explicit alias
319
- const refreshIdentifier = this.orgInfo.orgAlias || this.orgInfo.username;
320
- if (!refreshIdentifier) {
321
- throw new AuthenticationError("Cannot refresh token: no org alias or username available");
322
- }
323
- const updatedOrgInfo = await refreshOrgAuth(refreshIdentifier);
324
- if (!updatedOrgInfo) {
325
- return undefined;
326
- }
327
- this.orgInfo = updatedOrgInfo;
328
- // Notify plugin of token refresh so it can sync its orgInfo
329
- if (this.options?.onTokenRefresh) {
330
- this.options.onTokenRefresh(updatedOrgInfo);
331
- }
332
- return updatedOrgInfo;
280
+ const refreshIdentifier = this.orgInfo.orgAlias || this.orgInfo.username;
281
+ if (!refreshIdentifier) {
282
+ throw new AuthenticationError("Cannot refresh token: no org alias or username available");
333
283
  }
334
- async handleGraphQL(req, res) {
335
- try {
336
- if (!this.orgInfo) {
337
- this.sendNoOrgError(res);
338
- return;
339
- }
340
- const { rawInstanceUrl, apiVersion, accessToken } = this.orgInfo;
341
- let targetUrl = `${rawInstanceUrl}/services/data/v${apiVersion}/graphql`;
342
- if (this.options?.debug) {
343
- console.log(`[webapps-proxy] Forwarding GraphQL to Salesforce: ${targetUrl}`);
344
- }
345
- // Buffer the request body before sending. This allows us to retry requests
346
- // with the same body in case of authentication failures (401/403).
347
- const body = await getBody(req);
348
- const headers = {
349
- "Content-Type": "application/json",
350
- Accept: "application/json",
351
- Authorization: `Bearer ${accessToken}`,
352
- "X-Chatter-Entity-Encoding": "false",
353
- };
354
- let response = await fetch(targetUrl, {
355
- method: "POST",
356
- headers,
357
- body: body,
358
- });
359
- if (response.status === 401 || response.status === 403) {
360
- console.warn(`[webapps-proxy] Received ${response.status}, refreshing token...`);
361
- const updatedOrgInfo = await this.refreshToken();
362
- if (updatedOrgInfo !== undefined) {
363
- if (this.options?.debug) {
364
- console.log("[webapps-proxy] Token refreshed, retrying request");
365
- }
366
- const { rawInstanceUrl, apiVersion, accessToken } = updatedOrgInfo;
367
- targetUrl = `${rawInstanceUrl}/services/data/v${apiVersion}/graphql`;
368
- headers.Authorization = `Bearer ${accessToken}`;
369
- response = await fetch(targetUrl, {
370
- method: "POST",
371
- headers,
372
- body: body,
373
- });
374
- }
375
- else {
376
- console.error("[webapps-proxy] Failed to refresh token for GraphQL - authentication error");
377
- this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
378
- return;
379
- }
380
- }
381
- await this.sendResponse(res, response);
382
- }
383
- catch (error) {
384
- console.error("[webapps-proxy] GraphQL request failed:", error);
385
- this.sendAuthOrGatewayError(res, error, "Failed to forward GraphQL request to Salesforce");
386
- }
284
+ const updatedOrgInfo = await refreshOrgAuth(refreshIdentifier);
285
+ if (!updatedOrgInfo) {
286
+ return void 0;
387
287
  }
388
- async sendResponse(res, response) {
389
- const headers = {};
390
- const skipHeaders = new Set(["content-encoding", "content-length", "transfer-encoding"]);
391
- response.headers.forEach((value, key) => {
392
- if (!skipHeaders.has(key.toLowerCase())) {
393
- headers[key] = value;
394
- }
395
- });
396
- // Check if response is HTML and should have script injected
397
- const contentType = response.headers.get("content-type") || "";
398
- const isHtml = contentType.includes("text/html");
399
- if (isHtml && response.body) {
400
- // Buffer the entire response body to inject script
401
- const chunks = [];
402
- const reader = response.body.getReader();
403
- while (true) {
404
- const { done, value } = await reader.read();
405
- if (done)
406
- break;
407
- chunks.push(value);
408
- }
409
- // Combine all chunks into a single buffer
410
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
411
- const buffer = new Uint8Array(totalLength);
412
- let offset = 0;
413
- for (const chunk of chunks) {
414
- buffer.set(chunk, offset);
415
- offset += chunk.length;
416
- }
417
- // Convert to string, inject script, and convert back
418
- const html = new TextDecoder().decode(buffer);
419
- const modifiedHtml = WebAppProxyHandler.injectLivePreviewScript(html);
420
- // Update content-length header
421
- const modifiedBuffer = new TextEncoder().encode(modifiedHtml);
422
- headers["content-length"] = modifiedBuffer.length.toString();
423
- res.writeHead(response.status, headers);
424
- res.write(modifiedBuffer);
425
- }
426
- else {
427
- // For non-HTML responses, stream as before
428
- res.writeHead(response.status, headers);
429
- if (response.body) {
430
- const reader = response.body.getReader();
431
- while (true) {
432
- const { done, value } = await reader.read();
433
- if (done)
434
- break;
435
- res.write(value);
436
- }
437
- }
438
- }
439
- res.end();
288
+ this.orgInfo = updatedOrgInfo;
289
+ if (this.options?.onTokenRefresh) {
290
+ this.options.onTokenRefresh(updatedOrgInfo);
440
291
  }
441
- static injectLivePreviewScript(html) {
442
- if (html.includes(LIVE_PREVIEW_SCRIPT_MARKER)) {
443
- return html;
444
- }
445
- const scriptContent = getLivePreviewScriptContent();
446
- const scriptTag = `<script ${LIVE_PREVIEW_SCRIPT_MARKER}>` + scriptContent + "<" + "/script>";
447
- if (html.includes("</body>")) {
448
- const lastBodyIndex = html.lastIndexOf("</body>");
449
- return html.substring(0, lastBodyIndex) + scriptTag + html.substring(lastBodyIndex);
450
- }
451
- if (html.includes("</html>")) {
452
- const lastHtmlIndex = html.lastIndexOf("</html>");
453
- return html.substring(0, lastHtmlIndex) + scriptTag + html.substring(lastHtmlIndex);
292
+ return updatedOrgInfo;
293
+ }
294
+ async handleGraphQL(req, res) {
295
+ try {
296
+ if (!this.orgInfo) {
297
+ this.sendNoOrgError(res);
298
+ return;
299
+ }
300
+ const { rawInstanceUrl, apiVersion, accessToken } = this.orgInfo;
301
+ let targetUrl = `${rawInstanceUrl}/services/data/v${apiVersion}/graphql`;
302
+ if (this.options?.debug) {
303
+ console.log(`[webapps-proxy] Forwarding GraphQL to Salesforce: ${targetUrl}`);
304
+ }
305
+ const body = await getBody(req);
306
+ const headers = {
307
+ "Content-Type": "application/json",
308
+ Accept: "application/json",
309
+ Authorization: `Bearer ${accessToken}`,
310
+ "X-Chatter-Entity-Encoding": "false"
311
+ };
312
+ let response = await fetch(targetUrl, {
313
+ method: "POST",
314
+ headers,
315
+ body
316
+ });
317
+ if (response.status === 401 || response.status === 403) {
318
+ console.warn(`[webapps-proxy] Received ${response.status}, refreshing token...`);
319
+ const updatedOrgInfo = await this.refreshToken();
320
+ if (updatedOrgInfo !== void 0) {
321
+ if (this.options?.debug) {
322
+ console.log("[webapps-proxy] Token refreshed, retrying request");
323
+ }
324
+ const { rawInstanceUrl: rawInstanceUrl2, apiVersion: apiVersion2, accessToken: accessToken2 } = updatedOrgInfo;
325
+ targetUrl = `${rawInstanceUrl2}/services/data/v${apiVersion2}/graphql`;
326
+ headers.Authorization = `Bearer ${accessToken2}`;
327
+ response = await fetch(targetUrl, {
328
+ method: "POST",
329
+ headers,
330
+ body
331
+ });
332
+ } else {
333
+ console.error(
334
+ "[webapps-proxy] Failed to refresh token for GraphQL - authentication error"
335
+ );
336
+ this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
337
+ return;
454
338
  }
455
- return html + scriptTag;
339
+ }
340
+ await this.sendResponse(res, response);
341
+ } catch (error) {
342
+ console.error("[webapps-proxy] GraphQL request failed:", error);
343
+ this.sendAuthOrGatewayError(res, error, "Failed to forward GraphQL request to Salesforce");
456
344
  }
457
- /**
458
- * Proxy POST /chatter/handlers/file/body (XHR/file upload) to Salesforce.
459
- * Uses rawInstanceUrl for Chatter API. Preserves multipart/form-data from XHR.
460
- */
461
- async handleFileUpload(req, res, url) {
462
- try {
463
- if (!this.orgInfo) {
464
- this.sendNoOrgError(res);
465
- return;
466
- }
467
- // Strip basePath when forwarding to Salesforce (Salesforce expects /chatter/handlers/file/body)
468
- const pathForSalesforce = this.basePath && url.pathname.startsWith(this.basePath)
469
- ? url.pathname.slice(this.basePath.length) || "/"
470
- : url.pathname;
471
- const uploadUrl = `${this.orgInfo.rawInstanceUrl}${pathForSalesforce}${url.search}`;
472
- const body = await getBody(req);
473
- if (!body?.length) {
474
- res.writeHead(400, { "Content-Type": "application/json" });
475
- res.end(JSON.stringify({
476
- error: "BAD_REQUEST",
477
- message: "Request body is empty",
478
- }));
479
- return;
480
- }
481
- const contentType = req.headers["content-type"];
482
- if (!contentType?.includes("multipart/form-data")) {
483
- res.writeHead(400, { "Content-Type": "application/json" });
484
- res.end(JSON.stringify({
485
- error: "BAD_REQUEST",
486
- message: "Content-Type must be multipart/form-data",
487
- }));
488
- return;
489
- }
490
- const headers = {
491
- "Content-Type": contentType,
492
- "Content-Length": String(body.length),
493
- Cookie: `sid=${this.orgInfo.accessToken}`,
494
- Authorization: `Bearer ${this.orgInfo.accessToken}`,
495
- };
496
- const response = await fetch(uploadUrl, {
497
- method: "POST",
498
- headers,
499
- body: new Uint8Array(body),
500
- });
501
- await this.sendResponse(res, response);
502
- }
503
- catch (error) {
504
- console.error("[webapps-proxy] File upload proxy failed:", error);
505
- res.writeHead(502, { "Content-Type": "application/json" });
506
- res.end(JSON.stringify({
507
- error: "GATEWAY_ERROR",
508
- message: "Failed to forward file upload",
509
- }));
345
+ }
346
+ async sendResponse(res, response) {
347
+ const headers = {};
348
+ const skipHeaders = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding"]);
349
+ response.headers.forEach((value, key) => {
350
+ if (!skipHeaders.has(key.toLowerCase())) {
351
+ headers[key] = value;
352
+ }
353
+ });
354
+ const contentType = response.headers.get("content-type") || "";
355
+ const isHtml = contentType.includes("text/html");
356
+ if (isHtml && response.body) {
357
+ const chunks = [];
358
+ const reader = response.body.getReader();
359
+ while (true) {
360
+ const { done, value } = await reader.read();
361
+ if (done) break;
362
+ chunks.push(value);
363
+ }
364
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
365
+ const buffer = new Uint8Array(totalLength);
366
+ let offset = 0;
367
+ for (const chunk of chunks) {
368
+ buffer.set(chunk, offset);
369
+ offset += chunk.length;
370
+ }
371
+ const html = new TextDecoder().decode(buffer);
372
+ const modifiedHtml = WebAppProxyHandler.injectLivePreviewScript(html);
373
+ const modifiedBuffer = new TextEncoder().encode(modifiedHtml);
374
+ headers["content-length"] = modifiedBuffer.length.toString();
375
+ res.writeHead(response.status, headers);
376
+ res.write(modifiedBuffer);
377
+ } else {
378
+ res.writeHead(response.status, headers);
379
+ if (response.body) {
380
+ const reader = response.body.getReader();
381
+ while (true) {
382
+ const { done, value } = await reader.read();
383
+ if (done) break;
384
+ res.write(value);
510
385
  }
386
+ }
387
+ }
388
+ res.end();
389
+ }
390
+ static injectLivePreviewScript(html) {
391
+ if (html.includes(LIVE_PREVIEW_SCRIPT_MARKER)) {
392
+ return html;
393
+ }
394
+ const scriptContent = getLivePreviewScriptContent();
395
+ const scriptTag = `<script ${LIVE_PREVIEW_SCRIPT_MARKER}>` + scriptContent + "<\/script>";
396
+ if (html.includes("</body>")) {
397
+ const lastBodyIndex = html.lastIndexOf("</body>");
398
+ return html.substring(0, lastBodyIndex) + scriptTag + html.substring(lastBodyIndex);
511
399
  }
400
+ if (html.includes("</html>")) {
401
+ const lastHtmlIndex = html.lastIndexOf("</html>");
402
+ return html.substring(0, lastHtmlIndex) + scriptTag + html.substring(lastHtmlIndex);
403
+ }
404
+ return html + scriptTag;
405
+ }
406
+ /**
407
+ * Proxy POST /chatter/handlers/file/body (XHR/file upload) to Salesforce.
408
+ * Uses rawInstanceUrl for Chatter API. Preserves multipart/form-data from XHR.
409
+ */
410
+ async handleFileUpload(req, res, url) {
411
+ try {
412
+ if (!this.orgInfo) {
413
+ this.sendNoOrgError(res);
414
+ return;
415
+ }
416
+ const pathForSalesforce = this.basePath && url.pathname.startsWith(this.basePath) ? url.pathname.slice(this.basePath.length) || "/" : url.pathname;
417
+ const uploadUrl = `${this.orgInfo.rawInstanceUrl}${pathForSalesforce}${url.search}`;
418
+ const body = await getBody(req);
419
+ if (!body?.length) {
420
+ res.writeHead(400, { "Content-Type": "application/json" });
421
+ res.end(
422
+ JSON.stringify({
423
+ error: "BAD_REQUEST",
424
+ message: "Request body is empty"
425
+ })
426
+ );
427
+ return;
428
+ }
429
+ const contentType = req.headers["content-type"];
430
+ if (!contentType?.includes("multipart/form-data")) {
431
+ res.writeHead(400, { "Content-Type": "application/json" });
432
+ res.end(
433
+ JSON.stringify({
434
+ error: "BAD_REQUEST",
435
+ message: "Content-Type must be multipart/form-data"
436
+ })
437
+ );
438
+ return;
439
+ }
440
+ const headers = {
441
+ "Content-Type": contentType,
442
+ "Content-Length": String(body.length),
443
+ Cookie: `sid=${this.orgInfo.accessToken}`,
444
+ Authorization: `Bearer ${this.orgInfo.accessToken}`
445
+ };
446
+ const response = await fetch(uploadUrl, {
447
+ method: "POST",
448
+ headers,
449
+ body: new Uint8Array(body)
450
+ });
451
+ await this.sendResponse(res, response);
452
+ } catch (error) {
453
+ console.error("[webapps-proxy] File upload proxy failed:", error);
454
+ res.writeHead(502, { "Content-Type": "application/json" });
455
+ res.end(
456
+ JSON.stringify({
457
+ error: "GATEWAY_ERROR",
458
+ message: "Failed to forward file upload"
459
+ })
460
+ );
461
+ }
462
+ }
512
463
  }
513
- /**
514
- * Create proxy request handler
515
- *
516
- * @param manifest - WebApp manifest configuration
517
- * @param orgInfo - Salesforce org information
518
- * @param target - Target URL for dev server forwarding (optional)
519
- * @param basePath - Base path for routing (optional)
520
- * @param options - Proxy configuration options
521
- * @returns Async request handler function for Node.js HTTP server
522
- */
523
- export function createProxyHandler(manifest, orgInfo, target, basePath, options) {
524
- const handler = new WebAppProxyHandler(manifest, orgInfo, target, basePath, options);
525
- return (req, res, next) => handler.handle(req, res, next);
464
+ function createProxyHandler(manifest, orgInfo, target, basePath, options) {
465
+ const handler = new WebAppProxyHandler(manifest, orgInfo, target, basePath, options);
466
+ return (req, res, next) => handler.handle(req, res, next);
526
467
  }
527
- /**
528
- * Inject Live Preview script into HTML content.
529
- * Used by the Vite plugin's transformIndexHtml hook and the standalone proxy's sendResponse.
530
- */
531
- export function injectLivePreviewScript(html) {
532
- return WebAppProxyHandler.injectLivePreviewScript(html);
468
+ function injectLivePreviewScript(html) {
469
+ return WebAppProxyHandler.injectLivePreviewScript(html);
533
470
  }
534
471
  function getFilteredHeaders(headers) {
535
- const filtered = {};
536
- const hopByHopHeaders = new Set([
537
- "connection",
538
- "keep-alive",
539
- "proxy-authenticate",
540
- "proxy-authorization",
541
- "te",
542
- "trailer",
543
- "transfer-encoding",
544
- "upgrade",
545
- ]);
546
- for (const [key, value] of Object.entries(headers)) {
547
- if (!hopByHopHeaders.has(key.toLowerCase()) && value) {
548
- filtered[key] = Array.isArray(value)
549
- ? value.join(", ")
550
- : typeof value === "string"
551
- ? value
552
- : String(value);
553
- }
472
+ const filtered = {};
473
+ const hopByHopHeaders = /* @__PURE__ */ new Set([
474
+ "connection",
475
+ "keep-alive",
476
+ "proxy-authenticate",
477
+ "proxy-authorization",
478
+ "te",
479
+ "trailer",
480
+ "transfer-encoding",
481
+ "upgrade"
482
+ ]);
483
+ for (const [key, value] of Object.entries(headers)) {
484
+ if (!hopByHopHeaders.has(key.toLowerCase()) && value) {
485
+ filtered[key] = Array.isArray(value) ? value.join(", ") : typeof value === "string" ? value : String(value);
554
486
  }
555
- return filtered;
487
+ }
488
+ return filtered;
556
489
  }
557
490
  function getBody(req) {
558
- return new Promise((resolve, reject) => {
559
- const chunks = [];
560
- req.on("data", (chunk) => chunks.push(chunk));
561
- req.on("end", () => resolve(Buffer.concat(chunks)));
562
- req.on("error", reject);
563
- });
491
+ return new Promise((resolve, reject) => {
492
+ const chunks = [];
493
+ req.on("data", (chunk) => chunks.push(chunk));
494
+ req.on("end", () => resolve(Buffer.concat(chunks)));
495
+ req.on("error", reject);
496
+ });
564
497
  }
498
+ export {
499
+ WEBAPP_HEALTH_CHECK_PARAM,
500
+ WEBAPP_PROXY_HEADER,
501
+ createProxyHandler,
502
+ injectLivePreviewScript
503
+ };