diff --git a/package.json b/package.json index 6bc09029b..d4494109a 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "dedent": "^1.5.3", "javascript-time-ago": "^2.5.11", "libphonenumber-js": "1.13.6", + "miniflare": "^4.20260617.1", "open": "^10.1.0" }, "engines": { diff --git a/src/backend/drivers/workers/WorkerDriver.ts b/src/backend/drivers/workers/WorkerDriver.ts index b98ba7b7f..f712aff64 100644 --- a/src/backend/drivers/workers/WorkerDriver.ts +++ b/src/backend/drivers/workers/WorkerDriver.ts @@ -100,6 +100,8 @@ export class WorkerDriver extends PuterDriver { '[workers] preamble not build but workers configured to be enabled. Halting start', ); } + } else if (cfg.localServer) { + // } this.#subscribeHotReload(); } diff --git a/src/backend/services/localworker/LocalWorkerService.ts b/src/backend/services/localworker/LocalWorkerService.ts new file mode 100644 index 000000000..80e6e371f --- /dev/null +++ b/src/backend/services/localworker/LocalWorkerService.ts @@ -0,0 +1,118 @@ +import { Miniflare, RequestInit } from 'miniflare'; +import { puterServices } from '..'; +import { Actor } from '../../core'; +import { loadFileInput } from '../../drivers/util/fileInput'; +import { puterStores } from '../../stores'; +import { LayerInstances } from '../../types'; +import { PuterService } from '../types'; + +const MAX_SOURCE_SIZE = 10 * 1024 * 1024; // 10 MB + +interface SubdomainRow { + id: number; + uuid: string; + ts: number | string; // system timestamp + subdomain: string; // immutable name + user_id: number; // owner + app_owner: number | null; // owning app, if any + protected: 0 | 1; // access gate + database_id: string | null; // Cloudflare D1 binding + root_dir_id: number | null; // editable + associated_app_id: string | null; // editable + domain: string | null; // custom domain, editable +} + +const activeWorkers = new Map(); + +export class LocalWorkerService extends PuterService { + declare protected stores: LayerInstances; + declare protected services: LayerInstances; + async cfDeployLocal( + workerName: string, + authorization: string, + code: string, + ) { + const mf = new Miniflare({ + modules: false, + name: workerName, + bindings: { + puter_auth: authorization, + + //todo: maybe dont hardcode this + puter_endpoint: 'http://api.puter.localhost:4100/', + }, // Binds variable/secret to environment + script: code, + } as WorkerOptions); + activeWorkers.set(workerName, mf); + } + async cfCallLocal(workerName: string, request: Request) { + let mf = activeWorkers.get(workerName); + if (!mf) { + // cfDeployLocal here + const existingSub: SubdomainRow | null = + await this.stores.subdomain.getBySubdomain( + 'workers.puter.' + workerName, + ); + + if (!existingSub) { + return new Response('subdomain not found', { status: 404 }); + } + const [_, authorization, code] = await this.reconstructDeployArgs( + workerName, + existingSub, + ); + await this.cfDeployLocal(workerName, authorization, code); + mf = activeWorkers.get(workerName)!; + } + return mf.dispatchFetch(request.url, request as unknown as RequestInit); + } + async cfDeleteLocal(workerName: string) { + const mf = activeWorkers.get(workerName); + if (mf) { + mf.dispose(); + activeWorkers.delete(workerName); + } + return true; + } + async reconstructDeployArgs(workerName: string, row: SubdomainRow) { + const appOwnerId = row.app_owner as number | null; + let authorization: string; + const ownerUser = await this.stores.user.getById(row.user_id); + if (!ownerUser) throw new Error('Owner seems to not exist'); + const ownerActor = { user: ownerUser } as Actor; + + if (appOwnerId) { + const app = await this.stores.app.getById(appOwnerId); + if (!app) + throw new Error( + 'Local: Worker belongs to existant application', + ); // app gone + authorization = await this.services.auth.createWorkerAppToken( + ownerActor, + app.uid, + workerName, + ); + } else { + const session = await this.services.auth.createWorkerSessionToken( + ownerUser, + workerName, + ); + + authorization = session.token; + } + + const loaded = await loadFileInput( + { + fsEntry: this.stores.fsEntry, + s3Object: this.stores.s3Object, + }, + this.services.fs, + ownerActor, + row.root_dir_id, + { maxBytes: MAX_SOURCE_SIZE }, + ); + const code = loaded.buffer.toString('utf-8'); + + return [workerName, authorization, code]; + } +}