// Copyright (C) 2023 Vaughn Nugent // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { runtime } from "webextension-polyfill"; import { serializeError, deserializeError } from 'serialize-error'; import { JsonObject } from "type-fest"; import { cloneDeep, isArray, isObjectLike, set } from "lodash"; import { debugLog } from "@vnuge/vnlib.browser"; import { ChannelContext, createMessageChannel } from "../../messaging"; export interface BgRuntime { readonly state: T; onInstalled(callback: () => Promise): void; onConnected(callback: () => Promise): void; } export type FeatureApi = { [key: string]: (... args: any[]) => Promise }; export type SendMessageHandler = (action: string, data: any) => Promise export type VarArgsFunction = (...args: any[]) => T export type FeatureConstructor = () => IFeatureExport export type DummyApiExport = Array export interface IFeatureExport { /** * Initializes a feature for mapping in the background runtime context * @param bgRuntime The background runtime context * @returns The feature's background api handlers that maps to the foreground */ background(bgRuntime: BgRuntime): TFeature /** * Initializes the feature for mapping in any foreground runtime context * @returns The feature's foreground api stub methods for mapping. They must * match the background api */ foreground(): TFeature } export interface IForegroundUnwrapper { /** * Unwraps a foreground feature and builds it's method bindings to * the background handler * @param feature The foreground feature that will be mapped to it's * background handlers * @returns The foreground feature's api stub methods */ use: (feature: FeatureConstructor) => T } export interface IBackgroundWrapper { register(features: FeatureConstructor[]): void } export interface ProtectedFunction extends Function { readonly protection: ChannelContext[] } export const optionsOnly = (func: T): T => protectMethod(func, 'options'); export const popupOnly = (func: T): T => protectMethod(func, 'popup'); export const contentScriptOnly = (func: T): T => protectMethod(func, 'content-script'); export const popupAndOptionsOnly = (func: T): T => protectMethod(func, 'popup', 'options'); export const protectMethod = (func: T, ...protection: ChannelContext[]): T => { (func as any).protection = protection return func; } /** * Creates a background runtime context for registering background * script feature api handlers */ export const useBackgroundFeatures = (state: TState): IBackgroundWrapper => { const { openOnMessageChannel } = createMessageChannel('background'); const { onMessage } = openOnMessageChannel() const rt = { state, onConnected: runtime.onConnect.addListener, onInstalled: runtime.onInstalled.addListener, } as BgRuntime /** * Each plugin will export named methods. Background methods * are captured and registered as on-message handlers that * correspond to the method name. Foreground method calls * are redirected to the send-message of the same unique name */ return{ register: (features: FeatureConstructor[]) => { //Loop through features for (const feature of features) { //Init feature const f = feature().background(rt) //Get all exported function for (const externFuncName in f) { //get exported function const func = f[externFuncName] as Function const onMessageFuncName = `${feature.name}-${externFuncName}` //register method with api onMessage(onMessageFuncName, async (sender, payload) => { try { if ((func as ProtectedFunction).protection && !(func as ProtectedFunction).protection.includes(sender)) { throw new Error(`Unauthorized external call to ${onMessageFuncName}`) } const res = await func(...payload) if(isArray(res)){ return [...res] } else if(isObjectLike(res)){ return { ...res } } else{ return res } } catch (e: any) { debugLog(`Error in method ${onMessageFuncName}`, e) const s = serializeError(e) return { bridgeMessageException: JSON.stringify(s), axiosResponseError: JSON.stringify(e.response) } } }); } } } } } /** * Creates a foreground runtime context for unwrapping foreground stub * methods and redirecting them to thier background handler */ export const useForegoundFeatures = (context: ChannelContext): IForegroundUnwrapper => { const { openChannel } = createMessageChannel(context); const { sendMessage } = openChannel() /** * The goal of this function is to get the foreground interface object * that should match the background implementation. All methods are * intercepted and redirected to the background via send-message */ return{ use: (feature:FeatureConstructor): T => { //Register the feature const api = feature().foreground() const featureName = feature.name const proxied : T = {} as T //Loop through all methods for(const funcName in api){ //Create proxy for each method set(proxied, funcName, async (...args:any) => { //Check for exceptions const result = await sendMessage(`${featureName}-${funcName}`, cloneDeep(args)) as any if(result?.bridgeMessageException){ const str = JSON.parse(result.bridgeMessageException) const err = deserializeError(str) //Recover axios response if(result.axiosResponseError){ (err as any).response = JSON.parse(result.axiosResponseError) } throw err; } return result; }) } return proxied; } } } export const exportForegroundApi = (args: DummyApiExport): () => T => { //Create the type from the array of type properties const type = {} as T //Loop through all properties for(const prop of args){ //Default the property to an implementation error type[prop] = (async () => { throw new Error(`Method ${prop.toString()} not implemented`) }) as any } return () => type }