From 95d797b0476fc4af71cf7354d4310cd37475cbd9 Mon Sep 17 00:00:00 2001 From: Anany Singh <91945859+Deadeye102000@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:35:17 +0530 Subject: [PATCH] feat(workers): expose app_uid in puter.workers.get and puter.workers.list (#3596) --- .../workers/WorkerDriver.cloudflare.test.ts | 10 ++-- .../drivers/workers/WorkerDriver.test.ts | 50 +++++++++++++++++-- src/backend/drivers/workers/WorkerDriver.ts | 11 ++++ src/docs/src/Objects/workerinfo.md | 4 ++ src/puter-js/src/modules/Workers.js | 1 + .../tests/api/suites/workers.suite.ts | 19 +++++-- 6 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/backend/drivers/workers/WorkerDriver.cloudflare.test.ts b/src/backend/drivers/workers/WorkerDriver.cloudflare.test.ts index ba79b041c..c1ecc69f1 100644 --- a/src/backend/drivers/workers/WorkerDriver.cloudflare.test.ts +++ b/src/backend/drivers/workers/WorkerDriver.cloudflare.test.ts @@ -152,11 +152,11 @@ const waitFor = async ( const putCalls = () => fetchSpy.mock.calls.filter( - ([, init]) => (init as RequestInit | undefined)?.method === 'PUT', + (call) => (call[1] as RequestInit | undefined)?.method === 'PUT', ); const deleteCalls = () => fetchSpy.mock.calls.filter( - ([, init]) => (init as RequestInit | undefined)?.method === 'DELETE', + (call) => (call[1] as RequestInit | undefined)?.method === 'DELETE', ); // -- create ---------------------------------------------------------- @@ -504,6 +504,7 @@ describe('WorkerDriver.getFilePaths source resolution', () => { url: string; file_path: string | null; file_uid: string | null; + app_uid: string | null; created_at: string | null; }>; @@ -512,6 +513,7 @@ describe('WorkerDriver.getFilePaths source resolution', () => { expect(row.url).toBe(`https://${name}.puter.work`); expect(row.file_path).toBe(path); expect(row.file_uid).toBe(entry.uuid); + expect(row.app_uid).toBeNull(); expect(row.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T/); }); @@ -619,7 +621,7 @@ describe('WorkerDriver hot reload', () => { ); return !row; }, 'subdomain row removal after source delete'); - expect(deleteCalls().map(([u]) => u)).toContain( + expect(deleteCalls().map((call) => call[0])).toContain( `${SCRIPTS_BASE}/${name}/`, ); expect(path).toContain(user.username); @@ -646,7 +648,7 @@ describe('WorkerDriver hot reload', () => { ); return !row; }, 'subdomain row removal after trash move'); - expect(deleteCalls().map(([u]) => u)).toContain( + expect(deleteCalls().map((call) => call[0])).toContain( `${SCRIPTS_BASE}/${name}/`, ); }); diff --git a/src/backend/drivers/workers/WorkerDriver.test.ts b/src/backend/drivers/workers/WorkerDriver.test.ts index 4d8dbc673..dca323c28 100644 --- a/src/backend/drivers/workers/WorkerDriver.test.ts +++ b/src/backend/drivers/workers/WorkerDriver.test.ts @@ -190,7 +190,7 @@ describe('WorkerDriver', () => { it('rejects reserved worker names', async () => { const serverWithReserved = await setupTestServer({ reserved_words: ['admin', 'api'], - }); + } as never); const driverWithReserved = serverWithReserved.drivers .workers as unknown as WorkerDriver; try { @@ -486,6 +486,46 @@ describe('WorkerDriver', () => { expect.arrayContaining([`${tag}-bound`, `${tag}-loose`]), ); }); + + it('returns app_uid for sandboxed/app-bound workers and null for user-scoped workers', async () => { + const tag = `wkuid${Math.random().toString(36).slice(2, 8)}`; + const owner = await makeUser(tag); + const app = await server.stores.app.create( + { + name: `app-${tag}`, + title: `app-${tag}`, + index_url: `https://app-${tag}.example.com/`, + }, + { ownerUserId: owner.id }, + ); + await server.stores.subdomain.create({ + userId: owner.id, + subdomain: `workers.puter.${tag}-bound`, + rootDirId: null, + associatedAppId: null, + appOwner: app.id, + }); + await server.stores.subdomain.create({ + userId: owner.id, + subdomain: `workers.puter.${tag}-plain`, + rootDirId: null, + associatedAppId: null, + appOwner: null, + }); + + const seen = (await inCtx( + () => target.getFilePaths({}), + actorFor(owner), + )) as Array<{ name: string; app_uid: string | null }>; + + const bound = seen.find((r) => r.name === `${tag}-bound`); + const plain = seen.find((r) => r.name === `${tag}-plain`); + + expect(bound).toBeDefined(); + expect(bound?.app_uid).toBe(app.uid); + expect(plain).toBeDefined(); + expect(plain?.app_uid).toBeNull(); + }); }); describe('getFilePaths pagination', () => { @@ -586,7 +626,7 @@ describe('WorkerDriver', () => { it('returns the configured loggingUrl', async () => { const serverWithLogging = await setupTestServer({ workers: { loggingUrl: 'https://logs.test/view' }, - }); + } as never); const driverWithLogging = serverWithLogging.drivers .workers as unknown as WorkerDriver; try { @@ -622,7 +662,7 @@ describe('WorkerDriver', () => { it('rejects an unverified account without the bypass', async () => { const strictServer = await setupTestServer({ strict_email_verification_required: true, - }); + } as never); const strictDriver = strictServer.drivers .workers as unknown as WorkerDriver; try { @@ -646,7 +686,7 @@ describe('WorkerDriver', () => { it('lets the bypass past the verified-email gate', async () => { const strictServer = await setupTestServer({ strict_email_verification_required: true, - }); + } as never); const strictDriver = strictServer.drivers .workers as unknown as WorkerDriver; try { @@ -725,7 +765,7 @@ describe('WorkerDriver', () => { it('cannot be forged through caller-supplied args', async () => { const strictServer = await setupTestServer({ strict_email_verification_required: true, - }); + } as never); const strictDriver = strictServer.drivers .workers as unknown as WorkerDriver; // Driver args reach the method as parsed JSON from the request diff --git a/src/backend/drivers/workers/WorkerDriver.ts b/src/backend/drivers/workers/WorkerDriver.ts index 4f2944f6e..0bb4f5556 100644 --- a/src/backend/drivers/workers/WorkerDriver.ts +++ b/src/backend/drivers/workers/WorkerDriver.ts @@ -558,6 +558,11 @@ export class WorkerDriver extends PuterDriver { }); } + const appOwnerIds = rows + .map((r) => r.app_owner) + .filter((id): id is number => typeof id === 'number'); + const appsById = await this.stores.app.getByIds(appOwnerIds); + const items = rows.map((r) => { const name = String(r.subdomain ?? '') @@ -570,11 +575,17 @@ export class WorkerDriver extends PuterDriver { file_path = loaded?.path; file_uid = loaded?.uuid; } + let app_uid = null; + if (typeof r.app_owner === 'number') { + const loadedApp = appsById.get(r.app_owner); + app_uid = loadedApp?.uid ?? null; + } return { name, url: `https://${name}.puter.work`, file_path, file_uid, + app_uid, created_at: r.ts ? new Date(r.ts as string).toISOString() : null, diff --git a/src/docs/src/Objects/workerinfo.md b/src/docs/src/Objects/workerinfo.md index abca9ed9a..819430ec0 100644 --- a/src/docs/src/Objects/workerinfo.md +++ b/src/docs/src/Objects/workerinfo.md @@ -23,6 +23,10 @@ A string containing the file path of the worker source code. A string containing the unique identifier of the worker file. +#### `app_uid` (String) + +A string containing the unique identifier of the app or sandbox app associated with the worker, or `null` if the worker is user-scoped. + #### `created_at` (String) A string containing the date and time when the worker was created. diff --git a/src/puter-js/src/modules/Workers.js b/src/puter-js/src/modules/Workers.js index 916ce11a1..e5cba622b 100644 --- a/src/puter-js/src/modules/Workers.js +++ b/src/puter-js/src/modules/Workers.js @@ -11,6 +11,7 @@ import { fetchAllPages, iteratePages } from '../lib/pagination.js'; * @property {string} url The URL of the worker. * @property {string} file_path The file path of the worker's source code. * @property {string} file_uid The unique identifier of the worker file. + * @property {string | null} app_uid The unique identifier of the app or sandbox app owning the worker, or null if user-scoped. * @property {string} created_at The date and time when the worker was created. */ diff --git a/src/puter-js/tests/api/suites/workers.suite.ts b/src/puter-js/tests/api/suites/workers.suite.ts index 5323f0cc1..c221449bf 100644 --- a/src/puter-js/tests/api/suites/workers.suite.ts +++ b/src/puter-js/tests/api/suites/workers.suite.ts @@ -161,9 +161,20 @@ export default suite('workers', { }, 'get returns the deployed worker': async (t) => { - await deployWorker(t, 'workers-suite-get'); - const worker = await t.puter.workers.get('workers-suite-get'); + const name = 'workers-suite-get'; + await deployWorker(t, name); + const worker = await t.puter.workers.get(name); t.assert.ok(worker, 'get should return the worker'); + const sandboxApp = await t.puter.apps.get(`sandbox-${name}`); + t.assert.equal(worker.app_uid, sandboxApp.uid, 'worker app_uid should match sandbox app uid'); + }, + + 'get returns null app_uid for unsandboxed worker': async (t) => { + const name = 'workers-suite-get-plain'; + await deployWorker(t, name, { sandbox: false }); + const worker = await t.puter.workers.get(name); + t.assert.ok(worker, 'get should return the worker'); + t.assert.equal(worker.app_uid, null, 'unsandboxed worker app_uid should be null'); }, 'list includes deployed workers': async (t) => { @@ -231,7 +242,7 @@ export default suite('workers', { 'create binds the worker to a named app': async (t) => { const appName = 'workers-suite-host-app'; - await t.puter.apps.create(appName, 'https://example.com/worker-host'); + const hostApp = await t.puter.apps.create(appName, 'https://example.com/worker-host'); const name = 'workers-suite-bound'; const sourcePath = `${home(t)}/workers-suite-${name}.js`; await t.puter.fs.write(sourcePath, WORKER_SOURCE); @@ -242,6 +253,8 @@ export default suite('workers', { false, 'binding to an app should not create a sandbox app', ); + const worker = await t.puter.workers.get(name); + t.assert.equal(worker?.app_uid, hostApp.uid, 'bound worker app_uid should match host app uid'); }, 'create with an unknown app name rejects with app_not_found': async (t) => {