From 2c51bbfc722c25054566d30b2286a8886adefca9 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:33:40 -0500 Subject: [PATCH 1/5] get some sort of service (not tested) --- src/interfaces/IServiceHost.ts | 3 +- src/interfaces/services/IJsExec.ts | 11 + src/ts/jsexec/index.ts | 10 +- src/ts/servicehost/index.ts | 2 + src/ts/servicehost/services/JsExec/engine.ts | 12 + src/ts/servicehost/services/JsExec/index.ts | 206 ++++++++++++++++++ src/ts/servicehost/services/JsExec/props.ts | 134 ++++++++++++ .../services/JsExec/supplementary.ts | 123 +++++++++++ 8 files changed, 495 insertions(+), 6 deletions(-) create mode 100644 src/interfaces/services/IJsExec.ts create mode 100644 src/ts/servicehost/services/JsExec/engine.ts create mode 100644 src/ts/servicehost/services/JsExec/index.ts create mode 100644 src/ts/servicehost/services/JsExec/props.ts create mode 100644 src/ts/servicehost/services/JsExec/supplementary.ts diff --git a/src/interfaces/IServiceHost.ts b/src/interfaces/IServiceHost.ts index 20263a5b..1062b657 100644 --- a/src/interfaces/IServiceHost.ts +++ b/src/interfaces/IServiceHost.ts @@ -47,5 +47,6 @@ export type ServiceIdentifier = | "IconService" | "LibMgmtSvc" | "MigrationSvc" - | "RecentFilesSvc"; + | "RecentFilesSvc" + | "JsExecSvc"; // !endtpa diff --git a/src/interfaces/services/IJsExec.ts b/src/interfaces/services/IJsExec.ts new file mode 100644 index 00000000..35d6a423 --- /dev/null +++ b/src/interfaces/services/IJsExec.ts @@ -0,0 +1,11 @@ +import type { IBaseService } from "$interfaces/IServiceHost"; +import type { JsExecEngineData } from "$ts/servicehost/services/JsExec/engine"; +import type { App } from "$types/apps/app"; + +// !tpa +export interface IJsExecService extends IBaseService { + start(): Promise; + getContents(engine: JsExecEngineData): Promise; + setupEngine(filePath: string, app?: App, metaPath?: string, ...args: any[]): JsExecEngineData; + Invoke(filePath: string, app?: App, metaPath?: string, ...args: any[]): Promise; +} diff --git a/src/ts/jsexec/index.ts b/src/ts/jsexec/index.ts index d56a34b4..f121761b 100644 --- a/src/ts/jsexec/index.ts +++ b/src/ts/jsexec/index.ts @@ -52,7 +52,7 @@ export class JsExec extends Process { //#endregion //#region URL - async getTpaUrl(wrapped: string) { + private async getTpaUrl(wrapped: string) { this.Log(`Getting TPA file URL`); const { appId, userId, filename } = this.getTpaUrlInfo(); @@ -66,13 +66,13 @@ export class JsExec extends Process { } } - getTpaPostUrl() { + private getTpaPostUrl() { const { appId, userId, filename } = this.getTpaUrlInfo(); return `/tpa/v2/${userId}/${appId}/${filename}`; } - getTpaUrlInfo() { + private getTpaUrlInfo() { const appId = this.app?.id || "ArcOS"; const userId = Daemon?.userInfo?._id || "SYSTEM"; const filename = getItemNameFromPath(this.filePath!); @@ -83,7 +83,7 @@ export class JsExec extends Process { //#endregion //#region EXECUTION - async exec(tpaUrl: string) { + private async exec(tpaUrl: string) { this.Log(`Executing ${this.filePath}`); const code = await import(/* @vite-ignore */ tpaUrl); @@ -157,7 +157,7 @@ export class JsExec extends Process { return sourceFile; } - async testFileContents(unwrapped: string) { + private async testFileContents(unwrapped: string) { try { const ast = acorn.parse(unwrapped, { sourceType: "module", diff --git a/src/ts/servicehost/index.ts b/src/ts/servicehost/index.ts index 7fdbed25..25c8eb25 100644 --- a/src/ts/servicehost/index.ts +++ b/src/ts/servicehost/index.ts @@ -14,6 +14,7 @@ import { protoService } from "$ts/servicehost/services/ProtoService"; import { recentFilesService } from "$ts/servicehost/services/RecentFilesSvc"; import { shareService } from "$ts/servicehost/services/ShareMgmt"; import { trashService } from "$ts/servicehost/services/TrashSvc"; +import { jsExecService } from "$ts/servicehost/services/JsExec"; import { MessageBox } from "$ts/util/dialog"; import { Store } from "$ts/writable"; import type { ReadableServiceStore, Service, ServiceChangeResult, ServiceStore } from "$types/services/service"; @@ -127,6 +128,7 @@ export class ServiceHost extends Process implements IServiceHost { ["LibMgmtSvc", { ...libraryManagementService }], ["MigrationSvc", { ...migrationService }], ["RecentFilesSvc", { ...recentFilesService }], + ["JsExecSvc", { ...jsExecService }], ]); public loadStore(store: ServiceStore) { diff --git a/src/ts/servicehost/services/JsExec/engine.ts b/src/ts/servicehost/services/JsExec/engine.ts new file mode 100644 index 00000000..d6aa60d2 --- /dev/null +++ b/src/ts/servicehost/services/JsExec/engine.ts @@ -0,0 +1,12 @@ +import type { App } from "$types/apps/app"; +import type { ThirdPartyPropMap } from "$types/tpa/thirdparty"; + +export interface JsExecEngineData { + props?: ThirdPartyPropMap; + app?: App; + args: any[]; + metaPath?: string; + filePath?: string; + workingDirectory: string; + operationId: string; +} diff --git a/src/ts/servicehost/services/JsExec/index.ts b/src/ts/servicehost/services/JsExec/index.ts new file mode 100644 index 00000000..d81b4c16 --- /dev/null +++ b/src/ts/servicehost/services/JsExec/index.ts @@ -0,0 +1,206 @@ +/** + * ArcOS JavaScript Execution Engine + * + * This file executes JS files in ArcOS under a relatively controlled environment. + * It is part of the ArcOS TPA framework: a system for running third-party apps. + * + * All rights belong to their respective authors. + * + * © IzKuipers 2025 + */ +import type { IServiceHost } from "$interfaces/IServiceHost"; +import type { ITpaConnector } from "$interfaces/modules/server/ITpaConnector"; +import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; +import { ThirdPartyProps } from "./props"; +import { Daemon, Env, Fs, Stack } from "$ts/env"; +import { BaseService } from "$ts/servicehost/base"; +import { arrayBufferToText } from "$ts/util/convert"; +import { getItemNameFromPath, getParentDirectory } from "$ts/util/fs"; +import { UUID } from "$ts/util/uuid"; +import type { App } from "$types/apps/app"; +import type { Service } from "$types/services/service"; +import type { ParsedImportStatement, ThirdPartyPropMap } from "$types/tpa/thirdparty"; +import * as acorn from "acorn"; +import type { JsExecEngineData } from "./engine"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; + +export class JsExecService extends BaseService implements IJsExecService { + public readonly TPA_REVISION = ThirdPartyAppProcess.TPA_REV; + + //#region LIFECYCLE + + constructor(pid: number, parentPid: number, name: string, host: IServiceHost, initBroadcast?: (msg: string) => void) { + super(pid, parentPid, name, host, initBroadcast); + + this.setSource(__SOURCE__); + } + + async start() { + return Daemon?.preferences().security.enableThirdParty === true; + } + + //#endregion + //#region URL + + private async getTpaUrl(engine: JsExecEngineData, wrapped: string) { + this.Log(`Getting TPA file URL`); + + const { appId, userId, filename } = this.getTpaUrlInfo(engine); + try { + const urlResult = await Daemon!.GetConnector("TpaConnector").CreateUrl(wrapped, userId, appId, filename); + + if (!urlResult.success) throw new JsExecError(); + return urlResult.result!; + } catch (e: any) { + throw new JsExecError(`Failed to create momentary TPA URL: ${e?.message ?? e}`); + } + } + + private getTpaPostUrl(engine: JsExecEngineData) { + const { appId, userId, filename } = this.getTpaUrlInfo(engine); + + return `/tpa/v2/${userId}/${appId}/${filename}`; + } + + private getTpaUrlInfo(engine: JsExecEngineData) { + const appId = engine.app?.id || "ArcOS"; + const userId = Daemon?.userInfo?._id || "SYSTEM"; + const filename = getItemNameFromPath(engine.filePath!); + + return { appId, userId, filename }; + } + + //#endregion + //#region EXECUTION + + private async exec(engine: JsExecEngineData, tpaUrl: string) { + this.Log(`Executing ${engine.filePath}`); + + const code = await import(/* @vite-ignore */ tpaUrl); + + if (!code.default || !(code.default instanceof Function)) throw new JsExecError("Expected a default function"); + + try { + const result = await code.default(engine.props!); + return result; + } catch (e) { + throw e; + } finally { + await this.killSelf(); + } + } + + async getContents(engine: JsExecEngineData) { + this.Log(`Reading script contents`); + + const unwrapped = this.convertImportStatementsToRegex(arrayBufferToText((await Fs.readFile(engine.filePath!))!)!); + if (!unwrapped) throw new JsExecError(`Failed to read ${engine.filePath}: not found`); + + await this.testFileContents(unwrapped); + + const wrapped = this.wrap(engine, unwrapped); + const tpaUrl = await this.getTpaUrl(engine, wrapped); + + return await this.exec(engine, tpaUrl); + } + + //#endregion + //#region HELPERS + + private setApp(engine: JsExecEngineData, app: App, metaPath?: string) { + this.Log(`Setting app data to ${app.id} (${metaPath ?? ""})`); + + if (engine.app) return engine; + + if (app.tpaRevision && app.tpaRevision > this.TPA_REVISION) + throw new JsExecError( + `This application expects a newer version of the TPA framework than what ArcOS can supply. Please update your ArcOS version and try again.` + ); + + engine.app = app; + engine.metaPath = metaPath; + engine.props = ThirdPartyProps(engine); + } + + private wrap(engine: JsExecEngineData, contents: string) { + if (!engine.props) throw new JsExecError(`No TPA props to use`); + + return `export default async function({${Object.keys(engine.props).join(",")}}) {\nconst global = arguments;\n${contents}\n}`; + } + + private convertImportStatementsToRegex(sourceFile: string) { + if (!sourceFile) return sourceFile; + const regex = + /import(?:(?:(?:[ \n\t]+(?[^ *\n\t\{\},]+)[ \n\t]*(?:,|[ \n\t]+))?(?[ \n\t]*\{(?:[ \n\t]*[^ \n\t"'\{\}]+[ \n\t]*,?)+\})?[ \n\t]*)|[ \n\t]*\*[ \n\t]*as[ \n\t]+(?[^ \n\t\{\}]+)[ \n\t]+)from[ \n\t]*(?:['"])(?[^'"\n]+)(?['"])/gm; + const matches = sourceFile + .matchAll(regex) + .toArray() + .map((m) => ({ ...m.groups, original: m[0] })) as ParsedImportStatement[]; + + for (const match of matches) { + const { destructured, default: defaultImport, filename, quote, wildcard } = match; + let loadStatement = `const ${destructured || defaultImport || wildcard} = await load(${quote}${filename}${quote})`; + + sourceFile = sourceFile.replace(match.original, loadStatement); + } + + return sourceFile; + } + + private async testFileContents(unwrapped: string) { + try { + const ast = acorn.parse(unwrapped, { + sourceType: "module", + ecmaVersion: "latest", + allowReturnOutsideFunction: true, + allowAwaitOutsideFunction: true, + }); + const hasExport = ast.body.some((node) => node.type.startsWith("Export")); + const hasImport = ast.body.some((node) => node.type.startsWith("Import")); + const hasDebugger = ast.body.some((node) => node.type.startsWith("Debugger")); + + if (hasExport) throw new JsExecError("Export statements are not valid inside of ArcOS"); + if (hasImport) throw new JsExecError("Import statements are not valid inside of ArcOS"); + if (hasDebugger) throw new JsExecError("Debugger triggers are not valid inside of ArcOS"); + } catch (e) { + throw new JsExecError(`An error occurred while parsing the source file: ${e}`); + } + } + + //#endregion + + setupEngine(filePath: string, app?: App, metaPath?: string, ...args: any[]) { + let engine = {} as JsExecEngineData; + engine.args = args; + engine.filePath = filePath; + engine.workingDirectory = getParentDirectory(filePath); + engine.operationId = UUID(); + + if (app && metaPath) { + this.setApp(engine, app, metaPath); + } + + return engine; + } + + async Invoke(filePath: string, app?: App, metaPath?: string, ...args: any[]) { + let engine = this.setupEngine(filePath, app, metaPath, ...args); + + return this.getContents(engine); + } +} + +export class JsExecError extends Error { + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + + this.name = "JsExecError"; + } +} + +export const jsExecService: Service = { + name: "TPA Host", + description: "Provides the interface to spawn TPAs.", + process: JsExecService, + initialState: "started", +}; diff --git a/src/ts/servicehost/services/JsExec/props.ts b/src/ts/servicehost/services/JsExec/props.ts new file mode 100644 index 00000000..c9e4788e --- /dev/null +++ b/src/ts/servicehost/services/JsExec/props.ts @@ -0,0 +1,134 @@ +import type { IThirdPartyAppProcess } from "$interfaces/IThirdPartyAppProcess"; +import { AppProcess } from "$ts/apps/process"; +import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; +import { __Console__ } from "$ts/console"; +import { Daemon, Env, Fs, Stack } from "$ts/env"; +import { getAllImages } from "$ts/images"; +import { FilesystemDrive } from "$ts/kernel/mods/fs/drives/generic"; +import { Backend } from "$ts/kernel/mods/server/axios"; +import { Process } from "$ts/kernel/mods/stack/process/instance"; +import { BaseService } from "$ts/servicehost/base"; +import { Sleep } from "$ts/sleep"; +import { contextProps } from "$ts/ui/context/actions.svelte"; +import { CustomTitlebar } from "$ts/ui/thirdparty/titlebar"; +import { TrayIconProcess } from "$ts/ui/tray/process"; +import { HiddenUserPaths, SystemFolders, UserPathCaptions, UserPathIcons, UserPaths } from "$ts/user/store"; +import { CountInstances, decimalToHex, htmlspecialchars, Plural, sha256, sliceIntoChunks } from "$ts/util"; +import { arrayBufferToBlob, arrayBufferToText, blobToDataURL, blobToText, textToArrayBuffer, textToBlob } from "$ts/util/convert"; +import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog"; +import { + DownloadFile, + formatBytes, + getDriveLetter, + getItemNameFromPath, + getParentDirectory, + join, + onFileChange, + onFolderChange, +} from "$ts/util/fs"; +import { tryJsonStringify } from "$ts/util/json"; +import { Store } from "$ts/writable"; +import type { ThirdPartyPropMap } from "$types/tpa/thirdparty"; +import axios from "axios"; +import dayjs from "dayjs"; +import { ThirdPartyProcess } from "$ts/apps/tpa/process"; +import { SupplementaryThirdPartyPropFunctions } from "./supplementary"; +import type { JsExecEngineData } from "./engine"; + +export function ThirdPartyProps(engine: JsExecEngineData): ThirdPartyPropMap { + const props = { + env: Env, // TEMP + handler: Stack, // TEMP + fs: Fs, // TEMP + daemon: Daemon, // TEMP + serviceHost: Daemon!.serviceHost, // TEMP + MessageBox, + icons: getAllImages(), + util: { + htmlspecialchars, + Plural, + sliceIntoChunks, + decimalToHex, + sha256, + CountInstances, + join, + getItemNameFromPath, + getParentDirectory, + getDriveLetter, + formatBytes, + DownloadFile, + onFileChange, + onFolderChange, + }, + convert: { + arrayToText: arrayBufferToText, + textToArrayBuffer, + blobToText, + textToBlob, + arrayToBlob: arrayBufferToBlob, + blobToDataURL, + }, + workingDirectory: engine.workingDirectory || engine.app?.workingDirectory!, + Process, + AppProcess, + ThirdPartyAppProcess, + ThirdPartyProcess, + FilesystemDrive, + argv: engine.args, + app: engine.app, + Store, + Sleep, + $ENTRYPOINT: engine.filePath, + $METADATA: engine.metaPath, + SHELL_PID: +Env.get("shell_pid"), + OPERATION_ID: engine.operationId, + load: async (path: string): Promise => {}, + runApp: async ( + process: typeof ThirdPartyAppProcess, + metadataPath: string, + parentPid?: number, + ...args: any[] + ): Promise => undefined, + loadHtml: async (path: string): Promise => undefined, + axios, + Server: Backend, + BaseService, + TrayIconProcess, + Debug: (m: any) => { + MessageBox( + { + title: "🐛🪵", + message: tryJsonStringify(m, 2), + image: "WindowSettingsIcon", + sound: "arcos.dialog.info", + buttons: [BTN_OKAY_SUG], + }, + +Env.get("shell_pid") + ); + }, + CustomTitlebar, + contextProps, + dayjs, + console: __Console__, + LogLevel: { + info: 0, + warning: 1, + error: 2, + critical: 3, + }, + UserPaths, + UserPathCaptions, + UserPathIcons, + SystemFolders, + HiddenUserPaths, + }; + + const supplementary = SupplementaryThirdPartyPropFunctions(engine); + + for (const [key, supp] of Object.entries(supplementary)) { + (props as any)[key] = supp; + } + + //@ts-ignore + return props; +} diff --git a/src/ts/servicehost/services/JsExec/supplementary.ts b/src/ts/servicehost/services/JsExec/supplementary.ts new file mode 100644 index 00000000..089487e0 --- /dev/null +++ b/src/ts/servicehost/services/JsExec/supplementary.ts @@ -0,0 +1,123 @@ +import type { Constructs } from "$interfaces/common"; +import type { IThirdPartyAppProcess } from "$interfaces/IThirdPartyAppProcess"; +import { Daemon, Fs, Stack } from "$ts/env"; +import { detectJavaScript } from "$ts/util"; +import { arrayBufferToText } from "$ts/util/convert"; +import { join } from "$ts/util/fs"; +import { tryJsonParse } from "$ts/util/json"; +import { ThirdPartyProcess } from "$ts/apps/tpa/process"; +import type { JsExecEngineData } from "./engine"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; + +export function SupplementaryThirdPartyPropFunctions(engine: JsExecEngineData) { + return { + load: async (path: string) => { + if (path.startsWith("http")) { + try { + await import(/* @vite-ignore */ path); + } catch (e) { + throw e; + } + } + + try { + const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); + if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); + + const subEngine = await jsExecService.setupEngine(join(engine.workingDirectory, path), engine.app, engine.metaPath); + + return await jsExecService.getContents(subEngine); + } catch (e) { + throw e; + } + }, + runApp: async (process: Constructs, metadataPath: string, parentPid?: number, ...args: any[]) => { + if (process instanceof ThirdPartyProcess) { + throw new Error( + "Can't use runApp with a ThirdPartyProcess (non-app). Please directly return the process from the entrypoint." + ); + } + + const app = engine.app; + + if (!app || !Daemon) throw new Error(`Illegal runApp operation on a non-app JsExec`); + + try { + const metaStr = arrayBufferToText((await Fs.readFile(metadataPath))!); + const metadata = tryJsonParse(metaStr); + const renderTarget = Daemon.workspaces!.getCurrentDesktop(); + + if (typeof metadata === "string") throw new Error("Failed to parse metadata"); + + const proc = await Stack.spawn( + process, + renderTarget, + Daemon.userInfo!._id, + parentPid, + { + data: metadata, + id: metadata.id, + desktop: renderTarget ? renderTarget.id : undefined, + }, + engine.operationId, + app.workingDirectory, + ...args + ); + + app.process = proc; + + return proc; + } catch (e) { + throw e; + } + }, + runAppDirect: async ( + process: Constructs, + metadataPath: string, + parentPid?: number, + ...args: any[] + ) => { + const app = engine.app; + + if (!app || !Daemon) throw new Error(`Illegal runApp operation on a non-app JsExec`); + + try { + const metaStr = arrayBufferToText((await Fs.readFile(metadataPath))!); + const metadata = tryJsonParse(metaStr); + + if (typeof metadata === "string") throw new Error("Failed to parse metadata"); + + const proc = await Stack.spawn( + process, + undefined, + Daemon.userInfo!._id, + parentPid, + { + data: metadata, + id: metadata.id, + }, + app.workingDirectory, + ...args + ); + + app.process = proc; + + return proc; + } catch (e) { + throw e; + } + }, + loadHtml: async (path: string) => { + const htmlCode = arrayBufferToText((await Fs.readFile(join(engine.workingDirectory, path)))!); + + const detected = detectJavaScript(htmlCode!); + + if (detected) throw new Error(`- ${detected.join("\n- ")}`); + + return htmlCode; + }, + loadDirect: async (path: string) => { + const url = await Fs.direct(join(engine.workingDirectory!, path)); + }, + }; +} From 05b2e6940adf18b884914258657e9ce414f70f80 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:09:50 -0500 Subject: [PATCH 2/5] ..and it works! --- src/ts/apps/tpa/props.ts | 6 +- src/ts/apps/tpa/supplementary.ts | 22 +- src/ts/daemon/contexts/spawn.ts | 10 +- src/ts/jsexec/index.ts | 193 ------------------ src/ts/kernel/wavekernel.ts | 2 - src/ts/servicehost/services/JsExec/index.ts | 10 +- src/ts/servicehost/services/JsExec/props.ts | 134 ------------ .../services/JsExec/supplementary.ts | 123 ----------- .../servicehost/services/LibMgmtSvc/index.ts | 8 +- 9 files changed, 26 insertions(+), 482 deletions(-) delete mode 100644 src/ts/jsexec/index.ts delete mode 100644 src/ts/servicehost/services/JsExec/props.ts delete mode 100644 src/ts/servicehost/services/JsExec/supplementary.ts diff --git a/src/ts/apps/tpa/props.ts b/src/ts/apps/tpa/props.ts index a8910999..6796e41f 100644 --- a/src/ts/apps/tpa/props.ts +++ b/src/ts/apps/tpa/props.ts @@ -4,7 +4,6 @@ import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; import { __Console__ } from "$ts/console"; import { Daemon, Env, Fs, Stack } from "$ts/env"; import { getAllImages } from "$ts/images"; -import type { JsExec } from "$ts/jsexec"; import { FilesystemDrive } from "$ts/kernel/mods/fs/drives/generic"; import { Backend } from "$ts/kernel/mods/server/axios"; import { Process } from "$ts/kernel/mods/stack/process/instance"; @@ -32,10 +31,11 @@ import { Store } from "$ts/writable"; import type { ThirdPartyPropMap } from "$types/tpa/thirdparty"; import axios from "axios"; import dayjs from "dayjs"; -import { ThirdPartyProcess } from "./process"; +import { ThirdPartyProcess } from "$ts/apps/tpa/process"; import { SupplementaryThirdPartyPropFunctions } from "./supplementary"; +import type { JsExecEngineData } from "$ts/servicehost/services/JsExec/engine"; -export function ThirdPartyProps(engine: JsExec): ThirdPartyPropMap { +export function ThirdPartyProps(engine: JsExecEngineData): ThirdPartyPropMap { const props = { env: Env, // TEMP handler: Stack, // TEMP diff --git a/src/ts/apps/tpa/supplementary.ts b/src/ts/apps/tpa/supplementary.ts index 412a5e46..bf2bb573 100644 --- a/src/ts/apps/tpa/supplementary.ts +++ b/src/ts/apps/tpa/supplementary.ts @@ -1,14 +1,15 @@ import type { Constructs } from "$interfaces/common"; import type { IThirdPartyAppProcess } from "$interfaces/IThirdPartyAppProcess"; import { Daemon, Fs, Stack } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { detectJavaScript } from "$ts/util"; import { arrayBufferToText } from "$ts/util/convert"; import { join } from "$ts/util/fs"; import { tryJsonParse } from "$ts/util/json"; -import { ThirdPartyProcess } from "./process"; +import { ThirdPartyProcess } from "$ts/apps/tpa/process"; +import type { JsExecEngineData } from "$ts/servicehost/services/JsExec/engine"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; -export function SupplementaryThirdPartyPropFunctions(engine: JsExec) { +export function SupplementaryThirdPartyPropFunctions(engine: JsExecEngineData) { return { load: async (path: string) => { if (path.startsWith("http")) { @@ -20,19 +21,12 @@ export function SupplementaryThirdPartyPropFunctions(engine: JsExec) { } try { - const subEngine = await Stack.spawn( - JsExec, - undefined, - Daemon?.userInfo?._id, - engine.pid, - join(engine.workingDirectory, path) - ); + const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); + if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); - if (engine.app && engine.metaPath) { - subEngine?.setApp(engine.app, engine.metaPath); - } + const subEngine = await jsExecService.setupEngine(join(engine.workingDirectory, path), engine.app, engine.metaPath); - return await subEngine?.getContents(); + return await jsExecService.getContents(subEngine); } catch (e) { throw e; } diff --git a/src/ts/daemon/contexts/spawn.ts b/src/ts/daemon/contexts/spawn.ts index 16740b59..7a14b9dc 100644 --- a/src/ts/daemon/contexts/spawn.ts +++ b/src/ts/daemon/contexts/spawn.ts @@ -3,10 +3,10 @@ import type { ISpawnUserContext } from "$interfaces/contexts/ISpawnUserContext"; import type { ICommandResult } from "$interfaces/ICommandResult"; import type { IProcess } from "$interfaces/IProcess"; import type { IUserDaemon } from "$interfaces/IUserDaemon"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; import { ThirdPartyProcess } from "$ts/apps/tpa/process"; import { ArcOSVersion, Daemon, Env, Stack } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { CommandResult } from "$ts/result"; import { cloneAppMeta } from "$ts/util/apps"; import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog"; @@ -158,9 +158,11 @@ export class SpawnUserContext extends UserContext implements ISpawnUserContext { try { const entrypoint = join(app.workingDirectory, app.entrypoint); - const engine = await JsExec.Invoke(entrypoint, ...args); - engine?.setApp(app, app.tpaPath); - const result = await engine?.getContents(); + + const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); + if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); + + const result = await jsExecService.Invoke(entrypoint, app, app.tpaPath, ...args); gli?.stop?.(); diff --git a/src/ts/jsexec/index.ts b/src/ts/jsexec/index.ts deleted file mode 100644 index f121761b..00000000 --- a/src/ts/jsexec/index.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * ArcOS JavaScript Execution Engine - * - * This file executes JS files in ArcOS under a relatively controlled environment. - * It is part of the ArcOS TPA framework: a system for running third-party apps. - * - * All rights belong to their respective authors. - * - * © IzKuipers 2025 - */ -import type { ITpaConnector } from "$interfaces/modules/server/ITpaConnector"; -import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; -import { ThirdPartyProps } from "$ts/apps/tpa/props"; -import { Daemon, Env, Fs, Stack } from "$ts/env"; -import { Process } from "$ts/kernel/mods/stack/process/instance"; -import { arrayBufferToText } from "$ts/util/convert"; -import { getItemNameFromPath, getParentDirectory } from "$ts/util/fs"; -import { UUID } from "$ts/util/uuid"; -import type { App } from "$types/apps/app"; -import type { ParsedImportStatement, ThirdPartyPropMap } from "$types/tpa/thirdparty"; -import * as acorn from "acorn"; - -export class JsExec extends Process { - public readonly TPA_REVISION = ThirdPartyAppProcess.TPA_REV; - props?: ThirdPartyPropMap; - app?: App; - args: any[]; - metaPath?: string; - filePath?: string; - workingDirectory: string; - operationId: string; - - //#region LIFECYCLE - - constructor(pid: number, parentPid: number, filePath: string, ...args: any[]) { - super(pid, parentPid); - - this.args = args; - this.filePath = filePath; - this.workingDirectory = getParentDirectory(filePath); - this.name = "JsExec"; - this.setSource(__SOURCE__); - this.operationId = UUID(); - } - - async start() { - if (!this.filePath) return false; - - this.props = ThirdPartyProps(this); - } - - //#endregion - //#region URL - - private async getTpaUrl(wrapped: string) { - this.Log(`Getting TPA file URL`); - - const { appId, userId, filename } = this.getTpaUrlInfo(); - try { - const urlResult = await Daemon!.GetConnector("TpaConnector").CreateUrl(wrapped, userId, appId, filename); - - if (!urlResult.success) throw new JsExecError(); - return urlResult.result!; - } catch (e: any) { - throw new JsExecError(`Failed to create momentary TPA URL: ${e?.message ?? e}`); - } - } - - private getTpaPostUrl() { - const { appId, userId, filename } = this.getTpaUrlInfo(); - - return `/tpa/v2/${userId}/${appId}/${filename}`; - } - - private getTpaUrlInfo() { - const appId = this.app?.id || "ArcOS"; - const userId = Daemon?.userInfo?._id || "SYSTEM"; - const filename = getItemNameFromPath(this.filePath!); - - return { appId, userId, filename }; - } - - //#endregion - //#region EXECUTION - - private async exec(tpaUrl: string) { - this.Log(`Executing ${this.filePath}`); - - const code = await import(/* @vite-ignore */ tpaUrl); - - if (!code.default || !(code.default instanceof Function)) throw new JsExecError("Expected a default function"); - - try { - const result = await code.default(this.props!); - return result; - } catch (e) { - throw e; - } finally { - await this.killSelf(); - } - } - - async getContents() { - this.Log(`Reading script contents`); - - const unwrapped = this.convertImportStatementsToRegex(arrayBufferToText((await Fs.readFile(this.filePath!))!)!); - if (!unwrapped) throw new JsExecError(`Failed to read ${this.filePath}: not found`); - - await this.testFileContents(unwrapped); - - const wrapped = this.wrap(unwrapped); - const tpaUrl = await this.getTpaUrl(wrapped); - - return await this.exec(tpaUrl); - } - - //#endregion - //#region HELPERS - - setApp(app: App, metaPath?: string) { - this.Log(`Setting app data to ${app.id} (${metaPath ?? ""})`); - - if (this.app) return; - - if (app.tpaRevision && app.tpaRevision > this.TPA_REVISION) - throw new JsExecError( - `This application expects a newer version of the TPA framework than what ArcOS can supply. Please update your ArcOS version and try again.` - ); - - this.app = app; - this.metaPath = metaPath; - this.props = ThirdPartyProps(this); - } - - private wrap(contents: string) { - if (!this.props) throw new JsExecError(`No TPA props to use`); - - return `export default async function({${Object.keys(this.props).join(",")}}) {\nconst global = arguments;\n${contents}\n}`; - } - - private convertImportStatementsToRegex(sourceFile: string) { - if (!sourceFile) return sourceFile; - const regex = - /import(?:(?:(?:[ \n\t]+(?[^ *\n\t\{\},]+)[ \n\t]*(?:,|[ \n\t]+))?(?[ \n\t]*\{(?:[ \n\t]*[^ \n\t"'\{\}]+[ \n\t]*,?)+\})?[ \n\t]*)|[ \n\t]*\*[ \n\t]*as[ \n\t]+(?[^ \n\t\{\}]+)[ \n\t]+)from[ \n\t]*(?:['"])(?[^'"\n]+)(?['"])/gm; - const matches = sourceFile - .matchAll(regex) - .toArray() - .map((m) => ({ ...m.groups, original: m[0] })) as ParsedImportStatement[]; - - for (const match of matches) { - const { destructured, default: defaultImport, filename, quote, wildcard } = match; - let loadStatement = `const ${destructured || defaultImport || wildcard} = await load(${quote}${filename}${quote})`; - - sourceFile = sourceFile.replace(match.original, loadStatement); - } - - return sourceFile; - } - - private async testFileContents(unwrapped: string) { - try { - const ast = acorn.parse(unwrapped, { - sourceType: "module", - ecmaVersion: "latest", - allowReturnOutsideFunction: true, - allowAwaitOutsideFunction: true, - }); - const hasExport = ast.body.some((node) => node.type.startsWith("Export")); - const hasImport = ast.body.some((node) => node.type.startsWith("Import")); - const hasDebugger = ast.body.some((node) => node.type.startsWith("Debugger")); - - if (hasExport) throw new JsExecError("Export statements are not valid inside of ArcOS"); - if (hasImport) throw new JsExecError("Import statements are not valid inside of ArcOS"); - if (hasDebugger) throw new JsExecError("Debugger triggers are not valid inside of ArcOS"); - } catch (e) { - throw new JsExecError(`An error occurred while parsing the source file: ${e}`); - } - } - - //#endregion - - static async Invoke(filePath: string, ...args: any[]) { - return await Stack.spawn(JsExec, undefined, undefined, +Env.get("userdaemon_pid"), filePath, ...args); - } -} - -export class JsExecError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options); - - this.name = "JsExecError"; - } -} diff --git a/src/ts/kernel/wavekernel.ts b/src/ts/kernel/wavekernel.ts index 3e16f585..8afa90e5 100644 --- a/src/ts/kernel/wavekernel.ts +++ b/src/ts/kernel/wavekernel.ts @@ -4,7 +4,6 @@ import type { IProcessHandler } from "$interfaces/modules/IProcessHandler"; import type { ISystemDispatch } from "$interfaces/modules/ISystemDispatch"; import { __Console__ } from "$ts/console"; import { ArcOSVersion, SetCurrentKernel, SetKernelExports } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { getBuild } from "$ts/metadata/build"; import { ChangeLogs } from "$ts/metadata/changelog"; import { getLicense } from "$ts/metadata/license"; @@ -41,7 +40,6 @@ export class WaveKernel implements IWaveKernel { if (import.meta.env.DEV) { const win = window as any; win.kernel = this; - win.JsExec = JsExec; } } diff --git a/src/ts/servicehost/services/JsExec/index.ts b/src/ts/servicehost/services/JsExec/index.ts index d81b4c16..4c0d7742 100644 --- a/src/ts/servicehost/services/JsExec/index.ts +++ b/src/ts/servicehost/services/JsExec/index.ts @@ -1,5 +1,5 @@ /** - * ArcOS JavaScript Execution Engine + * ArcOS JavaScript Execution Engine Service * * This file executes JS files in ArcOS under a relatively controlled environment. * It is part of the ArcOS TPA framework: a system for running third-party apps. @@ -11,15 +11,15 @@ import type { IServiceHost } from "$interfaces/IServiceHost"; import type { ITpaConnector } from "$interfaces/modules/server/ITpaConnector"; import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; -import { ThirdPartyProps } from "./props"; -import { Daemon, Env, Fs, Stack } from "$ts/env"; +import { ThirdPartyProps } from "$ts/apps/tpa/props"; +import { Daemon, Fs } from "$ts/env"; import { BaseService } from "$ts/servicehost/base"; import { arrayBufferToText } from "$ts/util/convert"; import { getItemNameFromPath, getParentDirectory } from "$ts/util/fs"; import { UUID } from "$ts/util/uuid"; import type { App } from "$types/apps/app"; import type { Service } from "$types/services/service"; -import type { ParsedImportStatement, ThirdPartyPropMap } from "$types/tpa/thirdparty"; +import type { ParsedImportStatement } from "$types/tpa/thirdparty"; import * as acorn from "acorn"; import type { JsExecEngineData } from "./engine"; import type { IJsExecService } from "$interfaces/services/IJsExec"; @@ -85,8 +85,6 @@ export class JsExecService extends BaseService implements IJsExecService { return result; } catch (e) { throw e; - } finally { - await this.killSelf(); } } diff --git a/src/ts/servicehost/services/JsExec/props.ts b/src/ts/servicehost/services/JsExec/props.ts deleted file mode 100644 index c9e4788e..00000000 --- a/src/ts/servicehost/services/JsExec/props.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { IThirdPartyAppProcess } from "$interfaces/IThirdPartyAppProcess"; -import { AppProcess } from "$ts/apps/process"; -import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; -import { __Console__ } from "$ts/console"; -import { Daemon, Env, Fs, Stack } from "$ts/env"; -import { getAllImages } from "$ts/images"; -import { FilesystemDrive } from "$ts/kernel/mods/fs/drives/generic"; -import { Backend } from "$ts/kernel/mods/server/axios"; -import { Process } from "$ts/kernel/mods/stack/process/instance"; -import { BaseService } from "$ts/servicehost/base"; -import { Sleep } from "$ts/sleep"; -import { contextProps } from "$ts/ui/context/actions.svelte"; -import { CustomTitlebar } from "$ts/ui/thirdparty/titlebar"; -import { TrayIconProcess } from "$ts/ui/tray/process"; -import { HiddenUserPaths, SystemFolders, UserPathCaptions, UserPathIcons, UserPaths } from "$ts/user/store"; -import { CountInstances, decimalToHex, htmlspecialchars, Plural, sha256, sliceIntoChunks } from "$ts/util"; -import { arrayBufferToBlob, arrayBufferToText, blobToDataURL, blobToText, textToArrayBuffer, textToBlob } from "$ts/util/convert"; -import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog"; -import { - DownloadFile, - formatBytes, - getDriveLetter, - getItemNameFromPath, - getParentDirectory, - join, - onFileChange, - onFolderChange, -} from "$ts/util/fs"; -import { tryJsonStringify } from "$ts/util/json"; -import { Store } from "$ts/writable"; -import type { ThirdPartyPropMap } from "$types/tpa/thirdparty"; -import axios from "axios"; -import dayjs from "dayjs"; -import { ThirdPartyProcess } from "$ts/apps/tpa/process"; -import { SupplementaryThirdPartyPropFunctions } from "./supplementary"; -import type { JsExecEngineData } from "./engine"; - -export function ThirdPartyProps(engine: JsExecEngineData): ThirdPartyPropMap { - const props = { - env: Env, // TEMP - handler: Stack, // TEMP - fs: Fs, // TEMP - daemon: Daemon, // TEMP - serviceHost: Daemon!.serviceHost, // TEMP - MessageBox, - icons: getAllImages(), - util: { - htmlspecialchars, - Plural, - sliceIntoChunks, - decimalToHex, - sha256, - CountInstances, - join, - getItemNameFromPath, - getParentDirectory, - getDriveLetter, - formatBytes, - DownloadFile, - onFileChange, - onFolderChange, - }, - convert: { - arrayToText: arrayBufferToText, - textToArrayBuffer, - blobToText, - textToBlob, - arrayToBlob: arrayBufferToBlob, - blobToDataURL, - }, - workingDirectory: engine.workingDirectory || engine.app?.workingDirectory!, - Process, - AppProcess, - ThirdPartyAppProcess, - ThirdPartyProcess, - FilesystemDrive, - argv: engine.args, - app: engine.app, - Store, - Sleep, - $ENTRYPOINT: engine.filePath, - $METADATA: engine.metaPath, - SHELL_PID: +Env.get("shell_pid"), - OPERATION_ID: engine.operationId, - load: async (path: string): Promise => {}, - runApp: async ( - process: typeof ThirdPartyAppProcess, - metadataPath: string, - parentPid?: number, - ...args: any[] - ): Promise => undefined, - loadHtml: async (path: string): Promise => undefined, - axios, - Server: Backend, - BaseService, - TrayIconProcess, - Debug: (m: any) => { - MessageBox( - { - title: "🐛🪵", - message: tryJsonStringify(m, 2), - image: "WindowSettingsIcon", - sound: "arcos.dialog.info", - buttons: [BTN_OKAY_SUG], - }, - +Env.get("shell_pid") - ); - }, - CustomTitlebar, - contextProps, - dayjs, - console: __Console__, - LogLevel: { - info: 0, - warning: 1, - error: 2, - critical: 3, - }, - UserPaths, - UserPathCaptions, - UserPathIcons, - SystemFolders, - HiddenUserPaths, - }; - - const supplementary = SupplementaryThirdPartyPropFunctions(engine); - - for (const [key, supp] of Object.entries(supplementary)) { - (props as any)[key] = supp; - } - - //@ts-ignore - return props; -} diff --git a/src/ts/servicehost/services/JsExec/supplementary.ts b/src/ts/servicehost/services/JsExec/supplementary.ts deleted file mode 100644 index 089487e0..00000000 --- a/src/ts/servicehost/services/JsExec/supplementary.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { Constructs } from "$interfaces/common"; -import type { IThirdPartyAppProcess } from "$interfaces/IThirdPartyAppProcess"; -import { Daemon, Fs, Stack } from "$ts/env"; -import { detectJavaScript } from "$ts/util"; -import { arrayBufferToText } from "$ts/util/convert"; -import { join } from "$ts/util/fs"; -import { tryJsonParse } from "$ts/util/json"; -import { ThirdPartyProcess } from "$ts/apps/tpa/process"; -import type { JsExecEngineData } from "./engine"; -import type { IJsExecService } from "$interfaces/services/IJsExec"; - -export function SupplementaryThirdPartyPropFunctions(engine: JsExecEngineData) { - return { - load: async (path: string) => { - if (path.startsWith("http")) { - try { - await import(/* @vite-ignore */ path); - } catch (e) { - throw e; - } - } - - try { - const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); - if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); - - const subEngine = await jsExecService.setupEngine(join(engine.workingDirectory, path), engine.app, engine.metaPath); - - return await jsExecService.getContents(subEngine); - } catch (e) { - throw e; - } - }, - runApp: async (process: Constructs, metadataPath: string, parentPid?: number, ...args: any[]) => { - if (process instanceof ThirdPartyProcess) { - throw new Error( - "Can't use runApp with a ThirdPartyProcess (non-app). Please directly return the process from the entrypoint." - ); - } - - const app = engine.app; - - if (!app || !Daemon) throw new Error(`Illegal runApp operation on a non-app JsExec`); - - try { - const metaStr = arrayBufferToText((await Fs.readFile(metadataPath))!); - const metadata = tryJsonParse(metaStr); - const renderTarget = Daemon.workspaces!.getCurrentDesktop(); - - if (typeof metadata === "string") throw new Error("Failed to parse metadata"); - - const proc = await Stack.spawn( - process, - renderTarget, - Daemon.userInfo!._id, - parentPid, - { - data: metadata, - id: metadata.id, - desktop: renderTarget ? renderTarget.id : undefined, - }, - engine.operationId, - app.workingDirectory, - ...args - ); - - app.process = proc; - - return proc; - } catch (e) { - throw e; - } - }, - runAppDirect: async ( - process: Constructs, - metadataPath: string, - parentPid?: number, - ...args: any[] - ) => { - const app = engine.app; - - if (!app || !Daemon) throw new Error(`Illegal runApp operation on a non-app JsExec`); - - try { - const metaStr = arrayBufferToText((await Fs.readFile(metadataPath))!); - const metadata = tryJsonParse(metaStr); - - if (typeof metadata === "string") throw new Error("Failed to parse metadata"); - - const proc = await Stack.spawn( - process, - undefined, - Daemon.userInfo!._id, - parentPid, - { - data: metadata, - id: metadata.id, - }, - app.workingDirectory, - ...args - ); - - app.process = proc; - - return proc; - } catch (e) { - throw e; - } - }, - loadHtml: async (path: string) => { - const htmlCode = arrayBufferToText((await Fs.readFile(join(engine.workingDirectory, path)))!); - - const detected = detectJavaScript(htmlCode!); - - if (detected) throw new Error(`- ${detected.join("\n- ")}`); - - return htmlCode; - }, - loadDirect: async (path: string) => { - const url = await Fs.direct(join(engine.workingDirectory!, path)); - }, - }; -} diff --git a/src/ts/servicehost/services/LibMgmtSvc/index.ts b/src/ts/servicehost/services/LibMgmtSvc/index.ts index 6181897b..8bbcfdb0 100644 --- a/src/ts/servicehost/services/LibMgmtSvc/index.ts +++ b/src/ts/servicehost/services/LibMgmtSvc/index.ts @@ -1,7 +1,7 @@ import type { IServiceHost } from "$interfaces/IServiceHost"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; import type { ILibraryManagement } from "$interfaces/services/ILibraryManagement"; import { Daemon, Fs, Stack } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { BaseService } from "$ts/servicehost/base"; import { UserPaths } from "$ts/user/store"; import { join } from "$ts/util/fs"; @@ -83,9 +83,11 @@ export class LibraryManagement extends BaseService implements ILibraryManagement try { const filePath = join(UserPaths.Libraries, id, library.entrypoint); - const engine = await Stack.spawn(JsExec, undefined, Daemon?.userInfo._id, Daemon?.pid, filePath); - return (await engine?.getContents()) as T; + const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); + if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); + + return (await jsExecService.Invoke(filePath)) as T; } catch { return defaultReturnValue as T; // TODO: determine } From ecec8ae2af7bbcf676008bb928162110e3cf17ad Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:10:51 -0500 Subject: [PATCH 3/5] add service auto start and stop with TPA setting --- src/apps/components/firstrun/store.ts | 3 ++- src/ts/daemon/contexts/applications.ts | 4 ++++ src/ts/servicehost/services/JsExec/index.ts | 7 +++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/apps/components/firstrun/store.ts b/src/apps/components/firstrun/store.ts index 675ee6de..fd88dc1c 100755 --- a/src/apps/components/firstrun/store.ts +++ b/src/apps/components/firstrun/store.ts @@ -131,11 +131,12 @@ export const FirstRunPages = new Map([ }, { caption: "Enable", - action: (process) => { + action: async (process) => { process.userPreferences.update((v) => { v.security.enableThirdParty = true; return v; }); + await Daemon!.serviceHost?.startService("JsExecSvc"); process.switchPage("finish"); }, suggested: true, diff --git a/src/ts/daemon/contexts/applications.ts b/src/ts/daemon/contexts/applications.ts index 26e26952..3b9ceffb 100644 --- a/src/ts/daemon/contexts/applications.ts +++ b/src/ts/daemon/contexts/applications.ts @@ -128,6 +128,8 @@ export class ApplicationsUserContext extends UserContext implements IApplication v.security.enableThirdParty = true; return v; }); + + await Daemon!.serviceHost?.startService("JsExecSvc"); } async disableThirdParty() { @@ -151,5 +153,7 @@ export class ApplicationsUserContext extends UserContext implements IApplication for (const [pid, proc] of [...store]) { if (!proc._disposed && (proc instanceof ThirdPartyAppProcess || proc instanceof ThirdPartyProcess)) Stack.kill(pid, true); } + + await Daemon!.serviceHost?.stopService("JsExecSvc"); } } diff --git a/src/ts/servicehost/services/JsExec/index.ts b/src/ts/servicehost/services/JsExec/index.ts index 4c0d7742..40ba42db 100644 --- a/src/ts/servicehost/services/JsExec/index.ts +++ b/src/ts/servicehost/services/JsExec/index.ts @@ -36,7 +36,7 @@ export class JsExecService extends BaseService implements IJsExecService { } async start() { - return Daemon?.preferences().security.enableThirdParty === true; + this.initBroadcast?.("Starting TPA service"); } //#endregion @@ -197,8 +197,11 @@ export class JsExecError extends Error { } export const jsExecService: Service = { - name: "TPA Host", + name: "TPA Service", description: "Provides the interface to spawn TPAs.", process: JsExecService, initialState: "started", + startCondition(daemon) { + return daemon.preferences().security.enableThirdParty; + }, }; From 444f18bed749fe477d2231f176133d7ee12fffe3 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:21:35 -0500 Subject: [PATCH 4/5] fix blown reference? --- src/interfaces/services/IJsExec.ts | 2 +- src/{ts/servicehost/services/JsExec => types/tpa}/engine.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{ts/servicehost/services/JsExec => types/tpa}/engine.ts (100%) diff --git a/src/interfaces/services/IJsExec.ts b/src/interfaces/services/IJsExec.ts index 35d6a423..9d1b6b92 100644 --- a/src/interfaces/services/IJsExec.ts +++ b/src/interfaces/services/IJsExec.ts @@ -1,5 +1,5 @@ import type { IBaseService } from "$interfaces/IServiceHost"; -import type { JsExecEngineData } from "$ts/servicehost/services/JsExec/engine"; +import type { JsExecEngineData } from "$types/tpa/engine"; import type { App } from "$types/apps/app"; // !tpa diff --git a/src/ts/servicehost/services/JsExec/engine.ts b/src/types/tpa/engine.ts similarity index 100% rename from src/ts/servicehost/services/JsExec/engine.ts rename to src/types/tpa/engine.ts From 2b908002abd60441b404ea8e4a8d490917faf0d9 Mon Sep 17 00:00:00 2001 From: Blockyheadman <80011716+Blockyheadman@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:24:31 -0500 Subject: [PATCH 5/5] whoops --- src/ts/apps/tpa/props.ts | 2 +- src/ts/apps/tpa/supplementary.ts | 2 +- src/ts/servicehost/services/JsExec/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ts/apps/tpa/props.ts b/src/ts/apps/tpa/props.ts index 6796e41f..012ab287 100644 --- a/src/ts/apps/tpa/props.ts +++ b/src/ts/apps/tpa/props.ts @@ -33,7 +33,7 @@ import axios from "axios"; import dayjs from "dayjs"; import { ThirdPartyProcess } from "$ts/apps/tpa/process"; import { SupplementaryThirdPartyPropFunctions } from "./supplementary"; -import type { JsExecEngineData } from "$ts/servicehost/services/JsExec/engine"; +import type { JsExecEngineData } from "$types/tpa/engine"; export function ThirdPartyProps(engine: JsExecEngineData): ThirdPartyPropMap { const props = { diff --git a/src/ts/apps/tpa/supplementary.ts b/src/ts/apps/tpa/supplementary.ts index bf2bb573..1a067d10 100644 --- a/src/ts/apps/tpa/supplementary.ts +++ b/src/ts/apps/tpa/supplementary.ts @@ -6,7 +6,7 @@ import { arrayBufferToText } from "$ts/util/convert"; import { join } from "$ts/util/fs"; import { tryJsonParse } from "$ts/util/json"; import { ThirdPartyProcess } from "$ts/apps/tpa/process"; -import type { JsExecEngineData } from "$ts/servicehost/services/JsExec/engine"; +import type { JsExecEngineData } from "$types/tpa/engine"; import type { IJsExecService } from "$interfaces/services/IJsExec"; export function SupplementaryThirdPartyPropFunctions(engine: JsExecEngineData) { diff --git a/src/ts/servicehost/services/JsExec/index.ts b/src/ts/servicehost/services/JsExec/index.ts index 40ba42db..62e8ea74 100644 --- a/src/ts/servicehost/services/JsExec/index.ts +++ b/src/ts/servicehost/services/JsExec/index.ts @@ -21,7 +21,7 @@ import type { App } from "$types/apps/app"; import type { Service } from "$types/services/service"; import type { ParsedImportStatement } from "$types/tpa/thirdparty"; import * as acorn from "acorn"; -import type { JsExecEngineData } from "./engine"; +import type { JsExecEngineData } from "$types/tpa/engine"; import type { IJsExecService } from "$interfaces/services/IJsExec"; export class JsExecService extends BaseService implements IJsExecService {