diff --git a/packages/cli/README.md b/packages/cli/README.md index 2c442f584..d7b7b1ac9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -6,7 +6,7 @@ Ocap Kernel cli. ### `ocap bundle ` -Bundle the supplied file or directory targets. Expects each target to be a `.js` file or a directory containing `.js` files. Each `.js` file will be bundled using `@endo/bundle-source` and written to an associated `.bundle`. +Bundle the supplied file or directory targets. Expects each target to be a `.js` file or a directory containing `.js` files. Each `.js` file will be bundled using `vite` and written to an associated `.bundle`. ### `ocap watch ` diff --git a/packages/cli/package.json b/packages/cli/package.json index 93ba4196a..eb367c130 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,8 +36,6 @@ "dependencies": { "@chainsafe/libp2p-noise": "^16.1.3", "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch", - "@endo/bundle-source": "^4.1.2", - "@endo/init": "^1.1.12", "@endo/promise-kit": "^1.1.13", "@libp2p/autonat": "2.0.38", "@libp2p/circuit-relay-v2": "3.2.24", @@ -53,10 +51,12 @@ "@metamask/snaps-utils": "^11.7.1", "@metamask/utils": "^11.9.0", "@types/node": "^22.13.1", + "acorn": "^8.15.0", "chokidar": "^4.0.1", "glob": "^11.0.0", "libp2p": "2.10.0", "serve-handler": "^6.1.6", + "vite": "^7.3.0", "yargs": "^17.7.2" }, "devDependencies": { @@ -86,12 +86,12 @@ "jsdom": "^27.4.0", "prettier": "^3.5.3", "rimraf": "^6.0.1", + "rollup": "^4.55.3", "ses": "^1.14.0", "turbo": "^2.5.6", "typedoc": "^0.28.1", "typescript": "~5.8.2", "typescript-eslint": "^8.29.0", - "vite": "^7.3.0", "vitest": "^4.0.16" }, "engines": { diff --git a/packages/cli/src/app.ts b/packages/cli/src/app.ts index 121e86a9c..0ce8b70ec 100755 --- a/packages/cli/src/app.ts +++ b/packages/cli/src/app.ts @@ -1,5 +1,4 @@ -import '@endo/init'; - +import '@metamask/kernel-shims/endoify'; import { Logger } from '@metamask/logger'; import path from 'node:path'; import yargs from 'yargs'; diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index e83fec3c7..a4a3608bc 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -12,7 +12,7 @@ import { fileExists } from '../file.ts'; const mocks = vi.hoisted(() => { return { - endoBundleSource: vi.fn(), + bundleVat: vi.fn(), Logger: vi.fn( () => ({ @@ -25,12 +25,10 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock('@endo/bundle-source', () => ({ - default: mocks.endoBundleSource, +vi.mock('../vite/vat-bundler.ts', () => ({ + bundleVat: mocks.bundleVat, })); -vi.mock('@endo/init', () => ({})); - vi.mock('@metamask/logger', () => ({ Logger: mocks.Logger, })); @@ -68,8 +66,13 @@ describe('bundle', async () => { async ({ source, bundle }) => { expect(await fileExists(bundle)).toBe(false); - const testContent = { source: 'test-content' }; - mocks.endoBundleSource.mockImplementationOnce(() => testContent); + const testContent = { + moduleFormat: 'iife', + code: 'test-code', + exports: [], + modules: {}, + }; + mocks.bundleVat.mockImplementationOnce(() => testContent); await bundleFile(source, { logger }); @@ -84,7 +87,7 @@ describe('bundle', async () => { ); it('throws if bundling fails', async () => { - mocks.endoBundleSource.mockImplementationOnce(() => { + mocks.bundleVat.mockImplementationOnce(() => { throw new Error('test error'); }); await expect( @@ -97,9 +100,12 @@ describe('bundle', async () => { it('bundles a directory', async () => { expect(await globBundles()).toStrictEqual([]); - mocks.endoBundleSource.mockImplementation(() => { - return 'test content'; - }); + mocks.bundleVat.mockImplementation(() => ({ + moduleFormat: 'iife', + code: 'test content', + exports: [], + modules: {}, + })); await bundleDir(testBundleRoot, { logger }); @@ -111,7 +117,7 @@ describe('bundle', async () => { }); it('throws if bundling fails', async () => { - mocks.endoBundleSource.mockImplementationOnce(() => { + mocks.bundleVat.mockImplementationOnce(() => { throw new Error('test error'); }); await expect(bundleDir(testBundleRoot, { logger })).rejects.toThrow( diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index 8c4813870..79162f980 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -1,12 +1,11 @@ -import '@endo/init'; -import endoBundleSource from '@endo/bundle-source'; -import { Logger } from '@metamask/logger'; +import type { Logger } from '@metamask/logger'; import { glob } from 'glob'; import { writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { isDirectory } from '../file.ts'; import { resolveBundlePath } from '../path.ts'; +import { bundleVat } from '../vite/vat-bundler.ts'; type BundleFileOptions = { logger: Logger; @@ -30,7 +29,7 @@ export async function bundleFile( const { logger, targetPath } = options; const sourceFullPath = resolve(sourcePath); const bundlePath = targetPath ?? resolveBundlePath(sourceFullPath); - const bundle = await endoBundleSource(sourceFullPath); + const bundle = await bundleVat(sourceFullPath); const bundleContent = JSON.stringify(bundle); await writeFile(bundlePath, bundleContent); logger.info(`Wrote ${bundlePath}: ${new Blob([bundleContent]).size} bytes`); diff --git a/packages/cli/src/commands/watch.ts b/packages/cli/src/commands/watch.ts index 71dd6cde7..ca13361c8 100644 --- a/packages/cli/src/commands/watch.ts +++ b/packages/cli/src/commands/watch.ts @@ -1,4 +1,5 @@ import { makePromiseKit } from '@endo/promise-kit'; +import type { PromiseKit } from '@endo/promise-kit'; import { Logger } from '@metamask/logger'; import { watch } from 'chokidar'; import type { FSWatcher, MatchFunction } from 'chokidar'; @@ -17,8 +18,8 @@ type WatchDirReturn = { export const makeWatchEvents = ( watcher: FSWatcher, - readyResolve: ReturnType>['resolve'], - throwError: ReturnType>['reject'], + readyResolve: PromiseKit['resolve'], + throwError: PromiseKit['reject'], logger: Logger, ): { ready: () => void; diff --git a/packages/cli/src/vite/export-metadata-plugin.ts b/packages/cli/src/vite/export-metadata-plugin.ts new file mode 100644 index 000000000..c28b6f279 --- /dev/null +++ b/packages/cli/src/vite/export-metadata-plugin.ts @@ -0,0 +1,48 @@ +import type { Plugin, RenderedModule } from 'rollup'; + +export type BundleMetadata = { + exports: string[]; + modules: Record< + string, + { renderedExports: string[]; removedExports: string[] } + >; +}; + +/** + * Rollup plugin that captures export metadata from the bundle. + * + * Uses the `generateBundle` hook to extract the exports array and + * per-module metadata (renderedExports and removedExports) from the + * entry chunk. + * + * @returns A plugin with an additional `getMetadata()` method. + */ +export function exportMetadataPlugin(): Plugin & { + getMetadata: () => BundleMetadata; +} { + const metadata: BundleMetadata = { exports: [], modules: {} }; + + return { + name: 'export-metadata', + generateBundle(_, bundle) { + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk' && chunk.isEntry) { + const outputChunk = chunk; + metadata.exports = outputChunk.exports; + metadata.modules = Object.fromEntries( + Object.entries(outputChunk.modules).map( + ([id, info]: [string, RenderedModule]) => [ + id, + { + renderedExports: info.renderedExports, + removedExports: info.removedExports, + }, + ], + ), + ); + } + } + }, + getMetadata: () => metadata, + }; +} diff --git a/packages/cli/src/vite/strip-comments-plugin.test.ts b/packages/cli/src/vite/strip-comments-plugin.test.ts new file mode 100644 index 000000000..951f8d8eb --- /dev/null +++ b/packages/cli/src/vite/strip-comments-plugin.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; + +import { stripCommentsPlugin } from './strip-comments-plugin.ts'; + +describe('stripCommentsPlugin', () => { + const plugin = stripCommentsPlugin(); + const renderChunk = plugin.renderChunk as (code: string) => string | null; + + it.each([ + [ + 'single-line comment', + 'const x = 1; // comment\nconst y = 2;', + 'const x = 1; \nconst y = 2;', + ], + [ + 'multi-line comment', + 'const x = 1; /* comment */ const y = 2;', + 'const x = 1; const y = 2;', + ], + [ + 'multiple comments', + '/* a */ const x = 1; // b\n/* c */', + ' const x = 1; \n', + ], + [ + 'comment containing import()', + 'const x = 1; // import("module")\nconst y = 2;', + 'const x = 1; \nconst y = 2;', + ], + [ + 'comment with string content preserved', + 'const x = "// in string"; // real comment', + 'const x = "// in string"; ', + ], + ['code that is only a comment', '// just a comment', ''], + ])('removes %s', (_name, code, expected) => { + expect(renderChunk(code)).toBe(expected); + }); + + it.each([ + ['string with // pattern', 'const x = "// not a comment";'], + ['string with /* */ pattern', 'const x = "/* not a comment */";'], + ['regex literal like //', 'const re = /\\/\\//;'], + ['template literal with // pattern', 'const x = `// not a comment`;'], + ['nested quotes in string', 'const x = "a \\"// not comment\\" b";'], + ['no comments', 'const x = 1;'], + ['empty code', ''], + ])('returns null for %s', (_name, code) => { + expect(renderChunk(code)).toBeNull(); + }); +}); diff --git a/packages/cli/src/vite/strip-comments-plugin.ts b/packages/cli/src/vite/strip-comments-plugin.ts new file mode 100644 index 000000000..c8bd03b35 --- /dev/null +++ b/packages/cli/src/vite/strip-comments-plugin.ts @@ -0,0 +1,46 @@ +import type { Comment } from 'acorn'; +import { parse } from 'acorn'; +import type { Plugin } from 'rollup'; + +/** + * Rollup plugin that strips comments from bundled code using AST parsing. + * + * SES rejects code containing `import(` patterns, even when they appear + * in comments. This plugin uses Acorn to definitively identify comment nodes + * and removes them to avoid triggering that detection. + * + * Uses the `renderChunk` hook to process the final output. + * + * @returns A Rollup plugin. + */ +export function stripCommentsPlugin(): Plugin { + return { + name: 'strip-comments', + renderChunk(code) { + const comments: Comment[] = []; + + parse(code, { + ecmaVersion: 'latest', + sourceType: 'module', + onComment: comments, + }); + + if (comments.length === 0) { + return null; + } + + // Build result by copying non-comment ranges. + // Comments are sorted by position since acorn parses linearly. + let result = ''; + let position = 0; + + for (const comment of comments) { + result += code.slice(position, comment.start); + position = comment.end; + } + + result += code.slice(position); + return result; + }, + }; +} diff --git a/packages/cli/src/vite/vat-bundler.ts b/packages/cli/src/vite/vat-bundler.ts new file mode 100644 index 000000000..4a526b2f6 --- /dev/null +++ b/packages/cli/src/vite/vat-bundler.ts @@ -0,0 +1,60 @@ +import type { VatBundle } from '@metamask/kernel-utils'; +import { build } from 'vite'; +import type { Rollup, PluginOption } from 'vite'; + +import { exportMetadataPlugin } from './export-metadata-plugin.ts'; +import { stripCommentsPlugin } from './strip-comments-plugin.ts'; + +export type { VatBundle }; + +/** + * Bundle a vat source file using vite. + * + * Produces an IIFE bundle that assigns exports to a `__vatExports__` global, + * along with metadata about the bundle's exports and modules. + * + * @param sourcePath - Absolute path to the vat entry point. + * @returns The bundle object containing code and metadata. + */ +export async function bundleVat(sourcePath: string): Promise { + const metadataPlugin = exportMetadataPlugin(); + + const result = await build({ + configFile: false, + logLevel: 'silent', + build: { + write: false, + lib: { + entry: sourcePath, + formats: ['iife'], + name: '__vatExports__', + }, + rollupOptions: { + output: { + exports: 'named', + inlineDynamicImports: true, + }, + plugins: [ + stripCommentsPlugin() as unknown as PluginOption, + metadataPlugin as unknown as PluginOption, + ], + }, + minify: false, + }, + }); + + const output = Array.isArray(result) ? result[0] : result; + const chunk = (output as Rollup.RollupOutput).output.find( + (item): item is Rollup.OutputChunk => item.type === 'chunk' && item.isEntry, + ); + + if (!chunk) { + throw new Error(`Failed to produce bundle for ${sourcePath}`); + } + + return { + moduleFormat: 'iife', + code: chunk.code, + ...metadataPlugin.getMetadata(), + }; +} diff --git a/packages/cli/test/integration/serve.test.ts b/packages/cli/test/integration/serve.test.ts index 2f2a4d200..84cc14111 100644 --- a/packages/cli/test/integration/serve.test.ts +++ b/packages/cli/test/integration/serve.test.ts @@ -1,8 +1,5 @@ import '@metamask/kernel-shims/endoify'; -import type { BundleSourceResult } from '@endo/bundle-source'; -import { makeCounter, stringify } from '@metamask/kernel-utils'; -import { isObject, hasProperty } from '@metamask/utils'; -import { createHash } from 'node:crypto'; +import { isVatBundle, makeCounter, stringify } from '@metamask/kernel-utils'; import { readFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -12,17 +9,6 @@ import { defaultConfig } from '../../src/config.ts'; import { withTimeout } from '../../src/utils.ts'; import { makeTestBundleStage, validTestBundleNames } from '../bundles.ts'; -const isBundleSourceResult = ( - value: unknown, -): value is BundleSourceResult<'endoZipBase64'> => - isObject(value) && - hasProperty(value, 'moduleFormat') && - value.moduleFormat === 'endoZipBase64' && - hasProperty(value, 'endoZipBase64') && - typeof value.endoZipBase64 === 'string' && - hasProperty(value, 'endoZipBase64Sha512') && - typeof value.endoZipBase64Sha512 === 'string'; - describe('serve', async () => { beforeEach(() => { vi.resetModules(); @@ -86,7 +72,7 @@ describe('serve', async () => { try { const bundleData = await readFile(bundlePath); const expectedBundleContent = JSON.parse(bundleData.toString()); - if (!isBundleSourceResult(expectedBundleContent)) { + if (!isVatBundle(expectedBundleContent)) { throw new Error( [ `Could not read expected bundle ${bundlePath}`, @@ -94,19 +80,16 @@ describe('serve', async () => { ].join('\n'), ); } - const expectedBundleHash = expectedBundleContent.endoZipBase64Sha512; + const expectedCode = expectedBundleContent.code; const receivedBundleContent = await requestBundle(bundleName); - if (!isBundleSourceResult(receivedBundleContent)) { + if (!isVatBundle(receivedBundleContent)) { throw new Error( `Received unexpected response from server: ${stringify(receivedBundleContent)}`, ); } - const receivedBundleHash = createHash('sha512') - .update(Buffer.from(receivedBundleContent.endoZipBase64)) - .digest('hex'); - expect(receivedBundleHash).toStrictEqual(expectedBundleHash); + expect(receivedBundleContent.code).toStrictEqual(expectedCode); } finally { await withTimeout(close(), 400).catch(console.error); } diff --git a/packages/cli/test/test.bundle b/packages/cli/test/test.bundle index cb26f3a08..5dce507f1 100644 --- a/packages/cli/test/test.bundle +++ b/packages/cli/test/test.bundle @@ -1 +1 @@ -{"moduleFormat":"endoZipBase64","endoZipBase64":"This is merely a test bundle!","endoZipBase64Sha512":"d0ef05812fb8a7cce57dcb8d6f6fbe5677f175e2ef966df1d81dec9868a8f56b0e89b691fdb3d43a3902962adcf37d1eee6902a113476cca85f8d76957bdf154"} \ No newline at end of file +{"moduleFormat":"iife","code":"var __vatExports__ = (function(exports) { exports.test = 'This is merely a test bundle!'; return exports; })({});","exports":["test"],"modules":{}} diff --git a/packages/kernel-test/src/supervisor.test.ts b/packages/kernel-test/src/supervisor.test.ts index a6b18fa00..cd9a08cdb 100644 --- a/packages/kernel-test/src/supervisor.test.ts +++ b/packages/kernel-test/src/supervisor.test.ts @@ -38,7 +38,7 @@ const makeVatSupervisor = async ({ const bundleContent = await readFile(bundlePath, 'utf-8'); return { ok: true, - json: async () => JSON.parse(bundleContent), + text: async () => bundleContent, // eslint-disable-next-line n/no-unsupported-features/node-builtins } as Response; }, diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index 2b84f0ea6..67cef419d 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -20,6 +20,7 @@ describe('index', () => { 'isPrimitive', 'isTypedArray', 'isTypedObject', + 'isVatBundle', 'makeCounter', 'makeDefaultExo', 'makeDefaultInterface', diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index d573379ce..934437874 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -26,6 +26,8 @@ export { export { waitUntilQuiescent } from './wait-quiescent.ts'; export { fromHex, toHex } from './hex.ts'; export { mergeDisjointRecords } from './merge-disjoint-records.ts'; +export type { VatBundle } from './vat-bundle.ts'; +export { isVatBundle } from './vat-bundle.ts'; export { retry, retryWithBackoff, diff --git a/packages/kernel-utils/src/vat-bundle.test.ts b/packages/kernel-utils/src/vat-bundle.test.ts new file mode 100644 index 000000000..fd16ca075 --- /dev/null +++ b/packages/kernel-utils/src/vat-bundle.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; + +import { isVatBundle } from './vat-bundle.ts'; + +describe('isVatBundle', () => { + describe('valid bundles', () => { + it('accepts complete VatBundle with all properties', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + exports: ['foo', 'bar'], + modules: { + './module.js': { renderedExports: ['a'], removedExports: ['b'] }, + }, + }; + expect(isVatBundle(bundle)).toBe(true); + }); + + it('accepts VatBundle with empty exports and modules', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(true); + }); + }); + + describe('invalid bundles - missing required properties', () => { + it('rejects object missing moduleFormat', () => { + const bundle = { + code: 'var __vatExports__ = {};', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + it('rejects object missing code', () => { + const bundle = { + moduleFormat: 'iife', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + // BUG: isVatBundle does not check for exports property + // See PR #763 bugbot claim #7 + it.fails('rejects object missing exports', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + // BUG: isVatBundle does not check for modules property + // See PR #763 bugbot claim #7 + it.fails('rejects object missing modules', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + exports: [], + }; + expect(isVatBundle(bundle)).toBe(false); + }); + }); + + describe('invalid bundles - wrong property types', () => { + it('rejects wrong moduleFormat value', () => { + const bundle = { + moduleFormat: 'cjs', + code: 'var __vatExports__ = {};', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + it('rejects non-string code property', () => { + const bundle = { + moduleFormat: 'iife', + code: 123, + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + }); + + describe('invalid bundles - non-objects', () => { + it.each` + label | value + ${'null'} | ${null} + ${'undefined'} | ${undefined} + ${'string'} | ${'not a bundle'} + ${'number'} | ${42} + ${'array'} | ${[]} + `('rejects $label', ({ value }) => { + expect(isVatBundle(value)).toBe(false); + }); + }); +}); diff --git a/packages/kernel-utils/src/vat-bundle.ts b/packages/kernel-utils/src/vat-bundle.ts new file mode 100644 index 000000000..855ced019 --- /dev/null +++ b/packages/kernel-utils/src/vat-bundle.ts @@ -0,0 +1,30 @@ +import { isObject, hasProperty } from '@metamask/utils'; + +/** + * A bundle produced by the vat bundler. + * + * Contains the bundled code as an IIFE that assigns exports to `__vatExports__`, + * along with metadata about the bundle's exports and modules. + */ +export type VatBundle = { + moduleFormat: 'iife'; + code: string; + exports: string[]; + modules: Record< + string, + { renderedExports: string[]; removedExports: string[] } + >; +}; + +/** + * Type guard to check if a value is a VatBundle. + * + * @param value - The value to check. + * @returns True if the value is a VatBundle. + */ +export const isVatBundle = (value: unknown): value is VatBundle => + isObject(value) && + hasProperty(value, 'moduleFormat') && + value.moduleFormat === 'iife' && + hasProperty(value, 'code') && + typeof value.code === 'string'; diff --git a/packages/ocap-kernel/package.json b/packages/ocap-kernel/package.json index 10f251a7b..00d278769 100644 --- a/packages/ocap-kernel/package.json +++ b/packages/ocap-kernel/package.json @@ -72,7 +72,6 @@ "@chainsafe/libp2p-noise": "^16.1.3", "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch", "@endo/errors": "^1.2.13", - "@endo/import-bundle": "^1.5.2", "@endo/marshal": "^1.8.0", "@endo/pass-style": "^1.6.3", "@endo/promise-kit": "^1.1.13", diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.ts b/packages/ocap-kernel/src/vats/VatSupervisor.ts index e0adbdf13..6bff69d11 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.ts @@ -4,7 +4,6 @@ import type { VatSyscallObject, VatSyscallResult, } from '@agoric/swingset-liveslots'; -import { importBundle } from '@endo/import-bundle'; import { makeMarshal } from '@endo/marshal'; import type { CapData } from '@endo/marshal'; import { @@ -24,6 +23,7 @@ import type { DuplexStream } from '@metamask/streams'; import { isJsonRpcRequest, isJsonRpcResponse } from '@metamask/utils'; import type { PlatformFactory } from '@ocap/kernel-platforms'; +import { loadBundle } from './bundle-loader.ts'; import { makeGCAndFinalize } from '../garbage-collection/gc-finalize.ts'; import { makeDummyMeterControl } from '../liveslots/meter-control.ts'; import { makeSupervisorSyscall } from '../liveslots/syscall.ts'; @@ -308,7 +308,7 @@ export class VatSupervisor { if (!fetched.ok) { throw Error(`fetch of user code ${bundleSpec} failed: ${fetched.status}`); } - const bundle = await fetched.json(); + const bundleContent = await fetched.text(); const buildVatNamespace = async ( lsEndowments: Record, inescapableGlobalProperties: object, @@ -333,8 +333,7 @@ export class VatSupervisor { // Otherwise, just rethrow the error. throw error; } - const vatNS = await importBundle(bundle, { - filePrefix: `vat-${this.id}/...`, + const vatNS = loadBundle(bundleContent, { endowments, inescapableGlobalProperties, }); diff --git a/packages/ocap-kernel/src/vats/bundle-loader.test.ts b/packages/ocap-kernel/src/vats/bundle-loader.test.ts new file mode 100644 index 000000000..e741e6660 --- /dev/null +++ b/packages/ocap-kernel/src/vats/bundle-loader.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; + +import { loadBundle } from './bundle-loader.ts'; + +describe('loadBundle', () => { + describe('input validation', () => { + it('throws on unknown bundle format', () => { + const content = JSON.stringify({ moduleFormat: 'unknown' }); + expect(() => loadBundle(content)).toThrow( + 'Unknown bundle format: unknown', + ); + }); + + it('throws on missing moduleFormat', () => { + const content = JSON.stringify({}); + expect(() => loadBundle(content)).toThrow( + 'Unknown bundle format: undefined', + ); + }); + + // BUG: loadBundle does not validate code property before using it + // See PR #763 bugbot claim #2 + it.fails('throws on missing code property', () => { + const content = JSON.stringify({ moduleFormat: 'iife' }); + expect(() => loadBundle(content)).toThrow('Invalid bundle: missing code'); + }); + + // BUG: loadBundle does not validate code property before using it + // See PR #763 bugbot claim #2 + it.fails('throws on non-string code property', () => { + const content = JSON.stringify({ moduleFormat: 'iife', code: 123 }); + expect(() => loadBundle(content)).toThrow( + 'Invalid bundle: code must be a string', + ); + }); + }); + + describe('bundle evaluation', () => { + it('evaluates valid iife bundle and returns exports', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { foo: "bar" };', + }); + const result = loadBundle(content); + expect(result).toStrictEqual({ foo: 'bar' }); + }); + + it('provides harden global to compartment', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { hasHarden: typeof harden === "function" };', + }); + const result = loadBundle(content); + expect(result).toStrictEqual({ hasHarden: true }); + }); + + it('passes endowments to compartment', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { customValue: customEndowment };', + }); + const result = loadBundle(content, { + endowments: { customEndowment: 42 }, + }); + expect(result).toStrictEqual({ customValue: 42 }); + }); + + it('passes inescapableGlobalProperties to compartment', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { inescapableValue: globalProp };', + }); + const result = loadBundle(content, { + inescapableGlobalProperties: { globalProp: 'test' }, + }); + expect(result).toStrictEqual({ inescapableValue: 'test' }); + }); + }); +}); diff --git a/packages/ocap-kernel/src/vats/bundle-loader.ts b/packages/ocap-kernel/src/vats/bundle-loader.ts new file mode 100644 index 000000000..b12812413 --- /dev/null +++ b/packages/ocap-kernel/src/vats/bundle-loader.ts @@ -0,0 +1,40 @@ +import type { VatBundle } from '@metamask/kernel-utils'; + +export type LoadBundleOptions = { + endowments?: object; + inescapableGlobalProperties?: object; +}; + +/** + * Load an iife bundle and return its namespace. + * + * @param content - The bundle content as a JSON string. + * @param options - Options for loading the bundle. + * @returns The namespace exported by the bundle. + */ +export function loadBundle( + content: string, + options: LoadBundleOptions = {}, +): Record { + const parsed = JSON.parse(content) as Record; + const { endowments = {}, inescapableGlobalProperties = {} } = options; + + if (parsed.moduleFormat !== 'iife') { + throw new Error(`Unknown bundle format: ${String(parsed.moduleFormat)}`); + } + const bundle = parsed as unknown as VatBundle; + + const compartment = new Compartment({ + // SES globals that may be used by bundled code + harden: globalThis.harden, + assert: globalThis.assert, + ...endowments, + ...inescapableGlobalProperties, + }); + // The code declares `var __vatExports__ = (function(){...})({});` + // We wrap it in an IIFE to capture and return the result. + const vatExports = compartment.evaluate( + `(function() { ${bundle.code}; return __vatExports__; })()`, + ); + return vatExports as Record; +} diff --git a/yarn.lock b/yarn.lock index 3e8e2e1f9..c2bb72a5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -755,19 +755,6 @@ __metadata: languageName: node linkType: hard -"@endo/import-bundle@npm:^1.5.2": - version: 1.5.2 - resolution: "@endo/import-bundle@npm:1.5.2" - dependencies: - "@endo/base64": "npm:^1.0.12" - "@endo/compartment-mapper": "npm:^1.6.3" - "@endo/errors": "npm:^1.2.13" - "@endo/where": "npm:^1.0.11" - ses: "npm:^1.14.0" - checksum: 10/521a4bc3a0e7b75626df996e5f965e9bedc8e463586b8dee57d90958371731617a205b98d825afaeeb920b64a1fc105a1acc1f3716742100097068e114169b43 - languageName: node - linkType: hard - "@endo/init@npm:^1.1.12, @endo/init@npm:^1.1.9": version: 1.1.12 resolution: "@endo/init@npm:1.1.12" @@ -2700,7 +2687,6 @@ __metadata: "@chainsafe/libp2p-noise": "npm:^16.1.3" "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch" "@endo/errors": "npm:^1.2.13" - "@endo/import-bundle": "npm:^1.5.2" "@endo/marshal": "npm:^1.8.0" "@endo/pass-style": "npm:^1.6.3" "@endo/promise-kit": "npm:^1.1.13" @@ -3355,8 +3341,6 @@ __metadata: "@arethetypeswrong/cli": "npm:^0.17.4" "@chainsafe/libp2p-noise": "npm:^16.1.3" "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch" - "@endo/bundle-source": "npm:^4.1.2" - "@endo/init": "npm:^1.1.12" "@endo/promise-kit": "npm:^1.1.13" "@libp2p/autonat": "npm:2.0.38" "@libp2p/circuit-relay-v2": "npm:3.2.24" @@ -3385,6 +3369,7 @@ __metadata: "@typescript-eslint/parser": "npm:^8.29.0" "@typescript-eslint/utils": "npm:^8.29.0" "@vitest/eslint-plugin": "npm:^1.6.5" + acorn: "npm:^8.15.0" chokidar: "npm:^4.0.1" depcheck: "npm:^1.4.7" eslint: "npm:^9.23.0" @@ -3400,6 +3385,7 @@ __metadata: libp2p: "npm:2.10.0" prettier: "npm:^3.5.3" rimraf: "npm:^6.0.1" + rollup: "npm:^4.55.3" serve-handler: "npm:^6.1.6" ses: "npm:^1.14.0" turbo: "npm:^2.5.6" @@ -4619,142 +4605,177 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.46.3" +"@rollup/rollup-android-arm-eabi@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.55.3" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-android-arm64@npm:4.46.3" +"@rollup/rollup-android-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-android-arm64@npm:4.55.3" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-darwin-arm64@npm:4.46.3" +"@rollup/rollup-darwin-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-darwin-arm64@npm:4.55.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-darwin-x64@npm:4.46.3" +"@rollup/rollup-darwin-x64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-darwin-x64@npm:4.55.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.46.3" +"@rollup/rollup-freebsd-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.55.3" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-freebsd-x64@npm:4.46.3" +"@rollup/rollup-freebsd-x64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-freebsd-x64@npm:4.55.3" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.46.3" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.55.3" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.46.3" +"@rollup/rollup-linux-arm-musleabihf@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.55.3" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.46.3" +"@rollup/rollup-linux-arm64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.55.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.46.3" +"@rollup/rollup-linux-arm64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.55.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.46.3" +"@rollup/rollup-linux-loong64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.55.3" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.46.3" +"@rollup/rollup-linux-loong64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.55.3" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.55.3" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.46.3" +"@rollup/rollup-linux-ppc64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.55.3" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.55.3" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.46.3" +"@rollup/rollup-linux-riscv64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.55.3" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.46.3" +"@rollup/rollup-linux-s390x-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.55.3" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.46.3" +"@rollup/rollup-linux-x64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.55.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.46.3" +"@rollup/rollup-linux-x64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.55.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.46.3" +"@rollup/rollup-openbsd-x64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-openbsd-x64@npm:4.55.3" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.55.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.55.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.46.3" +"@rollup/rollup-win32-ia32-msvc@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.55.3" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.46.3" +"@rollup/rollup-win32-x64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.55.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.55.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -13179,30 +13200,35 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.43.0": - version: 4.46.3 - resolution: "rollup@npm:4.46.3" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.46.3" - "@rollup/rollup-android-arm64": "npm:4.46.3" - "@rollup/rollup-darwin-arm64": "npm:4.46.3" - "@rollup/rollup-darwin-x64": "npm:4.46.3" - "@rollup/rollup-freebsd-arm64": "npm:4.46.3" - "@rollup/rollup-freebsd-x64": "npm:4.46.3" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.46.3" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.46.3" - "@rollup/rollup-linux-arm64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-arm64-musl": "npm:4.46.3" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-riscv64-musl": "npm:4.46.3" - "@rollup/rollup-linux-s390x-gnu": "npm:4.46.3" - "@rollup/rollup-linux-x64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-x64-musl": "npm:4.46.3" - "@rollup/rollup-win32-arm64-msvc": "npm:4.46.3" - "@rollup/rollup-win32-ia32-msvc": "npm:4.46.3" - "@rollup/rollup-win32-x64-msvc": "npm:4.46.3" +"rollup@npm:^4.43.0, rollup@npm:^4.55.3": + version: 4.55.3 + resolution: "rollup@npm:4.55.3" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.55.3" + "@rollup/rollup-android-arm64": "npm:4.55.3" + "@rollup/rollup-darwin-arm64": "npm:4.55.3" + "@rollup/rollup-darwin-x64": "npm:4.55.3" + "@rollup/rollup-freebsd-arm64": "npm:4.55.3" + "@rollup/rollup-freebsd-x64": "npm:4.55.3" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.55.3" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.55.3" + "@rollup/rollup-linux-arm64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-arm64-musl": "npm:4.55.3" + "@rollup/rollup-linux-loong64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-loong64-musl": "npm:4.55.3" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-ppc64-musl": "npm:4.55.3" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-riscv64-musl": "npm:4.55.3" + "@rollup/rollup-linux-s390x-gnu": "npm:4.55.3" + "@rollup/rollup-linux-x64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-x64-musl": "npm:4.55.3" + "@rollup/rollup-openbsd-x64": "npm:4.55.3" + "@rollup/rollup-openharmony-arm64": "npm:4.55.3" + "@rollup/rollup-win32-arm64-msvc": "npm:4.55.3" + "@rollup/rollup-win32-ia32-msvc": "npm:4.55.3" + "@rollup/rollup-win32-x64-gnu": "npm:4.55.3" + "@rollup/rollup-win32-x64-msvc": "npm:4.55.3" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -13226,10 +13252,14 @@ __metadata: optional: true "@rollup/rollup-linux-arm64-musl": optional: true - "@rollup/rollup-linux-loongarch64-gnu": + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true "@rollup/rollup-linux-riscv64-musl": @@ -13240,17 +13270,23 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true "@rollup/rollup-win32-arm64-msvc": optional: true "@rollup/rollup-win32-ia32-msvc": optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true "@rollup/rollup-win32-x64-msvc": optional: true fsevents: optional: true bin: rollup: dist/bin/rollup - checksum: 10/671eaf282f102fa7f99c4db95b7652cf895a923f8c372bd0fe037a77e20f25dea472879f14a7429018bbd5eba2ac3b8dcbafa48b4c275c52825a9965d079bc6a + checksum: 10/9abdb43e96e24309cad838aeb0a97055d93c1b6820cb1d55041cdf414a07e3fe377564b711476e511f2d373484b04dd68b6129c42efc201deb8f761bf4c2de7e languageName: node linkType: hard