Skip to content

Commit

Permalink
feat: script injection via browser extension
Browse files Browse the repository at this point in the history
  • Loading branch information
ScarletFlash committed Nov 10, 2024
1 parent 5c2ed08 commit bd39c21
Show file tree
Hide file tree
Showing 21 changed files with 265 additions and 43 deletions.
3 changes: 1 addition & 2 deletions core/src/consy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CommandDefinition, EXPOSED_KEYS_PROPERTY_NAME, InteractiveObject } from '@consy/declarations';
import { Acessor } from './accessor';
import { Acessor, CommandDefinition, EXPOSED_KEYS_PROPERTY_NAME, InteractiveObject } from '@consy/declarations';
import { InteractiveObjectBuilder } from './interactive-object-builder';

export class Consy<K extends string = string> {
Expand Down
3 changes: 1 addition & 2 deletions core/src/interactive-object-builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { CallableCommand, CommandDefinition, InteractiveObject } from '@consy/declarations';
import { Acessor, CallableCommand, CommandDefinition, InteractiveObject } from '@consy/declarations';
import { getValidatedParameterizableCommandParams, isParameterizableCommand } from '@consy/utilities';
import { Acessor } from './accessor';

export class InteractiveObjectBuilder {
readonly #payload: InteractiveObject;
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions declarations/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './helpers/accessor';
export * from './constants/exposed-keys-property-name.const';
export * from './types/callable-command.type';
export * from './types/command-definition.type';
Expand Down
12 changes: 10 additions & 2 deletions examples/src/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<!doctype html>

<html lang="en">
<html
lang="en"
class="w-screen h-screen"
>
<head>
<meta charset="utf-8" />
<meta
Expand All @@ -14,5 +17,10 @@
/>
</head>

<body class="bg-[#ff0103]"></body>
<body
class="size-full p-4 flex flex-col items-center justify-center gap-4 *:border *:border-neutral-900 *:p-2 *:w-full"
>
<a href="/non-parameterizable-command/">Non-parameterizable command example</a>
<a href="/parameterizable-command/">Parameterizable command example</a>
</body>
</html>
9 changes: 9 additions & 0 deletions examples/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"start": {
"dependsOn": ["^build"]
}
}
}
2 changes: 2 additions & 0 deletions extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"service_worker": "src/service-worker.ts"
},
"description": "Consy UI extension",
"host_permissions": ["https://*/*", "http://*/*"],

"manifest_version": 3,
"name": "Consy",
"permissions": ["scripting", "activeTab"],
Expand Down
8 changes: 6 additions & 2 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
"main": "src/index.ts",
"typings": "src/index.ts",
"scripts": {
"compiler__run": "tsc --project tsconfig.json",
"compiler__check": "tsc --project tsconfig.json --noEmit",
"compiler__run": "",
"compiler__run:build": "tsc --project tsconfig.build.json",
"compiler__run:scripts": "tsc --project tsconfig.scripts.json",
"compiler__check": "",
"compiler__check:build": "tsc --project tsconfig.build.json --noEmit",
"compiler__check:scripts": "tsc --project tsconfig.scripts.json --noEmit",
"formatter__check": "prettier --config node_modules/@consy/configs/prettier/config.json --ignore-path node_modules/@consy/configs/prettier/ignore --cache --cache-location .prettiercache --cache-strategy content --check .",
"formatter__fix": "prettier --config node_modules/@consy/configs/prettier/config.json --ignore-path node_modules/@consy/configs/prettier/ignore --cache --cache-location .prettiercache --cache-strategy content --write .",
"build": "ts-node --project tsconfig.scripts.json ./build.script.ts"
Expand Down
57 changes: 57 additions & 0 deletions extension/src/communication/message-bus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Message } from './message';
import { MessageData } from './message-data';

type SubscriptionCallback<T = unknown> = (payload: T) => void;
type MessageEventListener = (event: MessageEvent) => void;

