diff --git a/SECURITY.md b/SECURITY.md
index b06b9273..c324ffbf 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -31,9 +31,9 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
- **Content Security Policies (CSP):**
privacy.sexy actively follows security guidelines from the Open Web Application Security Project (OWASP) at strictest level.
This approach protects against attacks like Cross Site Scripting (XSS) and data injection.
-- **Context Isolation:**
- The desktop application isolates different code sections based on their access level.
- This separation prevents attackers from introducing harmful code into the app, known as injection attacks.
+- **Host System Access Control:**
+ The desktop application segregates code sections based on their access levels.
+ This provides a critical defense mechanism, prevents attackers from introducing harmful code into the app, known as injection attacks.
### Update Security and Integrity
diff --git a/docs/architecture.md b/docs/architecture.md
index 17fdf161..d0d6c881 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -27,13 +27,14 @@ Application uses highly decoupled models & services in different DDD layers:
**Domain layer**:
- Serves as the system's core and central truth.
-- Facilitates communication between the application and presentation layers through the domain model.
+- It should be independent of other layers and encapsulate the core business concepts.
**Infrastructure layer**:
-- Manages technical implementations without dependencies on other layers or domain knowledge.
+- Provides technical implementations.
+- Depends on the application and domain layers in terms of interfaces and contracts but should not include business logic.
-
+
### Application state
diff --git a/docs/presentation.md b/docs/presentation.md
index d5cdb281..1ab29b41 100644
--- a/docs/presentation.md
+++ b/docs/presentation.md
@@ -92,6 +92,8 @@ Shared components include:
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
+Host system access is strictly controlled. The [`preloader`](./../src/presentation/electron/preload/) isolates logic that interacts with the host system. These functionalities are then securely exposed to the renderer process (Vue application) using context-bridging. [`ApiContextBridge.ts`](./../src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts) handles the configuration of the exposed APIs, ensuring a secure bridge between the Electron and Vue layers.
+
## Styles
### Style location
diff --git a/img/architecture/app-ddd.drawio b/img/architecture/app-ddd.drawio
deleted file mode 100644
index 4df829db..00000000
--- a/img/architecture/app-ddd.drawio
+++ /dev/null
@@ -1 +0,0 @@
-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
\ No newline at end of file
diff --git a/img/architecture/app-ddd.drawio.png b/img/architecture/app-ddd.drawio.png
new file mode 100644
index 00000000..e53e0db1
Binary files /dev/null and b/img/architecture/app-ddd.drawio.png differ
diff --git a/img/architecture/app-ddd.png b/img/architecture/app-ddd.png
deleted file mode 100644
index 2deaa2e2..00000000
Binary files a/img/architecture/app-ddd.png and /dev/null differ
diff --git a/src/TypeHelpers.ts b/src/TypeHelpers.ts
index 562eb95c..c41522a1 100644
--- a/src/TypeHelpers.ts
+++ b/src/TypeHelpers.ts
@@ -14,3 +14,35 @@ export type ConstructorArguments =
export type FunctionKeys = {
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
}[keyof T];
+
+export function isString(value: unknown): value is string {
+ return typeof value === 'string';
+}
+
+export function isNumber(value: unknown): value is number {
+ return typeof value === 'number';
+}
+
+export function isBoolean(value: unknown): value is boolean {
+ return typeof value === 'boolean';
+}
+
+export function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
+ return typeof value === 'function';
+}
+
+export function isArray(value: unknown): value is Array {
+ return Array.isArray(value);
+}
+
+export function isPlainObject(
+ variable: unknown,
+): variable is object & Record {
+ return Boolean(variable) // the data type of null is an object
+ && typeof variable === 'object'
+ && !Array.isArray(variable);
+}
+
+export function isNullOrUndefined(value: unknown): value is (null | undefined) {
+ return typeof value === 'undefined' || value === null;
+}
diff --git a/src/application/CodeRunner.ts b/src/application/CodeRunner.ts
new file mode 100644
index 00000000..6c08a6d2
--- /dev/null
+++ b/src/application/CodeRunner.ts
@@ -0,0 +1,7 @@
+import { OperatingSystem } from '@/domain/OperatingSystem';
+
+export interface CodeRunner {
+ runCode(
+ code: string, folderName: string, fileExtension: string, os: OperatingSystem,
+ ): Promise;
+}
diff --git a/src/application/Common/CustomError.ts b/src/application/Common/CustomError.ts
index b219a945..8a31884d 100644
--- a/src/application/Common/CustomError.ts
+++ b/src/application/Common/CustomError.ts
@@ -1,3 +1,5 @@
+import { isFunction } from '@/TypeHelpers';
+
/*
Provides a unified and resilient way to extend errors across platforms.
@@ -50,8 +52,3 @@ function ensureStackTrace(target: Error) {
}
captureStackTrace(target, target.constructor);
}
-
-// eslint-disable-next-line @typescript-eslint/ban-types
-function isFunction(func: unknown): func is Function {
- return typeof func === 'function';
-}
diff --git a/src/application/Common/Enum.ts b/src/application/Common/Enum.ts
index b7f37da7..74f42218 100644
--- a/src/application/Common/Enum.ts
+++ b/src/application/Common/Enum.ts
@@ -1,3 +1,5 @@
+import { isString } from '@/TypeHelpers';
+
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
export type EnumType = number | string;
export type EnumVariable
@@ -23,7 +25,7 @@ function parseEnumValue(
if (!value) {
throw new Error(`missing ${enumName}`);
}
- if (typeof value !== 'string') {
+ if (!isString(value)) {
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
}
const casedValue = getEnumNames(enumVariable)
@@ -40,7 +42,7 @@ export function getEnumNames
): string[] {
return Object
.values(enumVariable)
- .filter((enumMember) => typeof enumMember === 'string') as string[];
+ .filter((enumMember): enumMember is string => isString(enumMember));
}
export function getEnumValues(
diff --git a/src/application/Parser/DocumentationParser.ts b/src/application/Parser/DocumentationParser.ts
index 9d1b0ca3..2a177fb2 100644
--- a/src/application/Parser/DocumentationParser.ts
+++ b/src/application/Parser/DocumentationParser.ts
@@ -1,4 +1,5 @@
import type { DocumentableData, DocumentationData } from '@/application/collections/';
+import { isString, isArray } from '@/TypeHelpers';
export function parseDocs(documentable: DocumentableData): readonly string[] {
const { docs } = documentable;
@@ -14,11 +15,9 @@ function addDocs(
docs: DocumentationData,
container: DocumentationContainer,
): DocumentationContainer {
- if (docs instanceof Array) {
- if (docs.length > 0) {
- container.addParts(docs);
- }
- } else if (typeof docs === 'string') {
+ if (isArray(docs)) {
+ docs.forEach((doc) => container.addPart(doc));
+ } else if (isString(docs)) {
container.addPart(docs);
} else {
throwInvalidType();
@@ -29,27 +28,21 @@ function addDocs(
class DocumentationContainer {
private readonly parts = new Array();
- public addPart(documentation: string) {
+ public addPart(documentation: unknown): void {
if (!documentation) {
throw Error('missing documentation');
}
- if (typeof documentation !== 'string') {
+ if (!isString(documentation)) {
throwInvalidType();
}
this.parts.push(documentation);
}
- public addParts(parts: readonly string[]) {
- for (const part of parts) {
- this.addPart(part);
- }
- }
-
public getAll(): ReadonlyArray {
return this.parts;
}
}
-function throwInvalidType() {
+function throwInvalidType(): never {
throw new Error('docs field (documentation) must be an array of strings');
}
diff --git a/src/application/Parser/NodeValidation/NodeValidator.ts b/src/application/Parser/NodeValidation/NodeValidator.ts
index 4c154dbd..3d06932c 100644
--- a/src/application/Parser/NodeValidation/NodeValidator.ts
+++ b/src/application/Parser/NodeValidation/NodeValidator.ts
@@ -1,3 +1,4 @@
+import { isString } from '@/TypeHelpers';
import { INodeDataErrorContext, NodeDataError } from './NodeDataError';
import { NodeData } from './NodeData';
@@ -13,7 +14,7 @@ export class NodeValidator {
'missing name',
)
.assert(
- () => typeof nameValue === 'string',
+ () => isString(nameValue),
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
);
}
diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts
index 59c8382e..2e613041 100644
--- a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts
+++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts
@@ -1,4 +1,5 @@
import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/';
+import { isArray, isPlainObject } from '@/TypeHelpers';
import { FunctionCall } from './FunctionCall';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
@@ -10,13 +11,13 @@ export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
}
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
- if (typeof calls !== 'object') {
- throw new Error('called function(s) must be an object');
+ if (!isPlainObject(calls) && !isArray(calls)) {
+ throw new Error('called function(s) must be an object or array');
}
- if (calls instanceof Array) {
+ if (isArray(calls)) {
return calls as FunctionCallData[];
}
- const singleCall = calls;
+ const singleCall = calls as FunctionCallData;
return [singleCall];
}
diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts
index 5d978111..4a9af7cc 100644
--- a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts
+++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts
@@ -6,6 +6,7 @@ import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValida
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
+import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection';
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
@@ -121,8 +122,11 @@ function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
}
function ensureExpectedParametersType(functions: readonly FunctionData[]) {
+ const hasValidParameters = (
+ func: FunctionData,
+ ) => isNullOrUndefined(func.parameters) || isArrayOfObjects(func.parameters);
const unexpectedFunctions = functions
- .filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
+ .filter((func) => !hasValidParameters(func));
if (unexpectedFunctions.length) {
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
throw new Error(errorMessage);
@@ -130,8 +134,7 @@ function ensureExpectedParametersType(functions: readonly FunctionData[]) {
}
function isArrayOfObjects(value: unknown): boolean {
- return Array.isArray(value)
- && value.every((item) => typeof item === 'object');
+ return isArray(value) && value.every((item) => isPlainObject(item));
}
function printNames(holders: readonly FunctionData[]) {
diff --git a/src/infrastructure/SystemOperations/NodeSystemOperations.ts b/src/infrastructure/CodeRunner/SystemOperations/NodeSystemOperations.ts
similarity index 65%
rename from src/infrastructure/SystemOperations/NodeSystemOperations.ts
rename to src/infrastructure/CodeRunner/SystemOperations/NodeSystemOperations.ts
index 6979ec61..88cbf747 100644
--- a/src/infrastructure/SystemOperations/NodeSystemOperations.ts
+++ b/src/infrastructure/CodeRunner/SystemOperations/NodeSystemOperations.ts
@@ -1,10 +1,10 @@
-import { tmpdir } from 'os';
-import { join } from 'path';
-import { chmod, mkdir, writeFile } from 'fs/promises';
-import { exec } from 'child_process';
-import { ISystemOperations } from './ISystemOperations';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { chmod, mkdir, writeFile } from 'node:fs/promises';
+import { exec } from 'node:child_process';
+import { SystemOperations } from './SystemOperations';
-export function createNodeSystemOperations(): ISystemOperations {
+export function createNodeSystemOperations(): SystemOperations {
return {
operatingSystem: {
getTempDirectory: () => tmpdir(),
@@ -33,7 +33,14 @@ export function createNodeSystemOperations(): ISystemOperations {
) => writeFile(filePath, data),
},
command: {
- execute: (command) => exec(command),
+ execute: (command) => new Promise((resolve, reject) => {
+ exec(command, (error) => {
+ if (error) {
+ reject(error);
+ }
+ resolve();
+ });
+ }),
},
};
}
diff --git a/src/infrastructure/CodeRunner/SystemOperations/SystemOperations.ts b/src/infrastructure/CodeRunner/SystemOperations/SystemOperations.ts
new file mode 100644
index 00000000..0d2d9d1f
--- /dev/null
+++ b/src/infrastructure/CodeRunner/SystemOperations/SystemOperations.ts
@@ -0,0 +1,24 @@
+export interface SystemOperations {
+ readonly operatingSystem: OperatingSystemOps;
+ readonly location: LocationOps;
+ readonly fileSystem: FileSystemOps;
+ readonly command: CommandOps;
+}
+
+export interface OperatingSystemOps {
+ getTempDirectory(): string;
+}
+
+export interface LocationOps {
+ combinePaths(...pathSegments: string[]): string;
+}
+
+export interface CommandOps {
+ execute(command: string): Promise;
+}
+
+export interface FileSystemOps {
+ setFilePermissions(filePath: string, mode: string | number): Promise;
+ createDirectory(directoryPath: string, isRecursive?: boolean): Promise;
+ writeToFile(filePath: string, data: string): Promise;
+}
diff --git a/src/infrastructure/CodeRunner.ts b/src/infrastructure/CodeRunner/TemporaryFileCodeRunner.ts
similarity index 65%
rename from src/infrastructure/CodeRunner.ts
rename to src/infrastructure/CodeRunner/TemporaryFileCodeRunner.ts
index c6135c7d..5a2714b3 100644
--- a/src/infrastructure/CodeRunner.ts
+++ b/src/infrastructure/CodeRunner/TemporaryFileCodeRunner.ts
@@ -1,18 +1,19 @@
-import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem';
-import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations';
+import { CodeRunner } from '@/application/CodeRunner';
+import { SystemOperations } from './SystemOperations/SystemOperations';
+import { createNodeSystemOperations } from './SystemOperations/NodeSystemOperations';
-export class CodeRunner {
+export class TemporaryFileCodeRunner implements CodeRunner {
constructor(
- private readonly system = getWindowInjectedSystemOperations(),
- private readonly environment = RuntimeEnvironment.CurrentEnvironment,
+ private readonly system: SystemOperations = createNodeSystemOperations(),
) { }
- public async runCode(code: string, folderName: string, fileExtension: string): Promise {
- const { os } = this.environment;
- if (os === undefined) {
- throw new Error('Unidentified operating system');
- }
+ public async runCode(
+ code: string,
+ folderName: string,
+ fileExtension: string,
+ os: OperatingSystem,
+ ): Promise {
const dir = this.system.location.combinePaths(
this.system.operatingSystem.getTempDirectory(),
folderName,
@@ -22,7 +23,7 @@ export class CodeRunner {
await this.system.fileSystem.writeToFile(filePath, code);
await this.system.fileSystem.setFilePermissions(filePath, '755');
const command = getExecuteCommand(filePath, os);
- this.system.command.execute(command);
+ await this.system.command.execute(command);
}
}
diff --git a/src/infrastructure/Entity/BaseEntity.ts b/src/infrastructure/Entity/BaseEntity.ts
index 90e1a6f7..5c3958cf 100644
--- a/src/infrastructure/Entity/BaseEntity.ts
+++ b/src/infrastructure/Entity/BaseEntity.ts
@@ -1,8 +1,9 @@
+import { isNumber } from '@/TypeHelpers';
import { IEntity } from './IEntity';
export abstract class BaseEntity implements IEntity {
protected constructor(public id: TId) {
- if (typeof id !== 'number' && !id) {
+ if (!isNumber(id) && !id) {
throw new Error('Id cannot be null or empty');
}
}
diff --git a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts
index 2f01af12..4b20ae17 100644
--- a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts
+++ b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts
@@ -1,3 +1,4 @@
+import { isBoolean, isFunction } from '@/TypeHelpers';
import { IEnvironmentVariables } from './IEnvironmentVariables';
/* Validation is externalized to keep the environment objects simple */
@@ -15,7 +16,7 @@ export function validateEnvironmentVariables(environment: IEnvironmentVariables)
function getKeysMissingValues(keyValuePairs: Record): string[] {
return Object.entries(keyValuePairs)
.reduce((acc, [key, value]) => {
- if (!value && typeof value !== 'boolean') {
+ if (!value && !isBoolean(value)) {
acc.push(key);
}
return acc;
@@ -38,7 +39,7 @@ function capturePropertyValues(instance: object): Record {
// Capture getter properties from the instance's prototype
for (const [key, descriptor] of Object.entries(descriptors)) {
- if (typeof descriptor.get === 'function') {
+ if (isFunction(descriptor.get)) {
obj[key] = descriptor.get.call(instance);
}
}
diff --git a/src/infrastructure/Log/ElectronLogger.ts b/src/infrastructure/Log/ElectronLogger.ts
index 7cbdfb14..c25f68e9 100644
--- a/src/infrastructure/Log/ElectronLogger.ts
+++ b/src/infrastructure/Log/ElectronLogger.ts
@@ -2,14 +2,8 @@ import log from 'electron-log/main';
import { Logger } from '@/application/Common/Log/Logger';
import type { LogFunctions } from 'electron-log';
-// Using plain-function rather than class so it can be used in Electron's context-bridging.
export function createElectronLogger(logger: LogFunctions = log): Logger {
- return {
- info: (...params) => logger.info(...params),
- debug: (...params) => logger.debug(...params),
- warn: (...params) => logger.warn(...params),
- error: (...params) => logger.error(...params),
- };
+ return logger;
}
export const ElectronLogger = createElectronLogger();
diff --git a/src/infrastructure/SystemOperations/ISystemOperations.ts b/src/infrastructure/SystemOperations/ISystemOperations.ts
deleted file mode 100644
index 8915d0f5..00000000
--- a/src/infrastructure/SystemOperations/ISystemOperations.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export interface ISystemOperations {
- readonly operatingSystem: IOperatingSystemOps;
- readonly location: ILocationOps;
- readonly fileSystem: IFileSystemOps;
- readonly command: ICommandOps;
-}
-
-export interface IOperatingSystemOps {
- getTempDirectory(): string;
-}
-
-export interface ILocationOps {
- combinePaths(...pathSegments: string[]): string;
-}
-
-export interface ICommandOps {
- execute(command: string): void;
-}
-
-export interface IFileSystemOps {
- setFilePermissions(filePath: string, mode: string | number): Promise;
- createDirectory(directoryPath: string, isRecursive?: boolean): Promise;
- writeToFile(filePath: string, data: string): Promise;
-}
diff --git a/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts b/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts
deleted file mode 100644
index b1be9800..00000000
--- a/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { WindowVariables } from '../WindowVariables/WindowVariables';
-import { ISystemOperations } from './ISystemOperations';
-
-export function getWindowInjectedSystemOperations(
- windowVariables: Partial = window,
-): ISystemOperations {
- if (!windowVariables) {
- throw new Error('missing window');
- }
- if (!windowVariables.system) {
- throw new Error('missing system');
- }
- return windowVariables.system;
-}
diff --git a/src/infrastructure/WindowVariables/WindowVariables.ts b/src/infrastructure/WindowVariables/WindowVariables.ts
index 4d96c27c..aa61f9a2 100644
--- a/src/infrastructure/WindowVariables/WindowVariables.ts
+++ b/src/infrastructure/WindowVariables/WindowVariables.ts
@@ -1,11 +1,11 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
-import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
import { Logger } from '@/application/Common/Log/Logger';
+import { CodeRunner } from '@/application/CodeRunner';
/* Primary entry point for platform-specific injections */
export interface WindowVariables {
readonly isDesktop: boolean;
- readonly system?: ISystemOperations;
+ readonly codeRunner?: CodeRunner;
readonly os?: OperatingSystem;
readonly log: Logger;
}
diff --git a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts
index 8d2a98b7..a197fbff 100644
--- a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts
+++ b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts
@@ -1,12 +1,14 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
-import { PropertyKeys } from '@/TypeHelpers';
+import {
+ PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject,
+} from '@/TypeHelpers';
import { WindowVariables } from './WindowVariables';
/**
* Checks for consistency in runtime environment properties injected by Electron preloader.
*/
export function validateWindowVariables(variables: Partial) {
- if (!isObject(variables)) {
+ if (!isPlainObject(variables)) {
throw new Error('window is not an object');
}
const errors = [...testEveryProperty(variables)];
@@ -21,7 +23,7 @@ function* testEveryProperty(variables: Partial): Iterable): boolean {
if (!variables.isDesktop) {
return true;
}
- return isObject(variables.log);
+ return isPlainObject(variables.log);
}
-function testSystem(variables: Partial): boolean {
+function testCodeRunner(variables: Partial): boolean {
if (!variables.isDesktop) {
return true;
}
- return isObject(variables.system);
+ return isPlainObject(variables.codeRunner)
+ && isFunction(variables.codeRunner.runCode);
}
function testIsDesktop(isDesktop: unknown): boolean {
@@ -65,17 +68,3 @@ function testIsDesktop(isDesktop: unknown): boolean {
}
return isBoolean(isDesktop);
}
-
-function isNumber(variable: unknown): variable is number {
- return typeof variable === 'number';
-}
-
-function isBoolean(variable: unknown): variable is boolean {
- return typeof variable === 'boolean';
-}
-
-function isObject(variable: unknown): variable is object {
- return Boolean(variable) // the data type of null is an object
- && typeof variable === 'object'
- && !Array.isArray(variable);
-}
diff --git a/src/presentation/bootstrapping/DependencyProvider.ts b/src/presentation/bootstrapping/DependencyProvider.ts
index b3c70691..72124c9a 100644
--- a/src/presentation/bootstrapping/DependencyProvider.ts
+++ b/src/presentation/bootstrapping/DependencyProvider.ts
@@ -13,6 +13,7 @@ import {
import { PropertyKeys } from '@/TypeHelpers';
import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
import { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
+import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner';
export function provideDependencies(
context: IApplicationContext,
@@ -62,6 +63,10 @@ export function provideDependencies(
InjectionKeys.useLogger,
useLogger,
),
+ useCodeRunner: (di) => di.provide(
+ InjectionKeys.useCodeRunner,
+ useCodeRunner,
+ ),
};
registerAll(Object.values(resolvers), api);
}
diff --git a/src/presentation/components/Code/CodeButtons/CodeRunButton.vue b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue
index a3c579d8..3a481251 100644
--- a/src/presentation/components/Code/CodeButtons/CodeRunButton.vue
+++ b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue
@@ -11,8 +11,6 @@
import { defineComponent, computed } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
-import { CodeRunner } from '@/infrastructure/CodeRunner';
-import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
import IconButton from './IconButton.vue';
export default defineComponent({
@@ -22,11 +20,19 @@ export default defineComponent({
setup() {
const { currentState, currentContext } = injectKey((keys) => keys.useCollectionState);
const { os, isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
+ const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
const canRun = computed(() => getCanRunState(currentState.value.os, isDesktop, os));
async function executeCode() {
- await runCode(currentContext);
+ if (!codeRunner) { throw new Error('missing code runner'); }
+ if (os === undefined) { throw new Error('unidentified host operating system'); }
+ await codeRunner.runCode(
+ currentContext.state.code.current,
+ currentContext.app.info.name,
+ currentState.value.collection.scripting.fileExtension,
+ os,
+ );
}
return {
@@ -45,13 +51,4 @@ function getCanRunState(
const isRunningOnSelectedOs = selectedOs === hostOs;
return isDesktopVersion && isRunningOnSelectedOs;
}
-
-async function runCode(context: IReadOnlyApplicationContext) {
- const runner = new CodeRunner();
- await runner.runCode(
- /* code: */ context.state.code.current,
- /* appName: */ context.app.info.name,
- /* fileExtension: */ context.state.collection.scripting.fileExtension,
- );
-}
diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts
index fe16b6bc..a42bf61d 100644
--- a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts
+++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts
@@ -1,3 +1,4 @@
+import { isArray } from '@/TypeHelpers';
import { TreeInputNodeData } from '../../Bindings/TreeInputNodeData';
import { TreeNode } from '../../Node/TreeNode';
import { TreeNodeManager } from '../../Node/TreeNodeManager';
@@ -5,7 +6,7 @@ import { TreeNodeManager } from '../../Node/TreeNodeManager';
export function parseTreeInput(
input: readonly TreeInputNodeData[],
): TreeNode[] {
- if (!Array.isArray(input)) {
+ if (!isArray(input)) {
throw new Error('input data must be an array');
}
const nodes = input.map((nodeData) => createNode(nodeData));
diff --git a/src/presentation/components/Shared/Hooks/UseCodeRunner.ts b/src/presentation/components/Shared/Hooks/UseCodeRunner.ts
new file mode 100644
index 00000000..4c4a6f44
--- /dev/null
+++ b/src/presentation/components/Shared/Hooks/UseCodeRunner.ts
@@ -0,0 +1,9 @@
+import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
+
+export function useCodeRunner(
+ window: WindowVariables = globalThis.window,
+) {
+ return {
+ codeRunner: window.codeRunner,
+ };
+}
diff --git a/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts b/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts
new file mode 100644
index 00000000..f3bd78d2
--- /dev/null
+++ b/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts
@@ -0,0 +1,17 @@
+import { contextBridge } from 'electron';
+import { bindObjectMethods } from './MethodContextBinder';
+import { provideWindowVariables } from './RendererApiProvider';
+
+export function connectApisWithContextBridge(
+ bridgeConnector: BridgeConnector = contextBridge.exposeInMainWorld,
+ apiObject: object = provideWindowVariables(),
+ methodContextBinder: MethodContextBinder = bindObjectMethods,
+) {
+ Object.entries(apiObject).forEach(([key, value]) => {
+ bridgeConnector(key, methodContextBinder(value));
+ });
+}
+
+export type BridgeConnector = typeof contextBridge.exposeInMainWorld;
+
+export type MethodContextBinder = typeof bindObjectMethods;
diff --git a/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts b/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts
new file mode 100644
index 00000000..fb530774
--- /dev/null
+++ b/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts
@@ -0,0 +1,52 @@
+import {
+ isArray, isFunction, isNullOrUndefined, isPlainObject,
+} from '@/TypeHelpers';
+
+/**
+ * Binds method contexts to their original object instances and recursively processes
+ * nested objects and arrays. This is particularly useful when exposing objects across
+ * different contexts in Electron, such as from the main process to the renderer process
+ * via the `contextBridge`.
+ *
+ * In Electron's context isolation environment, methods of objects passed through the
+ * `contextBridge` lose their original context (`this` binding). This function ensures that
+ * each method retains its binding to its original object, allowing it to work as intended
+ * when invoked from the renderer process.
+ *
+ * This approach decouples context isolation concerns from class implementations, enabling
+ * classes to operate normally without needing explicit binding or arrow functions to maintain
+ * the context.
+ */
+export function bindObjectMethods(obj: T): T {
+ if (isNullOrUndefined(obj)) {
+ return obj;
+ }
+ if (isPlainObject(obj)) {
+ bindMethodsOfObject(obj);
+ Object.values(obj).forEach((value) => {
+ if (!isNullOrUndefined(value) && !isFunction(value)) {
+ bindObjectMethods(value);
+ }
+ });
+ } else if (isArray(obj)) {
+ obj.forEach((item) => bindObjectMethods(item));
+ }
+ return obj;
+}
+
+function bindMethodsOfObject(obj: T): T {
+ const prototype = Object.getPrototypeOf(obj);
+ if (!prototype) {
+ return obj;
+ }
+ Object.getOwnPropertyNames(prototype).forEach((property) => {
+ if (!prototype.hasOwnProperty.call(obj, property)) {
+ return; // Skip properties not directly on the prototype
+ }
+ const value = obj[property];
+ if (isFunction(value)) {
+ (obj as object)[property] = value.bind(obj);
+ }
+ });
+ return obj;
+}
diff --git a/src/presentation/electron/preload/NodeOsMapper.ts b/src/presentation/electron/preload/ContextBridging/NodeOsMapper.ts
similarity index 100%
rename from src/presentation/electron/preload/NodeOsMapper.ts
rename to src/presentation/electron/preload/ContextBridging/NodeOsMapper.ts
diff --git a/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts b/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts
new file mode 100644
index 00000000..9f9ffcfb
--- /dev/null
+++ b/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts
@@ -0,0 +1,27 @@
+import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
+import { Logger } from '@/application/Common/Log/Logger';
+import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
+import { TemporaryFileCodeRunner } from '@/infrastructure/CodeRunner/TemporaryFileCodeRunner';
+import { CodeRunner } from '@/application/CodeRunner';
+import { convertPlatformToOs } from './NodeOsMapper';
+import { createSecureFacade } from './SecureFacadeCreator';
+
+export function provideWindowVariables(
+ createCodeRunner: CodeRunnerFactory = () => new TemporaryFileCodeRunner(),
+ createLogger: LoggerFactory = () => createElectronLogger(),
+ convertToOs = convertPlatformToOs,
+ createApiFacade: ApiFacadeFactory = createSecureFacade,
+): WindowVariables {
+ return {
+ isDesktop: true,
+ log: createApiFacade(createLogger(), ['info', 'debug', 'warn', 'error']),
+ os: convertToOs(process.platform),
+ codeRunner: createApiFacade(createCodeRunner(), ['runCode']),
+ };
+}
+
+export type LoggerFactory = () => Logger;
+
+export type CodeRunnerFactory = () => CodeRunner;
+
+export type ApiFacadeFactory = typeof createSecureFacade;
diff --git a/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts b/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts
new file mode 100644
index 00000000..809e4099
--- /dev/null
+++ b/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts
@@ -0,0 +1,42 @@
+import { isFunction } from '@/TypeHelpers';
+
+/**
+ * Creates a secure proxy for the specified object, exposing only the public properties
+ * of its interface.
+ *
+ * This approach prevents the full exposure of the object, thereby reducing the risk
+ * of unintended access or misuse. For instance, creating a facade for a class rather
+ * than exposing the class itself ensures that private members and dependencies
+ * (such as file access or internal state) remain encapsulated and inaccessible.
+ */
+export function createSecureFacade(
+ originalObject: T,
+ accessibleMembers: KeyTypeCombinations,
+): T {
+ const facade: Partial = {};
+
+ accessibleMembers.forEach((key: keyof T) => {
+ const member = originalObject[key];
+ if (isFunction(member)) {
+ facade[key] = ((...args: unknown[]) => {
+ return member.apply(originalObject, args);
+ }) as T[keyof T];
+ } else {
+ facade[key] = member;
+ }
+ });
+
+ return facade as T;
+}
+
+type PrependTuple = H extends unknown ? T extends unknown ?
+ ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never : never;
+type RecursionDepthControl = [
+ never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+];
+type AllKeyCombinations = T extends unknown ?
+ PrependTuple extends infer X ? {
+ 0: [], 1: AllKeyCombinations
+ }[[X] extends [never] ? 0 : 1] : never> :
+ never;
+type KeyTypeCombinations = AllKeyCombinations;
diff --git a/src/presentation/electron/preload/WindowVariablesProvider.ts b/src/presentation/electron/preload/WindowVariablesProvider.ts
deleted file mode 100644
index 3cc5808b..00000000
--- a/src/presentation/electron/preload/WindowVariablesProvider.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { createNodeSystemOperations } from '@/infrastructure/SystemOperations/NodeSystemOperations';
-import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
-import { Logger } from '@/application/Common/Log/Logger';
-import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
-import { convertPlatformToOs } from './NodeOsMapper';
-
-export function provideWindowVariables(
- createSystem = createNodeSystemOperations,
- createLogger: () => Logger = () => createElectronLogger(),
- convertToOs = convertPlatformToOs,
-): WindowVariables {
- return {
- system: createSystem(),
- isDesktop: true,
- log: createLogger(),
- os: convertToOs(process.platform),
- };
-}
diff --git a/src/presentation/electron/preload/index.ts b/src/presentation/electron/preload/index.ts
index 629f7d5d..f4aeb82f 100644
--- a/src/presentation/electron/preload/index.ts
+++ b/src/presentation/electron/preload/index.ts
@@ -1,9 +1,8 @@
// This file is used to securely expose Electron APIs to the application.
-import { contextBridge } from 'electron';
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
-import { provideWindowVariables } from './WindowVariablesProvider';
+import { connectApisWithContextBridge } from './ContextBridging/ApiContextBridge';
validateRuntimeSanity({
// Validate metadata as a preventive measure for fail-fast,
@@ -14,10 +13,7 @@ validateRuntimeSanity({
validateWindowVariables: false,
});
-const windowVariables = provideWindowVariables();
-Object.entries(windowVariables).forEach(([key, value]) => {
- contextBridge.exposeInMainWorld(key, value);
-});
+connectApisWithContextBridge();
// Do not remove [PRELOAD_INIT]; it's a marker used in tests.
ElectronLogger.info('[PRELOAD_INIT] Preload script successfully initialized and executed.');
diff --git a/src/presentation/injectionSymbols.ts b/src/presentation/injectionSymbols.ts
index 0e5fcf60..3d8cf46e 100644
--- a/src/presentation/injectionSymbols.ts
+++ b/src/presentation/injectionSymbols.ts
@@ -7,6 +7,7 @@ import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseC
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
import type { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
+import type { useCodeRunner } from './components/Shared/Hooks/UseCodeRunner';
export const InjectionKeys = {
useCollectionState: defineTransientKey>('useCollectionState'),
@@ -17,6 +18,7 @@ export const InjectionKeys = {
useCurrentCode: defineTransientKey>('useCurrentCode'),
useUserSelectionState: defineTransientKey>('useUserSelectionState'),
useLogger: defineTransientKey>('useLogger'),
+ useCodeRunner: defineTransientKey>('useCodeRunner'),
};
export interface InjectionKeyWithLifetime {
diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts
index 8176761b..9a6acf2b 100644
--- a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts
+++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts
@@ -1,3 +1,5 @@
+import { isString } from '@/TypeHelpers';
+
export function indentText(
text: string,
indentLevel = 1,
@@ -21,7 +23,7 @@ export function filterEmpty(texts: readonly (string | undefined | null)[]): stri
}
function validateText(text: string): void {
- if (typeof text !== 'string') {
+ if (!isString(text)) {
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
}
}
diff --git a/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts b/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts
index b10f7a69..b4fcd0ad 100644
--- a/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts
+++ b/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts
@@ -1,6 +1,7 @@
import { describe } from 'vitest';
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
+import { isBoolean } from '@/TypeHelpers';
describe('SanityChecks', () => {
describe('validateRuntimeSanity', () => {
@@ -42,7 +43,7 @@ function generateBooleanPermutations(object: T | undefined): T[] {
const currentKey = keys[0];
const currentValue = object[currentKey];
- if (typeof currentValue !== 'boolean') {
+ if (!isBoolean(currentValue)) {
return generateBooleanPermutations({
...object,
[currentKey]: currentValue,
diff --git a/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts b/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts
new file mode 100644
index 00000000..679aa7eb
--- /dev/null
+++ b/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts
@@ -0,0 +1,15 @@
+import { it, describe, expect } from 'vitest';
+import { connectApisWithContextBridge } from '@/presentation/electron/preload/ContextBridging/ApiContextBridge';
+
+describe('ApiContextBridge', () => {
+ describe('connectApisWithContextBridge', () => {
+ it('can provide keys and values', () => {
+ // arrange
+ const bridgeConnector = () => {};
+ // act
+ const act = () => connectApisWithContextBridge(bridgeConnector);
+ // assert
+ expect(act).to.not.throw();
+ });
+ });
+});
diff --git a/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts b/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts
new file mode 100644
index 00000000..29364b01
--- /dev/null
+++ b/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts
@@ -0,0 +1,66 @@
+import { it, describe, expect } from 'vitest';
+import { provideWindowVariables } from '@/presentation/electron/preload/ContextBridging/RendererApiProvider';
+import {
+ isArray, isBoolean, isFunction, isNullOrUndefined, isNumber, isPlainObject, isString,
+} from '@/TypeHelpers';
+
+describe('RendererApiProvider', () => {
+ describe('provideWindowVariables', () => {
+ describe('conforms to Electron\'s context bridging requirements', () => {
+ // https://www.electronjs.org/docs/latest/api/context-bridge
+ const variables = provideWindowVariables();
+ Object.entries(variables).forEach(([key, value]) => {
+ it(`\`${key}\` conforms to allowed types for context bridging`, () => {
+ // act
+ const act = () => checkAllowedType(value);
+ // assert
+ expect(act).to.not.throw();
+ });
+ });
+ });
+ });
+});
+
+function checkAllowedType(value: unknown): void {
+ if (isBasicType(value)) {
+ return;
+ }
+ if (isArray(value)) {
+ checkArrayElements(value);
+ return;
+ }
+ if (!isPlainObject(value)) {
+ throw new Error(`Type error: Expected a valid object, array, or primitive type, but received type '${typeof value}'.`);
+ }
+ if (isNullOrUndefined(value)) {
+ throw new Error('Type error: Value is null or undefined, which is not allowed.');
+ }
+ checkObjectProperties(value);
+}
+
+function isBasicType(value: unknown): boolean {
+ return isString(value) || isNumber(value) || isBoolean(value) || isFunction(value);
+}
+
+function checkArrayElements(array: unknown[]): void {
+ array.forEach((item, index) => {
+ try {
+ checkAllowedType(item);
+ } catch (error) {
+ throw new Error(`Invalid array element at index ${index}: ${error.message}`);
+ }
+ });
+}
+
+function checkObjectProperties(obj: NonNullable