Skip to content

Commit

Permalink
add JWT parsing on sw, separate IndexedDB logic, and implement named …
Browse files Browse the repository at this point in the history
…responses cache by did
  • Loading branch information
gkatrakazas committed Jun 19, 2024
1 parent f2824b2 commit 1275244
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 90 deletions.
23 changes: 23 additions & 0 deletions src/indexedDB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { openDB } from 'idb';

const DB_NAME = "wwwallet-db";
const DB_VERSION = 1;
const DB_STORAGE_VC_NAME = "storage";

const dbPromise = openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
db.createObjectStore(DB_STORAGE_VC_NAME);
},
});

export async function saveResponseToIndexedDB(did, url, responseText) {
const db = await dbPromise;
const key = `${did}-${url}`;
await db.put(DB_STORAGE_VC_NAME, responseText, key);
}

export async function getResponseFromIndexedDB(did, url) {
const db = await dbPromise;
const key = `${did}-${url}`;
return db.get(DB_STORAGE_VC_NAME, key);
}
132 changes: 42 additions & 90 deletions src/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,154 +5,94 @@ import { ExpirationPlugin } from "workbox-expiration";
import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies";
import { openDB } from 'idb';

const DB_NAME = "wwwallet-db";
const DB_VERSION = 1;
const DB_STORAGE_VC_NAME = "storage";

const dbPromise = openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
db.createObjectStore(DB_STORAGE_VC_NAME);
},
});
import {
saveResponseToIndexedDB,
getResponseFromIndexedDB,
} from './indexedDB';

clientsClaim();

// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute([
...self.__WB_MANIFEST,
{ url: '/manifest.json', revision: '1' },
{ url: '/favicon.ico', revision: '1' },
]);

// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => {
// If this isn't a navigation, skip.
if (request.mode !== "navigate") {
return false;
}
// If this is a URL that starts with /_, skip.
if (url.pathname.startsWith("/_")) {
return false;
}
// If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
// Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL('/index.html') // Assumes your index.html is in the root of the public directory
createHandlerBoundToURL('/index.html')
);

// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
// and cross-origin .png requests for credential logo images
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst.
({ url }) => url.pathname.endsWith(".png"),
new StaleWhileRevalidate({
cacheName: "images",
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);

// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
let currentDid = null;

self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
if (event.data && event.data.type === 'ADD_APP_TOKEN') {
handleSessionStorageChange(event.data.value);
const did = parseJwt(event.data.value).did;
currentDid = did;
handleSessionStorageChange(event.data.value, did);
}
});

// In-memory store for sensitive data
const inMemoryStore = {};

async function fetchAndSaveResponse(request) {
async function fetchAndSaveResponse(request, did) {
try {
const response = await fetch(request);
if (response.ok) {
const url = request.url;
if (isSensitiveResponse(url)) {
const responseText = await response.clone().text();
inMemoryStore[url] = responseText;
console.log(`Cached sensitive response in memory for URL: ${url}`);
} else {
const responseText = await response.clone().text();
await saveResponseToIndexedDB(url, responseText);
console.log(`Cached response in IndexedDB for URL: ${url}`);
}
const responseText = await response.clone().text();
await saveResponseToIndexedDB(did, url, responseText);
console.log(`Cached response in IndexedDB for URL: ${url}`);
return response;
}
} catch (error) {
const url = request.url;
if (isSensitiveResponse(url)) {
const storedResponse = inMemoryStore[url];
if (storedResponse) {
console.log(`Retrieved sensitive response from memory for URL: ${url}`);
return new Response(storedResponse);
}
} else {
const responseFromIndexedDB = await getResponseFromIndexedDB(url);
if (responseFromIndexedDB) {
console.log(`Retrieved response from IndexedDB for URL: ${url}`);
return new Response(responseFromIndexedDB);
}
const responseFromIndexedDB = await getResponseFromIndexedDB(did, url);
if (responseFromIndexedDB) {
console.log(`Retrieved response from IndexedDB for URL: ${url}`);
return new Response(responseFromIndexedDB);
}
throw error;
}
}

async function saveResponseToIndexedDB(url, responseText) {
const db = await dbPromise;
await db.put(DB_STORAGE_VC_NAME, responseText, url);
}

async function getResponseFromIndexedDB(url) {
const db = await dbPromise;
return db.get(DB_STORAGE_VC_NAME, url);
}

function isSensitiveResponse(url) {
const sensitiveEndpoints = [
'/storage/vc',
'/storage/vp',
'/user/session/account-info',
];
return sensitiveEndpoints.some(endpoint => url.includes(endpoint));
}

const matchVCStorageCb = ({ url }) => url.pathname.endsWith("/storage/vc");
const handlerVCStorageCb = async ({ request }) => await fetchAndSaveResponse(request);
const handlerVCStorageCb = async ({ request }) => await fetchAndSaveResponse(request, currentDid);

const matchVCStorageVp = ({ url }) => url.pathname.endsWith("/storage/vp");
const handlerVCStorageVp = async ({ request }) => await fetchAndSaveResponse(request);
const handlerVCStorageVp = async ({ request }) => await fetchAndSaveResponse(request, currentDid);

const matchIssuersCb = ({ url }) => url.pathname.endsWith("/legal_person/issuers/all");
const handlerIssuersCb = async ({ request }) => await fetchAndSaveResponse(request);
const handlerIssuersCb = async ({ request }) => await fetchAndSaveResponse(request, currentDid);

const matchVerifiersCb = ({ url }) => url.pathname.endsWith("/verifiers/all");
const handlerVerifiersCb = async ({ request }) => await fetchAndSaveResponse(request);
const handlerVerifiersCb = async ({ request }) => await fetchAndSaveResponse(request, currentDid);

const matchAccountInfoCb = ({ url }) => url.pathname.endsWith("/user/session/account-info");
const handlerAccountInfoCb = async ({ request }) => await fetchAndSaveResponse(request);
const handlerAccountInfoCb = async ({ request }) => await fetchAndSaveResponse(request, currentDid);

registerRoute(matchVCStorageCb, handlerVCStorageCb);
registerRoute(matchVCStorageVp, handlerVCStorageVp);
Expand All @@ -164,20 +104,19 @@ function getBackendUrl() {
return process.env.REACT_APP_WALLET_BACKEND_URL;
}

async function fetchAndCache(appToken, url) {
async function fetchAndCache(appToken, url, did) {
const walletBackendUrl = getBackendUrl();
const request = new Request(`${walletBackendUrl}${url}`, {
headers: {
'Authorization': `Bearer ${appToken}`
}
});
return fetchAndSaveResponse(request);
return fetchAndSaveResponse(request, did);
}

async function handleSessionStorageChange(appToken) {
console.log(`Add app token:= ${appToken}`);
async function handleSessionStorageChange(appToken, did) {
console.log(`Add app token: ${appToken}, DID: ${did}`);

// List of URLs to fetch and cache
const urlsToFetch = [
'/storage/vc',
'/storage/vp',
Expand All @@ -188,9 +127,22 @@ async function handleSessionStorageChange(appToken) {

for (const url of urlsToFetch) {
try {
await fetchAndCache(appToken, url);
await fetchAndCache(appToken, url, did);
} catch (error) {
console.error(`Error fetching and caching data for ${url}`, error);
}
}
}

// Helper function to parse JWT and extract DID
function parseJwt(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
Array.prototype.map.call(atob(base64), (c) =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join('')
);

return JSON.parse(jsonPayload);
}

0 comments on commit 1275244

Please sign in to comment.