export class MessageBus {
#channel: BroadcastChannel = new BroadcastChannel('__consy-message-bus');

#eventListenerBySubscriptionCallback: Map<SubscriptionCallback<MessageData>, MessageEventListener> = new Map<
SubscriptionCallback<MessageData>,
MessageEventListener
>();

public publish(message: MessageData): void {
console.log('Publishing message', message);

this.#channel.postMessage(message);
}

public subscribe(callback: SubscriptionCallback<MessageData>): void {
if (this.#eventListenerBySubscriptionCallback.has(callback)) {
return;
}

const messageEventListener: MessageEventListener = (event: MessageEvent) => {
const payload: unknown = event.data;
if (!Message.isAbstractMessageData(payload)) {
throw new Error(`Received event doesn't contain a compatible message data object`);
}
callback(payload);
};
this.#channel.addEventListener('message', messageEventListener);
this.#eventListenerBySubscriptionCallback.set(callback, messageEventListener);
}

public unsubscribe(callback: SubscriptionCallback<MessageData>): void {
const messageEventListener: MessageEventListener | undefined =
this.#eventListenerBySubscriptionCallback.get(callback);
if (messageEventListener === undefined) {
return;
}

this.#channel.removeEventListener('message', messageEventListener);
this.#eventListenerBySubscriptionCallback.delete(callback);
}

public close(): void {
this.#eventListenerBySubscriptionCallback.forEach(
(_listener: MessageEventListener, callback: SubscriptionCallback<MessageData>) => {
this.unsubscribe(callback);
}
);

this.#channel.close();
}
}
4 changes: 4 additions & 0 deletions extension/src/communication/message-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface MessageData<P extends {} = {}> {
readonly type: Lowercase<string>;
readonly payload: P;
}
11 changes: 11 additions & 0 deletions extension/src/communication/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MessageData } from './message-data';

export abstract class Message<P extends {}> implements MessageData<P> {
abstract readonly type: Lowercase<string>;

constructor(public readonly payload: P) {}

public static isAbstractMessageData(message: unknown): message is MessageData {
return typeof message === 'object' && message !== null && 'type' in message;
}
}
16 changes: 16 additions & 0 deletions extension/src/communication/messages/mounted-instances.message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Message } from '../message';

interface MountedInstance {
key: string;
}

export class MountedInstancesMessage extends Message<MountedInstance[]> {
public static readonly type: 'mounted-instances' = 'mounted-instances';
public readonly type: typeof MountedInstancesMessage.type = MountedInstancesMessage.type;

public static isMessageData(
message: unknown
): message is Pick<InstanceType<typeof MountedInstancesMessage>, 'type' | 'payload'> {
return Message.isAbstractMessageData(message) && message.type === MountedInstancesMessage.type;
}
}
16 changes: 16 additions & 0 deletions extension/src/communication/messages/not-mounted.message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Message } from '../message';

export class NotMountedMessage extends Message<{}> {
public static readonly type: 'not-mounted' = 'not-mounted';
public readonly type: typeof NotMountedMessage.type = NotMountedMessage.type;

constructor() {
super({});
}

public static isMessageData(
message: unknown
): message is Pick<InstanceType<typeof NotMountedMessage>, 'type' | 'payload'> {
return Message.isAbstractMessageData(message) && message.type === NotMountedMessage.type;
}
}
16 changes: 16 additions & 0 deletions extension/src/injected-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Acessor, EXPOSED_KEYS_PROPERTY_NAME } from '@consy/declarations';
import { MessageBus } from './communication/message-bus';
import { MountedInstancesMessage } from './communication/messages/mounted-instances.message';
import { NotMountedMessage } from './communication/messages/not-mounted.message';

const messageBus: MessageBus = new MessageBus();

(() => {
const exposedKeysAccessor: Acessor<string[], string> = new Acessor<string[], string>(window);
if (!exposedKeysAccessor.isMounted(EXPOSED_KEYS_PROPERTY_NAME)) {
messageBus.publish(new NotMountedMessage());
return;
}
const exposedKeys: string[] = exposedKeysAccessor.getValue(EXPOSED_KEYS_PROPERTY_NAME);
messageBus.publish(new MountedInstancesMessage(exposedKeys.map((key: string) => ({ key }))));
})();
24 changes: 23 additions & 1 deletion extension/src/popup.ts
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
console.log('This is a popup!');
import { MessageBus } from './communication/message-bus';
import { MountedInstancesMessage } from './communication/messages/mounted-instances.message';
import { NotMountedMessage } from './communication/messages/not-mounted.message';

const messageBus: MessageBus = new MessageBus();

messageBus.subscribe((messageData: unknown) => {
switch (true) {
case NotMountedMessage.isMessageData(messageData): {
console.log('Not mounted');
return;
}

case MountedInstancesMessage.isMessageData(messageData): {
console.log(messageData.payload);
return;
}

default: {
return;
}
}
});
74 changes: 45 additions & 29 deletions extension/src/service-worker.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,54 @@
import { PromiseRejector } from '@consy/declarations';

