@conduit-client/salesforce-lightning-service-worker 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +68 -8
- package/dist/index.js.map +1 -1
- package/dist/types/__tests__/csrf.spec.d.ts +1 -0
- package/dist/types/csrf.d.ts +1 -8
- package/dist/types/index.d.ts +4 -3
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,72 @@
|
|
|
3
3
|
* All rights reserved.
|
|
4
4
|
* For full license text, see the LICENSE.txt file
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
const CACHE_VERSION = 1;
|
|
7
|
+
const CACHE_NAME = `salesforce-lightning-service-worker-${CACHE_VERSION}`;
|
|
8
|
+
async function withCache(callback) {
|
|
9
|
+
if (caches) {
|
|
10
|
+
const cache = await caches.open(CACHE_NAME);
|
|
11
|
+
return callback(cache);
|
|
12
|
+
} else {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function isProtectedMethod(method) {
|
|
17
|
+
const normalizedMethod = method.toLowerCase();
|
|
18
|
+
return normalizedMethod === "post" || normalizedMethod === "put" || normalizedMethod === "patch" || normalizedMethod === "delete";
|
|
19
|
+
}
|
|
20
|
+
function isProtectedUrl(urlString) {
|
|
21
|
+
const url = new URL(urlString);
|
|
22
|
+
return url.pathname.startsWith("/services");
|
|
23
|
+
}
|
|
24
|
+
async function isTokenInvalid(response) {
|
|
25
|
+
var _a;
|
|
26
|
+
if (response.status === 400) {
|
|
27
|
+
const body = await response.clone().json();
|
|
28
|
+
return ((_a = body[0]) == null ? void 0 : _a.errorCode) === "INVALID_ACCESS_TOKEN";
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function createCsrfHandler(version) {
|
|
33
|
+
const CSRF_TOKEN_URL = `/services/data/v${version}/ui-api/session/csrf`;
|
|
34
|
+
async function obtainToken() {
|
|
35
|
+
let response = await withCache((cache) => cache.match(CSRF_TOKEN_URL));
|
|
36
|
+
if (!response) {
|
|
37
|
+
response = await fetch(CSRF_TOKEN_URL, { method: "get" });
|
|
38
|
+
}
|
|
39
|
+
const csrfToken = (await response.clone().json()).csrfToken;
|
|
40
|
+
await withCache((cache) => cache.put(CSRF_TOKEN_URL, response));
|
|
41
|
+
return csrfToken;
|
|
42
|
+
}
|
|
43
|
+
let tokenPromise = obtainToken();
|
|
44
|
+
async function refreshToken() {
|
|
45
|
+
await withCache((cache) => cache.delete(CSRF_TOKEN_URL));
|
|
46
|
+
tokenPromise = obtainToken();
|
|
47
|
+
}
|
|
48
|
+
async function fetchWithToken(request) {
|
|
49
|
+
const headers = new Headers(request.headers);
|
|
50
|
+
headers.set("X-CSRF-Token", await tokenPromise);
|
|
51
|
+
return fetch(request, { headers });
|
|
52
|
+
}
|
|
53
|
+
return async (request) => {
|
|
54
|
+
if (isProtectedMethod(request.method) && isProtectedUrl(request.url)) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetchWithToken(request.clone());
|
|
57
|
+
if (await isTokenInvalid(response)) {
|
|
58
|
+
await refreshToken();
|
|
59
|
+
return fetchWithToken(request.clone());
|
|
60
|
+
} else {
|
|
61
|
+
return response;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return new Response(JSON.stringify({ error }), { status: 500 });
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
return fetch(request);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createServiceWorker({ version, debug }) {
|
|
7
72
|
const scope = self;
|
|
8
73
|
scope.addEventListener("install", (event) => {
|
|
9
74
|
if (debug) console.log("[Service Worker] Installed");
|
|
@@ -13,14 +78,9 @@ function createServiceWorker({ debug } = {}) {
|
|
|
13
78
|
if (debug) console.log("[Service Worker] Activated");
|
|
14
79
|
event.waitUntil(scope.clients.claim());
|
|
15
80
|
});
|
|
81
|
+
const applyCsrfProtection = createCsrfHandler(version);
|
|
16
82
|
scope.addEventListener("fetch", (event) => {
|
|
17
|
-
|
|
18
|
-
url: event.request.url,
|
|
19
|
-
method: event.request.method,
|
|
20
|
-
destination: event.request.destination,
|
|
21
|
-
mode: event.request.mode
|
|
22
|
-
});
|
|
23
|
-
event.respondWith(fetch(event.request));
|
|
83
|
+
event.respondWith(applyCsrfProtection(event.request));
|
|
24
84
|
});
|
|
25
85
|
}
|
|
26
86
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["type Config = {\n debug?: boolean;\n};\n\n/**\n * Adds event listeners for setting up service worker.\n *\n * @param debug\n */\nexport function createServiceWorker({ debug }: Config
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/csrf.ts","../src/index.ts"],"sourcesContent":["const CACHE_VERSION = 1;\nconst CACHE_NAME = `salesforce-lightning-service-worker-${CACHE_VERSION}`;\n\n/**\n * Retrieves the cache and supplies it to a callback.\n *\n * @param callback\n */\nasync function withCache<T>(callback: (cache: Cache) => Promise<T>): Promise<T | undefined> {\n // defend against the cache API not being available\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 * Determine if an HTTP method is one that mutates data.\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 * Determine if the URL is for a path that has CSRF protection.\n *\n * Note: Could allow customization\n */\nfunction isProtectedUrl(urlString: string) {\n const url = new URL(urlString);\n return url.pathname.startsWith('/services');\n}\n\n/**\n * Check response for status and error code of an invalid token\n */\nasync function isTokenInvalid(response: Response) {\n if (response.status === 400) {\n // clone response to read body without consuming it\n const body = await response.clone().json();\n\n // check for specific error code\n return body[0]?.errorCode === 'INVALID_ACCESS_TOKEN';\n }\n\n return false;\n}\n\nexport function createCsrfHandler(version: string) {\n const CSRF_TOKEN_URL = `/services/data/v${version}/ui-api/session/csrf`;\n\n async function obtainToken(): Promise<string> {\n // look up response in cache\n let response = await withCache((cache) => cache.match(CSRF_TOKEN_URL));\n\n if (!response) {\n // cached response not available to fetch\n response = await fetch(CSRF_TOKEN_URL, { method: 'get' });\n }\n\n // extract token using clone so caching can still process\n const csrfToken: string = (await response.clone().json()).csrfToken;\n\n // store token response\n await withCache((cache) => cache.put(CSRF_TOKEN_URL, response));\n\n return csrfToken;\n }\n\n let tokenPromise = obtainToken();\n\n /**\n * Clear any cached token and retrieve a new one.\n */\n async function refreshToken() {\n await withCache((cache) => cache.delete(CSRF_TOKEN_URL));\n\n tokenPromise = obtainToken();\n }\n\n /**\n * Make a request with the token header based on the supplied request\n */\n async function fetchWithToken(request: Request) {\n // combine original headers with new csrf header\n const headers = new Headers(request.headers);\n headers.set('X-CSRF-Token', await tokenPromise!);\n\n // make request with updated headers\n return fetch(request, { headers });\n }\n\n /**\n * Applies a valid CSRF token to targeted requests that modify data\n */\n return async (request: Request): Promise<Response> => {\n // see if the method and url qualify for CSRF\n if (isProtectedMethod(request.method) && isProtectedUrl(request.url)) {\n try {\n // make request with token, clone so that any retry isn't based on a consumed request\n const response = await fetchWithToken(request.clone());\n\n // see if token was bad\n if (await isTokenInvalid(response)) {\n // it was, so refresh and try again\n await refreshToken();\n return fetchWithToken(request.clone());\n } else {\n // use response\n return response;\n }\n } catch (error) {\n // always need to return a response\n return new Response(JSON.stringify({ error }), { status: 500 });\n }\n } else {\n // protection not required, run as is\n return fetch(request);\n }\n };\n}\n","import { createCsrfHandler } from './csrf';\n\nexport type Config = {\n version: string;\n debug?: boolean;\n};\n\n/**\n * Adds event listeners for setting up service worker.\n *\n * @param version\n * @param debug\n */\nexport function createServiceWorker({ version, debug }: Config) {\n const scope = self as any as ServiceWorkerGlobalScope;\n\n scope.addEventListener('install', (event) => {\n if (debug) console.log('[Service Worker] Installed');\n\n // Skip waiting to activate immediately\n event.waitUntil(scope.skipWaiting());\n });\n\n scope.addEventListener('activate', (event) => {\n if (debug) console.log('[Service Worker] Activated');\n\n // Claim all clients immediately\n event.waitUntil(scope.clients.claim());\n });\n\n const applyCsrfProtection = createCsrfHandler(version);\n scope.addEventListener('fetch', (event) => {\n // Apply CSRF protection\n event.respondWith(applyCsrfProtection(event.request));\n });\n}\n"],"names":[],"mappings":";;;;;AAAA,MAAM,gBAAgB;AACtB,MAAM,aAAa,uCAAuC,aAAa;AAOvE,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;AAKA,SAAS,kBAAkB,QAAgB;AACvC,QAAM,mBAAmB,OAAO,YAAA;AAChC,SACI,qBAAqB,UACrB,qBAAqB,SACrB,qBAAqB,WACrB,qBAAqB;AAE7B;AAOA,SAAS,eAAe,WAAmB;AACvC,QAAM,MAAM,IAAI,IAAI,SAAS;AAC7B,SAAO,IAAI,SAAS,WAAW,WAAW;AAC9C;AAKA,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;AAEO,SAAS,kBAAkB,SAAiB;AAC/C,QAAM,iBAAiB,mBAAmB,OAAO;AAEjD,iBAAe,cAA+B;AAE1C,QAAI,WAAW,MAAM,UAAU,CAAC,UAAU,MAAM,MAAM,cAAc,CAAC;AAErE,QAAI,CAAC,UAAU;AAEX,iBAAW,MAAM,MAAM,gBAAgB,EAAE,QAAQ,OAAO;AAAA,IAC5D;AAGA,UAAM,aAAqB,MAAM,SAAS,MAAA,EAAQ,QAAQ;AAG1D,UAAM,UAAU,CAAC,UAAU,MAAM,IAAI,gBAAgB,QAAQ,CAAC;AAE9D,WAAO;AAAA,EACX;AAEA,MAAI,eAAe,YAAA;AAKnB,iBAAe,eAAe;AAC1B,UAAM,UAAU,CAAC,UAAU,MAAM,OAAO,cAAc,CAAC;AAEvD,mBAAe,YAAA;AAAA,EACnB;AAKA,iBAAe,eAAe,SAAkB;AAE5C,UAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;AAC3C,YAAQ,IAAI,gBAAgB,MAAM,YAAa;AAG/C,WAAO,MAAM,SAAS,EAAE,SAAS;AAAA,EACrC;AAKA,SAAO,OAAO,YAAwC;AAElD,QAAI,kBAAkB,QAAQ,MAAM,KAAK,eAAe,QAAQ,GAAG,GAAG;AAClE,UAAI;AAEA,cAAM,WAAW,MAAM,eAAe,QAAQ,OAAO;AAGrD,YAAI,MAAM,eAAe,QAAQ,GAAG;AAEhC,gBAAM,aAAA;AACN,iBAAO,eAAe,QAAQ,OAAO;AAAA,QACzC,OAAO;AAEH,iBAAO;AAAA,QACX;AAAA,MACJ,SAAS,OAAO;AAEZ,eAAO,IAAI,SAAS,KAAK,UAAU,EAAE,MAAA,CAAO,GAAG,EAAE,QAAQ,KAAK;AAAA,MAClE;AAAA,IACJ,OAAO;AAEH,aAAO,MAAM,OAAO;AAAA,IACxB;AAAA,EACJ;AACJ;ACnHO,SAAS,oBAAoB,EAAE,SAAS,SAAiB;AAC5D,QAAM,QAAQ;AAEd,QAAM,iBAAiB,WAAW,CAAC,UAAU;AACzC,QAAI,MAAO,SAAQ,IAAI,4BAA4B;AAGnD,UAAM,UAAU,MAAM,aAAa;AAAA,EACvC,CAAC;AAED,QAAM,iBAAiB,YAAY,CAAC,UAAU;AAC1C,QAAI,MAAO,SAAQ,IAAI,4BAA4B;AAGnD,UAAM,UAAU,MAAM,QAAQ,MAAA,CAAO;AAAA,EACzC,CAAC;AAED,QAAM,sBAAsB,kBAAkB,OAAO;AACrD,QAAM,iBAAiB,SAAS,CAAC,UAAU;AAEvC,UAAM,YAAY,oBAAoB,MAAM,OAAO,CAAC;AAAA,EACxD,CAAC;AACL;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/csrf.d.ts
CHANGED
|
@@ -1,8 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Enforces a valid CSRF token for all requests that modify data
|
|
4
|
-
*
|
|
5
|
-
* @param targetClient - The client to add the interceptor to
|
|
6
|
-
* @param csrfClient - The client to use for fetching CSRF tokens (should not have interceptors to avoid circular dependencies)
|
|
7
|
-
*/
|
|
8
|
-
export declare function csrfInterceptor(targetClient: AxiosInstance, csrfClient: AxiosInstance): void;
|
|
1
|
+
export declare function createCsrfHandler(version: string): (request: Request) => Promise<Response>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
type Config = {
|
|
1
|
+
export type Config = {
|
|
2
|
+
version: string;
|
|
2
3
|
debug?: boolean;
|
|
3
4
|
};
|
|
4
5
|
/**
|
|
5
6
|
* Adds event listeners for setting up service worker.
|
|
6
7
|
*
|
|
8
|
+
* @param version
|
|
7
9
|
* @param debug
|
|
8
10
|
*/
|
|
9
|
-
export declare function createServiceWorker({ debug }
|
|
10
|
-
export {};
|
|
11
|
+
export declare function createServiceWorker({ version, debug }: Config): void;
|
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.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Service worker for accessing Salesforce data",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"clean": "rm -rf dist",
|
|
21
21
|
"dev": "npm run build && tsx scripts/dev.ts",
|
|
22
22
|
"test": "vitest run",
|
|
23
|
+
"test:coverage": "vitest run --coverage",
|
|
23
24
|
"test:size": "size-limit",
|
|
24
25
|
"watch": "npm run build --watch"
|
|
25
26
|
},
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"size-limit": [
|
|
32
33
|
{
|
|
33
34
|
"path": "dist/index.js",
|
|
34
|
-
"limit": "
|
|
35
|
+
"limit": "917 B"
|
|
35
36
|
}
|
|
36
37
|
]
|
|
37
38
|
}
|