Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a scaffold setup that runs tests with secure bundle #1505

Draft
wants to merge 6 commits into
base: secure-bundler-hybrid
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/compartment-mapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ export {
} from './src/import-archive.js';
export { search } from './src/search.js';
export { compartmentMapForNodeModules } from './src/node-modules.js';
export { makeBundle, writeBundle } from './src/bundle.js';
export {
makeBundle,
makeSecureBundle,
makeSecureBundleFromArchive,
writeBundle,
} from './src/bundle.js';
88 changes: 60 additions & 28 deletions packages/compartment-mapper/src/archive.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/** @typedef {import('./types.js').ArchiveOptions} ArchiveOptions */
/** @typedef {import('./types.js').ArchiveWriter} ArchiveWriter */
/** @typedef {import('./types.js').CompartmentDescriptor} CompartmentDescriptor */
/** @typedef {import('./types.js').CompartmentMapDescriptor} CompartmentMapDescriptor */
/** @typedef {import('./types.js').ModuleDescriptor} ModuleDescriptor */
/** @typedef {import('./types.js').ParserImplementation} ParserImplementation */
/** @typedef {import('./types.js').ReadFn} ReadFn */
Expand Down Expand Up @@ -206,27 +207,38 @@ const renameSources = (sources, compartmentRenames) => {
};

/**
* @param {ArchiveWriter} archive
* @param {Sources} sources
*/
const addSourcesToArchive = async (archive, sources) => {
export const locationsForSources = function* locationsForSources(sources) {
for (const compartment of keys(sources).sort()) {
const modules = sources[compartment];
const compartmentLocation = resolveLocation(`${compartment}/`, 'file:///');
for (const specifier of keys(modules).sort()) {
const { bytes, location } = modules[specifier];
const module = modules[specifier];
const { location } = module;
if (location !== undefined) {
const moduleLocation = resolveLocation(location, compartmentLocation);
const path = new URL(moduleLocation).pathname.slice(1); // elide initial "/"
if (bytes !== undefined) {
// eslint-disable-next-line no-await-in-loop
await archive.write(path, bytes);
}
yield { path, module, compartment };
}
}
}
};

/**
* @param {ArchiveWriter} archive
* @param {Sources} sources
*/
export const addSourcesToArchive = async (archive, sources) => {
for (const { path, module } of locationsForSources(sources)) {
const { bytes } = module;
if (bytes !== undefined) {
// eslint-disable-next-line no-await-in-loop
await archive.write(path, bytes);
}
}
};

/**
* @param {Sources} sources
* @param {CaptureSourceLocationHook} captureSourceLocation
Expand All @@ -243,6 +255,44 @@ const captureSourceLocations = async (sources, captureSourceLocation) => {
}
};

/**
* @param {CompartmentMapDescriptor} compartmentMap
* @param {Sources} sources
* @returns {{archiveCompartmentMap: CompartmentMapDescriptor, archiveSources: Sources, compartmentRenames: Record<string, string>}}
*/
export const makeArchiveCompartmentMap = (compartmentMap, sources) => {
const {
compartments,
entry: { compartment: entryCompartmentName, module: entryModuleSpecifier },
} = compartmentMap;

const compartmentRenames = renameCompartments(compartments);
const archiveCompartments = translateCompartmentMap(
compartments,
sources,
compartmentRenames,
);
const archiveEntryCompartmentName = compartmentRenames[entryCompartmentName];
const archiveSources = renameSources(sources, compartmentRenames);

const archiveCompartmentMap = {
tags: [],
entry: {
compartment: archiveEntryCompartmentName,
module: entryModuleSpecifier,
},
compartments: archiveCompartments,
};

// Cross-check:
// We assert that we have constructed a valid compartment map, not because it
// might not be, but to ensure that the assertCompartmentMap function can
// accept all valid compartment maps.
assertCompartmentMap(archiveCompartmentMap);

return { archiveCompartmentMap, archiveSources, compartmentRenames };
};

/**
* @param {ReadFn | ReadPowers} powers
* @param {string} moduleLocation
Expand Down Expand Up @@ -287,7 +337,7 @@ const digestLocation = async (powers, moduleLocation, options) => {

const {
compartments,
entry: { compartment: entryCompartmentName, module: entryModuleSpecifier },
entry: { module: entryModuleSpecifier },
} = compartmentMap;

/** @type {Sources} */
Expand Down Expand Up @@ -322,28 +372,10 @@ const digestLocation = async (powers, moduleLocation, options) => {
);
}

