Skip to content

Commit 6f79e13

Browse files
committed
feat: start SWContext, cleanup client-SW message pattern, update PWA lib & headers for OPFS
1 parent e0bd653 commit 6f79e13

File tree

6 files changed

+561
-40
lines changed

6 files changed

+561
-40
lines changed

web/contexts/SWContext.tsx

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React, {
2+
createContext,
3+
useCallback,
4+
useContext,
5+
useEffect,
6+
useRef,
7+
useState,
8+
} from 'react';
9+
import { showNotification } from '@mantine/notifications';
10+
import { DeadropMessage } from 'types/worker';
11+
12+
type MessageHandler = (msg: DeadropMessage) => void;
13+
14+
type ServiceWorkerCtx = {
15+
activateWorker: () => Promise<void>;
16+
workerController: ServiceWorker | null;
17+
sendMessage: MessageHandler;
18+
addMessageHandler: (handler: MessageHandler) => void;
19+
removeMessageHandler: (handler: MessageHandler) => void;
20+
};
21+
22+
const SWContext = createContext<ServiceWorkerCtx>(
23+
{} as ServiceWorkerCtx,
24+
);
25+
26+
export const useSWContext = () => useContext(SWContext);
27+
28+
export const SWProvider = ({
29+
children,
30+
}: {
31+
children: React.ReactNode;
32+
}) => {
33+
// SW controller instance
34+
const [workerController, setWorkerController] =
35+
useState<ServiceWorker | null>(null);
36+
37+
/**
38+
* set of handlers to run on each message
39+
* useRef b/c don't need re-renders
40+
*/
41+
const messageHandlers = useRef(new Set<MessageHandler>());
42+
43+
useEffect(() => {
44+
const updateController = () => {
45+
setWorkerController(navigator.serviceWorker.controller);
46+
};
47+
48+
// Set initial controller if available
49+
updateController();
50+
51+
// whenever SW indicates a change in controller, hydrate
52+
navigator.serviceWorker.addEventListener(
53+
'controllerchange',
54+
updateController,
55+
);
56+
57+
/**
58+
* loop over all mounted handlers
59+
* this allows you to mount message handlers lower in the tree
60+
* @param event deadrop SW meessage
61+
*/
62+
const messageCallback = (event: MessageEvent<DeadropMessage>) => {
63+
messageHandlers.current.forEach((handler) =>
64+
handler(event.data),
65+
);
66+
};
67+
68+
navigator.serviceWorker.addEventListener(
69+
'message',
70+
messageCallback,
71+
);
72+
73+
return () => {
74+
navigator.serviceWorker.removeEventListener(
75+
'controllerchange',
76+
updateController,
77+
);
78+
79+
navigator.serviceWorker.removeEventListener(
80+
'message',
81+
messageCallback,
82+
);
83+
84+
messageHandlers.current.clear();
85+
};
86+
}, []);
87+
88+
const addMessageHandler = useCallback((handler: MessageHandler) => {
89+
messageHandlers.current.add(handler);
90+
}, []);
91+
92+
const removeMessageHandler = useCallback(
93+
(handler: MessageHandler) => {
94+
messageHandlers.current.delete(handler);
95+
},
96+
[],
97+
);
98+
99+
useEffect(() => {
100+
const notificationHandler = (message: DeadropMessage) => {
101+
if (message.type === 'notification') {
102+
const { message: msg, variant } = message.payload;
103+
104+
showNotification({
105+
message: msg,
106+
color: variant === 'error' ? 'red' : undefined,
107+
});
108+
}
109+
};
110+
111+
addMessageHandler(notificationHandler);
112+
113+
return () => removeMessageHandler(notificationHandler);
114+
}, [addMessageHandler, removeMessageHandler]);
115+
116+
const activateServiceWorker = async () => {
117+
if (
118+
'serviceWorker' in navigator &&
119+
window.workbox !== undefined &&
120+
!workerController
121+
) {
122+
await window.workbox.register();
123+
}
124+
};
125+
126+
const sendMessage = useCallback((message: DeadropMessage) => {
127+
if (!workerController) {
128+
console.error('No active Service Worker controller found.');
129+
return;
130+
}
131+
132+
workerController.postMessage(message);
133+
}, []);
134+
135+
return (
136+
<SWContext.Provider
137+
value={{
138+
activateWorker: activateServiceWorker,
139+
workerController,
140+
sendMessage,
141+
addMessageHandler,
142+
removeMessageHandler,
143+
}}
144+
>
145+
{children}
146+
</SWContext.Provider>
147+
);
148+
};

web/next.config.mjs

+34-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ const withTM = nextTranspileModules(['shared']);
1111
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
1212
import { withSentryConfig } from '@sentry/nextjs';
1313

14-
import nextPwa from 'next-pwa';
14+
import nextPwa from '@ducanh2912/next-pwa';
1515

1616
const withPWA = nextPwa({
1717
dest: '/public',
18-
customWorkerDir: 'scripts/service-worker',
18+
customWorkerSrc: 'scripts/service-worker',
19+
register: false,
1920
});
2021

