diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-12-25 00:01:18 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-12-25 00:01:18 +0100 |
commit | e321534fbea9f09b139d440584f6b84ad0afb80f (patch) | |
tree | c4b4df109589475dd5f40a0d31f47a6aa9e43195 /packages | |
parent | feat: Shim userAgentData in all frames and workers (diff) | |
download | sophie-e321534fbea9f09b139d440584f6b84ad0afb80f.tar.gz sophie-e321534fbea9f09b139d440584f6b84ad0afb80f.tar.zst sophie-e321534fbea9f09b139d440584f6b84ad0afb80f.zip |
refactor: Simplify script injection
Inject CSS and main world scripts synchronously to avoid race conditions
with page loading.
Don't try to miming userAgentData for now, since it won't bypass
google's checks. However, simply omitting chrome from the user agent
does bypass them, at least for now.
Diffstat (limited to 'packages')
-rw-r--r-- | packages/main/src/index.ts | 84 | ||||
-rw-r--r-- | packages/service-inject/src/index.ts | 8 | ||||
-rw-r--r-- | packages/service-inject/src/shims/userAgentData.ts | 103 | ||||
-rw-r--r-- | packages/service-inject/src/utils.ts | 104 | ||||
-rw-r--r-- | packages/service-preload/src/index.ts | 21 | ||||
-rw-r--r-- | packages/service-shared/src/index.ts | 2 | ||||
-rw-r--r-- | packages/service-shared/src/schemas.ts | 7 |
7 files changed, 59 insertions, 270 deletions
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 02d6c97..0c0a585 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts | |||
@@ -22,7 +22,7 @@ import { | |||
22 | app, | 22 | app, |
23 | BrowserView, | 23 | BrowserView, |
24 | BrowserWindow, | 24 | BrowserWindow, |
25 | IpcMainEvent, | 25 | ipcMain, |
26 | } from 'electron'; | 26 | } from 'electron'; |
27 | import { readFile, readFileSync } from 'fs'; | 27 | import { readFile, readFileSync } from 'fs'; |
28 | import { autorun } from 'mobx'; | 28 | import { autorun } from 'mobx'; |
@@ -31,6 +31,7 @@ import { join } from 'path'; | |||
31 | import { | 31 | import { |
32 | ServiceToMainIpcMessage, | 32 | ServiceToMainIpcMessage, |
33 | unreadCount, | 33 | unreadCount, |
34 | WebSource, | ||
34 | } from '@sophie/service-shared'; | 35 | } from '@sophie/service-shared'; |
35 | import { | 36 | import { |
36 | browserViewBounds, | 37 | browserViewBounds, |
@@ -73,30 +74,26 @@ app.commandLine.appendSwitch( | |||
73 | // Remove sophie and electron from the user-agent string to avoid detection. | 74 | // Remove sophie and electron from the user-agent string to avoid detection. |
74 | const originalUserAgent = app.userAgentFallback; | 75 | const originalUserAgent = app.userAgentFallback; |
75 | const userAgent = originalUserAgent.replaceAll(/\s(sophie|Electron)\/\S+/g, ''); | 76 | const userAgent = originalUserAgent.replaceAll(/\s(sophie|Electron)\/\S+/g, ''); |
76 | const platformInUa = userAgent.match(/\((Win|Mac|X11; L)/); | 77 | const chromelessUserAgent = userAgent.replace(/ Chrome\/\S+/, ''); |
77 | let platform = 'Unknown'; | ||
78 | if (platformInUa !== null) { | ||
79 | switch (platformInUa[1]) { | ||
80 | case 'Win': | ||
81 | platform = 'Windows'; | ||
82 | break; | ||
83 | case 'Mac': | ||
84 | platform = 'macOS'; | ||
85 | break; | ||
86 | case 'X11; L': | ||
87 | platform = 'Linux'; | ||
88 | break; | ||
89 | } | ||
90 | } | ||
91 | const chromiumVersion = process.versions.chrome.split('.')[0]; | ||
92 | // Removing the electron version breaks redux devtools, so we only do this in production. | 78 | // Removing the electron version breaks redux devtools, so we only do this in production. |
93 | if (!isDevelopment) { | 79 | if (!isDevelopment) { |
94 | app.userAgentFallback = userAgent; | 80 | app.userAgentFallback = userAgent; |
95 | } | 81 | } |
96 | 82 | ||
83 | function getResourcePath(relativePath: string): string { | ||
84 | return join(__dirname, relativePath); | ||
85 | } | ||
86 | |||
87 | function getResourceUrl(relativePath: string): string { | ||
88 | return new URL(relativePath, `file://${__dirname}`).toString(); | ||
89 | } | ||
90 | |||
97 | let serviceInjectRelativePath = '../../service-inject/dist/index.cjs'; | 91 | let serviceInjectRelativePath = '../../service-inject/dist/index.cjs'; |
98 | let serviceInjectPath = join(__dirname, serviceInjectRelativePath); | 92 | let serviceInjectPath = getResourcePath(serviceInjectRelativePath); |
99 | let serviceInject: string = readFileSync(serviceInjectPath, 'utf8'); | 93 | let serviceInject: WebSource = { |
94 | code: readFileSync(serviceInjectPath, 'utf8'), | ||
95 | url: getResourceUrl(serviceInjectRelativePath), | ||
96 | }; | ||
100 | 97 | ||
101 | if (isDevelopment) { | 98 | if (isDevelopment) { |
102 | installDevToolsExtensions(app); | 99 | installDevToolsExtensions(app); |
@@ -111,12 +108,12 @@ function createWindow(): Promise<unknown> { | |||
111 | show: false, | 108 | show: false, |
112 | webPreferences: { | 109 | webPreferences: { |
113 | sandbox: true, | 110 | sandbox: true, |
114 | preload: join(__dirname, '../../preload/dist/index.cjs'), | 111 | preload: getResourcePath('../../preload/dist/index.cjs'), |
115 | }, | 112 | }, |
116 | }); | 113 | }); |
117 | 114 | ||
118 | if (isDevelopment) { | 115 | if (isDevelopment) { |
119 | // openDevToolsWhenReady(mainWindow); | 116 | openDevToolsWhenReady(mainWindow); |
120 | } | 117 | } |
121 | 118 | ||
122 | mainWindow.on('ready-to-show', () => { | 119 | mainWindow.on('ready-to-show', () => { |
@@ -131,7 +128,7 @@ function createWindow(): Promise<unknown> { | |||
131 | webPreferences: { | 128 | webPreferences: { |
132 | sandbox: true, | 129 | sandbox: true, |
133 | nodeIntegrationInSubFrames: true, | 130 | nodeIntegrationInSubFrames: true, |
134 | preload: join(__dirname, '../../service-preload/dist/index.cjs'), | 131 | preload: getResourcePath('../../service-preload/dist/index.cjs'), |
135 | partition: 'persist:service', | 132 | partition: 'persist:service', |
136 | }, | 133 | }, |
137 | }); | 134 | }); |
@@ -142,17 +139,6 @@ function createWindow(): Promise<unknown> { | |||
142 | }); | 139 | }); |
143 | mainWindow.setBrowserView(browserView); | 140 | mainWindow.setBrowserView(browserView); |
144 | 141 | ||
145 | browserView.webContents.on( | ||
146 | 'did-frame-navigate', | ||
147 | (_event, _url, _statusCode, _statusText, isMainFrame, _processId, routingId) => { | ||
148 | const { webContents: { mainFrame } } = browserView; | ||
149 | const frame = isMainFrame | ||
150 | ? mainFrame | ||
151 | : mainFrame.framesInSubtree.find((f) => f.routingId === routingId); | ||
152 | frame?.executeJavaScript(serviceInject).catch((err) => console.log(err)); | ||
153 | } | ||
154 | ); | ||
155 | |||
156 | webContents.on('ipc-message', (_event, channel, ...args) => { | 142 | webContents.on('ipc-message', (_event, channel, ...args) => { |
157 | try { | 143 | try { |
158 | switch (channel) { | 144 | switch (channel) { |
@@ -168,7 +154,7 @@ function createWindow(): Promise<unknown> { | |||
168 | case RendererToMainIpcMessage.ReloadAllServices: | 154 | case RendererToMainIpcMessage.ReloadAllServices: |
169 | readFile(serviceInjectPath, 'utf8', (err, data) => { | 155 | readFile(serviceInjectPath, 'utf8', (err, data) => { |
170 | if (err === null) { | 156 | if (err === null) { |
171 | serviceInject = data; | 157 | serviceInject.code = data; |
172 | } else { | 158 | } else { |
173 | console.error('Error while reloading', serviceInjectPath, err); | 159 | console.error('Error while reloading', serviceInjectPath, err); |
174 | } | 160 | } |
@@ -188,10 +174,17 @@ function createWindow(): Promise<unknown> { | |||
188 | webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); | 174 | webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); |
189 | }); | 175 | }); |
190 | 176 | ||
177 | ipcMain.on(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => { | ||
178 | event.returnValue = event.sender.id == browserView.webContents.id | ||
179 | ? serviceInject | ||
180 | : null; | ||
181 | }); | ||
182 | |||
191 | browserView.webContents.on('ipc-message', (_event, channel, ...args) => { | 183 | browserView.webContents.on('ipc-message', (_event, channel, ...args) => { |
192 | try { | 184 | try { |
193 | switch (channel) { | 185 | switch (channel) { |
194 | case ServiceToMainIpcMessage.ApiExposedInMainWorld: | 186 | case ServiceToMainIpcMessage.ApiExposedInMainWorld: |
187 | // Synchronous message must be handled with `ipcMain.on` | ||
195 | break; | 188 | break; |
196 | case ServiceToMainIpcMessage.SetUnreadCount: | 189 | case ServiceToMainIpcMessage.SetUnreadCount: |
197 | console.log('Unread count:', unreadCount.parse(args[0])); | 190 | console.log('Unread count:', unreadCount.parse(args[0])); |
@@ -205,37 +198,22 @@ function createWindow(): Promise<unknown> { | |||
205 | } | 198 | } |
206 | }); | 199 | }); |
207 | 200 | ||
208 | // Inject CSS to simulate `browserView.setBackgroundColor`. | ||
209 | // This is injected before the page loads, so the styles from the website will overwrite it. | ||
210 | browserView.webContents.on('did-navigate', () => { | ||
211 | browserView.webContents.insertCSS( | ||
212 | 'html { background-color: #fff; }', | ||
213 | { | ||
214 | cssOrigin: 'author', | ||
215 | }, | ||
216 | ); | ||
217 | }); | ||
218 | |||
219 | browserView.webContents.session.webRequest.onBeforeSendHeaders(({ url, requestHeaders }, callback) => { | 201 | browserView.webContents.session.webRequest.onBeforeSendHeaders(({ url, requestHeaders }, callback) => { |
220 | if (url.match(/accounts\.google/)) { | 202 | if (url.match(/^[^:]+:\/\/accounts\.google\.[^.\/]+\//)) { |
221 | requestHeaders['User-Agent'] = userAgent.replace(/ Chrome\/\S+/, ''); | 203 | requestHeaders['User-Agent'] = chromelessUserAgent; |
222 | } else { | 204 | } else { |
223 | requestHeaders['User-Agent'] = userAgent; | 205 | requestHeaders['User-Agent'] = userAgent; |
224 | } | 206 | } |
225 | requestHeaders['User-Agent'] = userAgent; | ||
226 | requestHeaders['Sec-CH-UA'] = `" Not A;Brand";v="99", "Chromium";v="${chromiumVersion}"`; | ||
227 | requestHeaders['Sec-CH-UA-Mobile'] = '?0'; | ||
228 | requestHeaders['Sec-CH-UA-Platform'] = platform; | ||
229 | callback({ requestHeaders }); | 207 | callback({ requestHeaders }); |
230 | }); | 208 | }); |
231 | 209 | ||
232 | const pageUrl = (isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined) | 210 | const pageUrl = (isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined) |
233 | ? import.meta.env.VITE_DEV_SERVER_URL | 211 | ? import.meta.env.VITE_DEV_SERVER_URL |
234 | : new URL('../renderer/dist/index.html', `file://${__dirname}`).toString(); | 212 | : getResourceUrl('../renderer/dist/index.html'); |
235 | 213 | ||
236 | return Promise.all([ | 214 | return Promise.all([ |
237 | mainWindow.loadURL(pageUrl), | 215 | mainWindow.loadURL(pageUrl), |
238 | browserView.webContents.loadURL('https://gmail.com').then(() => browserView.webContents.openDevTools()), | 216 | browserView.webContents.loadURL('https://git.marussy.com/sophie/about'), |
239 | ]); | 217 | ]); |
240 | } | 218 | } |
241 | 219 | ||
diff --git a/packages/service-inject/src/index.ts b/packages/service-inject/src/index.ts index f699f11..a7ada84 100644 --- a/packages/service-inject/src/index.ts +++ b/packages/service-inject/src/index.ts | |||
@@ -18,10 +18,4 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { shimUserAgentData } from './shims/userAgentData'; | 21 | export {} |
22 | |||
23 | try { | ||
24 | shimUserAgentData('96', 'Linux'); | ||
25 | } catch (err) { | ||
26 | console.log('Failed to execute injected script:', err); | ||
27 | } | ||
diff --git a/packages/service-inject/src/shims/userAgentData.ts b/packages/service-inject/src/shims/userAgentData.ts deleted file mode 100644 index 7e2c825..0000000 --- a/packages/service-inject/src/shims/userAgentData.ts +++ /dev/null | |||
@@ -1,103 +0,0 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import { | ||
22 | defineProtoProperty, | ||
23 | deleteProtoProperty, | ||
24 | simulateNativeClass, | ||
25 | simulateNativeFunction, | ||
26 | } from '../utils'; | ||
27 | |||
28 | export function shimUserAgentData(chromeVersion: string | null, platform: string): void { | ||
29 | const brands = [ | ||
30 | { | ||
31 | brand: ' Not A; Brand', | ||
32 | version: '99', | ||
33 | }, | ||
34 | ]; | ||
35 | if (chromeVersion !== null) { | ||
36 | brands.push({ | ||
37 | brand: 'Chromium', | ||
38 | version: '96', | ||
39 | }); | ||
40 | } | ||
41 | const mobile = false; | ||
42 | |||
43 | const simulatedNavigatorUa = simulateNativeClass('NavigatorUAData', function NavigatorUAData() { | ||
44 | // Nothing to initiailize. | ||
45 | }, { | ||
46 | brands: { | ||
47 | configurable: true, | ||
48 | enumerable: true, | ||
49 | get: simulateNativeFunction('brands', () => brands), | ||
50 | }, | ||
51 | mobile: { | ||
52 | configurable: true, | ||
53 | enumerable: true, | ||
54 | get: simulateNativeFunction('mobile', () => mobile), | ||
55 | }, | ||
56 | platform: { | ||
57 | configurable: true, | ||
58 | enumerable: true, | ||
59 | get: simulateNativeFunction('platform', () => platform), | ||
60 | }, | ||
61 | getHighEntropyValues: { | ||
62 | configurable: true, | ||
63 | enumerable: false, | ||
64 | value: simulateNativeFunction('getHighEntropyValues', (...args: unknown[]) => { | ||
65 | if (args.length == 0) { | ||
66 | throw new TypeError("Failed to execute 'getHighEntropyValues' on 'NavigatorUAData': 1 argument required, but only 0 present."); | ||
67 | } | ||
68 | const hints = Array.from(args[0] as Iterable<string>); | ||
69 | if (hints.length === 0) { | ||
70 | return {}; | ||
71 | } | ||
72 | const data: Record<string, unknown> = { | ||
73 | brands, | ||
74 | mobile, | ||
75 | } | ||
76 | if (hints.includes('platform')) { | ||
77 | data['platform'] = platform; | ||
78 | } | ||
79 | return Promise.resolve(data); | ||
80 | }) | ||
81 | }, | ||
82 | toJSON: { | ||
83 | configurable: true, | ||
84 | enumerable: false, | ||
85 | value: simulateNativeFunction('toJSON', () => ({ | ||
86 | brands, | ||
87 | mobile, | ||
88 | })), | ||
89 | writable: false, | ||
90 | }, | ||
91 | }); | ||
92 | |||
93 | const simulatedUserAgentData = Reflect.construct(simulatedNavigatorUa, []); | ||
94 | defineProtoProperty(globalThis.navigator, 'userAgentData', { | ||
95 | configurable: true, | ||
96 | enumerable: true, | ||
97 | get: simulateNativeFunction('userAgentData', () => simulatedUserAgentData), | ||
98 | }); | ||
99 | } | ||
100 | |||
101 | export function deleteUserAgentData(): void { | ||
102 | deleteProtoProperty(globalThis.navigator, 'userAgentData'); | ||
103 | } | ||
diff --git a/packages/service-inject/src/utils.ts b/packages/service-inject/src/utils.ts deleted file mode 100644 index 4bb3fba..0000000 --- a/packages/service-inject/src/utils.ts +++ /dev/null | |||
@@ -1,104 +0,0 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | /** | ||
22 | * Simulates a function defined in native code, i.e., one with | ||
23 | * `[native code]` in its `toString`. | ||
24 | * | ||
25 | * @param name The name of the function. | ||
26 | * @param f The function to transform. | ||
27 | * @return The transformed function. | ||
28 | */ | ||
29 | export function simulateNativeFunction<T, P extends unknown[]>( | ||
30 | name: string, | ||
31 | f: (this: null, ...args: P) => T, | ||
32 | ): (...args: P) => T { | ||
33 | // Bound functions say `[native code]`, but unfortunately they omit the function name: | ||
34 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString#description | ||
35 | // The type of `f` contains type variables, so we need some magic type casting. | ||
36 | const boundFunc = f.bind(null as ThisParameterType<typeof f>); | ||
37 | Object.defineProperty(boundFunc, 'name', { | ||
38 | configurable: true, | ||
39 | enumerable: false, | ||
40 | value: name, | ||
41 | writable: false, | ||
42 | }); | ||
43 | return boundFunc; | ||
44 | } | ||
45 | |||
46 | /** | ||
47 | * Simulates a native class available on `globalThis`. | ||
48 | * | ||
49 | * @param name The name of the class. | ||
50 | * @param constructor The constructor function. Must already be a constructor (a named `function`). | ||
51 | * @param properties The properties to define on the prototype. | ||
52 | */ | ||
53 | export function simulateNativeClass( | ||
54 | name: string, | ||
55 | constructor: () => void, | ||
56 | properties: PropertyDescriptorMap, | ||
57 | ) { | ||
58 | Object.defineProperties(constructor.prototype, { | ||
59 | [Symbol.toStringTag]: { | ||
60 | configurable: true, | ||
61 | enumerable: false, | ||
62 | value: name, | ||
63 | writable: false, | ||
64 | }, | ||
65 | ...properties, | ||
66 | }); | ||
67 | const simulatedConstructor = simulateNativeFunction(name, constructor); | ||
68 | Object.defineProperty(globalThis, name, { | ||
69 | configurable: true, | ||
70 | enumerable: true, | ||
71 | value: simulatedConstructor, | ||
72 | writable: true, | ||
73 | }); | ||
74 | return simulatedConstructor; | ||
75 | } | ||
76 | |||
77 | /** | ||
78 | * Defines a property on the prototype of an object. | ||
79 | * | ||
80 | * Only use this with singleton objects, e.g., `window.navigator`. | ||
81 | * | ||
82 | * @param o The object to modify. Must be a singleton. | ||
83 | * @param property The key of the property being defined or modified. | ||
84 | * @param attributes The descriptor of the property being defined or modified. | ||
85 | */ | ||
86 | export function defineProtoProperty( | ||
87 | o: object, | ||
88 | property: PropertyKey, | ||
89 | attributes: PropertyDescriptor, | ||
90 | ): void { | ||
91 | Object.defineProperty(Object.getPrototypeOf(o), property, attributes); | ||
92 | } | ||
93 | |||
94 | /** | ||
95 | * Deletes a property from the prototype of an object. | ||
96 | * | ||
97 | * Only use this with singleton objects, e.g., `window.navigator`. | ||
98 | * | ||
99 | * @param o The object to modify. Must be a singleton. | ||
100 | * @param property The key of the property being deleted. | ||
101 | */ | ||
102 | export function deleteProtoProperty(o: object, property: PropertyKey): void { | ||
103 | Reflect.deleteProperty(Object.getPrototypeOf(o), property); | ||
104 | } | ||
diff --git a/packages/service-preload/src/index.ts b/packages/service-preload/src/index.ts index 3f54c0b..e42c406 100644 --- a/packages/service-preload/src/index.ts +++ b/packages/service-preload/src/index.ts | |||
@@ -18,7 +18,22 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { ipcRenderer } from 'electron'; | 21 | import { ipcRenderer, webFrame } from 'electron'; |
22 | import { ServiceToMainIpcMessage } from '@sophie/service-shared'; | 22 | import { ServiceToMainIpcMessage, webSource } from '@sophie/service-shared'; |
23 | 23 | ||
24 | ipcRenderer.send(ServiceToMainIpcMessage.ApiExposedInMainWorld); | 24 | if (webFrame.parent === null) { |
25 | // Inject CSS to simulate `browserView.setBackgroundColor`. | ||
26 | // This is injected before the page loads, so the styles from the website will overwrite it. | ||
27 | webFrame.insertCSS('html { background-color: #fff; }'); | ||
28 | } | ||
29 | |||
30 | const injectSource = webSource.safeParse(ipcRenderer.sendSync(ServiceToMainIpcMessage.ApiExposedInMainWorld)); | ||
31 | if (injectSource.success) { | ||
32 | webFrame.executeJavaScriptInIsolatedWorld(0, [ | ||
33 | injectSource.data, | ||
34 | ]).catch((err) => { | ||
35 | console.log('Failed to inject source:', err); | ||
36 | }); | ||
37 | } else { | ||
38 | console.log('Invalid source to inject:', injectSource.error); | ||
39 | } | ||
diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts index c517959..564ebe8 100644 --- a/packages/service-shared/src/index.ts +++ b/packages/service-shared/src/index.ts | |||
@@ -22,7 +22,9 @@ export { ServiceToMainIpcMessage } from './ipc'; | |||
22 | 22 | ||
23 | export type { | 23 | export type { |
24 | UnreadCount, | 24 | UnreadCount, |
25 | WebSource, | ||
25 | } from './schemas'; | 26 | } from './schemas'; |
26 | export { | 27 | export { |
27 | unreadCount, | 28 | unreadCount, |
29 | webSource, | ||
28 | } from './schemas'; | 30 | } from './schemas'; |
diff --git a/packages/service-shared/src/schemas.ts b/packages/service-shared/src/schemas.ts index 1513e43..586750c 100644 --- a/packages/service-shared/src/schemas.ts +++ b/packages/service-shared/src/schemas.ts | |||
@@ -26,3 +26,10 @@ export const unreadCount = z.object({ | |||
26 | }); | 26 | }); |
27 | 27 | ||
28 | export type UnreadCount = z.infer<typeof unreadCount>; | 28 | export type UnreadCount = z.infer<typeof unreadCount>; |
29 | |||
30 | export const webSource = z.object({ | ||
31 | code: z.string(), | ||
32 | url: z.string().nonempty(), | ||
33 | }); | ||
34 | |||
35 | export type WebSource = z.infer<typeof webSource>; | ||