@conduit-client/salesforce-lightning-service-worker 3.6.3 → 3.7.0-dev1
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/index.js +281 -114
- package/dist/index.js.map +1 -1
- package/dist/sw.js +227 -108
- package/dist/types/csrf/__tests__/header.interceptor.spec.d.ts +1 -0
- package/dist/types/csrf/__tests__/retry.interceptor.spec.d.ts +1 -0
- package/dist/types/csrf/__tests__/retry.policy.spec.d.ts +1 -0
- package/dist/types/csrf/__tests__/token-manager.spec.d.ts +1 -0
- package/dist/types/csrf/header.interceptor.d.ts +12 -0
- package/dist/types/csrf/retry.interceptor.d.ts +7 -0
- package/dist/types/csrf/retry.policy.d.ts +37 -0
- package/dist/types/csrf/token-manager.d.ts +34 -0
- package/dist/types/fetch.d.ts +11 -23
- package/dist/types/index.d.ts +12 -9
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ function isPromiseLike(x) {
|
|
|
50
50
|
* All rights reserved.
|
|
51
51
|
* For full license text, see the LICENSE.txt file
|
|
52
52
|
*/
|
|
53
|
-
function buildServiceDescriptor(interceptors = {
|
|
53
|
+
function buildServiceDescriptor$1(interceptors = {
|
|
54
54
|
request: [],
|
|
55
55
|
retry: void 0,
|
|
56
56
|
response: [],
|
|
@@ -76,6 +76,9 @@ function buildServiceDescriptor(interceptors = {
|
|
|
76
76
|
if (retryInterceptor) {
|
|
77
77
|
return retryInterceptor(args2, retryService, context);
|
|
78
78
|
} else {
|
|
79
|
+
if (retryService) {
|
|
80
|
+
return retryService.applyRetry(() => fetch(...args2));
|
|
81
|
+
}
|
|
79
82
|
return fetch(...args2);
|
|
80
83
|
}
|
|
81
84
|
}).then((response) => {
|
|
@@ -94,119 +97,234 @@ function buildServiceDescriptor(interceptors = {
|
|
|
94
97
|
}
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
function setHeader(headerName, headerValue, [resource, options = {}], {
|
|
101
|
+
throwOnExisting = false,
|
|
102
|
+
errorMessage = `Unexpected ${headerName} header encountered`
|
|
103
|
+
} = {}) {
|
|
104
|
+
let hasHeaderBeenSet = false;
|
|
105
|
+
if (resource instanceof Request && !(options == null ? void 0 : options.headers)) {
|
|
106
|
+
if (throwOnExisting && resource.headers.has(headerName)) {
|
|
107
|
+
throw new Error(errorMessage);
|
|
108
|
+
}
|
|
109
|
+
resource.headers.set(headerName, headerValue);
|
|
110
|
+
hasHeaderBeenSet = true;
|
|
111
|
+
}
|
|
112
|
+
if ((options == null ? void 0 : options.headers) instanceof Headers) {
|
|
113
|
+
if (throwOnExisting && options.headers.has(headerName)) {
|
|
114
|
+
throw new Error(errorMessage);
|
|
115
|
+
}
|
|
116
|
+
options.headers.set(headerName, headerValue);
|
|
105
117
|
} else {
|
|
106
|
-
|
|
118
|
+
if (throwOnExisting && (options == null ? void 0 : options.headers) && Reflect.has(options.headers, headerName)) {
|
|
119
|
+
throw new Error(errorMessage);
|
|
120
|
+
}
|
|
121
|
+
if (!hasHeaderBeenSet) {
|
|
122
|
+
options.headers = {
|
|
123
|
+
...options == null ? void 0 : options.headers,
|
|
124
|
+
[headerName]: headerValue
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [resource, options];
|
|
129
|
+
}
|
|
130
|
+
/*!
|
|
131
|
+
* Copyright (c) 2022, Salesforce, Inc.,
|
|
132
|
+
* All rights reserved.
|
|
133
|
+
* For full license text, see the LICENSE.txt file
|
|
134
|
+
*/
|
|
135
|
+
class RetryService {
|
|
136
|
+
constructor(defaultRetryPolicy) {
|
|
137
|
+
this.defaultRetryPolicy = defaultRetryPolicy;
|
|
107
138
|
}
|
|
139
|
+
applyRetry(operation, retryPolicyOverride) {
|
|
140
|
+
return this.retry(operation, retryPolicyOverride || this.defaultRetryPolicy);
|
|
141
|
+
}
|
|
142
|
+
async retry(operation, policy) {
|
|
143
|
+
const startTime = Date.now();
|
|
144
|
+
let attempt = 0;
|
|
145
|
+
let result = await operation();
|
|
146
|
+
let context = {
|
|
147
|
+
attempt,
|
|
148
|
+
totalElapsedMs: Date.now() - startTime,
|
|
149
|
+
lastResult: result
|
|
150
|
+
};
|
|
151
|
+
while (await policy.shouldRetry(result, context)) {
|
|
152
|
+
const delay = await policy.calculateDelay(result, context);
|
|
153
|
+
await this.delay(delay);
|
|
154
|
+
if (policy.prepareRetry) {
|
|
155
|
+
await policy.prepareRetry(result, context);
|
|
156
|
+
}
|
|
157
|
+
attempt++;
|
|
158
|
+
result = await operation();
|
|
159
|
+
context = {
|
|
160
|
+
attempt,
|
|
161
|
+
totalElapsedMs: Date.now() - startTime,
|
|
162
|
+
lastResult: result
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
delay(ms) {
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
setTimeout(resolve, ms);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class RetryPolicy {
|
|
174
|
+
}
|
|
175
|
+
function buildServiceDescriptor(defaultRetryPolicy) {
|
|
176
|
+
return {
|
|
177
|
+
version: "1.0",
|
|
178
|
+
service: new RetryService(defaultRetryPolicy),
|
|
179
|
+
type: "retry"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const HEADER = "X-CSRF-Token";
|
|
183
|
+
function buildInterceptor$1(csrfTokenManager, config = {}) {
|
|
184
|
+
const { protectedUrls = [] } = config;
|
|
185
|
+
return async (fetchArgs) => {
|
|
186
|
+
const [input, init] = fetchArgs;
|
|
187
|
+
const request = new Request(input, init);
|
|
188
|
+
if (isProtectedMethod(request.method) && isProtectedUrl(protectedUrls, request.url) && !request.headers.has(HEADER)) {
|
|
189
|
+
const token = await csrfTokenManager.getToken();
|
|
190
|
+
fetchArgs = setHeader(HEADER, token, fetchArgs);
|
|
191
|
+
}
|
|
192
|
+
return resolvedPromiseLike(fetchArgs);
|
|
193
|
+
};
|
|
108
194
|
}
|
|
109
195
|
function isProtectedMethod(method) {
|
|
110
196
|
const normalizedMethod = method.toLowerCase();
|
|
111
197
|
return normalizedMethod === "post" || normalizedMethod === "put" || normalizedMethod === "patch" || normalizedMethod === "delete";
|
|
112
198
|
}
|
|
113
|
-
function isProtectedUrl(urlString) {
|
|
199
|
+
function isProtectedUrl(protectedUrls, urlString) {
|
|
114
200
|
const url = new URL(urlString);
|
|
115
|
-
return url.pathname.includes(
|
|
201
|
+
return protectedUrls.some((protectedUrl) => url.pathname.includes(protectedUrl));
|
|
202
|
+
}
|
|
203
|
+
function buildInterceptor(csrfTokenManager, config = {}) {
|
|
204
|
+
const headerInterceptor = buildInterceptor$1(csrfTokenManager, config);
|
|
205
|
+
async function makeRequest(fetchArgs) {
|
|
206
|
+
const args = await headerInterceptor(fetchArgs);
|
|
207
|
+
return fetch(...args);
|
|
208
|
+
}
|
|
209
|
+
return (fetchArgs, retryService) => {
|
|
210
|
+
if (!retryService) {
|
|
211
|
+
return makeRequest(fetchArgs);
|
|
212
|
+
}
|
|
213
|
+
return retryService.applyRetry(async () => {
|
|
214
|
+
return makeRequest(fetchArgs);
|
|
215
|
+
});
|
|
216
|
+
};
|
|
116
217
|
}
|
|
117
|
-
|
|
218
|
+
class CsrfTokenRetryPolicy extends RetryPolicy {
|
|
219
|
+
constructor(csrfTokenManager) {
|
|
220
|
+
super(csrfTokenManager);
|
|
221
|
+
this.csrfTokenManager = csrfTokenManager;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Determines if a failed request should be retried due to CSRF token issues.
|
|
225
|
+
*/
|
|
226
|
+
async shouldRetry(result, context) {
|
|
227
|
+
if (context.attempt >= 1) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (result.status !== 400) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return await isCsrfError(result);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* CSRF token refresh should happen immediately with no delay.
|
|
237
|
+
*/
|
|
238
|
+
async calculateDelay(_result, _context) {
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Called by retry service before each retry attempt.
|
|
243
|
+
*
|
|
244
|
+
* @param _result - The failed response that triggered the retry (unused, already validated)
|
|
245
|
+
* @param _context - Current retry context (unused but part of interface)
|
|
246
|
+
*/
|
|
247
|
+
async prepareRetry(_result, _context) {
|
|
248
|
+
await this.csrfTokenManager.refreshToken();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function isCsrfError(response) {
|
|
118
252
|
var _a;
|
|
119
|
-
|
|
253
|
+
try {
|
|
120
254
|
const body = await response.clone().json();
|
|
121
255
|
return ((_a = body[0]) == null ? void 0 : _a.errorCode) === "INVALID_ACCESS_TOKEN";
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
122
258
|
}
|
|
123
|
-
return false;
|
|
124
259
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let tokenProvider = obtainToken;
|
|
131
|
-
if (csrfTokenSource) {
|
|
132
|
-
if (typeof csrfTokenSource === "string" || csrfTokenSource instanceof URL) {
|
|
133
|
-
tokenUrl = csrfTokenSource;
|
|
134
|
-
} else if (typeof csrfTokenSource === "function") {
|
|
135
|
-
tokenProvider = csrfTokenSource;
|
|
136
|
-
}
|
|
260
|
+
class CsrfTokenManager {
|
|
261
|
+
constructor(endpoint, cacheName) {
|
|
262
|
+
this.endpoint = endpoint;
|
|
263
|
+
this.cacheName = cacheName;
|
|
264
|
+
this.tokenPromise = this.obtainToken();
|
|
137
265
|
}
|
|
138
|
-
|
|
139
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Returns the current token value as a Promise.
|
|
268
|
+
* Lazy-loads the token on first call (from cache or by fetching).
|
|
269
|
+
*/
|
|
270
|
+
async getToken() {
|
|
271
|
+
return this.tokenPromise;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Obtains and returns a new token value as a promise.
|
|
275
|
+
* This will clear the cached token and fetch a fresh one.
|
|
276
|
+
*/
|
|
277
|
+
async refreshToken() {
|
|
278
|
+
await this.withCache((cache) => cache.delete(this.endpoint));
|
|
279
|
+
this.tokenPromise = this.obtainToken();
|
|
280
|
+
return this.tokenPromise;
|
|
140
281
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Obtains a CSRF token, using cache when available or fetching a new one.
|
|
284
|
+
*
|
|
285
|
+
* @returns Promise that resolves to the CSRF token string
|
|
286
|
+
*/
|
|
287
|
+
async obtainToken() {
|
|
288
|
+
let response = await this.withCache((cache) => cache.match(this.endpoint));
|
|
289
|
+
let cacheMiss = false;
|
|
145
290
|
if (!response) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
fireEvent("csrf_token_fetch_complete", id, { status: response.status });
|
|
149
|
-
} else {
|
|
150
|
-
fireEvent("csrf_token_cache_hit", id);
|
|
291
|
+
response = await fetch(this.endpoint, { method: "get" });
|
|
292
|
+
cacheMiss = true;
|
|
151
293
|
}
|
|
152
294
|
const csrfToken = (await response.clone().json()).csrfToken;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return csrfToken;
|
|
156
|
-
}
|
|
157
|
-
let tokenPromise = tokenProvider();
|
|
158
|
-
async function refreshToken() {
|
|
159
|
-
const id = generateId();
|
|
160
|
-
fireEvent("csrf_token_refresh_start", id);
|
|
161
|
-
await withCache((cache) => cache.delete(tokenUrl));
|
|
162
|
-
tokenPromise = tokenProvider();
|
|
163
|
-
fireEvent("csrf_token_refresh_complete", id);
|
|
164
|
-
}
|
|
165
|
-
async function fetchWithToken(request) {
|
|
166
|
-
const headers = new Headers(request.headers);
|
|
167
|
-
if (!headers.has(CSRF_HEADER)) {
|
|
168
|
-
headers.set(CSRF_HEADER, await tokenPromise);
|
|
295
|
+
if (cacheMiss) {
|
|
296
|
+
await this.withCache((cache) => cache.put(this.endpoint, response));
|
|
169
297
|
}
|
|
170
|
-
return
|
|
298
|
+
return csrfToken;
|
|
171
299
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
fireEvent("protected_request_complete", id, {
|
|
183
|
-
method: request.method,
|
|
184
|
-
url: request.url,
|
|
185
|
-
status: retryResponse.status,
|
|
186
|
-
retried: true
|
|
187
|
-
});
|
|
188
|
-
return retryResponse;
|
|
189
|
-
} else {
|
|
190
|
-
fireEvent("protected_request_complete", id, {
|
|
191
|
-
method: request.method,
|
|
192
|
-
url: request.url,
|
|
193
|
-
status: response.status,
|
|
194
|
-
retried: false
|
|
195
|
-
});
|
|
196
|
-
return response;
|
|
197
|
-
}
|
|
300
|
+
/**
|
|
301
|
+
* Provides a safe way to interact with the Cache API with fallback for unsupported environments.
|
|
302
|
+
*
|
|
303
|
+
* @param callback - Function that receives the cache instance and returns a promise
|
|
304
|
+
* @returns The result of the callback, or undefined if caches API is not available
|
|
305
|
+
*/
|
|
306
|
+
async withCache(callback) {
|
|
307
|
+
if (this.cacheName && caches) {
|
|
308
|
+
const cache = await caches.open(this.cacheName);
|
|
309
|
+
return callback(cache);
|
|
198
310
|
} else {
|
|
199
|
-
|
|
200
|
-
return fetchService(request);
|
|
311
|
+
return void 0;
|
|
201
312
|
}
|
|
202
|
-
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function createFetchService(config) {
|
|
316
|
+
const csrfConfig = config.csrf;
|
|
317
|
+
const csrfTokenManager = new CsrfTokenManager(csrfConfig.endpoint, csrfConfig.cacheName);
|
|
318
|
+
return buildServiceDescriptor$1(
|
|
319
|
+
{ retry: buildInterceptor(csrfTokenManager, csrfConfig) },
|
|
320
|
+
buildServiceDescriptor(new CsrfTokenRetryPolicy(csrfTokenManager)).service
|
|
321
|
+
).service;
|
|
203
322
|
}
|
|
204
|
-
let clientFetch
|
|
323
|
+
let clientFetch;
|
|
205
324
|
let serviceWorkerLoading = false;
|
|
206
325
|
let pendingRequests = [];
|
|
326
|
+
let instance;
|
|
207
327
|
class ConduitClient {
|
|
208
|
-
constructor() {
|
|
209
|
-
}
|
|
210
328
|
/**
|
|
211
329
|
* Makes an HTTP request
|
|
212
330
|
*
|
|
@@ -220,45 +338,94 @@ class ConduitClient {
|
|
|
220
338
|
pendingRequests.push({ input, init, resolve });
|
|
221
339
|
});
|
|
222
340
|
}
|
|
223
|
-
return clientFetch(input, init);
|
|
341
|
+
return Promise.resolve(clientFetch(input, init));
|
|
224
342
|
}
|
|
225
343
|
/**
|
|
226
|
-
*
|
|
344
|
+
* Configures global settings for all ConduitClient instances.
|
|
345
|
+
* This will recreate the internal fetch service with the new configuration.
|
|
227
346
|
*
|
|
228
|
-
* @
|
|
347
|
+
* @param config - Configuration options for CSRF handling and other features
|
|
229
348
|
*/
|
|
230
|
-
static
|
|
231
|
-
|
|
349
|
+
static initialize(config) {
|
|
350
|
+
if (instance) {
|
|
351
|
+
throw new Error("ConduitClient.initialize can only be called once");
|
|
352
|
+
}
|
|
353
|
+
instance = new ConduitClient();
|
|
354
|
+
if (config.serviceWorkerUrl) {
|
|
355
|
+
return registerServiceWorker(config);
|
|
356
|
+
} else {
|
|
357
|
+
clientFetch = createFetchService(config);
|
|
358
|
+
return Promise.resolve();
|
|
359
|
+
}
|
|
232
360
|
}
|
|
233
361
|
/**
|
|
234
|
-
*
|
|
235
|
-
* When successfully registered, the client will switch to using native fetch
|
|
236
|
-
* as the service worker will handle CSRF protection.
|
|
362
|
+
* Factory method to create a new ConduitClient instance
|
|
237
363
|
*
|
|
238
|
-
* @
|
|
364
|
+
* @returns A new ConduitClient instance
|
|
239
365
|
*/
|
|
240
|
-
static
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
366
|
+
static instance() {
|
|
367
|
+
if (!instance) {
|
|
368
|
+
throw new Error("ConduitClient.initialize must be called first");
|
|
369
|
+
}
|
|
370
|
+
return instance;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function registerServiceWorker(config) {
|
|
374
|
+
if ("serviceWorker" in navigator) {
|
|
375
|
+
try {
|
|
376
|
+
serviceWorkerLoading = true;
|
|
377
|
+
const registration = await navigator.serviceWorker.register(config.serviceWorkerUrl, {
|
|
378
|
+
type: "classic"
|
|
379
|
+
});
|
|
380
|
+
clientFetch = fetch;
|
|
381
|
+
console.log("[Conduit Client] Service registration succeeded:", registration);
|
|
382
|
+
if (registration.active) {
|
|
383
|
+
await postConfigToServiceWorker(registration.active, config);
|
|
384
|
+
} else if (registration.waiting || registration.installing) {
|
|
385
|
+
const serviceWorker = registration.waiting || registration.installing;
|
|
386
|
+
await waitForServiceWorkerActive(serviceWorker);
|
|
387
|
+
if (registration.active) {
|
|
388
|
+
await postConfigToServiceWorker(registration.active, config);
|
|
389
|
+
}
|
|
256
390
|
}
|
|
257
|
-
}
|
|
258
|
-
console.log(
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.log(
|
|
393
|
+
"[Conduit Client] Service Worker registration failed (using decorated `fetch`):",
|
|
394
|
+
error
|
|
395
|
+
);
|
|
396
|
+
} finally {
|
|
397
|
+
processQueuedRequests();
|
|
259
398
|
}
|
|
399
|
+
} else {
|
|
400
|
+
console.log("[Conduit Client] Service Worker not supported (using decorated `fetch`):");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function postConfigToServiceWorker(serviceWorker, config) {
|
|
404
|
+
try {
|
|
405
|
+
const { serviceWorkerUrl, ...configData } = config;
|
|
406
|
+
serviceWorker.postMessage({
|
|
407
|
+
type: "fetch-config",
|
|
408
|
+
config: configData
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.warn("[Conduit Client] Failed to post config to service worker:", error);
|
|
260
412
|
}
|
|
261
413
|
}
|
|
414
|
+
function waitForServiceWorkerActive(serviceWorker) {
|
|
415
|
+
return new Promise((resolve) => {
|
|
416
|
+
if (serviceWorker.state === "activated") {
|
|
417
|
+
resolve();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
function handleStateChange() {
|
|
421
|
+
if (serviceWorker.state === "activated") {
|
|
422
|
+
serviceWorker.removeEventListener("statechange", handleStateChange);
|
|
423
|
+
resolve();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
serviceWorker.addEventListener("statechange", handleStateChange);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
262
429
|
function processQueuedRequests() {
|
|
263
430
|
serviceWorkerLoading = false;
|
|
264
431
|
pendingRequests.forEach(({ input, init, resolve }) => {
|