diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index b8b5e7761f..44d9e7f953 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -17,6 +17,7 @@ import { parseId, formatId } from './formula-identifier.js'; import { makeSerialJobs } from './serial-jobs.js'; import { makeWeakMultimap } from './weak-multimap.js'; import { makeLoopbackNetwork } from './networks/loopback.js'; +import { assertValidFormulaType } from './formula-type.js'; const delay = async (ms, cancelled) => { // Do not attempt to set up a timer if already cancelled. @@ -121,7 +122,7 @@ const makeDaemonCore = async ( rootEntropy, cryptoPowers.makeSha512(), ); - const peersId = formatId({ + const ownPeersId = formatId({ number: peersFormulaNumber, node: ownNodeIdentifier, }); @@ -182,19 +183,43 @@ const makeDaemonCore = async ( /** * The two functions "formulate" and "provide" share a responsibility for - * maintaining the memoization tables "controllerForId" and + * maintaining the memoization tables "controllerForId", "typeForId", and * "idForRef". * "formulate" is used for creating and persisting new formulas, whereas * "provide" is used for "reincarnating" the values of stored formulas. */ /** - * Reverse look-up, for answering "what is my name for this near or far - * reference", and not for "what is my name for this promise". + * Forward look-up, for answering "what is the value of this id". * @type {Map} */ const controllerForId = new Map(); + /** + * Forward look-up, for answering "what is the formula type of this id". + * @type {Map} + */ + const typeForId = new Map(); + + /** @param {string} id */ + const getTypeForId = async id => { + await null; + + const formulaType = typeForId.get(id); + if (formulaType !== undefined) { + return formulaType; + } + + if (parseId(id).node !== ownNodeIdentifier) { + typeForId.set(id, 'remote'); + return 'remote'; + } + + const formula = await persistencePowers.readFormula(parseId(id).number); + typeForId.set(id, formula.type); + return formula.type; + }; + /** * Reverse look-up, for answering "what is my name for this near or far * reference", and not for "what is my name for this promise". @@ -569,15 +594,11 @@ const makeDaemonCore = async ( ); }, addPeerInfo: async peerInfo => { - const peerPetstore = - /** @type {import('./types.js').PetStore} */ - // Behold, recursion: - // eslint-disable-next-line no-use-before-define - (await provide(formula.peers)); - const { node, addresses } = peerInfo; // eslint-disable-next-line no-use-before-define - const nodeName = petStoreNameForNodeIdentifier(node); - if (peerPetstore.has(nodeName)) { + const knownPeers = await provideKnownPeers(formula.peers); + const { node: nodeIdentifier, addresses } = peerInfo; + // eslint-disable-next-line no-use-before-define + if (knownPeers.has(nodeIdentifier)) { // We already have this peer. // TODO: merge connection info return; @@ -585,7 +606,7 @@ const makeDaemonCore = async ( const { id: peerId } = // eslint-disable-next-line no-use-before-define await formulatePeer(formula.networks, addresses); - await peerPetstore.write(nodeName, peerId); + await knownPeers.write(nodeIdentifier, peerId); }, }); return { @@ -670,35 +691,18 @@ const makeDaemonCore = async ( if (isRemote) { // eslint-disable-next-line no-use-before-define const peerIdentifier = await getPeerIdForNodeIdentifier(formulaNode); + typeForId.set(id, 'remote'); // Behold, forward reference: // eslint-disable-next-line no-use-before-define return provideRemoteValue(peerIdentifier, id); } + const formula = await persistencePowers.readFormula(formulaNumber); console.log(`Making ${formula.type} ${formulaNumber}`); - if ( - ![ - 'endo', - 'worker', - 'eval', - 'readable-blob', - 'make-unconfined', - 'make-bundle', - 'host', - 'guest', - 'least-authority', - 'loopback-network', - 'peer', - 'handle', - 'pet-inspector', - 'pet-store', - 'lookup', - 'directory', - ].includes(formula.type) - ) { - assert.Fail`Invalid formula identifier, unrecognized type ${q(id)}`; - } + assertValidFormulaType(formula.type); // TODO further validation + typeForId.set(id, formula.type); + return makeControllerForFormula(id, formulaNumber, formula, context); }; @@ -731,6 +735,7 @@ const makeDaemonCore = async ( internal: E.get(partial).internal, }); controllerForId.set(id, controller); + typeForId.set(id, formula.type); // The controller _must_ be constructed in the synchronous prelude of this function. const controllerValue = makeControllerForFormula( @@ -780,11 +785,6 @@ const makeDaemonCore = async ( return controller; }; - // TODO: sorry, forcing nodeId into a petstore name - const petStoreNameForNodeIdentifier = nodeIdentifier => { - return `p${nodeIdentifier.slice(0, 126)}`; - }; - /** * @param {string} nodeIdentifier * @returns {Promise} @@ -793,12 +793,9 @@ const makeDaemonCore = async ( if (nodeIdentifier === ownNodeIdentifier) { throw new Error(`Cannot get peer formula identifier for self`); } - const peerStore = /** @type {import('./types.js').PetStore} */ ( - // eslint-disable-next-line no-use-before-define - await provide(peersId) - ); - const nodeName = petStoreNameForNodeIdentifier(nodeIdentifier); - const peerId = peerStore.identifyLocal(nodeName); + // eslint-disable-next-line no-use-before-define + const knownPeers = await provideKnownPeers(ownPeersId); + const peerId = knownPeers.identify(nodeIdentifier); if (peerId === undefined) { throw new Error( `No peer found for node identifier ${q(nodeIdentifier)}.`, @@ -976,7 +973,7 @@ const makeDaemonCore = async ( }; /** - * @type {import('./types.js').DaemonCoreInternal['formulateHostDependencies']} + * @type {import('./types.js').DaemonCore['formulateHostDependencies']} */ const formulateHostDependencies = async specifiedIdentifiers => { const { specifiedWorkerId, ...remainingSpecifiedIdentifiers } = @@ -997,7 +994,7 @@ const makeDaemonCore = async ( }); }; - /** @type {import('./types.js').DaemonCoreInternal['formulateNumberedHost']} */ + /** @type {import('./types.js').DaemonCore['formulateNumberedHost']} */ const formulateNumberedHost = identifiers => { /** @type {import('./types.js').HostFormula} */ const formula = { @@ -1041,7 +1038,7 @@ const makeDaemonCore = async ( ); }; - /** @type {import('./types.js').DaemonCoreInternal['formulateGuestDependencies']} */ + /** @type {import('./types.js').DaemonCore['formulateGuestDependencies']} */ const formulateGuestDependencies = async hostId => harden({ guestFormulaNumber: await randomHex512(), @@ -1052,7 +1049,7 @@ const makeDaemonCore = async ( workerId: (await formulateNumberedWorker(await randomHex512())).id, }); - /** @type {import('./types.js').DaemonCoreInternal['formulateNumberedGuest']} */ + /** @type {import('./types.js').DaemonCore['formulateNumberedGuest']} */ const formulateNumberedGuest = identifiers => { /** @type {import('./types.js').GuestFormula} */ const formula = { @@ -1348,8 +1345,8 @@ const makeDaemonCore = async ( const { id: newPeersId } = await formulateNumberedPetStore( peersFormulaNumber, ); - if (newPeersId !== peersId) { - assert.Fail`Peers PetStore formula identifier did not match expected value, expected ${peersId}, got ${newPeersId}`; + if (newPeersId !== ownPeersId) { + assert.Fail`Peers PetStore formula identifier did not match expected value, expected ${ownPeersId}, got ${newPeersId}`; } // Ensure the default host is formulated and persisted. @@ -1372,7 +1369,7 @@ const makeDaemonCore = async ( const formula = { type: 'endo', networks: identifiers.networksDirectoryId, - peers: peersId, + peers: ownPeersId, host: identifiers.defaultHostId, leastAuthority: leastAuthorityId, }; @@ -1454,6 +1451,33 @@ const makeDaemonCore = async ( throw new Error('Cannot connect to peer: no supported addresses'); }; + /** + * The "known peers store" is like a pet store, but maps node identifiers to + * full peer ids. + * + * @type {import('./types.js').DaemonCore['provideKnownPeers']} + */ + const provideKnownPeers = async peersFormulaId => { + // "Known peers" is just a pet store with an adapter over it. + const petStore = /** @type {import('./types.js').PetStore} */ ( + await provide(peersFormulaId) + ); + + // Pet stores do not accept full ids as names. + /** @param {string} nodeIdentifier */ + const getNameFor = nodeIdentifier => { + return `p${nodeIdentifier.slice(0, 126)}`; + }; + + return harden({ + has: nodeIdentifier => petStore.has(getNameFor(nodeIdentifier)), + identify: nodeIdentifier => + petStore.identifyLocal(getNameFor(nodeIdentifier)), + write: (nodeIdentifier, peerId) => + petStore.write(getNameFor(nodeIdentifier), peerId), + }); + }; + /** * This is used to provide a value for a formula identifier that is known to * originate from the specified peer. @@ -1479,6 +1503,7 @@ const makeDaemonCore = async ( const { makeIdentifiedDirectory, makeDirectoryNode } = makeDirectoryMaker({ provide, getIdForRef, + getTypeForId, formulateDirectory, }); @@ -1620,32 +1645,12 @@ const makeDaemonCore = async ( return info; }; - /** @type {import('./types.js').DaemonCore} */ - const daemonCore = { - nodeIdentifier: ownNodeIdentifier, - provideController, - provideControllerAndResolveHandle, - provide, - formulate, - getIdForRef, - getAllNetworkAddresses, - cancelValue, - makeMailbox, - makeDirectoryNode, + /** @type {import('./types.js').DaemonCoreExternal} */ + return { formulateEndoBootstrap, - formulateNetworksDirectory, - formulateLoopbackNetwork, - formulateDirectory, - formulateWorker, - formulateHost, - formulateGuest, - formulatePeer, - formulateEval, - formulateUnconfined, - formulateReadableBlob, - formulateBundle, + provide, + nodeIdentifier: ownNodeIdentifier, }; - return daemonCore; }; /** diff --git a/packages/daemon/src/directory.js b/packages/daemon/src/directory.js index f6dd55b025..3a20683ee7 100644 --- a/packages/daemon/src/directory.js +++ b/packages/daemon/src/directory.js @@ -2,6 +2,7 @@ import { E, Far } from '@endo/far'; import { makeIteratorRef } from './reader-ref.js'; +import { formatLocator } from './locator.js'; const { quote: q } = assert; @@ -9,11 +10,13 @@ const { quote: q } = assert; * @param {object} args * @param {import('./types.js').DaemonCore['provide']} args.provide * @param {import('./types.js').DaemonCore['getIdForRef']} args.getIdForRef + * @param {import('./types.js').DaemonCore['getTypeForId']} args.getTypeForId * @param {import('./types.js').DaemonCore['formulateDirectory']} args.formulateDirectory */ export const makeDirectoryMaker = ({ provide, getIdForRef, + getTypeForId, formulateDirectory, }) => { /** @type {import('./types.js').MakeDirectoryNode} */ @@ -84,6 +87,17 @@ export const makeDirectoryMaker = ({ return hub.identify(name); }; + /** @type {import('./types.js').EndoDirectory['locate']} */ + const locate = async (...petNamePath) => { + const id = await identify(...petNamePath); + if (id === undefined) { + return undefined; + } + + const formulaType = await getTypeForId(id); + return formatLocator(id, formulaType); + }; + /** @type {import('./types.js').EndoDirectory['list']} */ const list = async (...petNamePath) => { if (petNamePath.length === 0) { @@ -192,6 +206,7 @@ export const makeDirectoryMaker = ({ const directory = { has, identify, + locate, list, listIdentifiers, followChanges, diff --git a/packages/daemon/src/formula-identifier.js b/packages/daemon/src/formula-identifier.js index 516b6ef379..b570b2ecd6 100644 --- a/packages/daemon/src/formula-identifier.js +++ b/packages/daemon/src/formula-identifier.js @@ -2,15 +2,22 @@ const { quote: q } = assert; +const numberPattern = /^[0-9a-f]{128}$/; const idPattern = /^(?[0-9a-f]{128}):(?[0-9a-f]{128})$/; +/** + * @param {string} allegedNumber - The formula number or node identifier to test. + */ +export const isValidNumber = allegedNumber => + typeof allegedNumber === 'string' && numberPattern.test(allegedNumber); + /** * @param {string} id * @param {string} [petName] * @returns {void} */ export const assertValidId = (id, petName) => { - if (!idPattern.test(id)) { + if (typeof id !== 'string' || !idPattern.test(id)) { let message = `Invalid formula identifier ${q(id)}`; if (petName !== undefined) { message += ` for pet name ${q(petName)}`; diff --git a/packages/daemon/src/formula-type.js b/packages/daemon/src/formula-type.js new file mode 100644 index 0000000000..44dcbe9d17 --- /dev/null +++ b/packages/daemon/src/formula-type.js @@ -0,0 +1,31 @@ +const { quote: q } = assert; + +// Note: Alphabetically sorted +const formulaTypes = new Set([ + 'directory', + 'endo', + 'eval', + 'guest', + 'handle', + 'host', + 'least-authority', + 'lookup', + 'loopback-network', + 'make-bundle', + 'make-unconfined', + 'peer', + 'pet-inspector', + 'pet-store', + 'readable-blob', + 'worker', +]); + +/** @param {string} allegedType */ +export const isValidFormulaType = allegedType => formulaTypes.has(allegedType); + +/** @param {string} allegedType */ +export const assertValidFormulaType = allegedType => { + if (!isValidFormulaType(allegedType)) { + assert.Fail`Unrecognized formula type ${q(allegedType)}`; + } +}; diff --git a/packages/daemon/src/guest.js b/packages/daemon/src/guest.js index 3f22c017e3..7f6cd2c7d0 100644 --- a/packages/daemon/src/guest.js +++ b/packages/daemon/src/guest.js @@ -64,6 +64,7 @@ export const makeGuestMaker = ({ const { has, identify, + locate, list, listIdentifiers, followChanges, @@ -93,6 +94,7 @@ export const makeGuestMaker = ({ // Directory has, identify, + locate, list, listIdentifiers, followChanges, diff --git a/packages/daemon/src/host.js b/packages/daemon/src/host.js index e613166bf1..bfb28935f9 100644 --- a/packages/daemon/src/host.js +++ b/packages/daemon/src/host.js @@ -455,6 +455,7 @@ export const makeHostMaker = ({ const { has, identify, + locate, list, listIdentifiers, followChanges, @@ -483,6 +484,7 @@ export const makeHostMaker = ({ // Directory has, identify, + locate, list, listIdentifiers, followChanges, diff --git a/packages/daemon/src/locator.js b/packages/daemon/src/locator.js new file mode 100644 index 0000000000..91596cb33c --- /dev/null +++ b/packages/daemon/src/locator.js @@ -0,0 +1,96 @@ +// @ts-check + +import { parseId, isValidNumber } from './formula-identifier.js'; +import { isValidFormulaType } from './formula-type.js'; + +const { quote: q } = assert; + +/** + * The endo locator format: + * ``` + * endo://{nodeIdentifier}/?id={formulaNumber}&type={formulaType} + * ``` + * Note that the `id` query param is just the formula number. + */ + +/** + * In addition to all valid formula types, the locator `type` query parameter + * also supports `remote` for remote values, since their actual formula type + * cannot be known. + * + * @param {string} allegedType + */ +const isValidLocatorType = allegedType => + isValidFormulaType(allegedType) || allegedType === 'remote'; + +/** + * @param {string} allegedType + */ +const assertValidLocatorType = allegedType => { + if (!isValidLocatorType(allegedType)) { + assert.Fail`Unrecognized locator type ${q(allegedType)}`; + } +}; + +/** @param {string} allegedLocator */ +export const parseLocator = allegedLocator => { + const errorPrefix = `Invalid locator ${q(allegedLocator)}:`; + + if (!URL.canParse(allegedLocator)) { + assert.Fail`${errorPrefix} Invalid URL.`; + } + const url = new URL(allegedLocator); + + if (!allegedLocator.startsWith('endo://')) { + assert.Fail`${errorPrefix} Invalid protocol.`; + } + + const node = url.host; + if (!isValidNumber(node)) { + assert.Fail`${errorPrefix} Invalid node identifier.`; + } + + if ( + url.searchParams.size !== 2 || + !url.searchParams.has('id') || + !url.searchParams.has('type') + ) { + assert.Fail`${errorPrefix} Invalid search params.`; + } + + const id = url.searchParams.get('id'); + if (id === null || !isValidNumber(id)) { + assert.Fail`${errorPrefix} Invalid id.`; + } + + const formulaType = url.searchParams.get('type'); + if (formulaType === null || !isValidLocatorType(formulaType)) { + assert.Fail`${errorPrefix} Invalid type.`; + } + + /** @type {{ formulaType: string, node: string, id: string }} */ + return { formulaType, node, id }; +}; + +/** @param {string} allegedLocator */ +export const assertValidLocator = allegedLocator => { + parseLocator(allegedLocator); +}; + +/** + * @param {string} id - The full formula identifier. + * @param {string} formulaType - The type of the formula with the given id. + */ +export const formatLocator = (id, formulaType) => { + const { number, node } = parseId(id); + const url = new URL(`endo://${node}`); + url.pathname = '/'; + + // The id query param is just the number + url.searchParams.set('id', number); + + assertValidLocatorType(formulaType); + url.searchParams.set('type', formulaType); + + return url.toString(); +}; diff --git a/packages/daemon/src/pet-store.js b/packages/daemon/src/pet-store.js index f553dba089..e55a7b2979 100644 --- a/packages/daemon/src/pet-store.js +++ b/packages/daemon/src/pet-store.js @@ -1,12 +1,10 @@ // @ts-check import { makeChangeTopic } from './pubsub.js'; -import { parseId, assertValidId } from './formula-identifier.js'; +import { parseId, assertValidId, isValidNumber } from './formula-identifier.js'; const { quote: q } = assert; -const validNumberPattern = /^[0-9a-f]{128}$/; - /** * @param {import('./types.js').FilePowers} filePowers * @param {import('./types.js').Locator} locator @@ -224,16 +222,18 @@ export const makePetStoreMaker = (filePowers, locator) => { }; /** - * @param {string} id + * @param {string} formulaNumber * @param {(name: string) => void} assertValidName * @returns {Promise} */ - const makeIdentifiedPetStore = (id, assertValidName) => { - if (!validNumberPattern.test(id)) { - throw new Error(`Invalid identifier for pet store ${q(id)}`); + const makeIdentifiedPetStore = (formulaNumber, assertValidName) => { + if (!isValidNumber(formulaNumber)) { + throw new Error( + `Invalid formula number for pet store ${q(formulaNumber)}`, + ); } - const prefix = id.slice(0, 2); - const suffix = id.slice(2); + const prefix = formulaNumber.slice(0, 2); + const suffix = formulaNumber.slice(2); const petNameDirectoryPath = filePowers.joinPath( locator.statePath, 'pet-store', diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index a3f37d0678..0b44b5e58d 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -384,6 +384,7 @@ export interface PetStore { export interface NameHub { has(...petNamePath: string[]): Promise; identify(...petNamePath: string[]): Promise; + locate(...petNamePath: string[]): Promise; list(...petNamePath: string[]): Promise>; listIdentifiers(...petNamePath: string[]): Promise>; followChanges( @@ -391,7 +392,7 @@ export interface NameHub { ): AsyncGenerator; lookup(...petNamePath: string[]): Promise; reverseLookup(value: unknown): Array; - write(petNamePath: string[], id): Promise; + write(petNamePath: string[], id: string): Promise; remove(...petNamePath: string[]): Promise; move(fromPetName: string[], toPetName: string[]): Promise; copy(fromPetName: string[], toPetName: string[]): Promise; @@ -487,6 +488,12 @@ export interface EndoPeer { export type EndoPeerControllerPartial = ControllerPartial; export type EndoPeerController = Controller; +export interface EndoKnownPeers { + has: (nodeIdentifier: string) => boolean; + identify: (nodeIdentifier: string) => string | undefined; + write: (nodeIdentifier: string, peerId: string) => Promise; +} + export interface EndoGateway { provide: (id: string) => Promise; } @@ -738,7 +745,45 @@ type FormulateNumberedHostParams = { networksDirectoryId: string; }; -export interface DaemonCoreInternal { +export interface DaemonCore { + cancelValue: (id: string, reason: Error) => Promise; + + formulate: ( + formulaNumber: string, + formula: Formula, + ) => Promise<{ + id: string; + value: unknown; + }>; + + formulateBundle: ( + hostId: string, + bundleId: string, + deferredTasks: DeferredTasks, + specifiedWorkerId?: string, + specifiedPowersId?: string, + ) => FormulateResult; + + formulateDirectory: () => FormulateResult; + + formulateEndoBootstrap: ( + specifiedFormulaNumber: string, + ) => FormulateResult; + + formulateEval: ( + hostId: string, + source: string, + codeNames: string[], + endowmentIdsOrPaths: (string | string[])[], + deferredTasks: DeferredTasks, + specifiedWorkerId?: string, + ) => FormulateResult; + + formulateGuest: ( + hostId: string, + deferredTasks: DeferredTasks, + ) => FormulateResult; + /** * Helper for callers of {@link formulateNumberedGuest}. * @param hostId - The formula identifier of the host to formulate a guest for. @@ -747,9 +792,14 @@ export interface DaemonCoreInternal { formulateGuestDependencies: ( hostId: string, ) => Promise>; - formulateNumberedGuest: ( - identifiers: FormulateNumberedGuestParams, - ) => FormulateResult; + + formulateHost: ( + endoId: string, + networksDirectoryId: string, + deferredTasks: DeferredTasks, + specifiedWorkerId?: string | undefined, + ) => FormulateResult; + /** * Helper for callers of {@link formulateNumberedHost}. * @param specifiedIdentifiers - The existing formula identifiers specified to the host formulation. @@ -758,54 +808,29 @@ export interface DaemonCoreInternal { formulateHostDependencies: ( specifiedIdentifiers: FormulateHostDependenciesParams, ) => Promise>; + + formulateLoopbackNetwork: () => FormulateResult; + + formulateNetworksDirectory: () => FormulateResult; + + formulateNumberedGuest: ( + identifiers: FormulateNumberedGuestParams, + ) => FormulateResult; + formulateNumberedHost: ( identifiers: FormulateNumberedHostParams, ) => FormulateResult; -} -export interface DaemonCore { - nodeIdentifier: string; - provide: (id: string) => Promise; - provideController: (id: string) => Controller; - provideControllerAndResolveHandle: (id: string) => Promise; - formulate: ( - formulaNumber: string, - formula: Formula, - ) => Promise<{ - id: string; - value: unknown; - }>; - getIdForRef: (ref: unknown) => string | undefined; - getAllNetworkAddresses: (networksDirectoryId: string) => Promise; - formulateEndoBootstrap: ( - specifiedFormulaNumber: string, - ) => FormulateResult; - formulateWorker: ( - deferredTasks: DeferredTasks, - ) => FormulateResult; - formulateDirectory: () => FormulateResult; - formulateHost: ( - endoId: string, - networksDirectoryId: string, - deferredTasks: DeferredTasks, - specifiedWorkerId?: string | undefined, - ) => FormulateResult; - formulateGuest: ( - hostId: string, - deferredTasks: DeferredTasks, - ) => FormulateResult; + formulatePeer: ( + networksId: string, + addresses: Array, + ) => FormulateResult; + formulateReadableBlob: ( readerRef: ERef>, deferredTasks: DeferredTasks, ) => FormulateResult; - formulateEval: ( - hostId: string, - source: string, - codeNames: string[], - endowmentIdsOrPaths: (string | string[])[], - deferredTasks: DeferredTasks, - specifiedWorkerId?: string, - ) => FormulateResult; + formulateUnconfined: ( hostId: string, specifier: string, @@ -813,22 +838,34 @@ export interface DaemonCore { specifiedWorkerId?: string, specifiedPowersId?: string, ) => FormulateResult; - formulateBundle: ( - hostId: string, - bundleId: string, - deferredTasks: DeferredTasks, - specifiedWorkerId?: string, - specifiedPowersId?: string, - ) => FormulateResult; - formulatePeer: ( - networksId: string, - addresses: Array, - ) => FormulateResult; - formulateNetworksDirectory: () => FormulateResult; - formulateLoopbackNetwork: () => FormulateResult; - cancelValue: (id: string, reason: Error) => Promise; - makeMailbox: MakeMailbox; + + formulateWorker: ( + deferredTasks: DeferredTasks, + ) => FormulateResult; + + getAllNetworkAddresses: (networksDirectoryId: string) => Promise; + + getIdForRef: (ref: unknown) => string | undefined; + + getTypeForId: (id: string) => Promise; + makeDirectoryNode: MakeDirectoryNode; + + makeMailbox: MakeMailbox; + + provide: (id: string) => Promise; + + provideController: (id: string) => Controller; + + provideControllerAndResolveHandle: (id: string) => Promise; + + provideKnownPeers: (peersFormulaId: string) => Promise; +} + +export interface DaemonCoreExternal { + formulateEndoBootstrap: DaemonCore['formulateEndoBootstrap']; + nodeIdentifier: string; + provide: DaemonCore['provide']; } export type SerialJobs = { diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index 68bd22b118..faecb05fb7 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -24,6 +24,7 @@ import { } from '../index.js'; import { makeCryptoPowers } from '../src/daemon-node-powers.js'; import { formatId } from '../src/formula-identifier.js'; +import { parseLocator } from '../src/locator.js'; const cryptoPowers = makeCryptoPowers(crypto); @@ -46,6 +47,47 @@ const makeLocator = (...root) => { }; }; +/** + * @param {ReturnType} locator + * @param {Promise} cancelled + */ +const makeHostWithTestNetwork = async (locator, cancelled) => { + await stop(locator).catch(() => {}); + await purge(locator); + await start(locator); + + const { getBootstrap } = await makeEndoClient( + 'client', + locator.sockPath, + cancelled, + ); + const bootstrap = getBootstrap(); + + const host = E(bootstrap).host(); + // Install test network + const servicePath = path.join(dirname, 'src', 'networks', 'tcp-netstring.js'); + const serviceLocation = url.pathToFileURL(servicePath).href; + const network = E(host).makeUnconfined( + 'MAIN', + serviceLocation, + 'SELF', + 'test-network', + ); + + // set address via request + const iteratorRef = E(host).followMessages(); + const { value: message } = await E(iteratorRef).next(); + const { number } = E.get(message); + await E(host).evaluate('MAIN', '`127.0.0.1:0`', [], [], 'netport'); + await E(host).resolve(await number, 'netport'); + + // move test network to network dir + await network; + await E(host).move(['test-network'], ['NETS', 'tcp']); + + return host; +}; + // The id of the next bundle to make. let bundleId = 0; const textEncoder = new TextEncoder(); @@ -1357,7 +1399,7 @@ test('guest cannot access host methods', async t => { test('read unknown nodeId', async t => { const { promise: cancelled, reject: cancel } = makePromiseKit(); t.teardown(() => cancel(Error('teardown'))); - const locator = makeLocator('tmp', 'read unknown nodeId'); + const locator = makeLocator('tmp', 'read-unknown-nodeid'); await stop(locator).catch(() => {}); await purge(locator); @@ -1390,95 +1432,116 @@ test('read unknown nodeId', async t => { test('read remote value', async t => { const { promise: cancelled, reject: cancel } = makePromiseKit(); t.teardown(() => cancel(Error('teardown'))); - const locatorA = makeLocator('tmp', 'read remote value A'); - const locatorB = makeLocator('tmp', 'read remote value B'); - let hostA; + const locatorA = makeLocator('tmp', 'read-remote-value-a'); + const locatorB = makeLocator('tmp', 'read-remote-value-b'); + const hostA = await makeHostWithTestNetwork(locatorA, cancelled); + const hostB = await makeHostWithTestNetwork(locatorB, cancelled); + + // introduce nodes to each other + await E(hostA).addPeerInfo(await E(hostB).getPeerInfo()); + await E(hostB).addPeerInfo(await E(hostA).getPeerInfo()); + + // create value to share + await E(hostB).evaluate('MAIN', '"hello, world!"', [], [], 'salutations'); + const hostBValueIdentifier = await E(hostB).identify('salutations'); + + // insert in hostA out of band + await E(hostA).write(['greetings'], hostBValueIdentifier); + + const hostAValue = await E(hostA).lookup('greetings'); + t.is(hostAValue, 'hello, world!'); + + await stop(locatorA); + await stop(locatorB); +}); + +test('locate local value', async t => { + const { promise: cancelled, reject: cancel } = makePromiseKit(); + t.teardown(() => cancel(Error('teardown'))); + const locator = makeLocator('tmp', 'locate-local-value'); + + await stop(locator).catch(() => {}); + await purge(locator); + await start(locator); + + const { getBootstrap } = await makeEndoClient( + 'client', + locator.sockPath, + cancelled, + ); + const bootstrap = getBootstrap(); + const host = E(bootstrap).host(); + const ten = await E(host).evaluate('MAIN', '10', [], [], 'ten'); + t.is(ten, 10); + + const tenLocator = await E(host).locate('ten'); + const parsedLocator = parseLocator(tenLocator); + t.is(parsedLocator.formulaType, 'eval'); + + await stop(locator); +}); + +test('locate local persisted value', async t => { + const { promise: cancelled, reject: cancel } = makePromiseKit(); + t.teardown(() => cancel(Error('teardown'))); + const locator = makeLocator('tmp', 'locate-local-persisted-value'); + + await stop(locator).catch(() => {}); + await purge(locator); + await start(locator); + { - await stop(locatorA).catch(() => {}); - await purge(locatorA); - await start(locatorA); const { getBootstrap } = await makeEndoClient( 'client', - locatorA.sockPath, + locator.sockPath, cancelled, ); const bootstrap = getBootstrap(); - hostA = E(bootstrap).host(); - // Install test network - const servicePath = path.join( - dirname, - 'src', - 'networks', - 'tcp-netstring.js', - ); - const serviceLocation = url.pathToFileURL(servicePath).href; - const networkA = E(hostA).makeUnconfined( - 'MAIN', - serviceLocation, - 'SELF', - 'test-network', - ); - // set address via request - const iteratorRef = E(hostA).followMessages(); - const { value: message } = await E(iteratorRef).next(); - const { number } = E.get(message); - await E(hostA).evaluate('MAIN', '`127.0.0.1:0`', [], [], 'netport'); - await E(hostA).resolve(await number, 'netport'); - // move test network to network dir - await networkA; - await E(hostA).move(['test-network'], ['NETS', 'tcp']); + const host = E(bootstrap).host(); + const ten = await E(host).evaluate('MAIN', '10', [], [], 'ten'); + t.is(ten, 10); } - let hostB; + await restart(locator); + { - await stop(locatorB).catch(() => {}); - await purge(locatorB); - await start(locatorB); const { getBootstrap } = await makeEndoClient( 'client', - locatorB.sockPath, + locator.sockPath, cancelled, ); const bootstrap = getBootstrap(); - hostB = E(bootstrap).host(); - // Install test network - const servicePath = path.join( - dirname, - 'src', - 'networks', - 'tcp-netstring.js', - ); - const serviceLocation = url.pathToFileURL(servicePath).href; - const networkB = E(hostB).makeUnconfined( - 'MAIN', - serviceLocation, - 'SELF', - 'test-network', - ); - // set address via requestcd - const iteratorRef = E(hostB).followMessages(); - const { value: message } = await E(iteratorRef).next(); - const { number } = E.get(message); - await E(hostB).evaluate('MAIN', '`127.0.0.1:0`', [], [], 'netport'); - await E(hostB).resolve(await number, 'netport'); - // move test network to network dir - await networkB; - await E(hostB).move(['test-network'], ['NETS', 'tcp']); + const host = E(bootstrap).host(); + const tenLocator = await E(host).locate('ten'); + const parsedLocator = parseLocator(tenLocator); + t.is(parsedLocator.formulaType, 'eval'); } + await stop(locator); +}); + +test('locate remote value', async t => { + const { promise: cancelled, reject: cancel } = makePromiseKit(); + t.teardown(() => cancel(Error('teardown'))); + const locatorA = makeLocator('tmp', 'locate-remote-value-a'); + const locatorB = makeLocator('tmp', 'locate-remote-value-b'); + const hostA = await makeHostWithTestNetwork(locatorA, cancelled); + const hostB = await makeHostWithTestNetwork(locatorB, cancelled); + // introduce nodes to each other await E(hostA).addPeerInfo(await E(hostB).getPeerInfo()); await E(hostB).addPeerInfo(await E(hostA).getPeerInfo()); // create value to share - await E(hostB).evaluate('MAIN', '`haay wuurl`', [], [], 'salutations'); + await E(hostB).evaluate('MAIN', '"hello, world!"', [], [], 'salutations'); const hostBValueIdentifier = await E(hostB).identify('salutations'); // insert in hostA out of band await E(hostA).write(['greetings'], hostBValueIdentifier); - const hostAValue = await E(hostA).lookup('greetings'); - t.is(hostAValue, 'haay wuurl'); + const greetingsLocator = await E(hostA).locate('greetings'); + const parsedGreetingsLocator = parseLocator(greetingsLocator); + t.is(parsedGreetingsLocator.formulaType, 'remote'); await stop(locatorA); await stop(locatorB); diff --git a/packages/daemon/test/test-formula-type.js b/packages/daemon/test/test-formula-type.js new file mode 100644 index 0000000000..abd8820221 --- /dev/null +++ b/packages/daemon/test/test-formula-type.js @@ -0,0 +1,27 @@ +import test from '@endo/ses-ava/prepare-endo.js'; + +import { + assertValidFormulaType, + isValidFormulaType, +} from '../src/formula-type.js'; + +test('isValidFormulaType', t => { + [ + ['eval', true], + ['make-unconfined', true], + ['', false], + [null, false], + [undefined, false], + [{}, false], + ].forEach(([value, expected]) => { + t.is(isValidFormulaType(value), expected); + }); +}); + +test('assertValidFormulaType - valid', t => { + t.notThrows(() => assertValidFormulaType('eval')); +}); + +test('assertValidFormulaType - invalid', t => { + t.throws(() => assertValidFormulaType('foobar')); +}); diff --git a/packages/daemon/test/test-locator.js b/packages/daemon/test/test-locator.js new file mode 100644 index 0000000000..aa7b2dfa7f --- /dev/null +++ b/packages/daemon/test/test-locator.js @@ -0,0 +1,72 @@ +import test from '@endo/ses-ava/prepare-endo.js'; + +import { + assertValidLocator, + formatLocator, + parseLocator, +} from '../src/locator.js'; +import { formatId } from '../src/formula-identifier.js'; + +const validNode = + 'd5c98890be3d17ad375517464ec494068267de60bd4b3143ef0214cc895746f2892baca4fec19b6d4dfc1f683b7cf3d2a884dfcae568555dd89665c33dfdc4b3'; +const validId = + '5cf3d8b4d6e03fb51d71fbbb6fa6982edbff673cd193707c902b70a26b7b468017fbcfc5c2895f4379459badbe507a4ef00e1d3638f4a67e8a8c14fd1d85d9aa'; +const validType = 'eval'; + +const makeLocator = (components = {}) => { + const { + protocol = 'endo://', + host = validNode, + param1 = `id=${validId}`, + param2 = `type=${validType}`, + } = components; + return `${protocol}${host}/?${param1}&${param2}`; +}; + +test('assertValidLocator - valid', t => { + t.notThrows(() => assertValidLocator(makeLocator())); + + // Reverse search param order + t.notThrows(() => + assertValidLocator( + makeLocator({ + param1: `type=${validType}`, + param2: `id=${validId}`, + }), + ), + ); +}); + +test('assertValidLocator - invalid', t => { + [ + ['foobar', /Invalid URL.$/u], + ['', /Invalid URL.$/u], + [null, /Invalid URL.$/u], + [undefined, /Invalid URL.$/u], + [{}, /Invalid URL.$/u], + [makeLocator({ protocol: 'foobar://' }), /Invalid protocol.$/u], + [makeLocator({ host: 'foobar' }), /Invalid node identifier.$/u], + [makeLocator({ param1: 'foo=bar' }), /Invalid search params.$/u], + [makeLocator({ param2: 'foo=bar' }), /Invalid search params.$/u], + [`${makeLocator()}&foo=bar`, /Invalid search params.$/u], + [makeLocator({ param1: 'id=foobar' }), /Invalid id.$/u], + [makeLocator({ param2: 'type=foobar' }), /Invalid type.$/u], + ].forEach(([locator, reason]) => { + t.throws(() => assertValidLocator(locator), { message: reason }); + }); +}); + +test('parseLocator', t => { + t.deepEqual(parseLocator(makeLocator()), { + id: validId, + node: validNode, + formulaType: validType, + }); +}); + +test('formatLocator', t => { + t.is( + formatLocator(formatId({ number: validId, node: validNode }), validType), + makeLocator(), + ); +});