@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.
- package/dist/api/clients.js +72 -77
- package/dist/api/index.d.ts +4 -4
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +13 -11
- package/dist/api/utils/accounts.js +16 -30
- package/dist/api/utils/records.js +21 -20
- package/dist/api/utils/user.js +20 -28
- package/dist/app/index.d.ts +4 -4
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +7 -7
- package/dist/app/manifest.js +23 -37
- package/dist/app/org.js +45 -58
- package/dist/design/index.js +12 -19
- package/dist/design/interactions/interactionsController.d.ts +1 -6
- package/dist/design/interactions/interactionsController.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -10
- package/dist/package.json.js +4 -0
- package/dist/proxy/handler.d.ts +2 -7
- package/dist/proxy/handler.d.ts.map +1 -1
- package/dist/proxy/handler.js +462 -523
- package/dist/proxy/index.d.ts +2 -2
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +7 -6
- package/dist/proxy/livePreviewScript.js +11 -26
- package/dist/proxy/routing.d.ts +1 -6
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/routing.js +73 -103
- package/package.json +7 -6
- package/dist/api/clients.test.d.ts +0 -7
- package/dist/api/clients.test.d.ts.map +0 -1
- package/dist/api/clients.test.js +0 -167
- package/dist/api/graphql-operations-types.js +0 -44
- package/dist/api/utils/records.test.d.ts +0 -7
- package/dist/api/utils/records.test.d.ts.map +0 -1
- package/dist/api/utils/records.test.js +0 -190
- package/dist/api/utils/user.test.d.ts +0 -7
- package/dist/api/utils/user.test.d.ts.map +0 -1
- package/dist/api/utils/user.test.js +0 -108
- package/dist/design/interactions/communicationManager.js +0 -108
- package/dist/design/interactions/componentMatcher.js +0 -80
- package/dist/design/interactions/editableManager.js +0 -95
- package/dist/design/interactions/eventHandlers.js +0 -125
- package/dist/design/interactions/index.js +0 -47
- package/dist/design/interactions/interactionsController.js +0 -135
- package/dist/design/interactions/styleManager.js +0 -97
- package/dist/design/interactions/utils/cssUtils.js +0 -72
- package/dist/design/interactions/utils/sourceUtils.js +0 -99
- package/dist/proxy/livePreviewScript.test.d.ts +0 -7
- package/dist/proxy/livePreviewScript.test.d.ts.map +0 -1
- package/dist/proxy/livePreviewScript.test.js +0 -96
package/dist/proxy/handler.js
CHANGED
|
@@ -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 {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
87
|
+
if (next) {
|
|
88
|
+
next();
|
|
89
|
+
} else {
|
|
90
|
+
await this.forwardToDevServer(req, res);
|
|
146
91
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 (
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
487
|
+
}
|
|
488
|
+
return filtered;
|
|
556
489
|
}
|
|
557
490
|
function getBody(req) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
};
|