const compartmentRenames = renameCompartments(compartments);
const archiveCompartments = translateCompartmentMap(
compartments,
const { archiveCompartmentMap, archiveSources } = makeArchiveCompartmentMap(
compartmentMap,
sources,
compartmentRenames,
);
const archiveEntryCompartmentName = compartmentRenames[entryCompartmentName];
const archiveSources = renameSources(sources, compartmentRenames);

const archiveCompartmentMap = {
entry: {
compartment: archiveEntryCompartmentName,
module: moduleSpecifier,
},
compartments: archiveCompartments,
};

// Cross-check:
// We assert that we have constructed a valid compartment map, not because it
// might not be, but to ensure that the assertCompartmentMap function can
// accept all valid compartment maps.
assertCompartmentMap(archiveCompartmentMap);

const archiveCompartmentMapText = JSON.stringify(
archiveCompartmentMap,
Expand Down
16 changes: 13 additions & 3 deletions packages/compartment-mapper/src/bundle-cjs.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
/** quotes strings */
const q = JSON.stringify;

const maybeAppendDefaultValueForExport = exportName =>
exportName === 'default' ? `, {}` : '';

const exportsCellRecord = exportsList =>
''.concat(
...exportsList.map(
exportName => `\
${exportName}: cell(${q(exportName)}),
${exportName}: cell(${q(exportName)}${maybeAppendDefaultValueForExport(
exportName,
)}),
`,
),
);
Expand All @@ -15,7 +20,12 @@ const runtime = function wrapCjsFunctor(num) {
/* eslint-disable no-undef */
return ({ imports = {} }) => {
const cModule = Object.freeze(
Object.defineProperty({}, 'exports', cells[num].default),
Object.defineProperties(
{},
{
exports: cells[num].default,
},
),
);
// TODO: specifier not found handling
const requireImpl = specifier => cells[imports[specifier]].default.get();
Expand All @@ -39,7 +49,7 @@ export default {
return {
getFunctor: () => `\
// === functors[${index}] ===
${cjsFunctor},
${cjsFunctor}
`,
getCells: () => `\
{
Expand Down
2 changes: 1 addition & 1 deletion packages/compartment-mapper/src/bundle-mjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default {
return {
getFunctor: () => `\
// === functors[${index}] ===
${__syncModuleProgram__},
${__syncModuleProgram__}
`,
getCells: () => `\
{
Expand Down
170 changes: 170 additions & 0 deletions packages/compartment-mapper/src/bundle-runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// @ts-check
/** @typedef {import('ses').RedirectStaticModuleInterface} RedirectStaticModuleInterface */
/** @typedef {import('./types.js').ExecuteFn} ExecuteFn */

/* globals globalThis */
import { scopeTerminators } from 'ses/tools.js';
import { link } from './link.js';
import { makeArchiveImportHookMaker } from './import-archive.js';

const { strictScopeTerminator } = scopeTerminators;
const textEncoder = new TextEncoder();

export function loadApplication(
compartmentMap,
moduleRegistry,
loadModuleFunctors,
archiveLocation,
) {
const lookupModule = moduleLocation =>
textEncoder.encode(JSON.stringify(moduleRegistry[moduleLocation]));

const {
compartments: compartmentDescriptors,
entry: { module: entrySpecifier },
} = compartmentMap;

const archiveMakeImportHook = makeArchiveImportHookMaker(
lookupModule, // <-- this is our get function
compartmentDescriptors,
archiveLocation,
);

// see getEvalKitForCompartment definition for note on hoisting
/* eslint-disable-next-line no-use-before-define */
const moduleFunctors = loadModuleFunctors(getEvalKitForCompartment);

const makeImportHook = (packageLocation, packageName) => {
const archiveImportHook = archiveMakeImportHook(
packageLocation,
packageName,
);
const { modules: moduleDescriptors } =
compartmentDescriptors[packageLocation];
const importHook = async moduleSpecifier => {
const staticModuleRecord = await archiveImportHook(moduleSpecifier);
// archiveImportHook always setups on an alias record
// loadRecord will read the alias so use that
const aliasModuleRecord = /** @type {RedirectStaticModuleInterface} */ (
staticModuleRecord
).record;
// put module functor on the staticModuleRecord
const moduleDescriptor = moduleDescriptors[moduleSpecifier];
const moduleLocation = `${packageLocation}/${moduleDescriptor.location}`;
const makeModuleFunctor = moduleFunctors[moduleLocation];
/* eslint-disable-next-line no-underscore-dangle */
/** @type {any} */ (aliasModuleRecord).__syncModuleFunctor__ =
makeModuleFunctor();
return staticModuleRecord;
};
return importHook;
};

/*
wiring would be cleaner if `link()` could take a compartments collection

reference cycle:
- `getEvalKitForCompartment` needs `compartments`
- `compartments` is created by `link()`
- `link()` needs `makeImportHook`
- `makeImportHook` needs `getEvalKitForCompartment`
*/
const {
compartment: entryCompartment,
compartments,
pendingJobsPromise,
} = link(compartmentMap, {
makeImportHook,
globals: globalThis,
// transforms,
});

function getCompartmentByName(name) {
let compartment = compartments[name];
if (compartment === undefined) {
compartment = new Compartment();
compartments[name] = compartment;
}
return compartment;
}

// this relies on hoisting to close a reference triangle
// as noted above, this could be fixed if we could pass `compartments` into `link()`
function getEvalKitForCompartment(compartmentName) {
const compartment = getCompartmentByName(compartmentName);
const scopeTerminator = strictScopeTerminator;
const { globalThis } = compartment;
return { globalThis, scopeTerminator };
}

/** @type {ExecuteFn} */
const execute = async () => {
await pendingJobsPromise;

// eslint-disable-next-line dot-notation
return entryCompartment['import'](entrySpecifier);
};

return { execute, compartments };
}

/*

NOTES


// we want to approximately create an archive compartmentMap with functors
// and then link the compartmentMap into an application via a custom importHook that uses getFunctor

// link turns a compartmentMap (CompartmentDescriptors) into an application (Compartments)
// - actual module sources are loaded via `get`
// -> get could be replaced with getFunctor <----------------------
// - makeArchiveImportHookMaker/importHook creates the records via the parser and `get` and `compartmentMap`
// -> can call getFunctor and put it on the record
// - parser creates a functor via compartment.evaluate
// -> could provide custom compartments that when evaluate is called refer to a precompile
// -> parser could pull module functor off of compartment

// can make an alternate parser for language that pulls the functors out from somewhere


application.execute
ses/src/compartment-shim import
ses/src/module-load load
memoizedLoadWithErrorAnnotation
loadWithoutErrorAnnotation
importHook (via makeArchiveImportHookMaker) returns { record, specifier: moduleSpecifier } as staticModuleRecord
parse (via parserForLanguage) returns { record }
loadRecord assumes record is an alias, returns moduleRecord wrapping record
compartmentImportNow
ses/src/module-link link()
ses/src/module-link instantiate()
if(isPrecompiled)
makeModuleInstance <-- COULD call our functor
compartmentEvaluate
else
makeThirdPartyModuleInstance
staticModuleRecord.execute <-- calls our execute


moduleRecord <- from loadRecord
staticModuleRecord <- from importHook
record <- from parserForLanguage[language].parse

if(isPrecompiled)
makeModuleInstance
sets execute: __syncModuleFunctor__
else
makeThirdPartyModuleInstance
sets execute: staticModuleRecord.execute
execute (via parserForLanguage) from parse-pre-cjs.js

>>>>>>>>> questions

import-archive always invokes module record aliases - is this intentional?

makeModuleInstance + makeThirdPartyModuleInstance
dont rhyme as much as I'd like


*/
Loading