function getActiveTab(): Promise<chrome.tabs.Tab> {
return new Promise((resolve: PromiseRejector<chrome.tabs.Tab>, reject: PromiseRejector<Error>) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => {
const firstActiveTab: chrome.tabs.Tab | undefined = tabs[0];
if (firstActiveTab === undefined) {
return reject(new Error('No active tab found'));
}
import { MessageBus } from './communication/message-bus';
import { MountedInstancesMessage } from './communication/messages/mounted-instances.message';
import { NotMountedMessage } from './communication/messages/not-mounted.message';

resolve(firstActiveTab);
});
});
}
const messageBus: MessageBus = new MessageBus();

Promise.resolve()
.then(async () => {
const { id: tabId, url: activeTabUrl }: chrome.tabs.Tab = await getActiveTab();
messageBus.subscribe((messageData: unknown) => {
switch (true) {
case NotMountedMessage.isMessageData(messageData): {
console.log('Not mounted');
return;
}

if (activeTabUrl === undefined || activeTabUrl.startsWith('chrome://')) {
case MountedInstancesMessage.isMessageData(messageData): {
console.log(messageData.payload);
return;
}

if (tabId === undefined) {
throw new Error('No active tab ID found');
default: {
return;
}
}
});

try {
chrome.tabs.onUpdated.addListener(
async (
tabId: number,
changeInfo: {
status?: chrome.tabs.TabStatus;
},
tab: chrome.tabs.Tab
) => {
if (changeInfo.status !== 'complete' || tab.url === undefined) {
return;
}

if (tabId === undefined) {
throw new Error('No active tab ID found');
}

await chrome.scripting.executeScript({
target: { tabId, allFrames: true },
func: () => {
console.log('injected script');
const [injectionResult]: chrome.scripting.InjectionResult[] = await chrome.scripting.executeScript({
target: { tabId, allFrames: true },
files: ['injected-script.js']
});

if (injectionResult === undefined) {
throw new Error('Script injection failed');
}
});
console.log('script injected in all frames');
})
.catch((error: unknown) => {
console.error(error instanceof Error ? error.message : 'Unknown error');
});
}
);
} catch (error: unknown) {
console.error(error instanceof Error ? error.message : 'Unknown error');
}
5 changes: 2 additions & 3 deletions extension/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "./tsc-out",
"types": ["chrome-types"],
"declaration": false
"outDir": "./tsc-out/build",
"types": ["chrome-types"]
},
"extends": "@consy/configs/typescript/browser.json",
"include": ["./src"]
Expand Down
3 changes: 3 additions & 0 deletions extension/tsconfig.scripts.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "./tsc-out/scripts"
},
"extends": "@consy/configs/typescript/node.json",
"include": ["*.script.ts"]
}
34 changes: 34 additions & 0 deletions extension/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"compiler__run:build": {
"cache": true,
"persistent": false,
"inputs": ["./src/**/*.ts", "./tsconfig.build.json"],
"outputs": [],
"outputLogs": "errors-only"
},
"compiler__run:scripts": {
"cache": true,
"persistent": false,
"inputs": ["*.script.ts", "tsconfig.scripts.json"],
"outputs": [],
"outputLogs": "errors-only"
},
"compiler__run": {
"dependsOn": ["compiler__run:build", "compiler__run:scripts"]
},
"compiler__check:build": {
"cache": true,
"persistent": false,
"inputs": ["./src/**/*.ts", "./tsconfig.build.json"],
"outputs": [],
"outputLogs": "errors-only"
},
"compiler__check:scripts": {
"cache": true,
"persistent": false,
"inputs": ["*.script.ts", "tsconfig.scripts.json"],
"outputs": [],
"outputLogs": "errors-only"
},
"compiler__check": {
"dependsOn": ["compiler__check:build", "compiler__check:scripts"]
},
"build": {
"inputs": ["src/index.ts", "build.script.ts", "tsconfig.*.json"],
"outputs": ["./dist"],
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"formatter__fix:root": "prettier --config node_modules/@consy/configs/prettier/config.json --ignore-path node_modules/@consy/configs/prettier/ignore --cache --cache-location .prettiercache --cache-strategy content --write ./*",
"extract-api": "pnpm turbo run extract-api",
"build": "pnpm turbo run build",
"start": "pnpm turbo run start",
"turbo": "turbo"
},
"devDependencies": {
Expand All @@ -19,8 +20,8 @@
"prettier": "catalog:codebase"
},
"engines": {
"pnpm": "9.10.0",
"pnpm": "9.12.0",
"node": "22.8.0"
},
"packageManager": "pnpm@9.10.0"
"packageManager": "pnpm@9.12.0"
}
Loading

0 comments on commit bd39c21

Please sign in to comment.