2122
const nonce = randomBytes(8).toString('base64');
@@ -62,6 +63,17 @@ const assetsDomains = [
6263

6364
const deadropWorkerDomain = process.env.NEXT_PUBLIC_DEADROP_API_URL;
6465

66+
const connectSrcEntries = [
67+
`'self'`,
68+
clerkDomains,
69+
peerDomain,
70+
vercelMetricsDomains,
71+
captchaDomains,
72+
sentryDomain,
73+
assetsDomains,
74+
deadropWorkerDomain,
75+
].join(' ');
76+
6577
const safeConfig = {
6678
isDev: process.env.NODE_ENV !== 'production',
6779
contentTypeOptions: 'nosniff',
@@ -70,7 +82,7 @@ const safeConfig = {
7082
frameOptions: 'DENY',
7183
permissionsPolicy: false,
7284
contentSecurityPolicy: {
73-
'connect-src': `'self' ${clerkDomains} ${peerDomain} ${vercelMetricsDomains} ${captchaDomains} ${sentryDomain} ${assetsDomains} ${deadropWorkerDomain}`,
85+
'connect-src': connectSrcEntries,
7486
'default-src': `'self'`,
7587
'font-src': `'self' data: ${vercelAssetsDomain} ${googleFontsDomain}`,
7688
'frame-src': `${vercelLiveDomain} ${captchaDomains}`,
@@ -86,6 +98,25 @@ const headers = [
8698
key: 'Strict-Transport-Security',
8799
value: 'max-age=63072000; includeSubDomains; preload',
88100
},
101+
/**
102+
* Cross-Origin-Embedder-Policy & Cross-Origin-Opener-Policy
103+
* are required for OPFS
104+
*/
105+
{
106+
key: 'Cross-Origin-Embedder-Policy',
107+
value: 'require-corp',
108+
},
109+
{
110+
key: 'Cross-Origin-Opener-Policy',
111+
value: 'same-origin',
112+
},
113+
/**
114+
* required for hcaptcha and external libs
115+
*/
116+
{
117+
key: 'Cross-Origin-Resource-Policy',
118+
value: 'cross-origin',
119+
},
89120
...nextSafe(safeConfig),
90121
];
91122

web/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dependencies": {
1616
"@clerk/nextjs": "^5.6.0",
1717
"@clerk/themes": "^2.1.36",
18+
"@ducanh2912/next-pwa": "^10.2.9",
1819
"@emotion/react": "^11.10.0",
1920
"@emotion/server": "^11.10.0",
2021
"@hcaptcha/react-hcaptcha": "^1.4.4",

web/scripts/service-worker/index.ts

+25-22
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const randomBase64 = () => {
2121

2222
async function processMessage(
2323
message: DeadropMessage,
24-
): Promise<DeadropMessage> {
24+
): Promise<DeadropMessage | undefined> {
2525
const directoryHandler = await navigator.storage.getDirectory();
2626
const fileHandler = await directoryHandler.getFileHandle(
2727
CONFIG_FILE_NAME,
@@ -52,18 +52,18 @@ async function processMessage(
5252
if (message.type === 'get_config')
5353
return { type: 'config', payload: config };
5454

55-
let response: DeadropMessage = {
56-
type: 'notification',
57-
payload: {
58-
variant: 'success',
59-
message: '',
60-
},
61-
};
55+
let response: DeadropMessage | undefined;
6256

6357
if (message.type === 'set_config') {
6458
await writeConfig(message.payload);
6559

66-
response.payload.message = 'deadrop config updated!';
60+
response = {
61+
type: 'notification',
62+
payload: {
63+
variant: 'success',
64+
message: 'deadrop config updated!',
65+
},
66+
};
6767
} else if (message.type.includes('secret')) {
6868
const activeVault = config.vaults[config.active_vault.name];
6969

@@ -73,17 +73,29 @@ async function processMessage(
7373
activeVault.cloud,
7474
);
7575

76-
const { addSecrets, updateSecret, getSecret, getAllSecrets } =
76+
const { addSecrets, updateSecret, getSecret } =
7777
createSecretsHelpers(activeVault, db);
7878

7979
if (message.type === 'add_secret') {
8080
await addSecrets([message.payload]);
8181

82-
response.payload.message = 'secret added to vault!';
82+
response = {
83+
type: 'notification',
84+
payload: {
85+
variant: 'success',
86+
message: 'secret added to vault!',
87+
},
88+
};
8389
} else if (message.type === 'update_secret') {
8490
await updateSecret(message.payload);
8591

86-
response.payload.message = 'secret updated in vault!';
92+
response = {
93+
type: 'notification',
94+
payload: {
95+
variant: 'success',
96+
message: 'secret updated in vault!',
97+
},
98+
};
8799
} else if (message.type === 'get_secret') {
88100
const { name, environment } = message.payload;
89101

@@ -97,15 +109,6 @@ async function processMessage(
97109
environment,
98110
},
99111
};
100-
} else if (message.type === 'get_secrets') {
101-
const { environment } = message.payload;
102-
103-
const secrets = await getAllSecrets(environment);
104-
105-
response = {
106-
type: 'all_secrets',
107-
payload: [],
108-
};
109112
}
110113
}
111114

@@ -119,6 +122,6 @@ sw.addEventListener(
119122
async (msg: DeadropServiceWorkerMessage) => {
120123
const response = await processMessage(msg.data);
121124

122-
if (response) msg.ports[0].postMessage(response);
125+
if (response) msg.source?.postMessage(response);
123126
},
124127
);

web/types/global.d.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { Workbox } from 'workbox-window';
22

3-
// extend Crypto for WebKit interfaces
4-
interface Crypto {
5-
webkitSubtle?: SubtleCrypto;
6-
}
7-
8-
// extend Window for workbox (service worker)
93
declare global {
4+
// extend Crypto for WebKit interfaces
5+
interface Crypto {
6+
webkitSubtle?: SubtleCrypto;
7+
}
8+
9+
// extend Window for workbox (service worker)
1010
interface Window {
1111
workbox: Workbox;
1212
}

0 commit comments

Comments
 (0)