@salesforce/ui-bundle 1.117.2
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/LICENSE.txt +82 -0
- package/README.md +3 -0
- package/dist/api/clients.d.ts +22 -0
- package/dist/api/clients.d.ts.map +1 -0
- package/dist/api/clients.js +84 -0
- package/dist/api/graphql-operations-types.d.ts +225 -0
- package/dist/api/graphql-operations-types.d.ts.map +1 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +13 -0
- package/dist/api/utils/accounts.d.ts +33 -0
- package/dist/api/utils/accounts.d.ts.map +1 -0
- package/dist/api/utils/accounts.js +47 -0
- package/dist/api/utils/records.d.ts +16 -0
- package/dist/api/utils/records.d.ts.map +1 -0
- package/dist/api/utils/records.js +26 -0
- package/dist/api/utils/user.d.ts +17 -0
- package/dist/api/utils/user.d.ts.map +1 -0
- package/dist/api/utils/user.js +25 -0
- package/dist/app/index.d.ts +10 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +7 -0
- package/dist/app/manifest.d.ts +34 -0
- package/dist/app/manifest.d.ts.map +1 -0
- package/dist/app/manifest.js +28 -0
- package/dist/app/org.d.ts +28 -0
- package/dist/app/org.d.ts.map +1 -0
- package/dist/app/org.js +67 -0
- package/dist/design/design-mode-interactions.js +761 -0
- package/dist/design/index.d.ts +12 -0
- package/dist/design/index.d.ts.map +1 -0
- package/dist/design/index.js +14 -0
- package/dist/design/interactions/communicationManager.d.ts +25 -0
- package/dist/design/interactions/communicationManager.d.ts.map +1 -0
- package/dist/design/interactions/componentMatcher.d.ts +43 -0
- package/dist/design/interactions/componentMatcher.d.ts.map +1 -0
- package/dist/design/interactions/editableManager.d.ts +25 -0
- package/dist/design/interactions/editableManager.d.ts.map +1 -0
- package/dist/design/interactions/eventHandlers.d.ts +40 -0
- package/dist/design/interactions/eventHandlers.d.ts.map +1 -0
- package/dist/design/interactions/index.d.ts +7 -0
- package/dist/design/interactions/index.d.ts.map +1 -0
- package/dist/design/interactions/interactionsController.d.ts +36 -0
- package/dist/design/interactions/interactionsController.d.ts.map +1 -0
- package/dist/design/interactions/styleManager.d.ts +49 -0
- package/dist/design/interactions/styleManager.d.ts.map +1 -0
- package/dist/design/interactions/utils/cssUtils.d.ts +54 -0
- package/dist/design/interactions/utils/cssUtils.d.ts.map +1 -0
- package/dist/design/interactions/utils/sourceUtils.d.ts +36 -0
- package/dist/design/interactions/utils/sourceUtils.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/package.json.js +4 -0
- package/dist/proxy/handler.d.ts +38 -0
- package/dist/proxy/handler.d.ts.map +1 -0
- package/dist/proxy/handler.js +530 -0
- package/dist/proxy/index.d.ts +8 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +7 -0
- package/dist/proxy/livePreviewScript.d.ts +21 -0
- package/dist/proxy/livePreviewScript.d.ts.map +1 -0
- package/dist/proxy/livePreviewScript.js +16 -0
- package/dist/proxy/routing.d.ts +35 -0
- package/dist/proxy/routing.d.ts.map +1 -0
- package/dist/proxy/routing.js +83 -0
- package/dist/proxy/templates/livePreviewScript.js +553 -0
- package/package.json +65 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { LIVE_PREVIEW_SCRIPT_MARKER, getLivePreviewScriptContent } from "./livePreviewScript.js";
|
|
2
|
+
import { applyTrailingSlash, matchRoute } from "./routing.js";
|
|
3
|
+
import { version } from "../package.json.js";
|
|
4
|
+
import { refreshOrgAuth } from "../app/org.js";
|
|
5
|
+
import "node:fs/promises";
|
|
6
|
+
const UIBUNDLE_HEALTH_CHECK_PARAM = "sfProxyHealthCheck";
|
|
7
|
+
const UIBUNDLE_PROXY_HEADER = "X-Salesforce-UIBundle-Proxy";
|
|
8
|
+
const LIGHTNING_OUT_SINGLE_ACCESS_PATH = "/services/oauth2/singleaccess";
|
|
9
|
+
const SALESFORCE_API_PREFIXES = ["/services/", "/lwr/apex/"];
|
|
10
|
+
const SALESFORCE_FILE_UPLOAD_PREFIX = "/chatter/handlers/file/body";
|
|
11
|
+
const AUTH_FAILED_RESPONSE = {
|
|
12
|
+
error: "AUTHENTICATION_FAILED",
|
|
13
|
+
message: "Authentication failed. Please re-authenticate to your Salesforce org.",
|
|
14
|
+
status: 401
|
|
15
|
+
};
|
|
16
|
+
class AuthenticationError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "AuthenticationError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class UIBundleProxyHandler {
|
|
23
|
+
constructor(manifest, orgInfo, target, basePath, options) {
|
|
24
|
+
this.manifest = manifest;
|
|
25
|
+
this.orgInfo = orgInfo;
|
|
26
|
+
this.target = target;
|
|
27
|
+
this.basePath = basePath;
|
|
28
|
+
this.options = options;
|
|
29
|
+
}
|
|
30
|
+
startTime = Date.now();
|
|
31
|
+
async handle(req, res, next) {
|
|
32
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
33
|
+
if (url.searchParams.get(UIBUNDLE_HEALTH_CHECK_PARAM) === "true") {
|
|
34
|
+
this.handleHealthCheck(res);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let pathname = url.pathname;
|
|
38
|
+
if (pathname === "/__lo/frontdoor") {
|
|
39
|
+
await this.handleLightningOutFrontdoor(req, res, url);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (this.options?.debug) {
|
|
43
|
+
console.log(`[ui-bundle-proxy] ${req.method} ${pathname}`);
|
|
44
|
+
}
|
|
45
|
+
pathname = applyTrailingSlash(pathname, this.manifest.routing?.trailingSlash);
|
|
46
|
+
const match = matchRoute(
|
|
47
|
+
pathname,
|
|
48
|
+
this.basePath,
|
|
49
|
+
this.manifest.routing?.rewrites,
|
|
50
|
+
this.manifest.routing?.redirects
|
|
51
|
+
);
|
|
52
|
+
if (match) {
|
|
53
|
+
if (match.type === "api") {
|
|
54
|
+
await this.handleSalesforceApi(req, res);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (match.type === "gql") {
|
|
58
|
+
await this.handleGraphQL(req, res);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (match.type === "redirect" && match.target && match.statusCode) {
|
|
62
|
+
this.handleRedirect(res, match.target, match.statusCode);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (match.type === "rewrite" && match.target) {
|
|
66
|
+
url.pathname = `/${match.target}`.replace(/\/+/g, "/");
|
|
67
|
+
req.url = url.pathname + url.search;
|
|
68
|
+
if (this.options?.debug) {
|
|
69
|
+
console.log(`[ui-bundle-proxy] Rewrite to ${req.url}`);
|
|
70
|
+
}
|
|
71
|
+
if (next) {
|
|
72
|
+
next();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await this.forwardToDevServer(req, res);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (match.type === "file-upload") {
|
|
79
|
+
if (this.options?.debug) {
|
|
80
|
+
console.log("[ui-bundle-proxy] file-upload match found → handleFileUpload");
|
|
81
|
+
}
|
|
82
|
+
await this.handleFileUpload(req, res);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (pathname.startsWith(SALESFORCE_FILE_UPLOAD_PREFIX)) {
|
|
87
|
+
await this.handleFileUpload(req, res);
|
|
88
|
+
return;
|
|
89
|
+
} else if (SALESFORCE_API_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
|
|
90
|
+
await this.handleSalesforceApi(req, res);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (next) {
|
|
94
|
+
next();
|
|
95
|
+
} else {
|
|
96
|
+
await this.forwardToDevServer(req, res);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async forwardToDevServer(req, res) {
|
|
100
|
+
try {
|
|
101
|
+
const baseUrl = this.target ?? `http://${req.headers.host ?? "localhost"}`;
|
|
102
|
+
const url = new URL(req.url ?? "/", baseUrl);
|
|
103
|
+
if (this.options?.debug) {
|
|
104
|
+
console.log(`[ui-bundle-proxy] Forwarding to dev server: ${url.href}`);
|
|
105
|
+
}
|
|
106
|
+
const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : void 0;
|
|
107
|
+
const response = await fetch(url.href, {
|
|
108
|
+
method: req.method,
|
|
109
|
+
headers: getFilteredHeaders(req.headers),
|
|
110
|
+
body
|
|
111
|
+
});
|
|
112
|
+
await this.sendResponse(res, response);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error("[ui-bundle-proxy] Dev server request failed:", error);
|
|
115
|
+
this.sendAuthOrGatewayError(res, error, "Failed to forward request to dev server");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
handleRedirect(res, location, statusCode) {
|
|
119
|
+
res.writeHead(statusCode, { Location: location });
|
|
120
|
+
res.end();
|
|
121
|
+
}
|
|
122
|
+
handleHealthCheck(res) {
|
|
123
|
+
const response = {
|
|
124
|
+
proxyName: "@salesforce/ui-bundle",
|
|
125
|
+
proxyVersion: version,
|
|
126
|
+
port: this.target ? new URL(this.target).port : void 0,
|
|
127
|
+
org: this.orgInfo?.orgAlias ?? this.orgInfo?.username,
|
|
128
|
+
apiVersion: this.orgInfo?.apiVersion,
|
|
129
|
+
uiBundleName: this.manifest.name,
|
|
130
|
+
uiBundleFolderPath: this.basePath,
|
|
131
|
+
uptime: Date.now() - this.startTime,
|
|
132
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
133
|
+
debugMode: this.options?.debug ?? false
|
|
134
|
+
};
|
|
135
|
+
res.writeHead(200, {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
[UIBUNDLE_PROXY_HEADER]: "true"
|
|
138
|
+
});
|
|
139
|
+
res.end(JSON.stringify(response, null, 2));
|
|
140
|
+
}
|
|
141
|
+
sendNoOrgError(res) {
|
|
142
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
143
|
+
res.end(
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
error: "NO_ORG_FOUND",
|
|
146
|
+
message: "No default Salesforce org found. Run 'sf org login web --set-default' to authenticate."
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
sendJson(res, statusCode, body) {
|
|
151
|
+
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
152
|
+
res.end(JSON.stringify(body));
|
|
153
|
+
}
|
|
154
|
+
sendAuthOrGatewayError(res, error, gatewayMessage) {
|
|
155
|
+
if (error instanceof AuthenticationError) {
|
|
156
|
+
this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
|
|
157
|
+
} else {
|
|
158
|
+
this.sendJson(res, 502, {
|
|
159
|
+
error: "GATEWAY_ERROR",
|
|
160
|
+
message: gatewayMessage
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async handleLightningOutFrontdoor(req, res, _url) {
|
|
165
|
+
try {
|
|
166
|
+
if (!this.orgInfo) {
|
|
167
|
+
this.sendNoOrgError(res);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (req.method && req.method !== "GET") {
|
|
171
|
+
this.sendJson(res, 405, { error: "METHOD_NOT_ALLOWED", message: "Use GET" });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const { rawInstanceUrl, accessToken, orgId } = this.orgInfo;
|
|
175
|
+
const frontdoorUrl = await this.fetchLightningOutFrontdoorUrl(rawInstanceUrl, accessToken);
|
|
176
|
+
if (!frontdoorUrl) {
|
|
177
|
+
this.sendJson(res, 502, {
|
|
178
|
+
error: "FRONTDOOR_FAILED",
|
|
179
|
+
message: "No frontdoor URL returned from Salesforce"
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.sendJson(res, 200, {
|
|
184
|
+
frontdoorUrl,
|
|
185
|
+
instanceUrl: rawInstanceUrl,
|
|
186
|
+
orgId
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("[ui-bundle-proxy] Frontdoor request failed:", error);
|
|
190
|
+
this.sendJson(res, 502, {
|
|
191
|
+
error: "GATEWAY_ERROR",
|
|
192
|
+
message: "Failed to generate frontdoor URL"
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async fetchLightningOutFrontdoorUrl(rawInstanceUrl, accessToken) {
|
|
197
|
+
let baseUrl = rawInstanceUrl.replace(/\/$/, "");
|
|
198
|
+
let response = await fetch(`${baseUrl}${LIGHTNING_OUT_SINGLE_ACCESS_PATH}`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: {
|
|
201
|
+
Authorization: `Bearer ${accessToken}`,
|
|
202
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
if (response.status === 401 || response.status === 403) {
|
|
206
|
+
const updatedOrgInfo = await this.refreshToken();
|
|
207
|
+
if (!updatedOrgInfo) {
|
|
208
|
+
throw new AuthenticationError("Failed to refresh token");
|
|
209
|
+
}
|
|
210
|
+
baseUrl = updatedOrgInfo.rawInstanceUrl.replace(/\/$/, "");
|
|
211
|
+
response = await fetch(`${baseUrl}${LIGHTNING_OUT_SINGLE_ACCESS_PATH}`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
Authorization: `Bearer ${updatedOrgInfo.accessToken}`,
|
|
215
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const errorBody = await response.text();
|
|
221
|
+
throw new Error(`Frontdoor exchange failed: ${response.status} ${errorBody}`);
|
|
222
|
+
}
|
|
223
|
+
const data = await response.json();
|
|
224
|
+
return data.frontdoor_uri ?? null;
|
|
225
|
+
}
|
|
226
|
+
async handleSalesforceApi(req, res) {
|
|
227
|
+
try {
|
|
228
|
+
if (!this.orgInfo) {
|
|
229
|
+
this.sendNoOrgError(res);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
233
|
+
let pathIndex = url.pathname.indexOf("/lwr/apex/v");
|
|
234
|
+
if (pathIndex === -1) {
|
|
235
|
+
pathIndex = url.pathname.indexOf("/services/data/v");
|
|
236
|
+
}
|
|
237
|
+
const apiPath = url.pathname.substring(pathIndex);
|
|
238
|
+
let targetUrl = `${this.orgInfo.instanceUrl}${apiPath}${url.search}`;
|
|
239
|
+
if (this.options?.debug) {
|
|
240
|
+
console.log(`[ui-bundle-proxy] Forwarding to Salesforce: ${targetUrl}`);
|
|
241
|
+
}
|
|
242
|
+
const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : void 0;
|
|
243
|
+
let response = await fetch(targetUrl, {
|
|
244
|
+
method: req.method,
|
|
245
|
+
headers: {
|
|
246
|
+
...getFilteredHeaders(req.headers),
|
|
247
|
+
Cookie: `sid=${this.orgInfo.accessToken}`,
|
|
248
|
+
Accept: req.headers.accept ?? "application/json",
|
|
249
|
+
// necessary for Apex requests, for which SessionUtil.validateSessionUsage won't accept OAuth token as `sid` cookie
|
|
250
|
+
Authorization: `Bearer ${this.orgInfo.accessToken}`
|
|
251
|
+
},
|
|
252
|
+
body
|
|
253
|
+
});
|
|
254
|
+
if (response.status === 401 || response.status === 403) {
|
|
255
|
+
console.warn(`[ui-bundle-proxy] Received ${response.status}, refreshing token...`);
|
|
256
|
+
const updatedOrgInfo = await this.refreshToken();
|
|
257
|
+
if (updatedOrgInfo === void 0) {
|
|
258
|
+
console.error("[ui-bundle-proxy] Failed to refresh token - authentication error");
|
|
259
|
+
this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (this.options?.debug) {
|
|
263
|
+
console.log("[ui-bundle-proxy] Token refreshed, retrying request");
|
|
264
|
+
}
|
|
265
|
+
targetUrl = `${updatedOrgInfo.instanceUrl}${url.pathname}${url.search}`;
|
|
266
|
+
response = await fetch(targetUrl, {
|
|
267
|
+
method: req.method,
|
|
268
|
+
headers: {
|
|
269
|
+
...getFilteredHeaders(req.headers),
|
|
270
|
+
Cookie: `sid=${updatedOrgInfo.accessToken}`,
|
|
271
|
+
Accept: req.headers.accept ?? "application/json"
|
|
272
|
+
},
|
|
273
|
+
body
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
await this.sendResponse(res, response);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error("[ui-bundle-proxy] Salesforce API request failed:", error);
|
|
279
|
+
this.sendAuthOrGatewayError(res, error, "Failed to forward request to Salesforce");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async refreshToken() {
|
|
283
|
+
if (!this.orgInfo) {
|
|
284
|
+
return void 0;
|
|
285
|
+
}
|
|
286
|
+
const refreshIdentifier = this.orgInfo.orgAlias || this.orgInfo.username;
|
|
287
|
+
if (!refreshIdentifier) {
|
|
288
|
+
throw new AuthenticationError("Cannot refresh token: no org alias or username available");
|
|
289
|
+
}
|
|
290
|
+
const updatedOrgInfo = await refreshOrgAuth(refreshIdentifier);
|
|
291
|
+
if (!updatedOrgInfo) {
|
|
292
|
+
return void 0;
|
|
293
|
+
}
|
|
294
|
+
this.orgInfo = updatedOrgInfo;
|
|
295
|
+
if (this.options?.onTokenRefresh) {
|
|
296
|
+
this.options.onTokenRefresh(updatedOrgInfo);
|
|
297
|
+
}
|
|
298
|
+
return updatedOrgInfo;
|
|
299
|
+
}
|
|
300
|
+
async handleGraphQL(req, res) {
|
|
301
|
+
try {
|
|
302
|
+
if (!this.orgInfo) {
|
|
303
|
+
this.sendNoOrgError(res);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const { rawInstanceUrl, apiVersion, accessToken } = this.orgInfo;
|
|
307
|
+
let targetUrl = `${rawInstanceUrl}/services/data/v${apiVersion}/graphql`;
|
|
308
|
+
if (this.options?.debug) {
|
|
309
|
+
console.log(`[ui-bundle-proxy] Forwarding GraphQL to Salesforce: ${targetUrl}`);
|
|
310
|
+
}
|
|
311
|
+
const body = await getBody(req);
|
|
312
|
+
const headers = {
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
Accept: "application/json",
|
|
315
|
+
Authorization: `Bearer ${accessToken}`,
|
|
316
|
+
"X-Chatter-Entity-Encoding": "false"
|
|
317
|
+
};
|
|
318
|
+
let response = await fetch(targetUrl, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers,
|
|
321
|
+
body
|
|
322
|
+
});
|
|
323
|
+
if (response.status === 401 || response.status === 403) {
|
|
324
|
+
console.warn(`[ui-bundle-proxy] Received ${response.status}, refreshing token...`);
|
|
325
|
+
const updatedOrgInfo = await this.refreshToken();
|
|
326
|
+
if (updatedOrgInfo !== void 0) {
|
|
327
|
+
if (this.options?.debug) {
|
|
328
|
+
console.log("[ui-bundle-proxy] Token refreshed, retrying request");
|
|
329
|
+
}
|
|
330
|
+
const { rawInstanceUrl: rawInstanceUrl2, apiVersion: apiVersion2, accessToken: accessToken2 } = updatedOrgInfo;
|
|
331
|
+
targetUrl = `${rawInstanceUrl2}/services/data/v${apiVersion2}/graphql`;
|
|
332
|
+
headers.Authorization = `Bearer ${accessToken2}`;
|
|
333
|
+
response = await fetch(targetUrl, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers,
|
|
336
|
+
body
|
|
337
|
+
});
|
|
338
|
+
} else {
|
|
339
|
+
console.error(
|
|
340
|
+
"[ui-bundle-proxy] Failed to refresh token for GraphQL - authentication error"
|
|
341
|
+
);
|
|
342
|
+
this.sendJson(res, 401, AUTH_FAILED_RESPONSE);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
await this.sendResponse(res, response);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error("[ui-bundle-proxy] GraphQL request failed:", error);
|
|
349
|
+
this.sendAuthOrGatewayError(res, error, "Failed to forward GraphQL request to Salesforce");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async sendResponse(res, response) {
|
|
353
|
+
const headers = {};
|
|
354
|
+
const skipHeaders = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding"]);
|
|
355
|
+
response.headers.forEach((value, key) => {
|
|
356
|
+
if (!skipHeaders.has(key.toLowerCase())) {
|
|
357
|
+
headers[key] = value;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
const contentType = response.headers.get("content-type") || "";
|
|
361
|
+
const isHtml = contentType.includes("text/html");
|
|
362
|
+
if (isHtml && response.body) {
|
|
363
|
+
const chunks = [];
|
|
364
|
+
const reader = response.body.getReader();
|
|
365
|
+
while (true) {
|
|
366
|
+
const { done, value } = await reader.read();
|
|
367
|
+
if (done) break;
|
|
368
|
+
chunks.push(value);
|
|
369
|
+
}
|
|
370
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
371
|
+
const buffer = new Uint8Array(totalLength);
|
|
372
|
+
let offset = 0;
|
|
373
|
+
for (const chunk of chunks) {
|
|
374
|
+
buffer.set(chunk, offset);
|
|
375
|
+
offset += chunk.length;
|
|
376
|
+
}
|
|
377
|
+
const html = new TextDecoder().decode(buffer);
|
|
378
|
+
const modifiedHtml = UIBundleProxyHandler.injectLivePreviewScript(html);
|
|
379
|
+
const modifiedBuffer = new TextEncoder().encode(modifiedHtml);
|
|
380
|
+
headers["content-length"] = modifiedBuffer.length.toString();
|
|
381
|
+
res.writeHead(response.status, headers);
|
|
382
|
+
res.write(modifiedBuffer);
|
|
383
|
+
} else {
|
|
384
|
+
res.writeHead(response.status, headers);
|
|
385
|
+
if (response.body) {
|
|
386
|
+
const reader = response.body.getReader();
|
|
387
|
+
while (true) {
|
|
388
|
+
const { done, value } = await reader.read();
|
|
389
|
+
if (done) break;
|
|
390
|
+
res.write(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
res.end();
|
|
395
|
+
}
|
|
396
|
+
static injectLivePreviewScript(html) {
|
|
397
|
+
if (html.includes(LIVE_PREVIEW_SCRIPT_MARKER)) {
|
|
398
|
+
return html;
|
|
399
|
+
}
|
|
400
|
+
const scriptContent = getLivePreviewScriptContent();
|
|
401
|
+
const scriptTag = `<script ${LIVE_PREVIEW_SCRIPT_MARKER}>` + scriptContent + "<\/script>";
|
|
402
|
+
if (html.includes("</body>")) {
|
|
403
|
+
const lastBodyIndex = html.lastIndexOf("</body>");
|
|
404
|
+
return html.substring(0, lastBodyIndex) + scriptTag + html.substring(lastBodyIndex);
|
|
405
|
+
}
|
|
406
|
+
if (html.includes("</html>")) {
|
|
407
|
+
const lastHtmlIndex = html.lastIndexOf("</html>");
|
|
408
|
+
return html.substring(0, lastHtmlIndex) + scriptTag + html.substring(lastHtmlIndex);
|
|
409
|
+
}
|
|
410
|
+
return html + scriptTag;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Proxy POST /chatter/handlers/file/body (XHR/file upload) to Salesforce.
|
|
414
|
+
* Uses rawInstanceUrl for Chatter API. Preserves multipart/form-data from XHR.
|
|
415
|
+
*/
|
|
416
|
+
async handleFileUpload(req, res) {
|
|
417
|
+
try {
|
|
418
|
+
if (!this.orgInfo) {
|
|
419
|
+
this.sendNoOrgError(res);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
423
|
+
const pathIndex = url.pathname.indexOf("/chatter/handlers/file/body");
|
|
424
|
+
const apiPath = url.pathname.substring(pathIndex);
|
|
425
|
+
const uploadUrl = `${this.orgInfo.rawInstanceUrl}${apiPath}${url.search}`;
|
|
426
|
+
if (this.options?.debug) {
|
|
427
|
+
console.log(`[ui-bundle-proxy] Forwarding file upload to Salesforce: ${uploadUrl}`);
|
|
428
|
+
}
|
|
429
|
+
const body = await getBody(req);
|
|
430
|
+
if (!body?.length) {
|
|
431
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
432
|
+
res.end(
|
|
433
|
+
JSON.stringify({
|
|
434
|
+
error: "BAD_REQUEST",
|
|
435
|
+
message: "Request body is empty"
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const contentType = req.headers["content-type"];
|
|
441
|
+
if (!contentType?.includes("multipart/form-data")) {
|
|
442
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
443
|
+
res.end(
|
|
444
|
+
JSON.stringify({
|
|
445
|
+
error: "BAD_REQUEST",
|
|
446
|
+
message: "Content-Type must be multipart/form-data"
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const headers = {
|
|
452
|
+
"Content-Type": contentType,
|
|
453
|
+
"Content-Length": String(body.length),
|
|
454
|
+
Cookie: `sid=${this.orgInfo.accessToken}`,
|
|
455
|
+
Authorization: `Bearer ${this.orgInfo.accessToken}`
|
|
456
|
+
};
|
|
457
|
+
const response = await fetch(uploadUrl, {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers,
|
|
460
|
+
body: new Uint8Array(body)
|
|
461
|
+
});
|
|
462
|
+
const resHeaders = {};
|
|
463
|
+
const skipHeaders = /* @__PURE__ */ new Set(["content-encoding", "transfer-encoding"]);
|
|
464
|
+
response.headers.forEach((value, key) => {
|
|
465
|
+
if (!skipHeaders.has(key.toLowerCase())) {
|
|
466
|
+
resHeaders[key] = value;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
res.writeHead(response.status, resHeaders);
|
|
470
|
+
if (response.body) {
|
|
471
|
+
const reader = response.body.getReader();
|
|
472
|
+
while (true) {
|
|
473
|
+
const { done, value } = await reader.read();
|
|
474
|
+
if (done) break;
|
|
475
|
+
res.write(value);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
res.end();
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.error("[ui-bundle-proxy] File upload proxy failed:", error);
|
|
481
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
482
|
+
res.end(
|
|
483
|
+
JSON.stringify({
|
|
484
|
+
error: "GATEWAY_ERROR",
|
|
485
|
+
message: "Failed to forward file upload"
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function createProxyHandler(manifest, orgInfo, target, basePath, options) {
|
|
492
|
+
const handler = new UIBundleProxyHandler(manifest, orgInfo, target, basePath, options);
|
|
493
|
+
return (req, res, next) => handler.handle(req, res, next);
|
|
494
|
+
}
|
|
495
|
+
function injectLivePreviewScript(html) {
|
|
496
|
+
return UIBundleProxyHandler.injectLivePreviewScript(html);
|
|
497
|
+
}
|
|
498
|
+
function getFilteredHeaders(headers) {
|
|
499
|
+
const filtered = {};
|
|
500
|
+
const hopByHopHeaders = /* @__PURE__ */ new Set([
|
|
501
|
+
"connection",
|
|
502
|
+
"keep-alive",
|
|
503
|
+
"proxy-authenticate",
|
|
504
|
+
"proxy-authorization",
|
|
505
|
+
"te",
|
|
506
|
+
"trailer",
|
|
507
|
+
"transfer-encoding",
|
|
508
|
+
"upgrade"
|
|
509
|
+
]);
|
|
510
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
511
|
+
if (!hopByHopHeaders.has(key.toLowerCase()) && value) {
|
|
512
|
+
filtered[key] = Array.isArray(value) ? value.join(", ") : typeof value === "string" ? value : String(value);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return filtered;
|
|
516
|
+
}
|
|
517
|
+
function getBody(req) {
|
|
518
|
+
return new Promise((resolve, reject) => {
|
|
519
|
+
const chunks = [];
|
|
520
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
521
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
522
|
+
req.on("error", reject);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
export {
|
|
526
|
+
UIBUNDLE_HEALTH_CHECK_PARAM,
|
|
527
|
+
UIBUNDLE_PROXY_HEADER,
|
|
528
|
+
createProxyHandler,
|
|
529
|
+
injectLivePreviewScript
|
|
530
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
export type { ProxyOptions, ProxyHandler } from './handler';
|
|
7
|
+
export { createProxyHandler, injectLivePreviewScript, UIBUNDLE_HEALTH_CHECK_PARAM, UIBUNDLE_PROXY_HEADER, } from './handler';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACN,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,qBAAqB,GACrB,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Returns the JavaScript source that gets injected into previewed UI Bundles.
|
|
8
|
+
* Reads from templates/livePreviewScript.js at runtime (copied to dist/ by postbuild).
|
|
9
|
+
* Cached after first read.
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
* - Fetch interceptor for network-error detection (runs synchronously on load)
|
|
13
|
+
* - Runtime / compile / HMR error listeners
|
|
14
|
+
* - Error deduplication
|
|
15
|
+
* - postMessage bridge to the VS Code webview (when running inside an iframe)
|
|
16
|
+
* - Copy / paste / right-click bridge for VS Code webview
|
|
17
|
+
*/
|
|
18
|
+
export declare function getLivePreviewScriptContent(): string;
|
|
19
|
+
/** Data attribute used to detect (and prevent) double injection. */
|
|
20
|
+
export declare const LIVE_PREVIEW_SCRIPT_MARKER = "data-live-preview";
|
|
21
|
+
//# sourceMappingURL=livePreviewScript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"livePreviewScript.d.ts","sourceRoot":"","sources":["../../src/proxy/livePreviewScript.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,CAKpD;AAED,oEAAoE;AACpE,eAAO,MAAM,0BAA0B,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
let cached = null;
|
|
6
|
+
function getLivePreviewScriptContent() {
|
|
7
|
+
if (cached) return cached;
|
|
8
|
+
const scriptPath = join(__dirname$1, "templates", "livePreviewScript.js");
|
|
9
|
+
cached = readFileSync(scriptPath, "utf-8");
|
|
10
|
+
return cached;
|
|
11
|
+
}
|
|
12
|
+
const LIVE_PREVIEW_SCRIPT_MARKER = "data-live-preview";
|
|
13
|
+
export {
|
|
14
|
+
LIVE_PREVIEW_SCRIPT_MARKER,
|
|
15
|
+
getLivePreviewScriptContent
|
|
16
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { RedirectRule, RewriteRule } from '../app/index';
|
|
2
|
+
export interface RouteMatch {
|
|
3
|
+
type: "rewrite" | "redirect" | "api" | "gql" | "file-upload";
|
|
4
|
+
target?: string;
|
|
5
|
+
statusCode?: number;
|
|
6
|
+
params?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Match URL path against routing rules
|
|
10
|
+
*
|
|
11
|
+
* @param pathname - The URL pathname to match
|
|
12
|
+
* @param basePath - Optional base path of Salesforce server
|
|
13
|
+
* @param rewrites - Optional array of rewrite rules
|
|
14
|
+
* @param redirects - Optional array of redirect rules
|
|
15
|
+
* @returns Route match result indicating the type and target, or null if no match
|
|
16
|
+
*/
|
|
17
|
+
export declare function matchRoute(pathname: string, basePath?: string, rewrites?: RewriteRule[], redirects?: RedirectRule[]): RouteMatch | null;
|
|
18
|
+
/**
|
|
19
|
+
* Check if a path matches any of the given glob patterns
|
|
20
|
+
* Supports glob wildcards: * (matches anything except /), ** (matches anything including /), ? (single character)
|
|
21
|
+
*
|
|
22
|
+
* @param path - The path to test
|
|
23
|
+
* @param patterns - Array of glob patterns to match against
|
|
24
|
+
* @returns True if the path matches any pattern, false otherwise
|
|
25
|
+
*/
|
|
26
|
+
export declare function matchesPattern(path: string, patterns: string[] | undefined): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Apply trailing slash rules to pathname
|
|
29
|
+
*
|
|
30
|
+
* @param pathname - The URL pathname
|
|
31
|
+
* @param trailingSlash - Trailing slash handling strategy
|
|
32
|
+
* @returns Modified pathname with trailing slash applied according to rules
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyTrailingSlash(pathname: string, trailingSlash?: "always" | "never" | "auto"): string;
|
|
35
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/proxy/routing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,aAAa,CAAC;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AASD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,WAAW,EAAE,EACxB,SAAS,CAAC,EAAE,YAAY,EAAE,GACxB,UAAU,GAAG,IAAI,CAqEnB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAMpF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GACzC,MAAM,CAiBR"}
|