@conduit-client/salesforce-lightning-service-worker 3.4.0 → 3.6.1
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/README.md +14 -55
- package/dist/index.js +128 -37
- package/dist/index.js.map +1 -1
- package/dist/sw.js +149 -0
- package/dist/types/__tests__/sw.spec.d.ts +1 -0
- package/dist/types/fetch.d.ts +3 -1
- package/dist/types/index.d.ts +2 -11
- package/dist/types/sw.d.ts +1 -0
- package/package.json +15 -7
package/README.md
CHANGED
|
@@ -10,11 +10,12 @@ npm install @conduit-client/salesforce-lightning-service-worker
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
This package provides two
|
|
13
|
+
This package provides the `ConduitClient` which offers two mechanisms for CSRF protection:
|
|
14
14
|
|
|
15
|
-
### 1.
|
|
15
|
+
### 1. Default Behavior
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
By default, the `ConduitClient` API wrap its underlying calls to native `fetch` with automatic CSRF protection in the
|
|
18
|
+
form of including the CSRF header, when required as well as token refreshes with a retry:
|
|
18
19
|
|
|
19
20
|
```typescript
|
|
20
21
|
import { ConduitClient } from '@conduit-client/salesforce-lightning-service-worker';
|
|
@@ -29,37 +30,25 @@ const response = await client.fetch('/services/data/v65.0/sobjects/Account', {
|
|
|
29
30
|
});
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
### 2.
|
|
33
|
+
### 2. Service Worker (Advanced)
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
The only difference compared to the default behavior is that all CSRF processing is offloaded to a service worker,
|
|
36
|
+
enabling ConduitClient to make unmodified calls to `fetch`.
|
|
35
37
|
|
|
36
|
-
####
|
|
38
|
+
#### Modern Bundlers (Vite, Webpack 5+)
|
|
37
39
|
|
|
38
40
|
```typescript
|
|
39
41
|
import { ConduitClient } from '@conduit-client/salesforce-lightning-service-worker';
|
|
40
42
|
|
|
41
|
-
// Register service worker
|
|
42
|
-
await ConduitClient.registerServiceWorker(
|
|
43
|
+
// Register the built-in service worker
|
|
44
|
+
await ConduitClient.registerServiceWorker(`/sw.js`);
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
**Important Notes:**
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Define service worker behavior
|
|
52
|
-
ConduitClient.defineServiceWorker({ debug: true });
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**NOTE: ** Note, if service worker registration fails the wrapper approach will remain in
|
|
56
|
-
place in order to maintain CSRF protection.
|
|
57
|
-
|
|
58
|
-
**Important Configuration Notes:**
|
|
59
|
-
|
|
60
|
-
1. **Static File Name**: The service worker file must have a static name (e.g., `sw.js`) without hash tokens
|
|
61
|
-
2. **Module Type**: Use ES6 modules for modern bundlers
|
|
62
|
-
3. **Scope**: Service worker scope determines which requests it can intercept
|
|
49
|
+
1. **No Separate Files**: The built-in classic service worker eliminates the need to create and maintain separate service worker files
|
|
50
|
+
2. **Classic Registration**: Uses `type: 'classic'` for better browser compatibility and to avoid ES module security constraints
|
|
51
|
+
3. **Fallback Protection**: If service worker registration fails, the default behavior remains in place
|
|
63
52
|
|
|
64
53
|
## CSRF Protection Features
|
|
65
54
|
|
|
@@ -83,36 +72,6 @@ This package provides automatic CSRF protection with the following features:
|
|
|
83
72
|
- **Activate Handler**: Claims all clients immediately
|
|
84
73
|
- **Fetch Interception**: Intercepts and enhances requests with CSRF tokens
|
|
85
74
|
|
|
86
|
-
## Complete Example
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
// Main application code
|
|
90
|
-
import { ConduitClient } from '@conduit-client/salesforce-lightning-service-worker';
|
|
91
|
-
|
|
92
|
-
async function setupApiClient() {
|
|
93
|
-
// Optionally register service worker for enhanced protection
|
|
94
|
-
await ConduitClient.registerServiceWorker('./sw.js');
|
|
95
|
-
|
|
96
|
-
// Create client for API calls
|
|
97
|
-
const client = ConduitClient.create();
|
|
98
|
-
|
|
99
|
-
// Make API calls - CSRF protection is automatic
|
|
100
|
-
const account = await client.fetch('/services/data/v65.0/sobjects/Account', {
|
|
101
|
-
method: 'POST',
|
|
102
|
-
body: JSON.stringify({ Name: 'New Account' }),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return client;
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// sw.js - Service worker file if `registerServiceWorker` is used
|
|
111
|
-
import { ConduitClient } from '@conduit-client/salesforce-lightning-service-worker';
|
|
112
|
-
|
|
113
|
-
ConduitClient.defineServiceWorker({ debug: false });
|
|
114
|
-
```
|
|
115
|
-
|
|
116
75
|
## Development
|
|
117
76
|
|
|
118
77
|
### Running the Development Server
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,101 @@
|
|
|
3
3
|
* All rights reserved.
|
|
4
4
|
* For full license text, see the LICENSE.txt file
|
|
5
5
|
*/
|
|
6
|
+
/*!
|
|
7
|
+
* Copyright (c) 2022, Salesforce, Inc.,
|
|
8
|
+
* All rights reserved.
|
|
9
|
+
* For full license text, see the LICENSE.txt file
|
|
10
|
+
*/
|
|
11
|
+
function resolvedPromiseLike(result) {
|
|
12
|
+
if (isPromiseLike(result)) {
|
|
13
|
+
return result.then((nextResult) => nextResult);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
then: (onFulfilled, _onRejected) => {
|
|
17
|
+
try {
|
|
18
|
+
return resolvedPromiseLike(onFulfilled(result));
|
|
19
|
+
} catch (e) {
|
|
20
|
+
if (onFulfilled === void 0) {
|
|
21
|
+
return resolvedPromiseLike(result);
|
|
22
|
+
}
|
|
23
|
+
return rejectedPromiseLike(e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function rejectedPromiseLike(reason) {
|
|
29
|
+
if (isPromiseLike(reason)) {
|
|
30
|
+
return reason.then((nextResult) => nextResult);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
then: (_onFulfilled, onRejected) => {
|
|
34
|
+
if (typeof onRejected === "function") {
|
|
35
|
+
try {
|
|
36
|
+
return resolvedPromiseLike(onRejected(reason));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return rejectedPromiseLike(e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return rejectedPromiseLike(reason);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isPromiseLike(x) {
|
|
46
|
+
return typeof (x == null ? void 0 : x.then) === "function";
|
|
47
|
+
}
|
|
48
|
+
/*!
|
|
49
|
+
* Copyright (c) 2022, Salesforce, Inc.,
|
|
50
|
+
* All rights reserved.
|
|
51
|
+
* For full license text, see the LICENSE.txt file
|
|
52
|
+
*/
|
|
53
|
+
function buildServiceDescriptor(interceptors = {
|
|
54
|
+
request: [],
|
|
55
|
+
retry: void 0,
|
|
56
|
+
response: [],
|
|
57
|
+
finally: []
|
|
58
|
+
}, retryService) {
|
|
59
|
+
return {
|
|
60
|
+
type: "fetch",
|
|
61
|
+
version: "1.0",
|
|
62
|
+
service: function(...args) {
|
|
63
|
+
var _a;
|
|
64
|
+
const context = (_a = interceptors.createContext) == null ? void 0 : _a.call(interceptors);
|
|
65
|
+
const {
|
|
66
|
+
request: requestInterceptors = [],
|
|
67
|
+
retry: retryInterceptor = void 0,
|
|
68
|
+
response: responseInterceptors = [],
|
|
69
|
+
finally: finallyInterceptors = []
|
|
70
|
+
} = interceptors;
|
|
71
|
+
const pending = requestInterceptors.reduce(
|
|
72
|
+
(previousPromise, interceptor) => previousPromise.then((args2) => interceptor(args2, context)),
|
|
73
|
+
resolvedPromiseLike(args)
|
|
74
|
+
);
|
|
75
|
+
return Promise.resolve(pending).then((args2) => {
|
|
76
|
+
if (retryInterceptor) {
|
|
77
|
+
return retryInterceptor(args2, retryService, context);
|
|
78
|
+
} else {
|
|
79
|
+
return fetch(...args2);
|
|
80
|
+
}
|
|
81
|
+
}).then((response) => {
|
|
82
|
+
return responseInterceptors.reduce(
|
|
83
|
+
(previousPromise, interceptor) => previousPromise.then((response2) => interceptor(response2, context)),
|
|
84
|
+
resolvedPromiseLike(response)
|
|
85
|
+
);
|
|
86
|
+
}).finally(() => {
|
|
87
|
+
if (finallyInterceptors.length > 0) {
|
|
88
|
+
return finallyInterceptors.reduce(
|
|
89
|
+
(previousPromise, interceptor) => previousPromise.then(() => interceptor(context)),
|
|
90
|
+
Promise.resolve()
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
6
97
|
const CACHE_VERSION = 1;
|
|
7
98
|
const CACHE_NAME = `salesforce-lightning-service-worker-${CACHE_VERSION}`;
|
|
8
99
|
const CSRF_HEADER = "X-CSRF-Token";
|
|
100
|
+
const API_PATH_PREFIX = "/services/data/v";
|
|
9
101
|
async function withCache(callback) {
|
|
10
102
|
if (caches) {
|
|
11
103
|
const cache = await caches.open(CACHE_NAME);
|
|
@@ -20,7 +112,7 @@ function isProtectedMethod(method) {
|
|
|
20
112
|
}
|
|
21
113
|
function isProtectedUrl(urlString) {
|
|
22
114
|
const url = new URL(urlString);
|
|
23
|
-
return url.pathname.includes(
|
|
115
|
+
return url.pathname.includes(API_PATH_PREFIX);
|
|
24
116
|
}
|
|
25
117
|
async function isTokenInvalid(response) {
|
|
26
118
|
var _a;
|
|
@@ -32,14 +124,15 @@ async function isTokenInvalid(response) {
|
|
|
32
124
|
}
|
|
33
125
|
function createLightningFetch(config = {}) {
|
|
34
126
|
const { fireEvent = () => {
|
|
35
|
-
},
|
|
36
|
-
|
|
127
|
+
}, csrfTokenSource, interceptors } = config;
|
|
128
|
+
const { service: fetchService } = buildServiceDescriptor(interceptors);
|
|
129
|
+
let tokenUrl = `${API_PATH_PREFIX}65.0/ui-api/session/csrf`;
|
|
37
130
|
let tokenProvider = obtainToken;
|
|
38
|
-
if (
|
|
39
|
-
if (typeof
|
|
40
|
-
tokenUrl =
|
|
41
|
-
} else if (typeof
|
|
42
|
-
tokenProvider =
|
|
131
|
+
if (csrfTokenSource) {
|
|
132
|
+
if (typeof csrfTokenSource === "string" || csrfTokenSource instanceof URL) {
|
|
133
|
+
tokenUrl = csrfTokenSource;
|
|
134
|
+
} else if (typeof csrfTokenSource === "function") {
|
|
135
|
+
tokenProvider = csrfTokenSource;
|
|
43
136
|
}
|
|
44
137
|
}
|
|
45
138
|
function generateId() {
|
|
@@ -51,7 +144,7 @@ function createLightningFetch(config = {}) {
|
|
|
51
144
|
let response = await withCache((cache) => cache.match(tokenUrl));
|
|
52
145
|
if (!response) {
|
|
53
146
|
fireEvent("csrf_token_fetch_start", id);
|
|
54
|
-
response = await
|
|
147
|
+
response = await fetchService(tokenUrl, { method: "get" });
|
|
55
148
|
fireEvent("csrf_token_fetch_complete", id, { status: response.status });
|
|
56
149
|
} else {
|
|
57
150
|
fireEvent("csrf_token_cache_hit", id);
|
|
@@ -74,9 +167,9 @@ function createLightningFetch(config = {}) {
|
|
|
74
167
|
if (!headers.has(CSRF_HEADER)) {
|
|
75
168
|
headers.set(CSRF_HEADER, await tokenPromise);
|
|
76
169
|
}
|
|
77
|
-
return
|
|
170
|
+
return fetchService(request, { headers });
|
|
78
171
|
}
|
|
79
|
-
return async function
|
|
172
|
+
return async function lightningFetch(input, init) {
|
|
80
173
|
const id = generateId();
|
|
81
174
|
const request = new Request(input, init);
|
|
82
175
|
if (isProtectedMethod(request.method) && isProtectedUrl(request.url)) {
|
|
@@ -104,12 +197,13 @@ function createLightningFetch(config = {}) {
|
|
|
104
197
|
}
|
|
105
198
|
} else {
|
|
106
199
|
fireEvent("unprotected_request", id, { method: request.method, url: request.url });
|
|
107
|
-
return
|
|
200
|
+
return fetchService(request);
|
|
108
201
|
}
|
|
109
202
|
};
|
|
110
203
|
}
|
|
111
|
-
|
|
112
|
-
let
|
|
204
|
+
let clientFetch = createLightningFetch();
|
|
205
|
+
let serviceWorkerLoading = false;
|
|
206
|
+
let pendingRequests = [];
|
|
113
207
|
class ConduitClient {
|
|
114
208
|
constructor() {
|
|
115
209
|
}
|
|
@@ -121,6 +215,11 @@ class ConduitClient {
|
|
|
121
215
|
* @returns Promise that resolves to the Response object
|
|
122
216
|
*/
|
|
123
217
|
fetch(input, init = {}) {
|
|
218
|
+
if (serviceWorkerLoading) {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
pendingRequests.push({ input, init, resolve });
|
|
221
|
+
});
|
|
222
|
+
}
|
|
124
223
|
return clientFetch(input, init);
|
|
125
224
|
}
|
|
126
225
|
/**
|
|
@@ -136,44 +235,36 @@ class ConduitClient {
|
|
|
136
235
|
* When successfully registered, the client will switch to using native fetch
|
|
137
236
|
* as the service worker will handle CSRF protection.
|
|
138
237
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @param scriptURL - URL or path to the service worker script
|
|
238
|
+
* @param scriptURL - path to the service worker script
|
|
142
239
|
*/
|
|
143
240
|
static async registerServiceWorker(scriptURL) {
|
|
144
241
|
if ("serviceWorker" in navigator) {
|
|
145
242
|
try {
|
|
243
|
+
serviceWorkerLoading = true;
|
|
146
244
|
const registration = await navigator.serviceWorker.register(scriptURL, {
|
|
147
|
-
type: "
|
|
245
|
+
type: "classic"
|
|
148
246
|
});
|
|
149
247
|
clientFetch = fetch;
|
|
150
248
|
console.log("[Conduit Client] Service registration succeeded:", registration);
|
|
151
249
|
} catch (error) {
|
|
152
|
-
console.
|
|
250
|
+
console.log(
|
|
153
251
|
"[Conduit Client] Service Worker registration failed (using decorated `fetch`):",
|
|
154
252
|
error
|
|
155
253
|
);
|
|
254
|
+
} finally {
|
|
255
|
+
processQueuedRequests();
|
|
156
256
|
}
|
|
257
|
+
} else {
|
|
258
|
+
console.log("[Conduit Client] Service Worker not supported (using decorated `fetch`):");
|
|
157
259
|
}
|
|
158
260
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const scope = self;
|
|
167
|
-
scope.addEventListener("install", (event) => {
|
|
168
|
-
event.waitUntil(scope.skipWaiting());
|
|
169
|
-
});
|
|
170
|
-
scope.addEventListener("activate", (event) => {
|
|
171
|
-
event.waitUntil(scope.clients.claim());
|
|
172
|
-
});
|
|
173
|
-
scope.addEventListener("fetch", (event) => {
|
|
174
|
-
event.respondWith(lightningFetch(event.request));
|
|
175
|
-
});
|
|
176
|
-
}
|
|
261
|
+
}
|
|
262
|
+
function processQueuedRequests() {
|
|
263
|
+
serviceWorkerLoading = false;
|
|
264
|
+
pendingRequests.forEach(({ input, init, resolve }) => {
|
|
265
|
+
resolve(clientFetch(input, init));
|
|
266
|
+
});
|
|
267
|
+
pendingRequests = [];
|
|
177
268
|
}
|
|
178
269
|
export {
|
|
179
270
|
ConduitClient
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/fetch.ts","../src/index.ts"],"sourcesContent":["// current version of the cache for token storage\nconst CACHE_VERSION = 1;\n// name of the cache used to store CSRF tokens\nconst CACHE_NAME = `salesforce-lightning-service-worker-${CACHE_VERSION}`;\n// header name\nconst CSRF_HEADER = 'X-CSRF-Token';\n\n/**\n * Provides a safe way to interact with the Cache API with fallback for unsupported environments.\n *\n * @param callback - Function that receives the cache instance and returns a promise\n * @returns The result of the callback, or undefined if caches API is not available\n */\nasync function withCache<T>(callback: (cache: Cache) => Promise<T>): Promise<T | undefined> {\n // Defend against the cache API not being available (e.g., in some test environments)\n if (caches) {\n const cache = await caches.open(CACHE_NAME);\n return callback(cache);\n } else {\n return undefined;\n }\n}\n\n/**\n * Determines if an HTTP method is one that mutates data and requires CSRF protection.\n *\n * @param method - The HTTP method to check\n * @returns true if the method requires CSRF protection (POST, PUT, PATCH, DELETE)\n */\nfunction isProtectedMethod(method: string) {\n const normalizedMethod = method.toLowerCase();\n return (\n normalizedMethod === 'post' ||\n normalizedMethod === 'put' ||\n normalizedMethod === 'patch' ||\n normalizedMethod === 'delete'\n );\n}\n\n/**\n * Determines if the URL is for a path that requires CSRF protection.\n * Currently protects all Salesforce API endpoints under '/services'.\n *\n * @param urlString - The full URL to check\n * @returns true if the URL requires CSRF protection\n * @note This could be made configurable in the future to support custom protected paths\n */\nfunction isProtectedUrl(urlString: string) {\n const url = new URL(urlString);\n // Agentforce Vibes IDE has the form `absproxy/PORT/services/data/...`\n return url.pathname.includes('/services/data/v');\n}\n\n/**\n * Checks if a response indicates that the CSRF token is invalid.\n * Salesforce returns a 400 status with a specific error code when tokens are invalid.\n *\n * @param response - The HTTP response to check\n * @returns true if the response indicates an invalid CSRF token\n */\nasync function isTokenInvalid(response: Response) {\n if (response.status === 400) {\n // clone response to read body without consuming the original stream\n const body = await response.clone().json();\n\n // check for Salesforce's specific invalid token error code\n return body[0]?.errorCode === 'INVALID_ACCESS_TOKEN';\n }\n\n return false;\n}\n\n/**\n * Configuration options for the Lightning fetch creation.\n */\nexport interface LightningFetchConfig {\n /**\n * Optional source for CSRF tokens. Can be:\n * - string: URL path to token endpoint (e.g., '/custom/csrf-endpoint')\n * - URL: Full URL object for token endpoint\n * - function: Custom async function that returns a token string\n *\n * As a string or URL, default fetching and caching (if Cache API is\n * available) will be used to obtain tokens\n */\n tokenSource?: string | URL | (() => Promise<string>);\n\n /**\n * Optional callback for firing events related to fetch operations.\n * Can be used for instrumentation, logging, and monitoring.\n */\n fireEvent?: (eventName: string, id: string, data?: unknown) => void;\n}\n\n/**\n * Creates an enhanced fetch function with automatic CSRF token handling.\n * The returned function automatically adds CSRF tokens to protected requests\n * and handles token refresh when tokens become invalid.\n *\n * @param config - Optional configuration object\n * @returns An enhanced fetch function that handles CSRF protection\n */\nexport function createLightningFetch(config: LightningFetchConfig = {}): typeof fetch {\n const { fireEvent = () => {}, tokenSource } = config;\n\n // default url and provider\n let tokenUrl: string | URL = '/services/data/v65.0/ui-api/session/csrf';\n let tokenProvider = obtainToken;\n\n if (tokenSource) {\n if (typeof tokenSource === 'string' || tokenSource instanceof URL) {\n // use supplied URL with built-in provider\n tokenUrl = tokenSource;\n } else if (typeof tokenSource === 'function') {\n // use external provider\n tokenProvider = tokenSource;\n }\n }\n\n /**\n * Creates a unique identifier to correlate a series of related events.\n */\n function generateId() {\n return Date.now().toString(36);\n }\n\n /**\n * Obtains a CSRF token, using cache when available or fetching a new one.\n *\n * @returns Promise that resolves to the CSRF token string\n */\n async function obtainToken(): Promise<string> {\n const id = generateId();\n fireEvent('csrf_token_obtain_start', id);\n\n // try to get cached token response first\n let response = await withCache((cache) => cache.match(tokenUrl));\n\n if (!response) {\n // no cached response available, fetch a new token\n fireEvent('csrf_token_fetch_start', id);\n response = await fetch(tokenUrl, { method: 'get' });\n fireEvent('csrf_token_fetch_complete', id, { status: response.status });\n } else {\n fireEvent('csrf_token_cache_hit', id);\n }\n\n // extract token from response (clone to avoid consuming original stream)\n const csrfToken: string = (await response.clone().json()).csrfToken;\n\n // cache the response for future use\n await withCache((cache) => cache.put(tokenUrl, response));\n\n fireEvent('csrf_token_obtain_complete', id);\n return csrfToken;\n }\n\n let tokenPromise = tokenProvider();\n\n /**\n * Clears any cached token and initiates retrieval of a fresh one.\n * Used when the current token becomes invalid.\n */\n async function refreshToken() {\n const id = generateId();\n fireEvent('csrf_token_refresh_start', id);\n\n // remove the invalid token from cache\n await withCache((cache) => cache.delete(tokenUrl));\n\n // start obtaining a new token\n tokenPromise = tokenProvider();\n\n fireEvent('csrf_token_refresh_complete', id);\n }\n\n /**\n * Makes a request with the CSRF token header added.\n *\n * @param request - The original request to enhance with CSRF token\n * @returns Promise that resolves to the response\n */\n async function fetchWithToken(request: Request) {\n // clone original headers\n const headers = new Headers(request.headers);\n\n // either use provided token or add one that's been loaded\n if (!headers.has(CSRF_HEADER)) {\n headers.set(CSRF_HEADER, await tokenPromise!);\n }\n\n // execute request with CSRF token header\n return fetch(request, { headers });\n }\n\n /**\n * Enhanced fetch function that applies CSRF token protection to qualifying requests.\n * Automatically adds CSRF tokens to data-mutating requests to protected URLs,\n * with automatic token refresh when tokens become invalid.\n *\n * @param input - The request input (URL, Request, etc.)\n * @param init - Optional request initialization options\n * @returns Promise that resolves to the response\n */\n return async function lightningFetch(\n input: RequestInfo | URL,\n init?: RequestInit\n ): Promise<Response> {\n const id = generateId();\n const request = new Request(input, init);\n\n // check if this request requires CSRF protection (mutating method + protected URL)\n if (isProtectedMethod(request.method) && isProtectedUrl(request.url)) {\n fireEvent('protected_request_start', id, { method: request.method, url: request.url });\n\n // make request with CSRF token (clone to allow retry with fresh request)\n const response = await fetchWithToken(request.clone());\n\n // check if the token was rejected\n if (await isTokenInvalid(response)) {\n fireEvent('csrf_token_invalid', id, { status: response.status });\n\n // token is invalid, refresh and retry once\n await refreshToken();\n const retryResponse = await fetchWithToken(request.clone());\n\n fireEvent('protected_request_complete', id, {\n method: request.method,\n url: request.url,\n status: retryResponse.status,\n retried: true,\n });\n\n return retryResponse;\n } else {\n fireEvent('protected_request_complete', id, {\n method: request.method,\n url: request.url,\n status: response.status,\n retried: false,\n });\n\n // token was valid, return the response\n return response;\n }\n } else {\n fireEvent('unprotected_request', id, { method: request.method, url: request.url });\n\n // no CSRF protection required, use standard fetch\n return fetch(request);\n }\n };\n}\n","import { createLightningFetch } from './fetch';\n\n/**\n * Type alias for the native fetch function\n */\ntype Fetch = typeof fetch;\n\nconst lightningFetch = createLightningFetch();\n\n/**\n * The fetch function used by the client. Defaults to enhanced fetch for CSRF protection.\n * Will be switched to native fetch when CSRF-based service worker is successfully registered.\n */\nlet clientFetch: Fetch = lightningFetch;\n\n/**\n * A client for making HTTP requests with CSRF protection. By default, protection is provided by\n * wrapping the native `fetch` API with functionality that will apply a CSRF token to appropriate\n * requests. This includes functionality to detect expired tokens, triggering a token refresh and\n * retry of the request.\n *\n * Optionally, CSRF protection can be offloaded to a service worker by making the appropriate calls\n * to `registerServiceWorker` and `defineServiceWorker`\n */\nexport class ConduitClient {\n private constructor() {}\n\n /**\n * Makes an HTTP request\n *\n * @param input - The URL, Request object, or relative path to request\n * @param init - Optional request configuration that will be merged with defaults\n * @returns Promise that resolves to the Response object\n */\n fetch(input: string | URL | Request, init: RequestInit = {}): Promise<Response> {\n return clientFetch(input, init);\n }\n\n /**\n * Factory method to create a new ConduitClient instance\n *\n * @returns A new ConduitClient instance\n */\n static create() {\n return new ConduitClient();\n }\n\n /**\n * Registers a service worker for enhanced CSRF protection and caching.\n * When successfully registered, the client will switch to using native fetch\n * as the service worker will handle CSRF protection.\n *\n * The script URL must identify a source file that calls `defineServiceWorker`.\n *\n * @param scriptURL - URL or path to the service worker script\n */\n static async registerServiceWorker(scriptURL: string | URL) {\n // check if service workers are supported in this environment\n if ('serviceWorker' in navigator) {\n try {\n const registration = await navigator.serviceWorker.register(scriptURL, {\n type: 'module',\n });\n\n // successful registration, so switch to native fetch since service worker handles CSRF\n clientFetch = fetch;\n\n console.log('[Conduit Client] Service registration succeeded:', registration);\n } catch (error) {\n console.error(\n '[Conduit Client] Service Worker registration failed (using decorated `fetch`):',\n error\n );\n }\n }\n }\n\n /**\n * Defines the service worker behavior for CSRF protection.\n *\n * This method must be called within a service worker script whose URL is supplied to\n * `registerServiceWorker`\n */\n static defineServiceWorker() {\n const scope = self as any as ServiceWorkerGlobalScope;\n\n // handle service worker installation\n scope.addEventListener('install', (event) => {\n // skip waiting phase to activate immediately\n event.waitUntil(scope.skipWaiting());\n });\n\n // handle service worker activation\n scope.addEventListener('activate', (event) => {\n // take control of all clients immediately\n event.waitUntil(scope.clients.claim());\n });\n\n // intercept all fetch requests and apply CSRF protection\n scope.addEventListener('fetch', (event) => {\n // use enhanced fetch to automatically handle CSRF tokens for protected requests\n event.respondWith(lightningFetch(event.request));\n });\n }\n}\n"],"names":["lightningFetch"],"mappings":";;;;;AACA,MAAM,gBAAgB;AAEtB,MAAM,aAAa,uCAAuC,aAAa;AAEvE,MAAM,cAAc;AAQpB,eAAe,UAAa,UAAgE;AAExF,MAAI,QAAQ;AACR,UAAM,QAAQ,MAAM,OAAO,KAAK,UAAU;AAC1C,WAAO,SAAS,KAAK;AAAA,EACzB,OAAO;AACH,WAAO;AAAA,EACX;AACJ;AAQA,SAAS,kBAAkB,QAAgB;AACvC,QAAM,mBAAmB,OAAO,YAAA;AAChC,SACI,qBAAqB,UACrB,qBAAqB,SACrB,qBAAqB,WACrB,qBAAqB;AAE7B;AAUA,SAAS,eAAe,WAAmB;AACvC,QAAM,MAAM,IAAI,IAAI,SAAS;AAE7B,SAAO,IAAI,SAAS,SAAS,kBAAkB;AACnD;AASA,eAAe,eAAe,UAAoB;;AAC9C,MAAI,SAAS,WAAW,KAAK;AAEzB,UAAM,OAAO,MAAM,SAAS,MAAA,EAAQ,KAAA;AAGpC,aAAO,UAAK,CAAC,MAAN,mBAAS,eAAc;AAAA,EAClC;AAEA,SAAO;AACX;AAgCO,SAAS,qBAAqB,SAA+B,IAAkB;AAClF,QAAM,EAAE,YAAY,MAAM;AAAA,EAAC,GAAG,gBAAgB;AAG9C,MAAI,WAAyB;AAC7B,MAAI,gBAAgB;AAEpB,MAAI,aAAa;AACb,QAAI,OAAO,gBAAgB,YAAY,uBAAuB,KAAK;AAE/D,iBAAW;AAAA,IACf,WAAW,OAAO,gBAAgB,YAAY;AAE1C,sBAAgB;AAAA,IACpB;AAAA,EACJ;AAKA,WAAS,aAAa;AAClB,WAAO,KAAK,MAAM,SAAS,EAAE;AAAA,EACjC;AAOA,iBAAe,cAA+B;AAC1C,UAAM,KAAK,WAAA;AACX,cAAU,2BAA2B,EAAE;AAGvC,QAAI,WAAW,MAAM,UAAU,CAAC,UAAU,MAAM,MAAM,QAAQ,CAAC;AAE/D,QAAI,CAAC,UAAU;AAEX,gBAAU,0BAA0B,EAAE;AACtC,iBAAW,MAAM,MAAM,UAAU,EAAE,QAAQ,OAAO;AAClD,gBAAU,6BAA6B,IAAI,EAAE,QAAQ,SAAS,QAAQ;AAAA,IAC1E,OAAO;AACH,gBAAU,wBAAwB,EAAE;AAAA,IACxC;AAGA,UAAM,aAAqB,MAAM,SAAS,MAAA,EAAQ,QAAQ;AAG1D,UAAM,UAAU,CAAC,UAAU,MAAM,IAAI,UAAU,QAAQ,CAAC;AAExD,cAAU,8BAA8B,EAAE;AAC1C,WAAO;AAAA,EACX;AAEA,MAAI,eAAe,cAAA;AAMnB,iBAAe,eAAe;AAC1B,UAAM,KAAK,WAAA;AACX,cAAU,4BAA4B,EAAE;AAGxC,UAAM,UAAU,CAAC,UAAU,MAAM,OAAO,QAAQ,CAAC;AAGjD,mBAAe,cAAA;AAEf,cAAU,+BAA+B,EAAE;AAAA,EAC/C;AAQA,iBAAe,eAAe,SAAkB;AAE5C,UAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;AAG3C,QAAI,CAAC,QAAQ,IAAI,WAAW,GAAG;AAC3B,cAAQ,IAAI,aAAa,MAAM,YAAa;AAAA,IAChD;AAGA,WAAO,MAAM,SAAS,EAAE,SAAS;AAAA,EACrC;AAWA,SAAO,eAAeA,gBAClB,OACA,MACiB;AACjB,UAAM,KAAK,WAAA;AACX,UAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;AAGvC,QAAI,kBAAkB,QAAQ,MAAM,KAAK,eAAe,QAAQ,GAAG,GAAG;AAClE,gBAAU,2BAA2B,IAAI,EAAE,QAAQ,QAAQ,QAAQ,KAAK,QAAQ,KAAK;AAGrF,YAAM,WAAW,MAAM,eAAe,QAAQ,OAAO;AAGrD,UAAI,MAAM,eAAe,QAAQ,GAAG;AAChC,kBAAU,sBAAsB,IAAI,EAAE,QAAQ,SAAS,QAAQ;AAG/D,cAAM,aAAA;AACN,cAAM,gBAAgB,MAAM,eAAe,QAAQ,OAAO;AAE1D,kBAAU,8BAA8B,IAAI;AAAA,UACxC,QAAQ,QAAQ;AAAA,UAChB,KAAK,QAAQ;AAAA,UACb,QAAQ,cAAc;AAAA,UACtB,SAAS;AAAA,QAAA,CACZ;AAED,eAAO;AAAA,MACX,OAAO;AACH,kBAAU,8BAA8B,IAAI;AAAA,UACxC,QAAQ,QAAQ;AAAA,UAChB,KAAK,QAAQ;AAAA,UACb,QAAQ,SAAS;AAAA,UACjB,SAAS;AAAA,QAAA,CACZ;AAGD,eAAO;AAAA,MACX;AAAA,IACJ,OAAO;AACH,gBAAU,uBAAuB,IAAI,EAAE,QAAQ,QAAQ,QAAQ,KAAK,QAAQ,KAAK;AAGjF,aAAO,MAAM,OAAO;AAAA,IACxB;AAAA,EACJ;AACJ;ACrPA,MAAM,iBAAiB,qBAAA;AAMvB,IAAI,cAAqB;AAWlB,MAAM,cAAc;AAAA,EACf,cAAc;AAAA,EAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASvB,MAAM,OAA+B,OAAoB,IAAuB;AAC5E,WAAO,YAAY,OAAO,IAAI;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,SAAS;AACZ,WAAO,IAAI,cAAA;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,aAAa,sBAAsB,WAAyB;AAExD,QAAI,mBAAmB,WAAW;AAC9B,UAAI;AACA,cAAM,eAAe,MAAM,UAAU,cAAc,SAAS,WAAW;AAAA,UACnE,MAAM;AAAA,QAAA,CACT;AAGD,sBAAc;AAEd,gBAAQ,IAAI,oDAAoD,YAAY;AAAA,MAChF,SAAS,OAAO;AACZ,gBAAQ;AAAA,UACJ;AAAA,UACA;AAAA,QAAA;AAAA,MAER;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,sBAAsB;AACzB,UAAM,QAAQ;AAGd,UAAM,iBAAiB,WAAW,CAAC,UAAU;AAEzC,YAAM,UAAU,MAAM,aAAa;AAAA,IACvC,CAAC;AAGD,UAAM,iBAAiB,YAAY,CAAC,UAAU;AAE1C,YAAM,UAAU,MAAM,QAAQ,MAAA,CAAO;AAAA,IACzC,CAAC;AAGD,UAAM,iBAAiB,SAAS,CAAC,UAAU;AAEvC,YAAM,YAAY,eAAe,MAAM,OAAO,CAAC;AAAA,IACnD,CAAC;AAAA,EACL;AACJ;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../utils/dist/index.js","../../services/fetch-network/dist/v1/index.js","../src/fetch.ts","../src/index.ts"],"sourcesContent":["/*!\n * Copyright (c) 2022, Salesforce, Inc.,\n * All rights reserved.\n * For full license text, see the LICENSE.txt file\n */\nfunction bfs(start, predicate, getChildren) {\n const queue = [...start];\n const visited = /* @__PURE__ */ new Set([...start]);\n const matches2 = /* @__PURE__ */ new Set();\n while (queue.length) {\n const curr = queue.shift();\n if (predicate(curr)) {\n matches2.add(curr);\n }\n const children = getChildren(curr);\n for (const child of children) {\n if (!visited.has(child)) {\n visited.add(child);\n queue.push(child);\n }\n }\n }\n return matches2;\n}\nfunction lineFormatter(position, message, filePath) {\n return `${message} (${filePath}:${position.line}:${position.column})`;\n}\nclass DefaultFileParserLogger {\n constructor(services, filePath) {\n this.services = services;\n this.filePath = filePath;\n }\n trace(position, message) {\n this.services.logger.trace(this.format(position, message));\n }\n debug(position, message) {\n this.services.logger.debug(this.format(position, message));\n }\n info(position, message) {\n this.services.logger.info(this.format(position, message));\n }\n warn(position, message) {\n this.services.logger.warn(this.format(position, message));\n }\n error(position, message) {\n this.services.logger.error(this.format(position, message));\n }\n format(position, message) {\n return lineFormatter(position, message, this.filePath);\n }\n}\nfunction matches(test, s) {\n if (test === void 0) {\n return false;\n } else if (typeof test === \"string\") {\n return s === test;\n } else if (test instanceof RegExp) {\n return test.test(s);\n } else if (typeof test === \"function\") {\n return test(s);\n }\n return test.some((m) => matches(m, s));\n}\nfunction includes(incexc, s) {\n if (matches(incexc.exclude, s)) {\n return false;\n }\n if (matches(incexc.include, s)) {\n return true;\n }\n if (incexc.include) {\n return false;\n }\n return true;\n}\nconst { create, freeze, keys, entries } = Object;\nconst { hasOwnProperty } = Object.prototype;\nconst { isArray } = Array;\nconst { push, indexOf, slice } = Array.prototype;\nconst { stringify, parse } = JSON;\nconst WeakSetConstructor = WeakSet;\nconst LogLevelMap = {\n TRACE: 4,\n DEBUG: 3,\n INFO: 2,\n WARN: 1,\n ERROR: 0\n};\nclass ConsoleLogger {\n constructor(level = \"WARN\", printer = console.log, formatter = (level2, message) => `${level2}: ${message}`) {\n this.level = level;\n this.printer = printer;\n this.formatter = formatter;\n this.messages = [];\n }\n trace(message) {\n this.log(\"TRACE\", message);\n }\n debug(message) {\n this.log(\"DEBUG\", message);\n }\n info(message) {\n this.log(\"INFO\", message);\n }\n warn(message) {\n this.log(\"WARN\", message);\n }\n error(message) {\n this.log(\"ERROR\", message);\n }\n log(level, message) {\n if (LogLevelMap[level] > LogLevelMap[this.level]) {\n return;\n }\n this.printer(this.formatter(level, message));\n }\n}\nfunction loggerService(level, printer, formatter) {\n return new ConsoleLogger(level, printer, formatter);\n}\nclass Ok {\n constructor(value) {\n this.value = value;\n }\n isOk() {\n return true;\n }\n isErr() {\n return !this.isOk();\n }\n}\nclass Err {\n constructor(error) {\n this.error = error;\n }\n isOk() {\n return false;\n }\n isErr() {\n return !this.isOk();\n }\n}\nconst ok = (value) => new Ok(value);\nconst err = (err2) => new Err(err2);\nclass DataNotFoundError extends Error {\n constructor(message) {\n super(message);\n this.name = \"DataNotFoundError\";\n }\n}\nclass DataIncompleteError extends Error {\n constructor(message, partialData) {\n super(message);\n this.partialData = partialData;\n this.name = \"DataIncompleteError\";\n }\n}\nfunction isDataNotFoundError(error) {\n return error instanceof DataNotFoundError || error.name === \"DataNotFoundError\";\n}\nfunction isDataIncompleteError(error) {\n return error instanceof DataIncompleteError || error.name === \"DataIncompleteError\";\n}\nfunction isCacheHitOrError(value) {\n if (value.isErr() && (isDataIncompleteError(value.error) || isDataNotFoundError(value.error))) {\n return false;\n }\n return true;\n}\nfunction isCacheMiss(value) {\n return !isCacheHitOrError(value);\n}\nfunction isResult(value) {\n return value != null && typeof value === \"object\" && \"isOk\" in value && \"isErr\" in value && typeof value.isOk === \"function\" && typeof value.isErr === \"function\" && (value.isOk() === true && value.isErr() === false && \"value\" in value || value.isOk() === false && value.isErr() === true && \"error\" in value);\n}\nfunction setOverlaps(setA, setB) {\n for (const element of setA) {\n if (setB.has(element)) {\n return true;\n }\n }\n return false;\n}\nfunction setDifference(setA, setB) {\n const differenceSet = /* @__PURE__ */ new Set();\n for (const element of setA) {\n if (!setB.has(element)) {\n differenceSet.add(element);\n }\n }\n return differenceSet;\n}\nfunction addAllToSet(targetSet, sourceSet) {\n for (const element of sourceSet) {\n targetSet.add(element);\n }\n}\nconst toTypeScriptSafeIdentifier = (s) => s.length >= 1 ? s[0].replace(/[^$_\\p{ID_Start}]/u, \"_\") + s.slice(1).replace(/[^$\\u200c\\u200d\\p{ID_Continue}]/gu, \"_\") : \"\";\nfunction isSubscribable(obj) {\n return typeof obj === \"object\" && obj !== null && \"subscribe\" in obj && typeof obj.subscribe === \"function\" && \"refresh\" in obj && typeof obj.refresh === \"function\";\n}\nfunction isSubscribableResult(x) {\n if (!isResult(x)) {\n return false;\n }\n return isSubscribable(x.isOk() ? x.value : x.error);\n}\nfunction buildSubscribableResult(result, subscribe, refresh) {\n if (result.isOk()) {\n return ok({ data: result.value, subscribe, refresh });\n } else {\n return err({ failure: result.error, subscribe, refresh });\n }\n}\nfunction resolvedPromiseLike(result) {\n if (isPromiseLike(result)) {\n return result.then((nextResult) => nextResult);\n }\n return {\n then: (onFulfilled, _onRejected) => {\n try {\n return resolvedPromiseLike(onFulfilled(result));\n } catch (e) {\n if (onFulfilled === void 0) {\n return resolvedPromiseLike(result);\n }\n return rejectedPromiseLike(e);\n }\n }\n };\n}\nfunction rejectedPromiseLike(reason) {\n if (isPromiseLike(reason)) {\n return reason.then((nextResult) => nextResult);\n }\n return {\n then: (_onFulfilled, onRejected) => {\n if (typeof onRejected === \"function\") {\n try {\n return resolvedPromiseLike(onRejected(reason));\n } catch (e) {\n return rejectedPromiseLike(e);\n }\n }\n return rejectedPromiseLike(reason);\n }\n };\n}\nfunction isPromiseLike(x) {\n return typeof (x == null ? void 0 : x.then) === \"function\";\n}\nfunction racesync(values) {\n for (const value of values) {\n let settled = void 0;\n if (isPromiseLike(value)) {\n value.then(\n (_) => {\n settled = value;\n },\n (_) => {\n settled = value;\n }\n );\n } else {\n settled = resolvedPromiseLike(value);\n }\n if (settled !== void 0) {\n return settled;\n }\n }\n return Promise.race(values);\n}\nfunction withResolvers() {\n let resolve, reject;\n const promise = new Promise((res, rej) => {\n resolve = res;\n reject = rej;\n });\n return { promise, resolve, reject };\n}\nfunction deepEquals(x, y) {\n if (x === void 0) {\n return y === void 0;\n } else if (x === null) {\n return y === null;\n } else if (y === null) {\n return x === null;\n } else if (isArray(x)) {\n if (!isArray(y) || x.length !== y.length) {\n return false;\n }\n for (let i = 0; i < x.length; ++i) {\n if (!deepEquals(x[i], y[i])) {\n return false;\n }\n }\n return true;\n } else if (typeof x === \"object\") {\n if (typeof y !== \"object\") {\n return false;\n }\n const xkeys = Object.keys(x);\n const ykeys = Object.keys(y);\n if (xkeys.length !== ykeys.length) {\n return false;\n }\n for (let i = 0; i < xkeys.length; ++i) {\n const key = xkeys[i];\n if (!deepEquals(x[key], y[key])) {\n return false;\n }\n }\n return true;\n }\n return x === y;\n}\nfunction stableJSONStringify(node) {\n if (node && node.toJSON && typeof node.toJSON === \"function\") {\n node = node.toJSON();\n }\n if (node === void 0) {\n return;\n }\n if (typeof node === \"number\") {\n return isFinite(node) ? \"\" + node : \"null\";\n }\n if (typeof node !== \"object\") {\n return stringify(node);\n }\n let i;\n let out;\n if (isArray(node)) {\n out = \"[\";\n for (i = 0; i < node.length; i++) {\n if (i) {\n out += \",\";\n }\n out += stableJSONStringify(node[i]) || \"null\";\n }\n return out + \"]\";\n }\n if (node === null) {\n return \"null\";\n }\n const objKeys = keys(node).sort();\n out = \"\";\n for (i = 0; i < objKeys.length; i++) {\n const key = objKeys[i];\n const value = stableJSONStringify(node[key]);\n if (!value) {\n continue;\n }\n if (out) {\n out += \",\";\n }\n out += stringify(key) + \":\" + value;\n }\n return \"{\" + out + \"}\";\n}\nfunction toError(x) {\n if (x instanceof Error) {\n return x;\n }\n return new Error(typeof x === \"string\" ? x : JSON.stringify(x));\n}\nfunction deepCopy(x) {\n const stringified = stringify(x);\n return stringified ? parse(stringified) : void 0;\n}\nfunction readableStreamToAsyncIterable(stream) {\n if (stream.locked) {\n return err(new Error(\"ReadableStream is already locked\"));\n }\n if (Symbol.asyncIterator in stream) {\n return ok(stream);\n }\n const reader = stream.getReader();\n return ok({\n [Symbol.asyncIterator]: () => ({\n next: async () => {\n try {\n const result = await reader.read();\n if (result.done) {\n try {\n reader.releaseLock();\n } catch {\n }\n return { done: true, value: void 0 };\n }\n return {\n done: false,\n value: result.value\n };\n } catch (e) {\n try {\n reader.releaseLock();\n } catch {\n }\n throw e;\n }\n },\n return: async (value) => {\n try {\n await reader.cancel();\n } catch {\n }\n try {\n reader.releaseLock();\n } catch {\n }\n return { done: true, value };\n },\n throw: async (exception) => {\n try {\n await reader.cancel();\n } catch {\n }\n try {\n reader.releaseLock();\n } catch {\n }\n throw exception;\n }\n })\n });\n}\nfunction satisfies(provided, requested) {\n const providedN = provided.split(\".\").map((s) => parseInt(s));\n const requestedN = requested.split(\".\").map((s) => parseInt(s));\n return providedN[0] === requestedN[0] && providedN[1] >= requestedN[1];\n}\nfunction stringIsVersion(s) {\n const versionParts = s.split(\".\");\n return (versionParts.length === 2 || versionParts.length === 3) && versionParts.every((part) => part.match(/^\\d+$/));\n}\nvar HttpStatusCode = /* @__PURE__ */ ((HttpStatusCode2) => {\n HttpStatusCode2[HttpStatusCode2[\"Ok\"] = 200] = \"Ok\";\n HttpStatusCode2[HttpStatusCode2[\"Created\"] = 201] = \"Created\";\n HttpStatusCode2[HttpStatusCode2[\"NoContent\"] = 204] = \"NoContent\";\n HttpStatusCode2[HttpStatusCode2[\"NotModified\"] = 304] = \"NotModified\";\n HttpStatusCode2[HttpStatusCode2[\"BadRequest\"] = 400] = \"BadRequest\";\n HttpStatusCode2[HttpStatusCode2[\"Unauthorized\"] = 401] = \"Unauthorized\";\n HttpStatusCode2[HttpStatusCode2[\"Forbidden\"] = 403] = \"Forbidden\";\n HttpStatusCode2[HttpStatusCode2[\"NotFound\"] = 404] = \"NotFound\";\n HttpStatusCode2[HttpStatusCode2[\"ServerError\"] = 500] = \"ServerError\";\n HttpStatusCode2[HttpStatusCode2[\"GatewayTimeout\"] = 504] = \"GatewayTimeout\";\n return HttpStatusCode2;\n})(HttpStatusCode || {});\nfunction getFetchResponseFromAuraError(err2) {\n if (err2.data !== void 0 && err2.data.statusCode !== void 0) {\n let data = {};\n data = err2.data;\n if (err2.id !== void 0) {\n data.id = err2.id;\n }\n return new FetchResponse(data.statusCode, data);\n }\n return new FetchResponse(500, {\n error: err2.message\n });\n}\nasync function coerceResponseToFetchResponse(response) {\n const { status } = response;\n const responseHeaders = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n let responseBody = null;\n if (status !== 204) {\n const contentType = responseHeaders[\"content-type\"];\n responseBody = contentType && contentType.startsWith(\"application/json\") ? await response.json() : await response.text();\n }\n return new FetchResponse(status, responseBody, responseHeaders);\n}\nfunction getStatusText(status) {\n switch (status) {\n case 200:\n return \"OK\";\n case 201:\n return \"Created\";\n case 304:\n return \"Not Modified\";\n case 400:\n return \"Bad Request\";\n case 404:\n return \"Not Found\";\n case 500:\n return \"Server Error\";\n default:\n return `Unexpected HTTP Status Code: ${status}`;\n }\n}\nclass FetchResponse extends Error {\n constructor(status, body, headers) {\n super();\n this.status = status;\n this.body = body;\n this.headers = headers || {};\n this.ok = status >= 200 && this.status <= 299;\n this.statusText = getStatusText(status);\n }\n}\nconst deeplyFrozen = new WeakSetConstructor();\nfunction deepFreeze(value) {\n if (typeof value !== \"object\" || value === null || deeplyFrozen.has(value)) {\n return;\n }\n deeplyFrozen.add(value);\n if (isArray(value)) {\n for (let i = 0, len = value.length; i < len; i += 1) {\n deepFreeze(value[i]);\n }\n } else {\n const keys$1 = keys(value);\n for (let i = 0, len = keys$1.length; i < len; i += 1) {\n deepFreeze(value[keys$1[i]]);\n }\n }\n freeze(value);\n}\nfunction isScalar(value) {\n return typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\" || value === null || value === void 0;\n}\nfunction isScalarObject(value) {\n return Object.values(value).every((value2) => isScalar(value2));\n}\nfunction isScalarArray(value) {\n return value.every((item) => isScalar(item));\n}\nfunction encodeQueryParam(paramName, value, explode) {\n switch (typeof value) {\n case \"string\":\n return [`${paramName}=${encodeURIComponent(value)}`];\n case \"number\":\n case \"boolean\":\n return [`${paramName}=${value}`];\n case \"object\":\n if (value === null) {\n return [];\n }\n if (isArray(value)) {\n if (!isScalarArray(value)) {\n throw new Error(`Unsupported non-scalar array type for ${paramName}`);\n }\n if (explode) {\n return value.map(\n (item) => `${paramName}=${item ? encodeURIComponent(item) : item}`\n );\n }\n return [\n `${paramName}=${value.map((item) => item ? encodeURIComponent(item) : item).join(\",\")}`\n ];\n }\n if (!isScalarObject(value)) {\n throw new Error(`Unsupported non-scalar object type for ${paramName}`);\n }\n if (explode) {\n return entries(value).map(\n ([key, value2]) => `${key}=${value2 ? encodeURIComponent(value2) : value2}`\n );\n }\n return [\n `${paramName}=${entries(value).flat().map((item) => item ? encodeURIComponent(item) : item).join(\",\")}`\n ];\n default:\n return [];\n }\n}\nclass InternalError extends Error {\n constructor(data) {\n super();\n this.data = data;\n this.type = \"internal\";\n }\n}\nclass UserVisibleError extends Error {\n constructor(data) {\n super();\n this.data = data;\n this.type = \"user-visible\";\n }\n}\nfunction isUserVisibleError(error) {\n return error instanceof Error && \"type\" in error && error.type === \"user-visible\";\n}\nfunction logError(error) {\n if (isUserVisibleError(error)) {\n return;\n }\n console.error(\"OneStore Command threw an error that we did not expect\", error);\n}\nfunction applyDecorators(baseCommand, decorators, options) {\n if (!decorators || decorators.length === 0) {\n return baseCommand;\n }\n return decorators.reduce((command, decorator) => decorator(command, options), baseCommand);\n}\nexport {\n isArray as ArrayIsArray,\n indexOf as ArrayPrototypeIndexOf,\n push as ArrayPrototypePush,\n slice as ArrayPrototypeSlice,\n ConsoleLogger,\n DataIncompleteError,\n DataNotFoundError,\n DefaultFileParserLogger,\n Err,\n FetchResponse,\n HttpStatusCode,\n InternalError,\n parse as JSONParse,\n stringify as JSONStringify,\n LogLevelMap,\n create as ObjectCreate,\n entries as ObjectEntries,\n freeze as ObjectFreeze,\n keys as ObjectKeys,\n hasOwnProperty as ObjectPrototypeHasOwnProperty,\n Ok,\n UserVisibleError,\n WeakSetConstructor,\n addAllToSet,\n applyDecorators,\n bfs,\n buildSubscribableResult,\n coerceResponseToFetchResponse,\n deepCopy,\n deepEquals,\n deepFreeze,\n encodeQueryParam,\n err,\n getFetchResponseFromAuraError,\n includes,\n isCacheHitOrError,\n isCacheMiss,\n isDataIncompleteError,\n isDataNotFoundError,\n isPromiseLike,\n isResult,\n isSubscribable,\n isSubscribableResult,\n isUserVisibleError,\n lineFormatter,\n logError,\n loggerService,\n ok,\n racesync,\n readableStreamToAsyncIterable,\n rejectedPromiseLike,\n resolvedPromiseLike,\n satisfies,\n setDifference,\n setOverlaps,\n stableJSONStringify,\n stringIsVersion,\n toError,\n toTypeScriptSafeIdentifier,\n withResolvers\n};\n//# sourceMappingURL=index.js.map\n","/*!\n * Copyright (c) 2022, Salesforce, Inc.,\n * All rights reserved.\n * For full license text, see the LICENSE.txt file\n */\nimport { resolvedPromiseLike } from \"@conduit-client/utils\";\nfunction buildServiceDescriptor(interceptors = {\n request: [],\n retry: void 0,\n response: [],\n finally: []\n}, retryService) {\n return {\n type: \"fetch\",\n version: \"1.0\",\n service: function(...args) {\n var _a;\n const context = (_a = interceptors.createContext) == null ? void 0 : _a.call(interceptors);\n const {\n request: requestInterceptors = [],\n retry: retryInterceptor = void 0,\n response: responseInterceptors = [],\n finally: finallyInterceptors = []\n } = interceptors;\n const pending = requestInterceptors.reduce(\n (previousPromise, interceptor) => previousPromise.then((args2) => interceptor(args2, context)),\n resolvedPromiseLike(args)\n );\n return Promise.resolve(pending).then((args2) => {\n if (retryInterceptor) {\n return retryInterceptor(args2, retryService, context);\n } else {\n if (retryService) {\n return retryService.applyRetry(() => fetch(...args2));\n }\n return fetch(...args2);\n }\n }).then((response) => {\n return responseInterceptors.reduce(\n (previousPromise, interceptor) => previousPromise.then((response2) => interceptor(response2, context)),\n resolvedPromiseLike(response)\n );\n }).finally(() => {\n if (finallyInterceptors.length > 0) {\n return finallyInterceptors.reduce(\n (previousPromise, interceptor) => previousPromise.then(() => interceptor(context)),\n Promise.resolve()\n );\n }\n });\n }\n };\n}\nfunction setHeader(headerName, headerValue, [resource, options = {}], {\n throwOnExisting = false,\n errorMessage = `Unexpected ${headerName} header encountered`\n} = {}) {\n let hasHeaderBeenSet = false;\n if (resource instanceof Request && !(options == null ? void 0 : options.headers)) {\n if (throwOnExisting && resource.headers.has(headerName)) {\n throw new Error(errorMessage);\n }\n resource.headers.set(headerName, headerValue);\n hasHeaderBeenSet = true;\n }\n if ((options == null ? void 0 : options.headers) instanceof Headers) {\n if (throwOnExisting && options.headers.has(headerName)) {\n throw new Error(errorMessage);\n }\n options.headers.set(headerName, headerValue);\n } else {\n if (throwOnExisting && (options == null ? void 0 : options.headers) && Reflect.has(options.headers, headerName)) {\n throw new Error(errorMessage);\n }\n if (!hasHeaderBeenSet) {\n options.headers = {\n ...options == null ? void 0 : options.headers,\n [headerName]: headerValue\n };\n }\n }\n return [resource, options];\n}\nconst UNEXPECTED_AUTHORIZATION_HEADER_MESSAGE = \"Unexpected Authorization header encountered. To specify a custom Authorization header, use a Fetch service that is not configured with JwtRequestHeaderInterceptor\";\nfunction setHeaderAuthorization({ token }, fetchParams) {\n const authorizationValue = `Bearer ${token}`;\n return setHeader(\"Authorization\", authorizationValue, fetchParams, {\n throwOnExisting: true,\n errorMessage: UNEXPECTED_AUTHORIZATION_HEADER_MESSAGE\n });\n}\nfunction buildJwtRequestHeaderInterceptor(jwtManager, jwtRequestModifier = (_e, fetchArgs) => fetchArgs) {\n return (args) => {\n return resolvedPromiseLike(jwtManager.getJwt()).then((token) => {\n const fetchArgsWithRequestHeaderAuthorization = setHeaderAuthorization(token, args);\n return token.extraInfo ? jwtRequestModifier(token.extraInfo, fetchArgsWithRequestHeaderAuthorization) : fetchArgsWithRequestHeaderAuthorization;\n });\n };\n}\nclass AbortError extends Error {\n constructor(message = \"This operation was aborted\") {\n super(message);\n this.name = \"AbortError\";\n }\n}\nfunction buildMockFetchService(initialResponses = []) {\n let responses = [...initialResponses];\n const networkAdapter = (...args) => {\n var _a;\n const [url, fetchOptions = {}] = args;\n networkAdapter.requests.push(args);\n if ((_a = fetchOptions.signal) == null ? void 0 : _a.aborted) {\n return Promise.reject(new AbortError());\n }\n const result = responses.shift();\n if (result === void 0) {\n throw new Error(\"No more mock responses queued\");\n }\n networkAdapter.availableResponses = responses.length;\n networkAdapter.responsesUsed++;\n if (result instanceof Error) {\n return Promise.reject(result);\n }\n const delay = result.delay || 0;\n return new Promise((resolve, reject) => {\n let abortHandler = null;\n if (fetchOptions.signal) {\n abortHandler = () => {\n reject(new AbortError());\n };\n fetchOptions.signal.addEventListener(\"abort\", abortHandler);\n }\n const completeRequest = () => {\n var _a2;\n if (abortHandler && fetchOptions.signal) {\n fetchOptions.signal.removeEventListener(\"abort\", abortHandler);\n }\n if ((_a2 = fetchOptions.signal) == null ? void 0 : _a2.aborted) {\n reject(new AbortError());\n return;\n }\n resolve({\n ok: result.ok !== void 0 ? result.ok : true,\n statusText: result.statusText !== void 0 ? result.statusText : \"ok\",\n status: result.status !== void 0 ? result.status : 200,\n json: () => Promise.resolve(result.body)\n });\n };\n if (delay > 0) {\n setTimeout(completeRequest, delay);\n } else {\n setTimeout(completeRequest, 0);\n }\n });\n };\n networkAdapter.requests = [];\n networkAdapter.availableResponses = responses.length;\n networkAdapter.queueResponses = (newResponses) => {\n responses = responses.concat(newResponses);\n networkAdapter.availableResponses = responses.length;\n };\n networkAdapter.fetch = (args) => networkAdapter(args);\n networkAdapter.reset = () => {\n networkAdapter.requests = [];\n responses = [];\n networkAdapter.availableResponses = 0;\n networkAdapter.responsesUsed = 0;\n };\n networkAdapter.responsesUsed = 0;\n return {\n type: \"fetch\",\n version: \"1.0\",\n service: networkAdapter\n };\n}\nexport {\n buildJwtRequestHeaderInterceptor,\n buildMockFetchService,\n buildServiceDescriptor,\n setHeader,\n setHeaderAuthorization\n};\n//# sourceMappingURL=index.js.map\n","import { buildServiceDescriptor, Interceptors } from '@conduit-client/service-fetch-network/v1';\n\n// current version of the cache for token storage\nconst CACHE_VERSION = 1;\n// name of the cache used to store CSRF tokens\nconst CACHE_NAME = `salesforce-lightning-service-worker-${CACHE_VERSION}`;\n// header name\nconst CSRF_HEADER = 'X-CSRF-Token';\n\nconst API_PATH_PREFIX = '/services/data/v';\n\n/**\n * Provides a safe way to interact with the Cache API with fallback for unsupported environments.\n *\n * @param callback - Function that receives the cache instance and returns a promise\n * @returns The result of the callback, or undefined if caches API is not available\n */\nasync function withCache<T>(callback: (cache: Cache) => Promise<T>): Promise<T | undefined> {\n // Defend against the cache API not being available (e.g., in some test environments)\n if (caches) {\n const cache = await caches.open(CACHE_NAME);\n return callback(cache);\n } else {\n return undefined;\n }\n}\n\n/**\n * Determines if an HTTP method is one that mutates data and requires CSRF protection.\n *\n * @param method - The HTTP method to check\n * @returns true if the method requires CSRF protection (POST, PUT, PATCH, DELETE)\n */\nfunction isProtectedMethod(method: string) {\n const normalizedMethod = method.toLowerCase();\n return (\n normalizedMethod === 'post' ||\n normalizedMethod === 'put' ||\n normalizedMethod === 'patch' ||\n normalizedMethod === 'delete'\n );\n}\n\n/**\n * Determines if the URL is for a path that requires CSRF protection.\n * Currently protects all Salesforce API endpoints under '/services'.\n *\n * @param urlString - The full URL to check\n * @returns true if the URL requires CSRF protection\n * @note This could be made configurable in the future to support custom protected paths\n */\nfunction isProtectedUrl(urlString: string) {\n const url = new URL(urlString);\n // Agentforce Vibes IDE has the form `absproxy/PORT/services/data/...`\n return url.pathname.includes(API_PATH_PREFIX);\n}\n\n/**\n * Checks if a response indicates that the CSRF token is invalid.\n * Salesforce returns a 400 status with a specific error code when tokens are invalid.\n *\n * @param response - The HTTP response to check\n * @returns true if the response indicates an invalid CSRF token\n */\nasync function isTokenInvalid(response: Response) {\n if (response.status === 400) {\n // clone response to read body without consuming the original stream\n const body = await response.clone().json();\n\n // check for Salesforce's specific invalid token error code\n return body[0]?.errorCode === 'INVALID_ACCESS_TOKEN';\n }\n\n return false;\n}\n\n/**\n * Configuration options for the Lightning fetch creation.\n */\nexport interface LightningFetchConfig {\n interceptors?: Interceptors<void>;\n\n /**\n * Optional source for CSRF tokens. Can be:\n * - string: URL path to token endpoint (e.g., '/custom/csrf-endpoint')\n * - URL: Full URL object for token endpoint\n * - function: Custom async function that returns a token string\n *\n * As a string or URL, default fetching and caching (if Cache API is\n * available) will be used to obtain tokens\n */\n csrfTokenSource?: string | URL | (() => Promise<string>);\n\n /**\n * Optional callback for firing events related to fetch operations.\n * Can be used for instrumentation, logging, and monitoring.\n */\n fireEvent?: (eventName: string, id: string, data?: unknown) => void;\n}\n\n/**\n * Creates an enhanced fetch function with automatic CSRF token handling.\n * The returned function automatically adds CSRF tokens to protected requests\n * and handles token refresh when tokens become invalid.\n *\n * @param config - Optional configuration object\n * @returns An enhanced fetch function that handles CSRF protection\n */\nexport function createLightningFetch(config: LightningFetchConfig = {}): typeof fetch {\n const { fireEvent = () => {}, csrfTokenSource, interceptors } = config;\n const { service: fetchService } = buildServiceDescriptor(interceptors);\n\n // default url and provider\n let tokenUrl: string | URL = `${API_PATH_PREFIX}65.0/ui-api/session/csrf`;\n let tokenProvider = obtainToken;\n\n if (csrfTokenSource) {\n if (typeof csrfTokenSource === 'string' || csrfTokenSource instanceof URL) {\n // use supplied URL with built-in provider\n tokenUrl = csrfTokenSource;\n } else if (typeof csrfTokenSource === 'function') {\n // use external provider\n tokenProvider = csrfTokenSource;\n }\n }\n\n /**\n * Creates a unique identifier to correlate a series of related events.\n */\n function generateId() {\n return Date.now().toString(36);\n }\n\n /**\n * Obtains a CSRF token, using cache when available or fetching a new one.\n *\n * @returns Promise that resolves to the CSRF token string\n */\n async function obtainToken(): Promise<string> {\n const id = generateId();\n fireEvent('csrf_token_obtain_start', id);\n\n // try to get cached token response first\n let response = await withCache((cache) => cache.match(tokenUrl));\n\n if (!response) {\n // no cached response available, fetch a new token\n fireEvent('csrf_token_fetch_start', id);\n response = await fetchService(tokenUrl, { method: 'get' });\n fireEvent('csrf_token_fetch_complete', id, { status: response.status });\n } else {\n fireEvent('csrf_token_cache_hit', id);\n }\n\n // extract token from response (clone to avoid consuming original stream)\n const csrfToken: string = (await response.clone().json()).csrfToken;\n\n // cache the response for future use\n await withCache((cache) => cache.put(tokenUrl, response));\n\n fireEvent('csrf_token_obtain_complete', id);\n return csrfToken;\n }\n\n let tokenPromise = tokenProvider();\n\n /**\n * Clears any cached token and initiates retrieval of a fresh one.\n * Used when the current token becomes invalid.\n */\n async function refreshToken() {\n const id = generateId();\n fireEvent('csrf_token_refresh_start', id);\n\n // remove the invalid token from cache\n await withCache((cache) => cache.delete(tokenUrl));\n\n // start obtaining a new token\n tokenPromise = tokenProvider();\n\n fireEvent('csrf_token_refresh_complete', id);\n }\n\n /**\n * Makes a request with the CSRF token header added.\n *\n * @param request - The original request to enhance with CSRF token\n * @returns Promise that resolves to the response\n */\n async function fetchWithToken(request: Request) {\n // clone original headers\n const headers = new Headers(request.headers);\n\n // either use provided token or add one that's been loaded\n if (!headers.has(CSRF_HEADER)) {\n headers.set(CSRF_HEADER, await tokenPromise!);\n }\n\n // execute request with CSRF token header\n return fetchService(request, { headers });\n }\n\n /**\n * Enhanced fetch function that applies CSRF token protection to qualifying requests.\n * Automatically adds CSRF tokens to data-mutating requests to protected URLs,\n * with automatic token refresh when tokens become invalid.\n *\n * @param input - The request input (URL, Request, etc.)\n * @param init - Optional request initialization options\n * @returns Promise that resolves to the response\n */\n return async function lightningFetch(\n input: RequestInfo | URL,\n init?: RequestInit\n ): Promise<Response> {\n const id = generateId();\n const request = new Request(input, init);\n\n // check if this request requires CSRF protection (mutating method + protected URL)\n if (isProtectedMethod(request.method) && isProtectedUrl(request.url)) {\n fireEvent('protected_request_start', id, { method: request.method, url: request.url });\n\n // make request with CSRF token (clone to allow retry with fresh request)\n const response = await fetchWithToken(request.clone());\n\n // check if the token was rejected\n if (await isTokenInvalid(response)) {\n fireEvent('csrf_token_invalid', id, { status: response.status });\n\n // token is invalid, refresh and retry once\n await refreshToken();\n const retryResponse = await fetchWithToken(request.clone());\n\n fireEvent('protected_request_complete', id, {\n method: request.method,\n url: request.url,\n status: retryResponse.status,\n retried: true,\n });\n\n return retryResponse;\n } else {\n fireEvent('protected_request_complete', id, {\n method: request.method,\n url: request.url,\n status: response.status,\n retried: false,\n });\n\n // token was valid, return the response\n return response;\n }\n } else {\n fireEvent('unprotected_request', id, { method: request.method, url: request.url });\n\n // no CSRF protection required, use standard fetch\n return fetchService(request);\n }\n };\n}\n","import { createLightningFetch } from './fetch';\n\n/**\n * Type alias for the native fetch function\n */\ntype Fetch = typeof fetch;\n\ntype QueuedRequest = {\n input: string | URL | Request;\n init?: RequestInit;\n resolve: (response: Response | PromiseLike<Response>) => void;\n};\n\n/**\n * The fetch function used by the client. Defaults to enhanced fetch for CSRF protection.\n * Will be switched to native fetch when CSRF-based service worker is successfully registered.\n */\nlet clientFetch: Fetch = createLightningFetch();\n\n/**\n * When a service worker is to be used, we need to hold off on all fetches until the worker is\n * loaded. That way the logic provided by the worker is applied.\n */\nlet serviceWorkerLoading = false;\nlet pendingRequests: QueuedRequest[] = [];\n\n/**\n * A client for making HTTP requests with CSRF protection. By default, protection is provided by\n * wrapping the native `fetch` API with functionality that will apply a CSRF token to appropriate\n * requests. This includes functionality to detect expired tokens, triggering a token refresh and\n * retry of the request.\n *\n * Optionally, CSRF protection can be offloaded to a service worker by making the appropriate calls\n * to `registerServiceWorker` and `defineServiceWorker`\n */\nexport class ConduitClient {\n private constructor() {}\n\n /**\n * Makes an HTTP request\n *\n * @param input - The URL, Request object, or relative path to request\n * @param init - Optional request configuration that will be merged with defaults\n * @returns Promise that resolves to the Response object\n */\n fetch(input: string | URL | Request, init: RequestInit = {}): Promise<Response> {\n // queue any fetches until service worker is loaded\n if (serviceWorkerLoading) {\n return new Promise<Response>((resolve) => {\n pendingRequests.push({ input, init, resolve });\n });\n }\n\n return clientFetch(input, init);\n }\n\n /**\n * Factory method to create a new ConduitClient instance\n *\n * @returns A new ConduitClient instance\n */\n static create() {\n return new ConduitClient();\n }\n\n /**\n * Registers a service worker for enhanced CSRF protection and caching.\n * When successfully registered, the client will switch to using native fetch\n * as the service worker will handle CSRF protection.\n *\n * @param scriptURL - path to the service worker script\n */\n static async registerServiceWorker(scriptURL: string) {\n // check if service workers are supported in this environment\n if ('serviceWorker' in navigator) {\n try {\n serviceWorkerLoading = true;\n\n // register service worker URL - classic due to security restraints with `module`\n // Chrome: won't include cookies, thus 302 to authenticate request\n // Firefox: not supported at all\n const registration = await navigator.serviceWorker.register(scriptURL, {\n type: 'classic',\n });\n\n clientFetch = fetch;\n\n console.log('[Conduit Client] Service registration succeeded:', registration);\n } catch (error) {\n console.log(\n '[Conduit Client] Service Worker registration failed (using decorated `fetch`):',\n error\n );\n } finally {\n // with client finalized, process all queued requests\n processQueuedRequests();\n }\n } else {\n console.log('[Conduit Client] Service Worker not supported (using decorated `fetch`):');\n }\n }\n}\n\n/**\n * Complete the queued requests\n */\nfunction processQueuedRequests() {\n // stop queuing\n serviceWorkerLoading = false;\n\n // complete requests\n pendingRequests.forEach(({ input, init, resolve }) => {\n resolve(clientFetch(input, init));\n });\n pendingRequests = [];\n}\n"],"names":[],"mappings":"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"}
|
package/dist/sw.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
function _(e) {
|
|
7
|
+
return P(e) ? e.then((t) => t) : {
|
|
8
|
+
then: (t, o) => {
|
|
9
|
+
try {
|
|
10
|
+
return _(t(e));
|
|
11
|
+
} catch (a) {
|
|
12
|
+
return t === void 0 ? _(e) : E(a);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function E(e) {
|
|
18
|
+
return P(e) ? e.then((t) => t) : {
|
|
19
|
+
then: (t, o) => {
|
|
20
|
+
if (typeof o == "function")
|
|
21
|
+
try {
|
|
22
|
+
return _(o(e));
|
|
23
|
+
} catch (a) {
|
|
24
|
+
return E(a);
|
|
25
|
+
}
|
|
26
|
+
return E(e);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function P(e) {
|
|
31
|
+
return typeof (e == null ? void 0 : e.then) == "function";
|
|
32
|
+
}
|
|
33
|
+
/*!
|
|
34
|
+
* Copyright (c) 2022, Salesforce, Inc.,
|
|
35
|
+
* All rights reserved.
|
|
36
|
+
* For full license text, see the LICENSE.txt file
|
|
37
|
+
*/
|
|
38
|
+
function L(e = {
|
|
39
|
+
request: [],
|
|
40
|
+
retry: void 0,
|
|
41
|
+
response: [],
|
|
42
|
+
finally: []
|
|
43
|
+
}, t) {
|
|
44
|
+
return {
|
|
45
|
+
type: "fetch",
|
|
46
|
+
version: "1.0",
|
|
47
|
+
service: function(...o) {
|
|
48
|
+
var a;
|
|
49
|
+
const u = (a = e.createContext) == null ? void 0 : a.call(e), {
|
|
50
|
+
request: l = [],
|
|
51
|
+
retry: d = void 0,
|
|
52
|
+
response: h = [],
|
|
53
|
+
finally: v = []
|
|
54
|
+
} = e, m = l.reduce(
|
|
55
|
+
(c, f) => c.then((n) => f(n, u)),
|
|
56
|
+
_(o)
|
|
57
|
+
);
|
|
58
|
+
return Promise.resolve(m).then((c) => d ? d(c, t, u) : fetch(...c)).then((c) => h.reduce(
|
|
59
|
+
(f, n) => f.then((r) => n(r, u)),
|
|
60
|
+
_(c)
|
|
61
|
+
)).finally(() => {
|
|
62
|
+
if (v.length > 0)
|
|
63
|
+
return v.reduce(
|
|
64
|
+
(c, f) => c.then(() => f(u)),
|
|
65
|
+
Promise.resolve()
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const S = 1, T = `salesforce-lightning-service-worker-${S}`, C = "X-CSRF-Token", I = "/services/data/v";
|
|
72
|
+
async function w(e) {
|
|
73
|
+
if (caches) {
|
|
74
|
+
const t = await caches.open(T);
|
|
75
|
+
return e(t);
|
|
76
|
+
} else
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
function q(e) {
|
|
80
|
+
const t = e.toLowerCase();
|
|
81
|
+
return t === "post" || t === "put" || t === "patch" || t === "delete";
|
|
82
|
+
}
|
|
83
|
+
function R(e) {
|
|
84
|
+
return new URL(e).pathname.includes(I);
|
|
85
|
+
}
|
|
86
|
+
async function A(e) {
|
|
87
|
+
var t;
|
|
88
|
+
return e.status === 400 ? ((t = (await e.clone().json())[0]) == null ? void 0 : t.errorCode) === "INVALID_ACCESS_TOKEN" : !1;
|
|
89
|
+
}
|
|
90
|
+
function b(e = {}) {
|
|
91
|
+
const { fireEvent: t = () => {
|
|
92
|
+
}, csrfTokenSource: o, interceptors: a } = e, { service: u } = L(a);
|
|
93
|
+
let l = `${I}65.0/ui-api/session/csrf`, d = v;
|
|
94
|
+
o && (typeof o == "string" || o instanceof URL ? l = o : typeof o == "function" && (d = o));
|
|
95
|
+
function h() {
|
|
96
|
+
return Date.now().toString(36);
|
|
97
|
+
}
|
|
98
|
+
async function v() {
|
|
99
|
+
const n = h();
|
|
100
|
+
t("csrf_token_obtain_start", n);
|
|
101
|
+
let r = await w((i) => i.match(l));
|
|
102
|
+
r ? t("csrf_token_cache_hit", n) : (t("csrf_token_fetch_start", n), r = await u(l, { method: "get" }), t("csrf_token_fetch_complete", n, { status: r.status }));
|
|
103
|
+
const y = (await r.clone().json()).csrfToken;
|
|
104
|
+
return await w((i) => i.put(l, r)), t("csrf_token_obtain_complete", n), y;
|
|
105
|
+
}
|
|
106
|
+
let m = d();
|
|
107
|
+
async function c() {
|
|
108
|
+
const n = h();
|
|
109
|
+
t("csrf_token_refresh_start", n), await w((r) => r.delete(l)), m = d(), t("csrf_token_refresh_complete", n);
|
|
110
|
+
}
|
|
111
|
+
async function f(n) {
|
|
112
|
+
const r = new Headers(n.headers);
|
|
113
|
+
return r.has(C) || r.set(C, await m), u(n, { headers: r });
|
|
114
|
+
}
|
|
115
|
+
return async function(r, y) {
|
|
116
|
+
const i = h(), s = new Request(r, y);
|
|
117
|
+
if (q(s.method) && R(s.url)) {
|
|
118
|
+
t("protected_request_start", i, { method: s.method, url: s.url });
|
|
119
|
+
const k = await f(s.clone());
|
|
120
|
+
if (await A(k)) {
|
|
121
|
+
t("csrf_token_invalid", i, { status: k.status }), await c();
|
|
122
|
+
const g = await f(s.clone());
|
|
123
|
+
return t("protected_request_complete", i, {
|
|
124
|
+
method: s.method,
|
|
125
|
+
url: s.url,
|
|
126
|
+
status: g.status,
|
|
127
|
+
retried: !0
|
|
128
|
+
}), g;
|
|
129
|
+
} else
|
|
130
|
+
return t("protected_request_complete", i, {
|
|
131
|
+
method: s.method,
|
|
132
|
+
url: s.url,
|
|
133
|
+
status: k.status,
|
|
134
|
+
retried: !1
|
|
135
|
+
}), k;
|
|
136
|
+
} else
|
|
137
|
+
return t("unprotected_request", i, { method: s.method, url: s.url }), u(s);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const p = self, U = b();
|
|
141
|
+
p.addEventListener("install", (e) => {
|
|
142
|
+
e.waitUntil(p.skipWaiting());
|
|
143
|
+
});
|
|
144
|
+
p.addEventListener("activate", (e) => {
|
|
145
|
+
e.waitUntil(p.clients.claim());
|
|
146
|
+
});
|
|
147
|
+
p.addEventListener("fetch", (e) => {
|
|
148
|
+
e.respondWith(U(e.request));
|
|
149
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/fetch.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { Interceptors } from '@conduit-client/service-fetch-network/v1';
|
|
1
2
|
/**
|
|
2
3
|
* Configuration options for the Lightning fetch creation.
|
|
3
4
|
*/
|
|
4
5
|
export interface LightningFetchConfig {
|
|
6
|
+
interceptors?: Interceptors<void>;
|
|
5
7
|
/**
|
|
6
8
|
* Optional source for CSRF tokens. Can be:
|
|
7
9
|
* - string: URL path to token endpoint (e.g., '/custom/csrf-endpoint')
|
|
@@ -11,7 +13,7 @@ export interface LightningFetchConfig {
|
|
|
11
13
|
* As a string or URL, default fetching and caching (if Cache API is
|
|
12
14
|
* available) will be used to obtain tokens
|
|
13
15
|
*/
|
|
14
|
-
|
|
16
|
+
csrfTokenSource?: string | URL | (() => Promise<string>);
|
|
15
17
|
/**
|
|
16
18
|
* Optional callback for firing events related to fetch operations.
|
|
17
19
|
* Can be used for instrumentation, logging, and monitoring.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -28,16 +28,7 @@ export declare class ConduitClient {
|
|
|
28
28
|
* When successfully registered, the client will switch to using native fetch
|
|
29
29
|
* as the service worker will handle CSRF protection.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* @param scriptURL - URL or path to the service worker script
|
|
34
|
-
*/
|
|
35
|
-
static registerServiceWorker(scriptURL: string | URL): Promise<void>;
|
|
36
|
-
/**
|
|
37
|
-
* Defines the service worker behavior for CSRF protection.
|
|
38
|
-
*
|
|
39
|
-
* This method must be called within a service worker script whose URL is supplied to
|
|
40
|
-
* `registerServiceWorker`
|
|
31
|
+
* @param scriptURL - path to the service worker script
|
|
41
32
|
*/
|
|
42
|
-
static
|
|
33
|
+
static registerServiceWorker(scriptURL: string): Promise<void>;
|
|
43
34
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduit-client/salesforce-lightning-service-worker",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Service worker for accessing Salesforce data",
|
|
6
6
|
"type": "module",
|
|
@@ -12,11 +12,18 @@
|
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
13
13
|
"main": "dist/index.js",
|
|
14
14
|
"types": "dist/types/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"types": "./dist/types/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./sw": "./dist/sw.js"
|
|
21
|
+
},
|
|
15
22
|
"files": [
|
|
16
23
|
"dist/"
|
|
17
24
|
],
|
|
18
25
|
"scripts": {
|
|
19
|
-
"build": "vite build && tsc --build --emitDeclarationOnly",
|
|
26
|
+
"build": "vite build && vite build --config vite.sw.config.js && tsc --build --emitDeclarationOnly",
|
|
20
27
|
"clean": "rm -rf dist",
|
|
21
28
|
"dev": "npm run build && tsx scripts/dev.ts",
|
|
22
29
|
"test": "vitest run",
|
|
@@ -24,15 +31,16 @@
|
|
|
24
31
|
"test:size": "size-limit",
|
|
25
32
|
"watch": "npm run build --watch"
|
|
26
33
|
},
|
|
27
|
-
"
|
|
28
|
-
"@
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@conduit-client/service-fetch-network": "3.6.1"
|
|
36
|
+
},
|
|
37
|
+
"volta": {
|
|
38
|
+
"extends": "../../../package.json"
|
|
31
39
|
},
|
|
32
40
|
"size-limit": [
|
|
33
41
|
{
|
|
34
42
|
"path": "dist/index.js",
|
|
35
|
-
"limit": "
|
|
43
|
+
"limit": "2.82 kB"
|
|
36
44
|
}
|
|
37
45
|
]
|
|
38
46
|
}
|