@agimon-ai/po-core 0.0.0-alpha.20 → 0.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +149 -21
- package/NOTICE +41 -0
- package/dist/apiContracts.d.mts +54 -1
- package/dist/apiContracts.d.mts.map +1 -1
- package/dist/headlessHub.cjs +4 -0
- package/dist/headlessHub.cjs.map +1 -1
- package/dist/headlessHub.mjs +4 -0
- package/dist/headlessHub.mjs.map +1 -1
- package/dist/headlessServer.cjs +7 -2
- package/dist/headlessServer.cjs.map +1 -1
- package/dist/headlessServer.d.mts +2 -0
- package/dist/headlessServer.d.mts.map +1 -1
- package/dist/headlessServer.mjs +7 -2
- package/dist/headlessServer.mjs.map +1 -1
- package/dist/moduleResolution.cjs +24 -2
- package/dist/moduleResolution.cjs.map +1 -1
- package/dist/moduleResolution.d.mts +6 -0
- package/dist/moduleResolution.d.mts.map +1 -1
- package/dist/moduleResolution.mjs +24 -3
- package/dist/moduleResolution.mjs.map +1 -1
- package/dist/native.cjs +1 -0
- package/dist/native.d.mts +3 -3
- package/dist/native.mjs +2 -2
- package/dist/nodePlatform.cjs +1 -0
- package/dist/nodePlatform.cjs.map +1 -1
- package/dist/nodePlatform.d.mts +2 -0
- package/dist/nodePlatform.d.mts.map +1 -1
- package/dist/nodePlatform.mjs +1 -0
- package/dist/nodePlatform.mjs.map +1 -1
- package/dist/packageApiServer.cjs +11 -5
- package/dist/packageApiServer.cjs.map +1 -1
- package/dist/packageApiServer.d.mts.map +1 -1
- package/dist/packageApiServer.mjs +7 -1
- package/dist/packageApiServer.mjs.map +1 -1
- package/dist/platform.cjs +6 -0
- package/dist/platform.d.mts +2 -1
- package/dist/platform.mjs +2 -1
- package/dist/src/schemas/hubChannel.cjs.map +1 -1
- package/dist/src/schemas/hubChannel.d.mts +4 -0
- package/dist/src/schemas/hubChannel.d.mts.map +1 -1
- package/dist/src/schemas/hubChannel.mjs.map +1 -1
- package/dist/src/schemas/packageApi.cjs.map +1 -1
- package/dist/src/schemas/packageApi.d.mts +3 -0
- package/dist/src/schemas/packageApi.d.mts.map +1 -1
- package/dist/src/schemas/packageApi.mjs.map +1 -1
- package/dist/src/schemas/platform.cjs.map +1 -1
- package/dist/src/schemas/platform.d.mts +5 -0
- package/dist/src/schemas/platform.d.mts.map +1 -1
- package/dist/src/schemas/platform.mjs.map +1 -1
- package/dist/src/server/serverScope.cjs +4 -1
- package/dist/src/server/serverScope.cjs.map +1 -1
- package/dist/src/server/serverScope.d.mts.map +1 -1
- package/dist/src/server/serverScope.mjs +4 -1
- package/dist/src/server/serverScope.mjs.map +1 -1
- package/dist/src/services/sessionFiles/index.cjs +96 -0
- package/dist/src/services/sessionFiles/index.cjs.map +1 -0
- package/dist/src/services/sessionFiles/index.d.mts +33 -0
- package/dist/src/services/sessionFiles/index.d.mts.map +1 -0
- package/dist/src/services/sessionFiles/index.mjs +89 -0
- package/dist/src/services/sessionFiles/index.mjs.map +1 -0
- package/dist/src/services/sessionFiles/sessionDirectory.cjs +66 -0
- package/dist/src/services/sessionFiles/sessionDirectory.cjs.map +1 -0
- package/dist/src/services/sessionFiles/sessionDirectory.mjs +62 -0
- package/dist/src/services/sessionFiles/sessionDirectory.mjs.map +1 -0
- package/dist/src/systems/main/adapters/headlessSessionHost.cjs +18 -6
- package/dist/src/systems/main/adapters/headlessSessionHost.cjs.map +1 -1
- package/dist/src/systems/main/adapters/headlessSessionHost.d.mts.map +1 -1
- package/dist/src/systems/main/adapters/headlessSessionHost.mjs +16 -4
- package/dist/src/systems/main/adapters/headlessSessionHost.mjs.map +1 -1
- package/dist/src/web/types/nativeTemplate.d.mts +1 -1
- package/dist/src/web/types/nativeTemplate.d.mts.map +1 -1
- package/licenses/Codex-Apache-2.0.txt +201 -0
- package/package.json +8 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverScope.mjs","names":[],"sources":["../../../src/server/serverScope.ts"],"sourcesContent":["import { Context, type Events, type Fiber, Service } from '@deepseek-ai/cordis';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\n\nimport { PO_HEADLESS_OWNER, readPoHeadlessHost } from '../schemas/headless';\nimport type { PoApiHandler, PoApiScope } from '../schemas/packageApi';\nimport {\n PO_FS_SERVICE,\n PO_SESSION_STORAGE_SERVICE,\n PO_SHELL_SERVICE,\n PO_SQLITE_SERVICE,\n type PoPlatform,\n} from '../schemas/platform';\nimport type { PoServerBundleEntry } from '../schemas/serverBundle';\nimport { PO_SERVER_HOST_SERVICE, type PoServerHostService, type PoServerPlugin } from '../schemas/serverPlugin';\nimport {\n PO_SERVER_SCOPE_LEVELS,\n PO_SERVER_SCOPE_SERVICE,\n type PoServerPluginShare,\n type PoServerResourceKey,\n type PoServerResourceLease,\n type PoServerScopeService,\n type PoServiceContract,\n} from '../schemas/serverScope';\nimport { LIVE_FACTORY_TIMEOUT_MS, withTimeout } from './liveImport';\nimport type { LoadedServerPackage } from './serverPackages';\n\ninterface Binding {\n readonly name: string;\n readonly value: unknown;\n}\n\ninterface InstalledPackage {\n readonly plugin: PoServerPlugin;\n readonly declaration?: PoServerBundleEntry;\n readonly fiber: Fiber;\n}\n\n/** One shared value, how many levels hold it, and how its creator said to dispose it. */\ninterface SharedValue {\n readonly value: Promise<unknown>;\n holders: number;\n readonly disposeValue: ((value: unknown) => void | Promise<void>) | undefined;\n}\n\n/** What one plugin build shares in one tree: its keyed values, and what each scope running it holds. */\nclass PluginShare {\n readonly values = new Map<string, SharedValue>();\n readonly running = new Map<PoServerScope, unknown>();\n\n get idle(): boolean {\n return this.values.size === 0 && this.running.size === 0;\n }\n}\n\nconst resourceName = (key: PoServerResourceKey): string => JSON.stringify(typeof key === 'string' ? [key] : key);\n/** What a scope's owner publishes for the packages installed at its level. */\nexport interface PoServerScopeHost {\n /**\n * The level's mount table. A host that also routes requests (`createPoServerHost` does) answers\n * API requests for this level and every level below it that does not serve the API itself.\n */\n readonly host: PoServerHostService & { handlerFor?(basePath: string): PoApiHandler | undefined };\n /** Host capabilities published as `po/fs`, `po/shell`, `po/sqlite` and `po/session-storage`. */\n readonly platform: PoPlatform;\n}\n\nexport interface PoServerPackageSettledReport {\n readonly packageName: string;\n readonly durationMs: number;\n readonly outcome: 'installed' | 'failed' | 'timed_out';\n}\n\nexport interface PoServerInstallOptions {\n readonly onNotice?: (message: string) => void;\n /** Bound on each package's `apply`; defaults to `LIVE_FACTORY_TIMEOUT_MS`. */\n readonly factoryTimeoutMs?: number;\n /** Reports how long each package's `apply` took and how it ended, for telemetry. */\n readonly onPackageSettled?: (report: PoServerPackageSettledReport) => void;\n}\n\nconst depth = (level: PoApiScope): number => PO_SERVER_SCOPE_LEVELS.indexOf(level);\n\nconst describe = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/** Publishes one binding as a native service of the root it is plugged into. */\nfunction bindingProvider(context: Context, binding: Binding): void {\n context.provide(binding.name, binding.value);\n}\n\n/**\n * The scope service each root publishes.\n *\n * Cordis traceable services rebind `ctx` to the caller, so a binding made through\n * `provide` unwinds with the plugin that made it.\n */\nclass ScopeService extends Service<PoServerScopeService> implements PoServerScopeService {\n readonly level: PoApiScope;\n\n constructor(\n ctx: Context,\n private readonly scope: PoServerScope,\n ) {\n super(ctx, PO_SERVER_SCOPE_SERVICE);\n this.level = scope.level;\n }\n\n async provide<T>(contract: PoServiceContract<T>, value: T): Promise<() => Promise<void>> {\n return await this.ctx.effect(() => this.scope.bind(contract, value), `po scope provide ${contract.name}`);\n }\n\n resolve<T>(contract: PoServiceContract<T>): T | undefined {\n return this.scope.resolve(contract);\n }\n\n emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n this.scope.emit(name, ...args);\n }\n\n broadcast<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n this.scope.broadcast(name, ...args);\n }\n\n plugin<TRoots>(owner: object): PoServerPluginShare<TRoots> {\n return this.scope.share<TRoots>(owner);\n }\n}\n\n/**\n * Binds registrations to the Cordis fiber that calls the host service.\n *\n * The host itself is shared by every package at its level, but Cordis traceable\n * services rebind `ctx` to the caller. That lets a package's registration unwind\n * even when `apply` throws before it can return its disposer.\n */\nclass ScopedPoServerHost extends Service<PoServerHostService> implements PoServerHostService {\n readonly scope: PoServerHostService['scope'];\n readonly context: PoServerHostService['context'];\n\n constructor(\n ctx: Context,\n private readonly host: PoServerHostService,\n ) {\n super(ctx, PO_SERVER_HOST_SERVICE);\n this.scope = host.scope;\n this.context = host.context;\n }\n\n registerApi(api: Parameters<PoServerHostService['registerApi']>[0]) {\n const headless = this.scope === 'session' ? readPoHeadlessHost(this.ctx) : undefined;\n if (headless) {\n const activity = headless.registerActivity({\n name: `api:${api.basePath}`,\n start: () => {\n const registration = this.host.registerApi(api);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`package API '${api.basePath}' did not mount.`);\n }\n return () => {\n registration.dispose();\n };\n },\n });\n return {\n mounted: true,\n dispose: () => activity.dispose(),\n };\n }\n const registration = this.host.registerApi(api);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`package API '${api.basePath}' did not mount.`);\n }\n this.ctx.effect(() => () => registration.dispose(), 'server package API registration');\n return registration;\n }\n\n registerChannel(channel: Parameters<PoServerHostService['registerChannel']>[0]) {\n const registration = this.host.registerChannel(channel);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`hub channel '${channel.frameType}' did not mount.`);\n }\n this.ctx.effect(() => () => registration.dispose(), 'server package channel registration');\n return registration;\n }\n\n registerMethod: PoServerHostService['registerMethod'] = (definition, handler) => {\n const registration = this.host.registerMethod(definition, handler);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`plugin method '${definition.service}.${definition.method}' did not mount.`);\n }\n this.ctx.effect(() => () => registration.dispose(), 'server package method registration');\n return registration;\n };\n\n mounted(): readonly string[] {\n return this.host.mounted();\n }\n\n mountedChannels(): readonly string[] {\n return this.host.mountedChannels();\n }\n}\n\n/** Publishes the host service from inside a root-level plugin, so it unwinds with the scope. */\nfunction serverHostProvider(context: Context, host: PoServerHostService): void {\n new ScopedPoServerHost(context, host);\n}\n\n/** Publishes the platform services from inside a root-level plugin, so they unwind with the scope. */\nfunction platformProvider(context: Context, input: { platform: PoPlatform; cwd: string }): void {\n const env = input.platform.env(input.cwd);\n context.provide(PO_FS_SERVICE, env);\n context.provide(PO_SHELL_SERVICE, env);\n context.provide(PO_SQLITE_SERVICE, input.platform.sqlite);\n context.provide(PO_SESSION_STORAGE_SERVICE, input.platform.sessions);\n context.effect(() => () => env.cleanup(BACKGROUND_CONTEXT), 'platform execution env');\n}\n\n/**\n * One level of the server IoC tree: its own cordis root, a parent link, the contracts bound here,\n * and the packages installed here.\n *\n * Separate roots rather than one shared root, because cordis re-checks every fiber of a root on each\n * service change; a hundred sessions in one root would make every change scan all of them. A binding\n * is instead mirrored into each descendant root as a native service, so a plugin injects an outer\n * level's service exactly as it injects its own.\n */\nexport class PoServerScope {\n readonly root = new Context();\n private readonly children = new Set<PoServerScope>();\n private readonly bound = new Map<string, Binding>();\n private readonly mirrors = new Map<Binding, Fiber>();\n private readonly installed = new Map<string, InstalledPackage>();\n private installs: Promise<void> = Promise.resolve();\n /** Set once a package's apply never settled, to the bound it overran; its fiber may never finish disposing. */\n private abandonedTimeoutMs: number | undefined;\n private notice: (message: string) => void = () => undefined;\n private disposing: Promise<void> | undefined;\n /** The served mount table, read when a request routes through this level. */\n private router: PoServerScopeHost['host'] | undefined;\n /** Kept by the outermost scope only: what each plugin build shares across the whole tree. */\n private readonly shares = new Map<object, PluginShare>();\n\n private constructor(\n readonly level: PoApiScope,\n readonly parent: PoServerScope | undefined,\n ) {}\n\n /** Opens a scope below `parent`, with every binding visible from its ancestors already published. */\n static async open(level: PoApiScope, parent?: PoServerScope): Promise<PoServerScope> {\n if (parent?.disposing !== undefined) throw new Error(`The ${parent.level} scope is disposed.`);\n if (parent !== undefined && depth(level) <= depth(parent.level))\n throw new Error(`A ${level} scope cannot open below a ${parent.level} scope.`);\n const scope = new PoServerScope(level, parent);\n parent?.children.add(scope);\n try {\n await scope.root.plugin(ScopeService, scope);\n await Promise.all((parent?.visible() ?? []).map((binding) => scope.mirror(binding)));\n } catch (error) {\n await scope.dispose();\n throw error;\n }\n return scope;\n }\n\n get disposed(): boolean {\n return this.disposing !== undefined;\n }\n\n /**\n * Publishes what every package at this level reads: the owner's mount table as `po/server-host`,\n * and the platform services. They stay for the life of the scope; the owner still disposes the\n * mount table it handed in.\n */\n async serve(input: PoServerScopeHost): Promise<void> {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n if (input.host.scope !== this.level)\n throw new Error(`A ${input.host.scope} host cannot serve a ${this.level} scope.`);\n await this.root.plugin(serverHostProvider, input.host);\n this.router = input.host;\n // Published callers may hand a host without context (the packed authoring check does), so read it defensively.\n const hostContext = input.host.context as Partial<PoServerHostService['context']> | undefined;\n const cwd = hostContext?.cwd ?? hostContext?.workspaceRoot ?? input.platform.homeDirectory;\n await this.root.plugin(platformProvider, { platform: input.platform, cwd });\n }\n\n /**\n * Installs packages at this level, each as its own fiber in this root, one at a time.\n *\n * Installation only adds. A package installed from the same build (the same plugin object) is\n * skipped. One whose build changed is disposed and the new build applied; when that fails, the\n * previous build is applied again. A package missing from `packages` stays installed, because\n * which packages a session runs is its own composition's choice. Installs into one scope run one\n * at a time. Resolves with every package installed here, in install order.\n */\n install(\n packages: readonly (PoServerPlugin | LoadedServerPackage)[],\n options: PoServerInstallOptions = {},\n ): Promise<readonly string[]> {\n const run = this.installs.then(() => this.installAll(packages, options));\n this.installs = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Binds a contract owned by this level here and in every descendant, including ones opened later.\n * Settles once every descendant root publishes it. Hosts call this directly; plugins go through\n * the scope service so the binding follows their fiber.\n */\n async bind<T>(contract: PoServiceContract<T>, value: T): Promise<() => Promise<void>> {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n if (contract.scope !== this.level)\n throw new Error(\n `Contract '${contract.name}' belongs to the ${contract.scope} scope and cannot be provided from the ${this.level} scope.`,\n );\n if (this.bound.has(contract.name))\n throw new Error(`Contract '${contract.name}' is already provided in this ${this.level} scope.`);\n const binding: Binding = { name: contract.name, value };\n this.bound.set(contract.name, binding);\n const withdraw = async (): Promise<void> => {\n if (this.bound.get(contract.name) !== binding) return;\n this.bound.delete(contract.name);\n await Promise.all([...this.subtree()].map((scope) => scope.unmirror(binding)));\n };\n try {\n await Promise.all([...this.subtree()].map((scope) => scope.mirror(binding)));\n } catch (error) {\n await withdraw();\n throw error;\n }\n return withdraw;\n }\n\n resolve<T>(contract: PoServiceContract<T>): T | undefined {\n if (depth(contract.scope) > depth(this.level))\n throw new Error(`A ${this.level} scope cannot see the ${contract.scope} contract '${contract.name}'.`);\n for (const scope of this.ancestry()) {\n if (scope.level === contract.scope) return scope.bound.get(contract.name)?.value as T | undefined;\n }\n return undefined;\n }\n\n /**\n * The handler for an API, from this level or the nearest one above it that serves it. An API\n * declared at a level reaches every level below it, and a level that runs a package from an outer\n * level leaves that level's root-bound APIs to it; a level that declares the API itself wins.\n */\n apiHandler(basePath: string): PoApiHandler | undefined {\n for (const scope of this.ancestry()) {\n const handler = scope.router?.handlerFor?.(basePath);\n if (handler !== undefined) return handler;\n }\n return undefined;\n }\n\n emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n deliver([...this.ancestry()], name, args);\n }\n\n broadcast<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n deliver([...this.subtree()], name, args);\n }\n\n /**\n * Disposes descendants first, then the packages installed here in reverse install order, then\n * this root; a binding made here unwinds with its provider.\n */\n dispose(): Promise<void> {\n this.disposing ??= (async () => {\n await Promise.all([...this.children].map((child) => child.dispose()));\n this.parent?.children.delete(this);\n this.mirrors.clear();\n this.router = undefined;\n const packages = [...this.installed.values()].reverse();\n this.installed.clear();\n for (const { fiber } of packages) await fiber.dispose();\n const disposal = this.root.fiber.dispose();\n if (this.abandonedTimeoutMs === undefined) return await disposal;\n // An abandoned apply holds its fiber open, so teardown is bounded rather than left to hang.\n await withTimeout(disposal, this.abandonedTimeoutMs, `${this.level} scope dispose`).catch((error: unknown) =>\n this.notice(describe(error)),\n );\n })().finally(async () => {\n // What a level never released, such as a value an abandoned apply held, goes with the tree.\n if (this.parent === undefined) await this.disposeShares();\n });\n return this.disposing;\n }\n\n /**\n * What the plugin build `owner` shares in this tree, seen from this scope. The outermost scope keeps\n * one store per build, so every level of one hub sees the same values and no two hubs ever do.\n */\n share<TRoots>(owner: object): PoServerPluginShare<TRoots> {\n const top = this.outermost();\n const storeOf = (): PluginShare => {\n let store = top.shares.get(owner);\n if (store === undefined) {\n store = new PluginShare();\n top.shares.set(owner, store);\n }\n return store;\n };\n // An idle store is dropped, so builds a reload replaced do not accumulate.\n const settle = (store: PluginShare): void => {\n if (store.idle && top.shares.get(owner) === store) top.shares.delete(owner);\n };\n return {\n acquire: async <T>(\n key: PoServerResourceKey,\n create: () => T | Promise<T>,\n dispose?: (value: T) => void | Promise<void>,\n ): Promise<PoServerResourceLease<T>> => {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n const store = storeOf();\n const name = resourceName(key);\n let shared = store.values.get(name);\n if (shared === undefined) {\n const created: SharedValue = {\n value: Promise.resolve().then(create),\n holders: 0,\n disposeValue: dispose === undefined ? undefined : (value) => dispose(value as T),\n };\n store.values.set(name, created);\n // A value that could not be created is not kept, so the next acquire creates it again.\n created.value.catch(() => {\n if (store.values.get(name) === created) store.values.delete(name);\n settle(store);\n });\n shared = created;\n }\n const held = shared;\n held.holders += 1;\n let value: unknown;\n try {\n value = await held.value;\n } catch (error) {\n held.holders -= 1;\n throw error;\n }\n let released = false;\n return {\n value: value as T,\n release: async () => {\n if (released) return;\n released = true;\n held.holders -= 1;\n if (held.holders > 0 || store.values.get(name) !== held) return;\n store.values.delete(name);\n settle(store);\n await held.disposeValue?.(value);\n },\n };\n },\n run: (roots) => {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n const store = storeOf();\n store.running.set(this, roots);\n return () => {\n if (store.running.get(this) === roots) store.running.delete(this);\n settle(store);\n };\n },\n outer: (from) => {\n const store = top.shares.get(owner);\n if (store === undefined) return [];\n const held: TRoots[] = [];\n for (const scope of this.parent?.ancestry() ?? []) {\n if (depth(scope.level) >= depth(from) && store.running.has(scope))\n held.push(store.running.get(scope) as TRoots);\n }\n return held;\n },\n };\n }\n\n private async installAll(\n packages: readonly (PoServerPlugin | LoadedServerPackage)[],\n options: PoServerInstallOptions,\n ): Promise<readonly string[]> {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n const notice = options.onNotice ?? ((): void => {});\n this.notice = notice;\n if (\n packages.some((candidate) => 'retained' in candidate && candidate.retained === true) &&\n !readPoHeadlessHost(this.root)\n )\n throw new Error('Retained server candidates require a gated headless host');\n for (const candidate of packages) {\n const plugin = 'declaration' in candidate ? candidate.plugin : candidate;\n const declaration = 'declaration' in candidate ? candidate.declaration : undefined;\n const name = declaration?.packageName ?? plugin.name;\n const previous = this.installed.get(name);\n if (previous?.plugin === plugin) continue;\n if (previous !== undefined) {\n this.installed.delete(name);\n await previous.fiber.dispose();\n }\n try {\n const fiber = await this.apply(name, plugin, declaration, options);\n this.installed.set(name, { plugin, ...(declaration === undefined ? {} : { declaration }), fiber });\n } catch (error) {\n const message = `server package '${name}' did not install (${describe(error)})`;\n if (previous !== undefined) {\n await this.restore(name, previous, options, message);\n continue;\n }\n if (\n declaration?.required === true &&\n !('initiallyEligible' in candidate && candidate.initiallyEligible === false)\n )\n throw new Error(message, { cause: error });\n notice(message);\n }\n }\n // A level waiting for a contract that nothing at it or above it provides would never mount, silently.\n for (const candidate of packages) {\n const plugin = 'declaration' in candidate ? candidate.plugin : candidate;\n const name = ('declaration' in candidate ? candidate.declaration.packageName : undefined) ?? plugin.name;\n if (this.installed.get(name)?.plugin !== plugin) continue;\n for (const contract of plugin.injects?.[this.level] ?? []) {\n // An inner level's contract is never visible here; defineServerPlugin refuses one, a raw plugin may not.\n const missing = depth(contract.scope) > depth(this.level) || this.resolve(contract) === undefined;\n if (missing)\n notice(\n `server package '${name}' waits for contract '${contract.name}', which no ${contract.scope} package provides.`,\n );\n }\n }\n return [...this.installed.keys()];\n }\n\n /** Applies a replaced package's previous build again after its new build failed. */\n private async restore(\n name: string,\n previous: InstalledPackage,\n options: PoServerInstallOptions,\n message: string,\n ): Promise<void> {\n try {\n const fiber = await this.apply(name, previous.plugin, previous.declaration, options);\n this.installed.set(name, { ...previous, fiber });\n this.notice(`${message}; its previous build is restored.`);\n } catch (rollbackError) {\n this.notice(`${message}; its previous build could not be restored (${describe(rollbackError)}).`);\n }\n }\n\n /** Applies one package as its own fiber, bounded by the factory timeout, and reports how it settled. */\n private async apply(\n name: string,\n plugin: PoServerPlugin,\n declaration: PoServerBundleEntry | undefined,\n options: PoServerInstallOptions,\n ): Promise<Fiber> {\n const startedAt = Date.now();\n const timeoutMs = options.factoryTimeoutMs ?? LIVE_FACTORY_TIMEOUT_MS;\n const settled = (outcome: PoServerPackageSettledReport['outcome']): void =>\n options.onPackageSettled?.({ packageName: name, durationMs: Date.now() - startedAt, outcome });\n let fiber: Fiber | undefined;\n let applySettled = false;\n try {\n const owner = declaration ? this.root.extend({ [PO_HEADLESS_OWNER]: declaration }) : this.root;\n fiber = owner.plugin(plugin);\n const applied = fiber.await();\n const markSettled = (): void => {\n applySettled = true;\n };\n applied.then(markSettled, markSettled);\n await withTimeout(applied, timeoutMs, `server package '${name}' apply`);\n settled('installed');\n return fiber;\n } catch (error) {\n settled(fiber && !applySettled ? 'timed_out' : 'failed');\n if (fiber && !applySettled) {\n this.abandonedTimeoutMs = Math.max(this.abandonedTimeoutMs ?? 0, timeoutMs);\n void fiber\n .dispose()\n .catch((disposeError: unknown) =>\n this.notice(`abandoned server package '${name}' did not dispose (${describe(disposeError)})`),\n );\n } else await fiber?.dispose();\n throw error;\n }\n }\n\n private visible(): Binding[] {\n return [...(this.parent?.visible() ?? []), ...this.bound.values()];\n }\n\n /** This scope, then each parent out to the root. */\n private outermost(): PoServerScope {\n return this.parent === undefined ? this : this.parent.outermost();\n }\n\n /** Disposes every value the tree still holds, newest first; a failure is reported and the rest still go. */\n private async disposeShares(): Promise<void> {\n const values = [...this.shares.values()].flatMap((store) => [...store.values.values()]).reverse();\n this.shares.clear();\n for (const shared of values) {\n try {\n await shared.disposeValue?.(await shared.value);\n } catch (error) {\n this.notice(`a shared server value did not dispose (${describe(error)})`);\n }\n }\n }\n\n private *ancestry(): Generator<PoServerScope> {\n yield this;\n if (this.parent !== undefined) yield* this.parent.ancestry();\n }\n\n private *subtree(): Generator<PoServerScope> {\n if (this.disposed) return;\n yield this;\n for (const child of this.children) yield* child.subtree();\n }\n\n /**\n * Publishes a binding into this root and settles once it is live. Idempotent per binding, since a\n * child opening mid-bind is reached by both `open` and `bind`.\n */\n private async mirror(binding: Binding): Promise<void> {\n if (this.disposed) return;\n let fiber = this.mirrors.get(binding);\n if (fiber === undefined) {\n fiber = this.root.plugin(bindingProvider, binding);\n this.mirrors.set(binding, fiber);\n }\n await fiber.await();\n }\n\n private async unmirror(binding: Binding): Promise<void> {\n const fiber = this.mirrors.get(binding);\n this.mirrors.delete(binding);\n // A disposing root unwinds its own mirrors.\n if (!this.disposed) await fiber?.dispose();\n }\n}\n\n/** Every scope receives the event even when an earlier one's listener throws. */\nfunction deliver(scopes: readonly PoServerScope[], name: keyof Events, args: readonly unknown[]): void {\n const errors: unknown[] = [];\n for (const scope of scopes) {\n if (scope.disposed) continue;\n try {\n scope.root.events.emit(name, ...args);\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length > 0) throw new AggregateError(errors, `${errors.length} scope listener(s) of '${name}' failed.`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"serverScope.mjs","names":[],"sources":["../../../src/server/serverScope.ts"],"sourcesContent":["import { Context, type Events, type Fiber, Service } from '@deepseek-ai/cordis';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\n\nimport { PO_HEADLESS_OWNER, readPoHeadlessHost } from '../schemas/headless';\nimport type { PoApiHandler, PoApiScope } from '../schemas/packageApi';\nimport {\n PO_FS_SERVICE,\n PO_SESSION_STORAGE_SERVICE,\n PO_SHELL_SERVICE,\n PO_SQLITE_SERVICE,\n type PoPlatform,\n} from '../schemas/platform';\nimport type { PoServerBundleEntry } from '../schemas/serverBundle';\nimport { PO_SERVER_HOST_SERVICE, type PoServerHostService, type PoServerPlugin } from '../schemas/serverPlugin';\nimport {\n PO_SERVER_SCOPE_LEVELS,\n PO_SERVER_SCOPE_SERVICE,\n type PoServerPluginShare,\n type PoServerResourceKey,\n type PoServerResourceLease,\n type PoServerScopeService,\n type PoServiceContract,\n} from '../schemas/serverScope';\nimport { PO_SESSION_FILES_SERVICE, PO_SESSION_TEMP_DIR } from '../services/sessionFiles';\nimport { LIVE_FACTORY_TIMEOUT_MS, withTimeout } from './liveImport';\nimport type { LoadedServerPackage } from './serverPackages';\n\ninterface Binding {\n readonly name: string;\n readonly value: unknown;\n}\n\ninterface InstalledPackage {\n readonly plugin: PoServerPlugin;\n readonly declaration?: PoServerBundleEntry;\n readonly fiber: Fiber;\n}\n\n/** One shared value, how many levels hold it, and how its creator said to dispose it. */\ninterface SharedValue {\n readonly value: Promise<unknown>;\n holders: number;\n readonly disposeValue: ((value: unknown) => void | Promise<void>) | undefined;\n}\n\n/** What one plugin build shares in one tree: its keyed values, and what each scope running it holds. */\nclass PluginShare {\n readonly values = new Map<string, SharedValue>();\n readonly running = new Map<PoServerScope, unknown>();\n\n get idle(): boolean {\n return this.values.size === 0 && this.running.size === 0;\n }\n}\n\nconst resourceName = (key: PoServerResourceKey): string => JSON.stringify(typeof key === 'string' ? [key] : key);\n/** What a scope's owner publishes for the packages installed at its level. */\nexport interface PoServerScopeHost {\n /**\n * The level's mount table. A host that also routes requests (`createPoServerHost` does) answers\n * API requests for this level and every level below it that does not serve the API itself.\n */\n readonly host: PoServerHostService & { handlerFor?(basePath: string): PoApiHandler | undefined };\n /** Host capabilities published as `po/fs`, `po/shell`, `po/sqlite` and `po/session-storage`. */\n readonly platform: PoPlatform;\n}\n\nexport interface PoServerPackageSettledReport {\n readonly packageName: string;\n readonly durationMs: number;\n readonly outcome: 'installed' | 'failed' | 'timed_out';\n}\n\nexport interface PoServerInstallOptions {\n readonly onNotice?: (message: string) => void;\n /** Bound on each package's `apply`; defaults to `LIVE_FACTORY_TIMEOUT_MS`. */\n readonly factoryTimeoutMs?: number;\n /** Reports how long each package's `apply` took and how it ended, for telemetry. */\n readonly onPackageSettled?: (report: PoServerPackageSettledReport) => void;\n}\n\nconst depth = (level: PoApiScope): number => PO_SERVER_SCOPE_LEVELS.indexOf(level);\n\nconst describe = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/** Publishes one binding as a native service of the root it is plugged into. */\nfunction bindingProvider(context: Context, binding: Binding): void {\n context.provide(binding.name, binding.value);\n}\n\n/**\n * The scope service each root publishes.\n *\n * Cordis traceable services rebind `ctx` to the caller, so a binding made through\n * `provide` unwinds with the plugin that made it.\n */\nclass ScopeService extends Service<PoServerScopeService> implements PoServerScopeService {\n readonly level: PoApiScope;\n\n constructor(\n ctx: Context,\n private readonly scope: PoServerScope,\n ) {\n super(ctx, PO_SERVER_SCOPE_SERVICE);\n this.level = scope.level;\n }\n\n async provide<T>(contract: PoServiceContract<T>, value: T): Promise<() => Promise<void>> {\n return await this.ctx.effect(() => this.scope.bind(contract, value), `po scope provide ${contract.name}`);\n }\n\n resolve<T>(contract: PoServiceContract<T>): T | undefined {\n return this.scope.resolve(contract);\n }\n\n emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n this.scope.emit(name, ...args);\n }\n\n broadcast<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n this.scope.broadcast(name, ...args);\n }\n\n plugin<TRoots>(owner: object): PoServerPluginShare<TRoots> {\n return this.scope.share<TRoots>(owner);\n }\n}\n\n/**\n * Binds registrations to the Cordis fiber that calls the host service.\n *\n * The host itself is shared by every package at its level, but Cordis traceable\n * services rebind `ctx` to the caller. That lets a package's registration unwind\n * even when `apply` throws before it can return its disposer.\n */\nclass ScopedPoServerHost extends Service<PoServerHostService> implements PoServerHostService {\n readonly scope: PoServerHostService['scope'];\n readonly context: PoServerHostService['context'];\n\n constructor(\n ctx: Context,\n private readonly host: PoServerHostService,\n ) {\n super(ctx, PO_SERVER_HOST_SERVICE);\n this.scope = host.scope;\n this.context = host.context;\n }\n\n registerApi(api: Parameters<PoServerHostService['registerApi']>[0]) {\n const headless = this.scope === 'session' ? readPoHeadlessHost(this.ctx) : undefined;\n if (headless) {\n const activity = headless.registerActivity({\n name: `api:${api.basePath}`,\n start: () => {\n const registration = this.host.registerApi(api);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`package API '${api.basePath}' did not mount.`);\n }\n return () => {\n registration.dispose();\n };\n },\n });\n return {\n mounted: true,\n dispose: () => activity.dispose(),\n };\n }\n const registration = this.host.registerApi(api);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`package API '${api.basePath}' did not mount.`);\n }\n this.ctx.effect(() => () => registration.dispose(), 'server package API registration');\n return registration;\n }\n\n registerChannel(channel: Parameters<PoServerHostService['registerChannel']>[0]) {\n const registration = this.host.registerChannel(channel);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`hub channel '${channel.frameType}' did not mount.`);\n }\n this.ctx.effect(() => () => registration.dispose(), 'server package channel registration');\n return registration;\n }\n\n registerMethod: PoServerHostService['registerMethod'] = (definition, handler) => {\n const registration = this.host.registerMethod(definition, handler);\n if (registration.mounted !== true) {\n registration.dispose();\n throw new Error(`plugin method '${definition.service}.${definition.method}' did not mount.`);\n }\n this.ctx.effect(() => () => registration.dispose(), 'server package method registration');\n return registration;\n };\n\n mounted(): readonly string[] {\n return this.host.mounted();\n }\n\n mountedChannels(): readonly string[] {\n return this.host.mountedChannels();\n }\n}\n\n/** Publishes the host service from inside a root-level plugin, so it unwinds with the scope. */\nfunction serverHostProvider(context: Context, host: PoServerHostService): void {\n new ScopedPoServerHost(context, host);\n}\n\n/** Publishes the platform services from inside a root-level plugin, so they unwind with the scope. */\nfunction platformProvider(\n context: Context,\n input: { platform: PoPlatform; cwd: string; temporaryDirectory?: string },\n): void {\n const env = input.platform.env(input.cwd);\n context.provide(PO_FS_SERVICE, env);\n context.provide(PO_SHELL_SERVICE, env);\n context.provide(PO_SQLITE_SERVICE, input.platform.sqlite);\n context.provide(PO_SESSION_STORAGE_SERVICE, input.platform.sessions);\n if (input.temporaryDirectory !== undefined)\n context.provide(PO_SESSION_FILES_SERVICE, { temporaryDirectory: input.temporaryDirectory });\n context.effect(() => () => env.cleanup(BACKGROUND_CONTEXT), 'platform execution env');\n}\n\n/**\n * One level of the server IoC tree: its own cordis root, a parent link, the contracts bound here,\n * and the packages installed here.\n *\n * Separate roots rather than one shared root, because cordis re-checks every fiber of a root on each\n * service change; a hundred sessions in one root would make every change scan all of them. A binding\n * is instead mirrored into each descendant root as a native service, so a plugin injects an outer\n * level's service exactly as it injects its own.\n */\nexport class PoServerScope {\n readonly root = new Context();\n private readonly children = new Set<PoServerScope>();\n private readonly bound = new Map<string, Binding>();\n private readonly mirrors = new Map<Binding, Fiber>();\n private readonly installed = new Map<string, InstalledPackage>();\n private installs: Promise<void> = Promise.resolve();\n /** Set once a package's apply never settled, to the bound it overran; its fiber may never finish disposing. */\n private abandonedTimeoutMs: number | undefined;\n private notice: (message: string) => void = () => undefined;\n private disposing: Promise<void> | undefined;\n /** The served mount table, read when a request routes through this level. */\n private router: PoServerScopeHost['host'] | undefined;\n /** Kept by the outermost scope only: what each plugin build shares across the whole tree. */\n private readonly shares = new Map<object, PluginShare>();\n\n private constructor(\n readonly level: PoApiScope,\n readonly parent: PoServerScope | undefined,\n ) {}\n\n /** Opens a scope below `parent`, with every binding visible from its ancestors already published. */\n static async open(level: PoApiScope, parent?: PoServerScope): Promise<PoServerScope> {\n if (parent?.disposing !== undefined) throw new Error(`The ${parent.level} scope is disposed.`);\n if (parent !== undefined && depth(level) <= depth(parent.level))\n throw new Error(`A ${level} scope cannot open below a ${parent.level} scope.`);\n const scope = new PoServerScope(level, parent);\n parent?.children.add(scope);\n try {\n await scope.root.plugin(ScopeService, scope);\n await Promise.all((parent?.visible() ?? []).map((binding) => scope.mirror(binding)));\n } catch (error) {\n await scope.dispose();\n throw error;\n }\n return scope;\n }\n\n get disposed(): boolean {\n return this.disposing !== undefined;\n }\n\n /**\n * Publishes what every package at this level reads: the owner's mount table as `po/server-host`,\n * and the platform services. They stay for the life of the scope; the owner still disposes the\n * mount table it handed in.\n */\n async serve(input: PoServerScopeHost): Promise<void> {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n if (input.host.scope !== this.level)\n throw new Error(`A ${input.host.scope} host cannot serve a ${this.level} scope.`);\n await this.root.plugin(serverHostProvider, input.host);\n this.router = input.host;\n // Published callers may hand a host without context (the packed authoring check does), so read it defensively.\n const hostContext = input.host.context as Partial<PoServerHostService['context']> | undefined;\n const cwd = hostContext?.cwd ?? hostContext?.workspaceRoot ?? input.platform.homeDirectory;\n await this.root.plugin(platformProvider, {\n platform: input.platform,\n cwd,\n temporaryDirectory: this.level === 'session' ? hostContext?.environment?.[PO_SESSION_TEMP_DIR] : undefined,\n });\n }\n\n /**\n * Installs packages at this level, each as its own fiber in this root, one at a time.\n *\n * Installation only adds. A package installed from the same build (the same plugin object) is\n * skipped. One whose build changed is disposed and the new build applied; when that fails, the\n * previous build is applied again. A package missing from `packages` stays installed, because\n * which packages a session runs is its own composition's choice. Installs into one scope run one\n * at a time. Resolves with every package installed here, in install order.\n */\n install(\n packages: readonly (PoServerPlugin | LoadedServerPackage)[],\n options: PoServerInstallOptions = {},\n ): Promise<readonly string[]> {\n const run = this.installs.then(() => this.installAll(packages, options));\n this.installs = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Binds a contract owned by this level here and in every descendant, including ones opened later.\n * Settles once every descendant root publishes it. Hosts call this directly; plugins go through\n * the scope service so the binding follows their fiber.\n */\n async bind<T>(contract: PoServiceContract<T>, value: T): Promise<() => Promise<void>> {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n if (contract.scope !== this.level)\n throw new Error(\n `Contract '${contract.name}' belongs to the ${contract.scope} scope and cannot be provided from the ${this.level} scope.`,\n );\n if (this.bound.has(contract.name))\n throw new Error(`Contract '${contract.name}' is already provided in this ${this.level} scope.`);\n const binding: Binding = { name: contract.name, value };\n this.bound.set(contract.name, binding);\n const withdraw = async (): Promise<void> => {\n if (this.bound.get(contract.name) !== binding) return;\n this.bound.delete(contract.name);\n await Promise.all([...this.subtree()].map((scope) => scope.unmirror(binding)));\n };\n try {\n await Promise.all([...this.subtree()].map((scope) => scope.mirror(binding)));\n } catch (error) {\n await withdraw();\n throw error;\n }\n return withdraw;\n }\n\n resolve<T>(contract: PoServiceContract<T>): T | undefined {\n if (depth(contract.scope) > depth(this.level))\n throw new Error(`A ${this.level} scope cannot see the ${contract.scope} contract '${contract.name}'.`);\n for (const scope of this.ancestry()) {\n if (scope.level === contract.scope) return scope.bound.get(contract.name)?.value as T | undefined;\n }\n return undefined;\n }\n\n /**\n * The handler for an API, from this level or the nearest one above it that serves it. An API\n * declared at a level reaches every level below it, and a level that runs a package from an outer\n * level leaves that level's root-bound APIs to it; a level that declares the API itself wins.\n */\n apiHandler(basePath: string): PoApiHandler | undefined {\n for (const scope of this.ancestry()) {\n const handler = scope.router?.handlerFor?.(basePath);\n if (handler !== undefined) return handler;\n }\n return undefined;\n }\n\n emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n deliver([...this.ancestry()], name, args);\n }\n\n broadcast<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void {\n deliver([...this.subtree()], name, args);\n }\n\n /**\n * Disposes descendants first, then the packages installed here in reverse install order, then\n * this root; a binding made here unwinds with its provider.\n */\n dispose(): Promise<void> {\n this.disposing ??= (async () => {\n await Promise.all([...this.children].map((child) => child.dispose()));\n this.parent?.children.delete(this);\n this.mirrors.clear();\n this.router = undefined;\n const packages = [...this.installed.values()].reverse();\n this.installed.clear();\n for (const { fiber } of packages) await fiber.dispose();\n const disposal = this.root.fiber.dispose();\n if (this.abandonedTimeoutMs === undefined) return await disposal;\n // An abandoned apply holds its fiber open, so teardown is bounded rather than left to hang.\n await withTimeout(disposal, this.abandonedTimeoutMs, `${this.level} scope dispose`).catch((error: unknown) =>\n this.notice(describe(error)),\n );\n })().finally(async () => {\n // What a level never released, such as a value an abandoned apply held, goes with the tree.\n if (this.parent === undefined) await this.disposeShares();\n });\n return this.disposing;\n }\n\n /**\n * What the plugin build `owner` shares in this tree, seen from this scope. The outermost scope keeps\n * one store per build, so every level of one hub sees the same values and no two hubs ever do.\n */\n share<TRoots>(owner: object): PoServerPluginShare<TRoots> {\n const top = this.outermost();\n const storeOf = (): PluginShare => {\n let store = top.shares.get(owner);\n if (store === undefined) {\n store = new PluginShare();\n top.shares.set(owner, store);\n }\n return store;\n };\n // An idle store is dropped, so builds a reload replaced do not accumulate.\n const settle = (store: PluginShare): void => {\n if (store.idle && top.shares.get(owner) === store) top.shares.delete(owner);\n };\n return {\n acquire: async <T>(\n key: PoServerResourceKey,\n create: () => T | Promise<T>,\n dispose?: (value: T) => void | Promise<void>,\n ): Promise<PoServerResourceLease<T>> => {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n const store = storeOf();\n const name = resourceName(key);\n let shared = store.values.get(name);\n if (shared === undefined) {\n const created: SharedValue = {\n value: Promise.resolve().then(create),\n holders: 0,\n disposeValue: dispose === undefined ? undefined : (value) => dispose(value as T),\n };\n store.values.set(name, created);\n // A value that could not be created is not kept, so the next acquire creates it again.\n created.value.catch(() => {\n if (store.values.get(name) === created) store.values.delete(name);\n settle(store);\n });\n shared = created;\n }\n const held = shared;\n held.holders += 1;\n let value: unknown;\n try {\n value = await held.value;\n } catch (error) {\n held.holders -= 1;\n throw error;\n }\n let released = false;\n return {\n value: value as T,\n release: async () => {\n if (released) return;\n released = true;\n held.holders -= 1;\n if (held.holders > 0 || store.values.get(name) !== held) return;\n store.values.delete(name);\n settle(store);\n await held.disposeValue?.(value);\n },\n };\n },\n run: (roots) => {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n const store = storeOf();\n store.running.set(this, roots);\n return () => {\n if (store.running.get(this) === roots) store.running.delete(this);\n settle(store);\n };\n },\n outer: (from) => {\n const store = top.shares.get(owner);\n if (store === undefined) return [];\n const held: TRoots[] = [];\n for (const scope of this.parent?.ancestry() ?? []) {\n if (depth(scope.level) >= depth(from) && store.running.has(scope))\n held.push(store.running.get(scope) as TRoots);\n }\n return held;\n },\n };\n }\n\n private async installAll(\n packages: readonly (PoServerPlugin | LoadedServerPackage)[],\n options: PoServerInstallOptions,\n ): Promise<readonly string[]> {\n if (this.disposed) throw new Error(`The ${this.level} scope is disposed.`);\n const notice = options.onNotice ?? ((): void => {});\n this.notice = notice;\n if (\n packages.some((candidate) => 'retained' in candidate && candidate.retained === true) &&\n !readPoHeadlessHost(this.root)\n )\n throw new Error('Retained server candidates require a gated headless host');\n for (const candidate of packages) {\n const plugin = 'declaration' in candidate ? candidate.plugin : candidate;\n const declaration = 'declaration' in candidate ? candidate.declaration : undefined;\n const name = declaration?.packageName ?? plugin.name;\n const previous = this.installed.get(name);\n if (previous?.plugin === plugin) continue;\n if (previous !== undefined) {\n this.installed.delete(name);\n await previous.fiber.dispose();\n }\n try {\n const fiber = await this.apply(name, plugin, declaration, options);\n this.installed.set(name, { plugin, ...(declaration === undefined ? {} : { declaration }), fiber });\n } catch (error) {\n const message = `server package '${name}' did not install (${describe(error)})`;\n if (previous !== undefined) {\n await this.restore(name, previous, options, message);\n continue;\n }\n if (\n declaration?.required === true &&\n !('initiallyEligible' in candidate && candidate.initiallyEligible === false)\n )\n throw new Error(message, { cause: error });\n notice(message);\n }\n }\n // A level waiting for a contract that nothing at it or above it provides would never mount, silently.\n for (const candidate of packages) {\n const plugin = 'declaration' in candidate ? candidate.plugin : candidate;\n const name = ('declaration' in candidate ? candidate.declaration.packageName : undefined) ?? plugin.name;\n if (this.installed.get(name)?.plugin !== plugin) continue;\n for (const contract of plugin.injects?.[this.level] ?? []) {\n // An inner level's contract is never visible here; defineServerPlugin refuses one, a raw plugin may not.\n const missing = depth(contract.scope) > depth(this.level) || this.resolve(contract) === undefined;\n if (missing)\n notice(\n `server package '${name}' waits for contract '${contract.name}', which no ${contract.scope} package provides.`,\n );\n }\n }\n return [...this.installed.keys()];\n }\n\n /** Applies a replaced package's previous build again after its new build failed. */\n private async restore(\n name: string,\n previous: InstalledPackage,\n options: PoServerInstallOptions,\n message: string,\n ): Promise<void> {\n try {\n const fiber = await this.apply(name, previous.plugin, previous.declaration, options);\n this.installed.set(name, { ...previous, fiber });\n this.notice(`${message}; its previous build is restored.`);\n } catch (rollbackError) {\n this.notice(`${message}; its previous build could not be restored (${describe(rollbackError)}).`);\n }\n }\n\n /** Applies one package as its own fiber, bounded by the factory timeout, and reports how it settled. */\n private async apply(\n name: string,\n plugin: PoServerPlugin,\n declaration: PoServerBundleEntry | undefined,\n options: PoServerInstallOptions,\n ): Promise<Fiber> {\n const startedAt = Date.now();\n const timeoutMs = options.factoryTimeoutMs ?? LIVE_FACTORY_TIMEOUT_MS;\n const settled = (outcome: PoServerPackageSettledReport['outcome']): void =>\n options.onPackageSettled?.({ packageName: name, durationMs: Date.now() - startedAt, outcome });\n let fiber: Fiber | undefined;\n let applySettled = false;\n try {\n const owner = declaration ? this.root.extend({ [PO_HEADLESS_OWNER]: declaration }) : this.root;\n fiber = owner.plugin(plugin);\n const applied = fiber.await();\n const markSettled = (): void => {\n applySettled = true;\n };\n applied.then(markSettled, markSettled);\n await withTimeout(applied, timeoutMs, `server package '${name}' apply`);\n settled('installed');\n return fiber;\n } catch (error) {\n settled(fiber && !applySettled ? 'timed_out' : 'failed');\n if (fiber && !applySettled) {\n this.abandonedTimeoutMs = Math.max(this.abandonedTimeoutMs ?? 0, timeoutMs);\n void fiber\n .dispose()\n .catch((disposeError: unknown) =>\n this.notice(`abandoned server package '${name}' did not dispose (${describe(disposeError)})`),\n );\n } else await fiber?.dispose();\n throw error;\n }\n }\n\n private visible(): Binding[] {\n return [...(this.parent?.visible() ?? []), ...this.bound.values()];\n }\n\n /** This scope, then each parent out to the root. */\n private outermost(): PoServerScope {\n return this.parent === undefined ? this : this.parent.outermost();\n }\n\n /** Disposes every value the tree still holds, newest first; a failure is reported and the rest still go. */\n private async disposeShares(): Promise<void> {\n const values = [...this.shares.values()].flatMap((store) => [...store.values.values()]).reverse();\n this.shares.clear();\n for (const shared of values) {\n try {\n await shared.disposeValue?.(await shared.value);\n } catch (error) {\n this.notice(`a shared server value did not dispose (${describe(error)})`);\n }\n }\n }\n\n private *ancestry(): Generator<PoServerScope> {\n yield this;\n if (this.parent !== undefined) yield* this.parent.ancestry();\n }\n\n private *subtree(): Generator<PoServerScope> {\n if (this.disposed) return;\n yield this;\n for (const child of this.children) yield* child.subtree();\n }\n\n /**\n * Publishes a binding into this root and settles once it is live. Idempotent per binding, since a\n * child opening mid-bind is reached by both `open` and `bind`.\n */\n private async mirror(binding: Binding): Promise<void> {\n if (this.disposed) return;\n let fiber = this.mirrors.get(binding);\n if (fiber === undefined) {\n fiber = this.root.plugin(bindingProvider, binding);\n this.mirrors.set(binding, fiber);\n }\n await fiber.await();\n }\n\n private async unmirror(binding: Binding): Promise<void> {\n const fiber = this.mirrors.get(binding);\n this.mirrors.delete(binding);\n // A disposing root unwinds its own mirrors.\n if (!this.disposed) await fiber?.dispose();\n }\n}\n\n/** Every scope receives the event even when an earlier one's listener throws. */\nfunction deliver(scopes: readonly PoServerScope[], name: keyof Events, args: readonly unknown[]): void {\n const errors: unknown[] = [];\n for (const scope of scopes) {\n if (scope.disposed) continue;\n try {\n scope.root.events.emit(name, ...args);\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length > 0) throw new AggregateError(errors, `${errors.length} scope listener(s) of '${name}' failed.`);\n}\n"],"mappings":";;;;;;;;;;AA8CA,IAAM,cAAN,MAAkB;CAChB,yBAAkB,IAAI,IAAyB;CAC/C,0BAAmB,IAAI,IAA4B;CAEnD,IAAI,OAAgB;EAClB,OAAO,KAAK,OAAO,SAAS,KAAK,KAAK,QAAQ,SAAS;CACzD;AACF;AAEA,MAAM,gBAAgB,QAAqC,KAAK,UAAU,OAAO,QAAQ,WAAW,CAAC,GAAG,IAAI,GAAG;AA0B/G,MAAM,SAAS,UAA8B,uBAAuB,QAAQ,KAAK;AAEjF,MAAM,YAAY,UAA4B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;AAGnG,SAAS,gBAAgB,SAAkB,SAAwB;CACjE,QAAQ,QAAQ,QAAQ,MAAM,QAAQ,KAAK;AAC7C;;;;;;;AAQA,IAAM,eAAN,cAA2B,QAA8D;CAKpE;CAJnB;CAEA,YACE,KACA,OACA;EACA,MAAM,KAAK,uBAAuB;EAFjB,KAAA,QAAA;EAGjB,KAAK,QAAQ,MAAM;CACrB;CAEA,MAAM,QAAW,UAAgC,OAAwC;EACvF,OAAO,MAAM,KAAK,IAAI,aAAa,KAAK,MAAM,KAAK,UAAU,KAAK,GAAG,oBAAoB,SAAS,MAAM;CAC1G;CAEA,QAAW,UAA+C;EACxD,OAAO,KAAK,MAAM,QAAQ,QAAQ;CACpC;CAEA,KAA6B,MAAS,GAAG,MAAmC;EAC1E,KAAK,MAAM,KAAK,MAAM,GAAG,IAAI;CAC/B;CAEA,UAAkC,MAAS,GAAG,MAAmC;EAC/E,KAAK,MAAM,UAAU,MAAM,GAAG,IAAI;CACpC;CAEA,OAAe,OAA4C;EACzD,OAAO,KAAK,MAAM,MAAc,KAAK;CACvC;AACF;;;;;;;;AASA,IAAM,qBAAN,cAAiC,QAA4D;CAMxE;CALnB;CACA;CAEA,YACE,KACA,MACA;EACA,MAAM,KAAK,sBAAsB;EAFhB,KAAA,OAAA;EAGjB,KAAK,QAAQ,KAAK;EAClB,KAAK,UAAU,KAAK;CACtB;CAEA,YAAY,KAAwD;EAClE,MAAM,WAAW,KAAK,UAAU,YAAY,mBAAmB,KAAK,GAAG,IAAI,KAAA;EAC3E,IAAI,UAAU;GACZ,MAAM,WAAW,SAAS,iBAAiB;IACzC,MAAM,OAAO,IAAI;IACjB,aAAa;KACX,MAAM,eAAe,KAAK,KAAK,YAAY,GAAG;KAC9C,IAAI,aAAa,YAAY,MAAM;MACjC,aAAa,QAAQ;MACrB,MAAM,IAAI,MAAM,gBAAgB,IAAI,SAAS,iBAAiB;KAChE;KACA,aAAa;MACX,aAAa,QAAQ;KACvB;IACF;GACF,CAAC;GACD,OAAO;IACL,SAAS;IACT,eAAe,SAAS,QAAQ;GAClC;EACF;EACA,MAAM,eAAe,KAAK,KAAK,YAAY,GAAG;EAC9C,IAAI,aAAa,YAAY,MAAM;GACjC,aAAa,QAAQ;GACrB,MAAM,IAAI,MAAM,gBAAgB,IAAI,SAAS,iBAAiB;EAChE;EACA,KAAK,IAAI,mBAAmB,aAAa,QAAQ,GAAG,iCAAiC;EACrF,OAAO;CACT;CAEA,gBAAgB,SAAgE;EAC9E,MAAM,eAAe,KAAK,KAAK,gBAAgB,OAAO;EACtD,IAAI,aAAa,YAAY,MAAM;GACjC,aAAa,QAAQ;GACrB,MAAM,IAAI,MAAM,gBAAgB,QAAQ,UAAU,iBAAiB;EACrE;EACA,KAAK,IAAI,mBAAmB,aAAa,QAAQ,GAAG,qCAAqC;EACzF,OAAO;CACT;CAEA,kBAAyD,YAAY,YAAY;EAC/E,MAAM,eAAe,KAAK,KAAK,eAAe,YAAY,OAAO;EACjE,IAAI,aAAa,YAAY,MAAM;GACjC,aAAa,QAAQ;GACrB,MAAM,IAAI,MAAM,kBAAkB,WAAW,QAAQ,GAAG,WAAW,OAAO,iBAAiB;EAC7F;EACA,KAAK,IAAI,mBAAmB,aAAa,QAAQ,GAAG,oCAAoC;EACxF,OAAO;CACT;CAEA,UAA6B;EAC3B,OAAO,KAAK,KAAK,QAAQ;CAC3B;CAEA,kBAAqC;EACnC,OAAO,KAAK,KAAK,gBAAgB;CACnC;AACF;;AAGA,SAAS,mBAAmB,SAAkB,MAAiC;CAC7E,IAAI,mBAAmB,SAAS,IAAI;AACtC;;AAGA,SAAS,iBACP,SACA,OACM;CACN,MAAM,MAAM,MAAM,SAAS,IAAI,MAAM,GAAG;CACxC,QAAQ,QAAQ,eAAe,GAAG;CAClC,QAAQ,QAAQ,kBAAkB,GAAG;CACrC,QAAQ,QAAQ,mBAAmB,MAAM,SAAS,MAAM;CACxD,QAAQ,QAAQ,4BAA4B,MAAM,SAAS,QAAQ;CACnE,IAAI,MAAM,uBAAuB,KAAA,GAC/B,QAAQ,QAAQ,0BAA0B,EAAE,oBAAoB,MAAM,mBAAmB,CAAC;CAC5F,QAAQ,mBAAmB,IAAI,QAAQ,kBAAkB,GAAG,wBAAwB;AACtF;;;;;;;;;;AAWA,IAAa,gBAAb,MAAa,cAAc;CAiBd;CACA;CAjBX,OAAgB,IAAI,QAAQ;CAC5B,2BAA4B,IAAI,IAAmB;CACnD,wBAAyB,IAAI,IAAqB;CAClD,0BAA2B,IAAI,IAAoB;CACnD,4BAA6B,IAAI,IAA8B;CAC/D,WAAkC,QAAQ,QAAQ;;CAElD;CACA,eAAkD,KAAA;CAClD;;CAEA;;CAEA,yBAA0B,IAAI,IAAyB;CAEvD,YACE,OACA,QACA;EAFS,KAAA,QAAA;EACA,KAAA,SAAA;CACR;;CAGH,aAAa,KAAK,OAAmB,QAAgD;EACnF,IAAI,QAAQ,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,OAAO,OAAO,MAAM,oBAAoB;EAC7F,IAAI,WAAW,KAAA,KAAa,MAAM,KAAK,KAAK,MAAM,OAAO,KAAK,GAC5D,MAAM,IAAI,MAAM,KAAK,MAAM,6BAA6B,OAAO,MAAM,QAAQ;EAC/E,MAAM,QAAQ,IAAI,cAAc,OAAO,MAAM;EAC7C,QAAQ,SAAS,IAAI,KAAK;EAC1B,IAAI;GACF,MAAM,MAAM,KAAK,OAAO,cAAc,KAAK;GAC3C,MAAM,QAAQ,KAAK,QAAQ,QAAQ,KAAK,CAAC,EAAA,CAAG,KAAK,YAAY,MAAM,OAAO,OAAO,CAAC,CAAC;EACrF,SAAS,OAAO;GACd,MAAM,MAAM,QAAQ;GACpB,MAAM;EACR;EACA,OAAO;CACT;CAEA,IAAI,WAAoB;EACtB,OAAO,KAAK,cAAc,KAAA;CAC5B;;;;;;CAOA,MAAM,MAAM,OAAyC;EACnD,IAAI,KAAK,UAAU,MAAM,IAAI,MAAM,OAAO,KAAK,MAAM,oBAAoB;EACzE,IAAI,MAAM,KAAK,UAAU,KAAK,OAC5B,MAAM,IAAI,MAAM,KAAK,MAAM,KAAK,MAAM,uBAAuB,KAAK,MAAM,QAAQ;EAClF,MAAM,KAAK,KAAK,OAAO,oBAAoB,MAAM,IAAI;EACrD,KAAK,SAAS,MAAM;EAEpB,MAAM,cAAc,MAAM,KAAK;EAC/B,MAAM,MAAM,aAAa,OAAO,aAAa,iBAAiB,MAAM,SAAS;EAC7E,MAAM,KAAK,KAAK,OAAO,kBAAkB;GACvC,UAAU,MAAM;GAChB;GACA,oBAAoB,KAAK,UAAU,YAAY,aAAa,cAAc,uBAAuB,KAAA;EACnG,CAAC;CACH;;;;;;;;;;CAWA,QACE,UACA,UAAkC,CAAC,GACP;EAC5B,MAAM,MAAM,KAAK,SAAS,WAAW,KAAK,WAAW,UAAU,OAAO,CAAC;EACvE,KAAK,WAAW,IAAI,WACZ,KAAA,SACA,KAAA,CACR;EACA,OAAO;CACT;;;;;;CAOA,MAAM,KAAQ,UAAgC,OAAwC;EACpF,IAAI,KAAK,UAAU,MAAM,IAAI,MAAM,OAAO,KAAK,MAAM,oBAAoB;EACzE,IAAI,SAAS,UAAU,KAAK,OAC1B,MAAM,IAAI,MACR,aAAa,SAAS,KAAK,mBAAmB,SAAS,MAAM,yCAAyC,KAAK,MAAM,QACnH;EACF,IAAI,KAAK,MAAM,IAAI,SAAS,IAAI,GAC9B,MAAM,IAAI,MAAM,aAAa,SAAS,KAAK,gCAAgC,KAAK,MAAM,QAAQ;EAChG,MAAM,UAAmB;GAAE,MAAM,SAAS;GAAM;EAAM;EACtD,KAAK,MAAM,IAAI,SAAS,MAAM,OAAO;EACrC,MAAM,WAAW,YAA2B;GAC1C,IAAI,KAAK,MAAM,IAAI,SAAS,IAAI,MAAM,SAAS;GAC/C,KAAK,MAAM,OAAO,SAAS,IAAI;GAC/B,MAAM,QAAQ,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,UAAU,MAAM,SAAS,OAAO,CAAC,CAAC;EAC/E;EACA,IAAI;GACF,MAAM,QAAQ,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,UAAU,MAAM,OAAO,OAAO,CAAC,CAAC;EAC7E,SAAS,OAAO;GACd,MAAM,SAAS;GACf,MAAM;EACR;EACA,OAAO;CACT;CAEA,QAAW,UAA+C;EACxD,IAAI,MAAM,SAAS,KAAK,IAAI,MAAM,KAAK,KAAK,GAC1C,MAAM,IAAI,MAAM,KAAK,KAAK,MAAM,wBAAwB,SAAS,MAAM,aAAa,SAAS,KAAK,GAAG;EACvG,KAAK,MAAM,SAAS,KAAK,SAAS,GAChC,IAAI,MAAM,UAAU,SAAS,OAAO,OAAO,MAAM,MAAM,IAAI,SAAS,IAAI,CAAC,EAAE;CAG/E;;;;;;CAOA,WAAW,UAA4C;EACrD,KAAK,MAAM,SAAS,KAAK,SAAS,GAAG;GACnC,MAAM,UAAU,MAAM,QAAQ,aAAa,QAAQ;GACnD,IAAI,YAAY,KAAA,GAAW,OAAO;EACpC;CAEF;CAEA,KAA6B,MAAS,GAAG,MAAmC;EAC1E,QAAQ,CAAC,GAAG,KAAK,SAAS,CAAC,GAAG,MAAM,IAAI;CAC1C;CAEA,UAAkC,MAAS,GAAG,MAAmC;EAC/E,QAAQ,CAAC,GAAG,KAAK,QAAQ,CAAC,GAAG,MAAM,IAAI;CACzC;;;;;CAMA,UAAyB;EACvB,KAAK,eAAe,YAAY;GAC9B,MAAM,QAAQ,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,KAAK,UAAU,MAAM,QAAQ,CAAC,CAAC;GACpE,KAAK,QAAQ,SAAS,OAAO,IAAI;GACjC,KAAK,QAAQ,MAAM;GACnB,KAAK,SAAS,KAAA;GACd,MAAM,WAAW,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,QAAQ;GACtD,KAAK,UAAU,MAAM;GACrB,KAAK,MAAM,EAAE,WAAW,UAAU,MAAM,MAAM,QAAQ;GACtD,MAAM,WAAW,KAAK,KAAK,MAAM,QAAQ;GACzC,IAAI,KAAK,uBAAuB,KAAA,GAAW,OAAO,MAAM;GAExD,MAAM,YAAY,UAAU,KAAK,oBAAoB,GAAG,KAAK,MAAM,eAAe,CAAC,CAAC,OAAO,UACzF,KAAK,OAAO,SAAS,KAAK,CAAC,CAC7B;EACF,EAAA,CAAG,CAAC,CAAC,QAAQ,YAAY;GAEvB,IAAI,KAAK,WAAW,KAAA,GAAW,MAAM,KAAK,cAAc;EAC1D,CAAC;EACD,OAAO,KAAK;CACd;;;;;CAMA,MAAc,OAA4C;EACxD,MAAM,MAAM,KAAK,UAAU;EAC3B,MAAM,gBAA6B;GACjC,IAAI,QAAQ,IAAI,OAAO,IAAI,KAAK;GAChC,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ,IAAI,YAAY;IACxB,IAAI,OAAO,IAAI,OAAO,KAAK;GAC7B;GACA,OAAO;EACT;EAEA,MAAM,UAAU,UAA6B;GAC3C,IAAI,MAAM,QAAQ,IAAI,OAAO,IAAI,KAAK,MAAM,OAAO,IAAI,OAAO,OAAO,KAAK;EAC5E;EACA,OAAO;GACL,SAAS,OACP,KACA,QACA,YACsC;IACtC,IAAI,KAAK,UAAU,MAAM,IAAI,MAAM,OAAO,KAAK,MAAM,oBAAoB;IACzE,MAAM,QAAQ,QAAQ;IACtB,MAAM,OAAO,aAAa,GAAG;IAC7B,IAAI,SAAS,MAAM,OAAO,IAAI,IAAI;IAClC,IAAI,WAAW,KAAA,GAAW;KACxB,MAAM,UAAuB;MAC3B,OAAO,QAAQ,QAAQ,CAAC,CAAC,KAAK,MAAM;MACpC,SAAS;MACT,cAAc,YAAY,KAAA,IAAY,KAAA,KAAa,UAAU,QAAQ,KAAU;KACjF;KACA,MAAM,OAAO,IAAI,MAAM,OAAO;KAE9B,QAAQ,MAAM,YAAY;MACxB,IAAI,MAAM,OAAO,IAAI,IAAI,MAAM,SAAS,MAAM,OAAO,OAAO,IAAI;MAChE,OAAO,KAAK;KACd,CAAC;KACD,SAAS;IACX;IACA,MAAM,OAAO;IACb,KAAK,WAAW;IAChB,IAAI;IACJ,IAAI;KACF,QAAQ,MAAM,KAAK;IACrB,SAAS,OAAO;KACd,KAAK,WAAW;KAChB,MAAM;IACR;IACA,IAAI,WAAW;IACf,OAAO;KACE;KACP,SAAS,YAAY;MACnB,IAAI,UAAU;MACd,WAAW;MACX,KAAK,WAAW;MAChB,IAAI,KAAK,UAAU,KAAK,MAAM,OAAO,IAAI,IAAI,MAAM,MAAM;MACzD,MAAM,OAAO,OAAO,IAAI;MACxB,OAAO,KAAK;MACZ,MAAM,KAAK,eAAe,KAAK;KACjC;IACF;GACF;GACA,MAAM,UAAU;IACd,IAAI,KAAK,UAAU,MAAM,IAAI,MAAM,OAAO,KAAK,MAAM,oBAAoB;IACzE,MAAM,QAAQ,QAAQ;IACtB,MAAM,QAAQ,IAAI,MAAM,KAAK;IAC7B,aAAa;KACX,IAAI,MAAM,QAAQ,IAAI,IAAI,MAAM,OAAO,MAAM,QAAQ,OAAO,IAAI;KAChE,OAAO,KAAK;IACd;GACF;GACA,QAAQ,SAAS;IACf,MAAM,QAAQ,IAAI,OAAO,IAAI,KAAK;IAClC,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;IACjC,MAAM,OAAiB,CAAC;IACxB,KAAK,MAAM,SAAS,KAAK,QAAQ,SAAS,KAAK,CAAC,GAC9C,IAAI,MAAM,MAAM,KAAK,KAAK,MAAM,IAAI,KAAK,MAAM,QAAQ,IAAI,KAAK,GAC9D,KAAK,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAW;IAEhD,OAAO;GACT;EACF;CACF;CAEA,MAAc,WACZ,UACA,SAC4B;EAC5B,IAAI,KAAK,UAAU,MAAM,IAAI,MAAM,OAAO,KAAK,MAAM,oBAAoB;EACzE,MAAM,SAAS,QAAQ,mBAAyB,CAAC;EACjD,KAAK,SAAS;EACd,IACE,SAAS,MAAM,cAAc,cAAc,aAAa,UAAU,aAAa,IAAI,KACnF,CAAC,mBAAmB,KAAK,IAAI,GAE7B,MAAM,IAAI,MAAM,0DAA0D;EAC5E,KAAK,MAAM,aAAa,UAAU;GAChC,MAAM,SAAS,iBAAiB,YAAY,UAAU,SAAS;GAC/D,MAAM,cAAc,iBAAiB,YAAY,UAAU,cAAc,KAAA;GACzE,MAAM,OAAO,aAAa,eAAe,OAAO;GAChD,MAAM,WAAW,KAAK,UAAU,IAAI,IAAI;GACxC,IAAI,UAAU,WAAW,QAAQ;GACjC,IAAI,aAAa,KAAA,GAAW;IAC1B,KAAK,UAAU,OAAO,IAAI;IAC1B,MAAM,SAAS,MAAM,QAAQ;GAC/B;GACA,IAAI;IACF,MAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,QAAQ,aAAa,OAAO;IACjE,KAAK,UAAU,IAAI,MAAM;KAAE;KAAQ,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;KAAI;IAAM,CAAC;GACnG,SAAS,OAAO;IACd,MAAM,UAAU,mBAAmB,KAAK,qBAAqB,SAAS,KAAK,EAAE;IAC7E,IAAI,aAAa,KAAA,GAAW;KAC1B,MAAM,KAAK,QAAQ,MAAM,UAAU,SAAS,OAAO;KACnD;IACF;IACA,IACE,aAAa,aAAa,QAC1B,EAAE,uBAAuB,aAAa,UAAU,sBAAsB,QAEtE,MAAM,IAAI,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;IAC3C,OAAO,OAAO;GAChB;EACF;EAEA,KAAK,MAAM,aAAa,UAAU;GAChC,MAAM,SAAS,iBAAiB,YAAY,UAAU,SAAS;GAC/D,MAAM,QAAQ,iBAAiB,YAAY,UAAU,YAAY,cAAc,KAAA,MAAc,OAAO;GACpG,IAAI,KAAK,UAAU,IAAI,IAAI,CAAC,EAAE,WAAW,QAAQ;GACjD,KAAK,MAAM,YAAY,OAAO,UAAU,KAAK,UAAU,CAAC,GAGtD,IADgB,MAAM,SAAS,KAAK,IAAI,MAAM,KAAK,KAAK,KAAK,KAAK,QAAQ,QAAQ,MAAM,KAAA,GAEtF,OACE,mBAAmB,KAAK,wBAAwB,SAAS,KAAK,cAAc,SAAS,MAAM,mBAC7F;EAEN;EACA,OAAO,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC;CAClC;;CAGA,MAAc,QACZ,MACA,UACA,SACA,SACe;EACf,IAAI;GACF,MAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,SAAS,QAAQ,SAAS,aAAa,OAAO;GACnF,KAAK,UAAU,IAAI,MAAM;IAAE,GAAG;IAAU;GAAM,CAAC;GAC/C,KAAK,OAAO,GAAG,QAAQ,kCAAkC;EAC3D,SAAS,eAAe;GACtB,KAAK,OAAO,GAAG,QAAQ,8CAA8C,SAAS,aAAa,EAAE,GAAG;EAClG;CACF;;CAGA,MAAc,MACZ,MACA,QACA,aACA,SACgB;EAChB,MAAM,YAAY,KAAK,IAAI;EAC3B,MAAM,YAAY,QAAQ,oBAAA;EAC1B,MAAM,WAAW,YACf,QAAQ,mBAAmB;GAAE,aAAa;GAAM,YAAY,KAAK,IAAI,IAAI;GAAW;EAAQ,CAAC;EAC/F,IAAI;EACJ,IAAI,eAAe;EACnB,IAAI;GAEF,SADc,cAAc,KAAK,KAAK,OAAO,GAAG,oBAAoB,YAAY,CAAC,IAAI,KAAK,KAAA,CAC5E,OAAO,MAAM;GAC3B,MAAM,UAAU,MAAM,MAAM;GAC5B,MAAM,oBAA0B;IAC9B,eAAe;GACjB;GACA,QAAQ,KAAK,aAAa,WAAW;GACrC,MAAM,YAAY,SAAS,WAAW,mBAAmB,KAAK,QAAQ;GACtE,QAAQ,WAAW;GACnB,OAAO;EACT,SAAS,OAAO;GACd,QAAQ,SAAS,CAAC,eAAe,cAAc,QAAQ;GACvD,IAAI,SAAS,CAAC,cAAc;IAC1B,KAAK,qBAAqB,KAAK,IAAI,KAAK,sBAAsB,GAAG,SAAS;IAC1E,MACG,QAAQ,CAAC,CACT,OAAO,iBACN,KAAK,OAAO,6BAA6B,KAAK,qBAAqB,SAAS,YAAY,EAAE,EAAE,CAC9F;GACJ,OAAO,MAAM,OAAO,QAAQ;GAC5B,MAAM;EACR;CACF;CAEA,UAA6B;EAC3B,OAAO,CAAC,GAAI,KAAK,QAAQ,QAAQ,KAAK,CAAC,GAAI,GAAG,KAAK,MAAM,OAAO,CAAC;CACnE;;CAGA,YAAmC;EACjC,OAAO,KAAK,WAAW,KAAA,IAAY,OAAO,KAAK,OAAO,UAAU;CAClE;;CAGA,MAAc,gBAA+B;EAC3C,MAAM,SAAS,CAAC,GAAG,KAAK,OAAO,OAAO,CAAC,CAAC,CAAC,SAAS,UAAU,CAAC,GAAG,MAAM,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ;EAChG,KAAK,OAAO,MAAM;EAClB,KAAK,MAAM,UAAU,QACnB,IAAI;GACF,MAAM,OAAO,eAAe,MAAM,OAAO,KAAK;EAChD,SAAS,OAAO;GACd,KAAK,OAAO,0CAA0C,SAAS,KAAK,EAAE,EAAE;EAC1E;CAEJ;CAEA,CAAS,WAAqC;EAC5C,MAAM;EACN,IAAI,KAAK,WAAW,KAAA,GAAW,OAAO,KAAK,OAAO,SAAS;CAC7D;CAEA,CAAS,UAAoC;EAC3C,IAAI,KAAK,UAAU;EACnB,MAAM;EACN,KAAK,MAAM,SAAS,KAAK,UAAU,OAAO,MAAM,QAAQ;CAC1D;;;;;CAMA,MAAc,OAAO,SAAiC;EACpD,IAAI,KAAK,UAAU;EACnB,IAAI,QAAQ,KAAK,QAAQ,IAAI,OAAO;EACpC,IAAI,UAAU,KAAA,GAAW;GACvB,QAAQ,KAAK,KAAK,OAAO,iBAAiB,OAAO;GACjD,KAAK,QAAQ,IAAI,SAAS,KAAK;EACjC;EACA,MAAM,MAAM,MAAM;CACpB;CAEA,MAAc,SAAS,SAAiC;EACtD,MAAM,QAAQ,KAAK,QAAQ,IAAI,OAAO;EACtC,KAAK,QAAQ,OAAO,OAAO;EAE3B,IAAI,CAAC,KAAK,UAAU,MAAM,OAAO,QAAQ;CAC3C;AACF;;AAGA,SAAS,QAAQ,QAAkC,MAAoB,MAAgC;CACrG,MAAM,SAAoB,CAAC;CAC3B,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,UAAU;EACpB,IAAI;GACF,MAAM,KAAK,OAAO,KAAK,MAAM,GAAG,IAAI;EACtC,SAAS,OAAO;GACd,OAAO,KAAK,KAAK;EACnB;CACF;CACA,IAAI,OAAO,SAAS,GAAG,MAAM,IAAI,eAAe,QAAQ,GAAG,OAAO,OAAO,yBAAyB,KAAK,UAAU;AACnH"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_core = require("../piSettings/core.cjs");
|
|
3
|
+
const require_docs = require("../durableSession/docs.cjs");
|
|
4
|
+
const require_sessionDirectory = require("./sessionDirectory.cjs");
|
|
5
|
+
let node_crypto = require("node:crypto");
|
|
6
|
+
let node_path = require("node:path");
|
|
7
|
+
node_path = require_runtime.__toESM(node_path, 1);
|
|
8
|
+
let _earendil_works_pi_durable = require("@earendil-works/pi-durable");
|
|
9
|
+
let dist_interop_chordContext_mjs = require("../../../../dist/interop/chordContext.mjs");
|
|
10
|
+
//#region src/services/sessionFiles/index.ts
|
|
11
|
+
const PO_SESSION_TEMP_DIR = "PO_SESSION_TEMP_DIR";
|
|
12
|
+
const PO_SESSION_FILES_SERVICE = "po/session-files";
|
|
13
|
+
/** A host-owned path, never a client-selected root or an inherited parent session path. */
|
|
14
|
+
function sessionTempDirectory(options) {
|
|
15
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(options.sessionId)) throw new Error("Invalid temporary-file session identity.");
|
|
16
|
+
if (!node_path.default.isAbsolute(options.temporaryDirectory)) throw new Error("The host temporary directory must be absolute.");
|
|
17
|
+
const namespace = (0, node_crypto.createHash)("sha256").update(options.homeDirectory).update("\0").update(require_core.poHomeDirectory(options.homeDirectory, options.environment)).digest("hex").slice(0, 24);
|
|
18
|
+
return node_path.default.join(options.temporaryDirectory, `po-${namespace}`, "sessions", options.sessionId);
|
|
19
|
+
}
|
|
20
|
+
/** Both the execution host and package APIs call this with their admitted session identity. */
|
|
21
|
+
async function prepareSessionEnvironment(input) {
|
|
22
|
+
const environment = { ...input.environment };
|
|
23
|
+
delete environment[PO_SESSION_TEMP_DIR];
|
|
24
|
+
if (input.platform.temporaryDirectory === void 0) return Object.freeze(environment);
|
|
25
|
+
const directory = sessionTempDirectory({
|
|
26
|
+
sessionId: input.sessionId,
|
|
27
|
+
homeDirectory: input.platform.homeDirectory,
|
|
28
|
+
environment,
|
|
29
|
+
temporaryDirectory: input.platform.temporaryDirectory
|
|
30
|
+
});
|
|
31
|
+
const preparedDirectory = await require_sessionDirectory.prepareSessionDirectory(input.platform, directory);
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
...environment,
|
|
34
|
+
[PO_SESSION_TEMP_DIR]: preparedDirectory
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function checkedDirectories(value, sessionId) {
|
|
38
|
+
if (!value || typeof value !== "object") throw new Error("Invalid saved session output directories.");
|
|
39
|
+
const files = value;
|
|
40
|
+
const absolute = (directory) => typeof directory === "string" && !directory.includes("\0") && node_path.default.isAbsolute(directory) && node_path.default.resolve(directory) === directory;
|
|
41
|
+
if (!absolute(files.agentDirectory)) throw new Error("Invalid saved session agent directory.");
|
|
42
|
+
if (files.temporaryDirectory !== void 0) {
|
|
43
|
+
const directory = files.temporaryDirectory;
|
|
44
|
+
if (!absolute(directory) || node_path.default.basename(directory) !== sessionId || node_path.default.basename(node_path.default.dirname(directory)) !== "sessions" || !/^po-[a-f0-9]{24}$/u.test(node_path.default.basename(node_path.default.dirname(node_path.default.dirname(directory))))) throw new Error("Invalid saved session temporary directory.");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
agentDirectory: files.agentDirectory,
|
|
48
|
+
...files.temporaryDirectory === void 0 ? {} : { temporaryDirectory: files.temporaryDirectory }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Narrow server-written output metadata, without exposing a session's environment or accepting request roots. */
|
|
52
|
+
async function readSessionFileDirectories(sessions, sessionId) {
|
|
53
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new Error("Invalid session output identity.");
|
|
54
|
+
const storage = await sessions.openReadOnly(sessionId, dist_interop_chordContext_mjs.BACKGROUND_CONTEXT);
|
|
55
|
+
const session = (0, _earendil_works_pi_durable.createSession)(storage);
|
|
56
|
+
try {
|
|
57
|
+
if ((await session.snapshot(require_docs.SessionIdentityDoc, dist_interop_chordContext_mjs.BACKGROUND_CONTEXT))?.id !== sessionId) throw new Error("Saved output does not belong to this session.");
|
|
58
|
+
const metadata = await session.snapshot(require_docs.SessionMetadataDoc, dist_interop_chordContext_mjs.BACKGROUND_CONTEXT);
|
|
59
|
+
if (!metadata?.execution) return void 0;
|
|
60
|
+
const execution = JSON.parse(metadata.execution);
|
|
61
|
+
if (!execution || typeof execution !== "object" || !("files" in execution) || execution.files === void 0) return void 0;
|
|
62
|
+
return checkedDirectories(execution.files, sessionId);
|
|
63
|
+
} finally {
|
|
64
|
+
await session.close(dist_interop_chordContext_mjs.BACKGROUND_CONTEXT);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Explicit session removal only. Ordinary close, release and host shutdown preserve output. */
|
|
68
|
+
async function removeSessionFiles(input) {
|
|
69
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(input.sessionId) || node_path.default.basename(input.artifactDirectory) !== input.sessionId) throw new Error("Invalid session output cleanup identity.");
|
|
70
|
+
const directories = input.directories === void 0 ? void 0 : checkedDirectories(input.directories, input.sessionId);
|
|
71
|
+
const removals = [require_sessionDirectory.removeSessionOutputDirectory(input.platform, input.artifactDirectory)];
|
|
72
|
+
const temporaryDirectory = directories?.temporaryDirectory ?? (input.platform.temporaryDirectory === void 0 ? void 0 : sessionTempDirectory({
|
|
73
|
+
sessionId: input.sessionId,
|
|
74
|
+
homeDirectory: input.platform.homeDirectory,
|
|
75
|
+
environment: input.environment,
|
|
76
|
+
temporaryDirectory: input.platform.temporaryDirectory
|
|
77
|
+
}));
|
|
78
|
+
if (temporaryDirectory !== void 0) {
|
|
79
|
+
const originalBase = node_path.default.dirname(node_path.default.dirname(node_path.default.dirname(temporaryDirectory)));
|
|
80
|
+
removals.push(require_sessionDirectory.removeSessionDirectory({
|
|
81
|
+
...input.platform,
|
|
82
|
+
temporaryDirectory: originalBase
|
|
83
|
+
}, temporaryDirectory));
|
|
84
|
+
}
|
|
85
|
+
const failures = (await Promise.allSettled(removals)).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
86
|
+
if (failures.length > 0) throw new AggregateError(failures, `Session '${input.sessionId}' output cleanup failed`);
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
exports.PO_SESSION_FILES_SERVICE = PO_SESSION_FILES_SERVICE;
|
|
90
|
+
exports.PO_SESSION_TEMP_DIR = PO_SESSION_TEMP_DIR;
|
|
91
|
+
exports.prepareSessionEnvironment = prepareSessionEnvironment;
|
|
92
|
+
exports.readSessionFileDirectories = readSessionFileDirectories;
|
|
93
|
+
exports.removeSessionFiles = removeSessionFiles;
|
|
94
|
+
exports.sessionTempDirectory = sessionTempDirectory;
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["path","createHash","poHomeDirectory","prepareSessionDirectory","BACKGROUND_CONTEXT","createSession","SessionIdentityDoc","SessionMetadataDoc","removeSessionOutputDirectory","removeSessionDirectory"],"sources":["../../../../src/services/sessionFiles/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport path from 'node:path';\n\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { createSession } from '@earendil-works/pi-durable';\n\nimport type { PoPlatform, PoSessionStorageService } from '../../schemas/platform';\nimport { SessionIdentityDoc, SessionMetadataDoc } from '../durableSession/docs';\nimport { poHomeDirectory } from '../piSettings/core';\nimport { prepareSessionDirectory, removeSessionDirectory, removeSessionOutputDirectory } from './sessionDirectory';\n\nexport const PO_SESSION_TEMP_DIR = 'PO_SESSION_TEMP_DIR';\nexport const PO_SESSION_FILES_SERVICE = 'po/session-files';\n\nexport interface PoSessionFiles {\n readonly temporaryDirectory: string;\n}\n\nexport interface SessionTempDirectoryOptions {\n readonly sessionId: string;\n readonly homeDirectory: string;\n readonly environment: Readonly<Record<string, string | undefined>>;\n readonly temporaryDirectory: string;\n}\n\n/** A host-owned path, never a client-selected root or an inherited parent session path. */\nexport function sessionTempDirectory(options: SessionTempDirectoryOptions): string {\n if (!/^[a-zA-Z0-9_-]+$/u.test(options.sessionId)) throw new Error('Invalid temporary-file session identity.');\n if (!path.isAbsolute(options.temporaryDirectory)) throw new Error('The host temporary directory must be absolute.');\n const namespace = createHash('sha256')\n .update(options.homeDirectory)\n .update('\\0')\n .update(poHomeDirectory(options.homeDirectory, options.environment))\n .digest('hex')\n .slice(0, 24);\n return path.join(options.temporaryDirectory, `po-${namespace}`, 'sessions', options.sessionId);\n}\n\n/** Both the execution host and package APIs call this with their admitted session identity. */\nexport async function prepareSessionEnvironment(input: {\n readonly platform: PoPlatform;\n readonly sessionId: string;\n readonly environment: Readonly<Record<string, string | undefined>>;\n}): Promise<Readonly<Record<string, string | undefined>>> {\n const environment = { ...input.environment };\n // A child must never inherit its parent's browsable directory. Older embedders may lack this capability.\n delete environment[PO_SESSION_TEMP_DIR];\n if (input.platform.temporaryDirectory === undefined) return Object.freeze(environment);\n const directory = sessionTempDirectory({\n sessionId: input.sessionId,\n homeDirectory: input.platform.homeDirectory,\n environment,\n temporaryDirectory: input.platform.temporaryDirectory,\n });\n // ponytail: resumption uses the same host temp base; pinning across host temp changes needs durable session metadata.\n const preparedDirectory = await prepareSessionDirectory(input.platform, directory);\n return Object.freeze({ ...environment, [PO_SESSION_TEMP_DIR]: preparedDirectory });\n}\n\nexport interface PoSessionFileDirectories {\n readonly agentDirectory: string;\n readonly temporaryDirectory?: string;\n}\n\nfunction checkedDirectories(value: unknown, sessionId: string): PoSessionFileDirectories {\n if (!value || typeof value !== 'object') throw new Error('Invalid saved session output directories.');\n const files = value as Record<string, unknown>;\n const absolute = (directory: unknown): directory is string =>\n typeof directory === 'string' &&\n !directory.includes('\\0') &&\n path.isAbsolute(directory) &&\n path.resolve(directory) === directory;\n if (!absolute(files.agentDirectory)) throw new Error('Invalid saved session agent directory.');\n if (files.temporaryDirectory !== undefined) {\n const directory = files.temporaryDirectory;\n if (\n !absolute(directory) ||\n path.basename(directory) !== sessionId ||\n path.basename(path.dirname(directory)) !== 'sessions' ||\n !/^po-[a-f0-9]{24}$/u.test(path.basename(path.dirname(path.dirname(directory))))\n )\n throw new Error('Invalid saved session temporary directory.');\n }\n return {\n agentDirectory: files.agentDirectory,\n ...(files.temporaryDirectory === undefined ? {} : { temporaryDirectory: files.temporaryDirectory as string }),\n };\n}\n\n/** Narrow server-written output metadata, without exposing a session's environment or accepting request roots. */\nexport async function readSessionFileDirectories(\n sessions: PoSessionStorageService,\n sessionId: string,\n): Promise<PoSessionFileDirectories | undefined> {\n if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new Error('Invalid session output identity.');\n const storage = await sessions.openReadOnly(sessionId, BACKGROUND_CONTEXT);\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, BACKGROUND_CONTEXT);\n if (identity?.id !== sessionId) throw new Error('Saved output does not belong to this session.');\n const metadata = await session.snapshot(SessionMetadataDoc, BACKGROUND_CONTEXT);\n if (!metadata?.execution) return undefined;\n const execution: unknown = JSON.parse(metadata.execution);\n if (!execution || typeof execution !== 'object' || !('files' in execution) || execution.files === undefined)\n return undefined;\n return checkedDirectories(execution.files, sessionId);\n } finally {\n await session.close(BACKGROUND_CONTEXT);\n }\n}\n\n/** Explicit session removal only. Ordinary close, release and host shutdown preserve output. */\nexport async function removeSessionFiles(input: {\n readonly platform: PoPlatform;\n readonly sessionId: string;\n readonly environment: Readonly<Record<string, string | undefined>>;\n /** Exact session artifact directory selected by the composition root, never by a request. */\n readonly artifactDirectory: string;\n /** Previously stored roots, read by the host from the session's own durable metadata. */\n readonly directories?: PoSessionFileDirectories;\n}): Promise<void> {\n if (!/^[a-zA-Z0-9_-]+$/u.test(input.sessionId) || path.basename(input.artifactDirectory) !== input.sessionId)\n throw new Error('Invalid session output cleanup identity.');\n const directories =\n input.directories === undefined ? undefined : checkedDirectories(input.directories, input.sessionId);\n const removals = [removeSessionOutputDirectory(input.platform, input.artifactDirectory)];\n const temporaryDirectory =\n directories?.temporaryDirectory ??\n (input.platform.temporaryDirectory === undefined\n ? undefined\n : sessionTempDirectory({\n sessionId: input.sessionId,\n homeDirectory: input.platform.homeDirectory,\n environment: input.environment,\n temporaryDirectory: input.platform.temporaryDirectory,\n }));\n if (temporaryDirectory !== undefined) {\n const originalBase = path.dirname(path.dirname(path.dirname(temporaryDirectory)));\n removals.push(removeSessionDirectory({ ...input.platform, temporaryDirectory: originalBase }, temporaryDirectory));\n }\n const results = await Promise.allSettled(removals);\n const failures = results\n .filter((result): result is PromiseRejectedResult => result.status === 'rejected')\n .map((result) => result.reason);\n if (failures.length > 0) throw new AggregateError(failures, `Session '${input.sessionId}' output cleanup failed`);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PoPlatform, PoSessionStorageService } from "../../schemas/platform.mjs";
|
|
2
|
+
//#region src/services/sessionFiles/index.d.ts
|
|
3
|
+
export declare const PO_SESSION_TEMP_DIR = "PO_SESSION_TEMP_DIR";
|
|
4
|
+
export declare const PO_SESSION_FILES_SERVICE = "po/session-files";
|
|
5
|
+
export interface PoSessionFiles {
|
|
6
|
+
readonly temporaryDirectory: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SessionTempDirectoryOptions {
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly homeDirectory: string;
|
|
11
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
12
|
+
readonly temporaryDirectory: string;
|
|
13
|
+
}
|
|
14
|
+
/** A host-owned path, never a client-selected root or an inherited parent session path. */
|
|
15
|
+
export declare function sessionTempDirectory(options: SessionTempDirectoryOptions): string;
|
|
16
|
+
export interface PoSessionFileDirectories {
|
|
17
|
+
readonly agentDirectory: string;
|
|
18
|
+
readonly temporaryDirectory?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Narrow server-written output metadata, without exposing a session's environment or accepting request roots. */
|
|
21
|
+
export declare function readSessionFileDirectories(sessions: PoSessionStorageService, sessionId: string): Promise<PoSessionFileDirectories | undefined>;
|
|
22
|
+
/** Explicit session removal only. Ordinary close, release and host shutdown preserve output. */
|
|
23
|
+
export declare function removeSessionFiles(input: {
|
|
24
|
+
readonly platform: PoPlatform;
|
|
25
|
+
readonly sessionId: string;
|
|
26
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
27
|
+
/** Exact session artifact directory selected by the composition root, never by a request. */
|
|
28
|
+
readonly artifactDirectory: string;
|
|
29
|
+
/** Previously stored roots, read by the host from the session's own durable metadata. */
|
|
30
|
+
readonly directories?: PoSessionFileDirectories;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
//#endregion
|
|
33
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/sessionFiles/index.ts"],"mappings":";;qBAWa;qBACA;iBAEI;WACN;;iBAGM;WACN;WACA;WACA,aAAa,SAAS;WACtB;;;wBAIK,qBAAqB,SAAS;iBAiC7B;WACN;WACA;;;wBA6BW,2BACpB,UAAU,yBACV,oBACC,QAAQ;;wBAmBW,mBAAmB;WAC9B,UAAU;WACV;WACA,aAAa,SAAS;;WAEtB;;WAEA,cAAc;IACrB"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { poHomeDirectory } from "../piSettings/core.mjs";
|
|
2
|
+
import { SessionIdentityDoc, SessionMetadataDoc } from "../durableSession/docs.mjs";
|
|
3
|
+
import { prepareSessionDirectory, removeSessionDirectory, removeSessionOutputDirectory } from "./sessionDirectory.mjs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createSession } from "@earendil-works/pi-durable";
|
|
7
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
8
|
+
//#region src/services/sessionFiles/index.ts
|
|
9
|
+
const PO_SESSION_TEMP_DIR = "PO_SESSION_TEMP_DIR";
|
|
10
|
+
const PO_SESSION_FILES_SERVICE = "po/session-files";
|
|
11
|
+
/** A host-owned path, never a client-selected root or an inherited parent session path. */
|
|
12
|
+
function sessionTempDirectory(options) {
|
|
13
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(options.sessionId)) throw new Error("Invalid temporary-file session identity.");
|
|
14
|
+
if (!path.isAbsolute(options.temporaryDirectory)) throw new Error("The host temporary directory must be absolute.");
|
|
15
|
+
const namespace = createHash("sha256").update(options.homeDirectory).update("\0").update(poHomeDirectory(options.homeDirectory, options.environment)).digest("hex").slice(0, 24);
|
|
16
|
+
return path.join(options.temporaryDirectory, `po-${namespace}`, "sessions", options.sessionId);
|
|
17
|
+
}
|
|
18
|
+
/** Both the execution host and package APIs call this with their admitted session identity. */
|
|
19
|
+
async function prepareSessionEnvironment(input) {
|
|
20
|
+
const environment = { ...input.environment };
|
|
21
|
+
delete environment[PO_SESSION_TEMP_DIR];
|
|
22
|
+
if (input.platform.temporaryDirectory === void 0) return Object.freeze(environment);
|
|
23
|
+
const directory = sessionTempDirectory({
|
|
24
|
+
sessionId: input.sessionId,
|
|
25
|
+
homeDirectory: input.platform.homeDirectory,
|
|
26
|
+
environment,
|
|
27
|
+
temporaryDirectory: input.platform.temporaryDirectory
|
|
28
|
+
});
|
|
29
|
+
const preparedDirectory = await prepareSessionDirectory(input.platform, directory);
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
...environment,
|
|
32
|
+
[PO_SESSION_TEMP_DIR]: preparedDirectory
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function checkedDirectories(value, sessionId) {
|
|
36
|
+
if (!value || typeof value !== "object") throw new Error("Invalid saved session output directories.");
|
|
37
|
+
const files = value;
|
|
38
|
+
const absolute = (directory) => typeof directory === "string" && !directory.includes("\0") && path.isAbsolute(directory) && path.resolve(directory) === directory;
|
|
39
|
+
if (!absolute(files.agentDirectory)) throw new Error("Invalid saved session agent directory.");
|
|
40
|
+
if (files.temporaryDirectory !== void 0) {
|
|
41
|
+
const directory = files.temporaryDirectory;
|
|
42
|
+
if (!absolute(directory) || path.basename(directory) !== sessionId || path.basename(path.dirname(directory)) !== "sessions" || !/^po-[a-f0-9]{24}$/u.test(path.basename(path.dirname(path.dirname(directory))))) throw new Error("Invalid saved session temporary directory.");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
agentDirectory: files.agentDirectory,
|
|
46
|
+
...files.temporaryDirectory === void 0 ? {} : { temporaryDirectory: files.temporaryDirectory }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Narrow server-written output metadata, without exposing a session's environment or accepting request roots. */
|
|
50
|
+
async function readSessionFileDirectories(sessions, sessionId) {
|
|
51
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new Error("Invalid session output identity.");
|
|
52
|
+
const storage = await sessions.openReadOnly(sessionId, BACKGROUND_CONTEXT);
|
|
53
|
+
const session = createSession(storage);
|
|
54
|
+
try {
|
|
55
|
+
if ((await session.snapshot(SessionIdentityDoc, BACKGROUND_CONTEXT))?.id !== sessionId) throw new Error("Saved output does not belong to this session.");
|
|
56
|
+
const metadata = await session.snapshot(SessionMetadataDoc, BACKGROUND_CONTEXT);
|
|
57
|
+
if (!metadata?.execution) return void 0;
|
|
58
|
+
const execution = JSON.parse(metadata.execution);
|
|
59
|
+
if (!execution || typeof execution !== "object" || !("files" in execution) || execution.files === void 0) return void 0;
|
|
60
|
+
return checkedDirectories(execution.files, sessionId);
|
|
61
|
+
} finally {
|
|
62
|
+
await session.close(BACKGROUND_CONTEXT);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Explicit session removal only. Ordinary close, release and host shutdown preserve output. */
|
|
66
|
+
async function removeSessionFiles(input) {
|
|
67
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(input.sessionId) || path.basename(input.artifactDirectory) !== input.sessionId) throw new Error("Invalid session output cleanup identity.");
|
|
68
|
+
const directories = input.directories === void 0 ? void 0 : checkedDirectories(input.directories, input.sessionId);
|
|
69
|
+
const removals = [removeSessionOutputDirectory(input.platform, input.artifactDirectory)];
|
|
70
|
+
const temporaryDirectory = directories?.temporaryDirectory ?? (input.platform.temporaryDirectory === void 0 ? void 0 : sessionTempDirectory({
|
|
71
|
+
sessionId: input.sessionId,
|
|
72
|
+
homeDirectory: input.platform.homeDirectory,
|
|
73
|
+
environment: input.environment,
|
|
74
|
+
temporaryDirectory: input.platform.temporaryDirectory
|
|
75
|
+
}));
|
|
76
|
+
if (temporaryDirectory !== void 0) {
|
|
77
|
+
const originalBase = path.dirname(path.dirname(path.dirname(temporaryDirectory)));
|
|
78
|
+
removals.push(removeSessionDirectory({
|
|
79
|
+
...input.platform,
|
|
80
|
+
temporaryDirectory: originalBase
|
|
81
|
+
}, temporaryDirectory));
|
|
82
|
+
}
|
|
83
|
+
const failures = (await Promise.allSettled(removals)).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
84
|
+
if (failures.length > 0) throw new AggregateError(failures, `Session '${input.sessionId}' output cleanup failed`);
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { PO_SESSION_FILES_SERVICE, PO_SESSION_TEMP_DIR, prepareSessionEnvironment, readSessionFileDirectories, removeSessionFiles, sessionTempDirectory };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/sessionFiles/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport path from 'node:path';\n\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { createSession } from '@earendil-works/pi-durable';\n\nimport type { PoPlatform, PoSessionStorageService } from '../../schemas/platform';\nimport { SessionIdentityDoc, SessionMetadataDoc } from '../durableSession/docs';\nimport { poHomeDirectory } from '../piSettings/core';\nimport { prepareSessionDirectory, removeSessionDirectory, removeSessionOutputDirectory } from './sessionDirectory';\n\nexport const PO_SESSION_TEMP_DIR = 'PO_SESSION_TEMP_DIR';\nexport const PO_SESSION_FILES_SERVICE = 'po/session-files';\n\nexport interface PoSessionFiles {\n readonly temporaryDirectory: string;\n}\n\nexport interface SessionTempDirectoryOptions {\n readonly sessionId: string;\n readonly homeDirectory: string;\n readonly environment: Readonly<Record<string, string | undefined>>;\n readonly temporaryDirectory: string;\n}\n\n/** A host-owned path, never a client-selected root or an inherited parent session path. */\nexport function sessionTempDirectory(options: SessionTempDirectoryOptions): string {\n if (!/^[a-zA-Z0-9_-]+$/u.test(options.sessionId)) throw new Error('Invalid temporary-file session identity.');\n if (!path.isAbsolute(options.temporaryDirectory)) throw new Error('The host temporary directory must be absolute.');\n const namespace = createHash('sha256')\n .update(options.homeDirectory)\n .update('\\0')\n .update(poHomeDirectory(options.homeDirectory, options.environment))\n .digest('hex')\n .slice(0, 24);\n return path.join(options.temporaryDirectory, `po-${namespace}`, 'sessions', options.sessionId);\n}\n\n/** Both the execution host and package APIs call this with their admitted session identity. */\nexport async function prepareSessionEnvironment(input: {\n readonly platform: PoPlatform;\n readonly sessionId: string;\n readonly environment: Readonly<Record<string, string | undefined>>;\n}): Promise<Readonly<Record<string, string | undefined>>> {\n const environment = { ...input.environment };\n // A child must never inherit its parent's browsable directory. Older embedders may lack this capability.\n delete environment[PO_SESSION_TEMP_DIR];\n if (input.platform.temporaryDirectory === undefined) return Object.freeze(environment);\n const directory = sessionTempDirectory({\n sessionId: input.sessionId,\n homeDirectory: input.platform.homeDirectory,\n environment,\n temporaryDirectory: input.platform.temporaryDirectory,\n });\n // ponytail: resumption uses the same host temp base; pinning across host temp changes needs durable session metadata.\n const preparedDirectory = await prepareSessionDirectory(input.platform, directory);\n return Object.freeze({ ...environment, [PO_SESSION_TEMP_DIR]: preparedDirectory });\n}\n\nexport interface PoSessionFileDirectories {\n readonly agentDirectory: string;\n readonly temporaryDirectory?: string;\n}\n\nfunction checkedDirectories(value: unknown, sessionId: string): PoSessionFileDirectories {\n if (!value || typeof value !== 'object') throw new Error('Invalid saved session output directories.');\n const files = value as Record<string, unknown>;\n const absolute = (directory: unknown): directory is string =>\n typeof directory === 'string' &&\n !directory.includes('\\0') &&\n path.isAbsolute(directory) &&\n path.resolve(directory) === directory;\n if (!absolute(files.agentDirectory)) throw new Error('Invalid saved session agent directory.');\n if (files.temporaryDirectory !== undefined) {\n const directory = files.temporaryDirectory;\n if (\n !absolute(directory) ||\n path.basename(directory) !== sessionId ||\n path.basename(path.dirname(directory)) !== 'sessions' ||\n !/^po-[a-f0-9]{24}$/u.test(path.basename(path.dirname(path.dirname(directory))))\n )\n throw new Error('Invalid saved session temporary directory.');\n }\n return {\n agentDirectory: files.agentDirectory,\n ...(files.temporaryDirectory === undefined ? {} : { temporaryDirectory: files.temporaryDirectory as string }),\n };\n}\n\n/** Narrow server-written output metadata, without exposing a session's environment or accepting request roots. */\nexport async function readSessionFileDirectories(\n sessions: PoSessionStorageService,\n sessionId: string,\n): Promise<PoSessionFileDirectories | undefined> {\n if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new Error('Invalid session output identity.');\n const storage = await sessions.openReadOnly(sessionId, BACKGROUND_CONTEXT);\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, BACKGROUND_CONTEXT);\n if (identity?.id !== sessionId) throw new Error('Saved output does not belong to this session.');\n const metadata = await session.snapshot(SessionMetadataDoc, BACKGROUND_CONTEXT);\n if (!metadata?.execution) return undefined;\n const execution: unknown = JSON.parse(metadata.execution);\n if (!execution || typeof execution !== 'object' || !('files' in execution) || execution.files === undefined)\n return undefined;\n return checkedDirectories(execution.files, sessionId);\n } finally {\n await session.close(BACKGROUND_CONTEXT);\n }\n}\n\n/** Explicit session removal only. Ordinary close, release and host shutdown preserve output. */\nexport async function removeSessionFiles(input: {\n readonly platform: PoPlatform;\n readonly sessionId: string;\n readonly environment: Readonly<Record<string, string | undefined>>;\n /** Exact session artifact directory selected by the composition root, never by a request. */\n readonly artifactDirectory: string;\n /** Previously stored roots, read by the host from the session's own durable metadata. */\n readonly directories?: PoSessionFileDirectories;\n}): Promise<void> {\n if (!/^[a-zA-Z0-9_-]+$/u.test(input.sessionId) || path.basename(input.artifactDirectory) !== input.sessionId)\n throw new Error('Invalid session output cleanup identity.');\n const directories =\n input.directories === undefined ? undefined : checkedDirectories(input.directories, input.sessionId);\n const removals = [removeSessionOutputDirectory(input.platform, input.artifactDirectory)];\n const temporaryDirectory =\n directories?.temporaryDirectory ??\n (input.platform.temporaryDirectory === undefined\n ? undefined\n : sessionTempDirectory({\n sessionId: input.sessionId,\n homeDirectory: input.platform.homeDirectory,\n environment: input.environment,\n temporaryDirectory: input.platform.temporaryDirectory,\n }));\n if (temporaryDirectory !== undefined) {\n const originalBase = path.dirname(path.dirname(path.dirname(temporaryDirectory)));\n removals.push(removeSessionDirectory({ ...input.platform, temporaryDirectory: originalBase }, temporaryDirectory));\n }\n const results = await Promise.allSettled(removals);\n const failures = results\n .filter((result): result is PromiseRejectedResult => result.status === 'rejected')\n .map((result) => result.reason);\n if (failures.length > 0) throw new AggregateError(failures, `Session '${input.sessionId}' output cleanup failed`);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
let node_os = require("node:os");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
node_path = require_runtime.__toESM(node_path, 1);
|
|
5
|
+
let node_fs_promises = require("node:fs/promises");
|
|
6
|
+
//#region src/services/sessionFiles/sessionDirectory.ts
|
|
7
|
+
/** Validate every Po-owned ancestor. OS temp itself may be shared. */
|
|
8
|
+
async function checkedSessionDirectory(platform, directory, create) {
|
|
9
|
+
const base = await (0, node_fs_promises.realpath)(platform.temporaryDirectory);
|
|
10
|
+
const relative = node_path.default.relative(platform.temporaryDirectory, directory);
|
|
11
|
+
if (!relative || relative.startsWith("..") || node_path.default.isAbsolute(relative)) throw new Error("Unsafe session temp root.");
|
|
12
|
+
let current = base;
|
|
13
|
+
const uid = (0, node_os.userInfo)().uid;
|
|
14
|
+
for (const segment of relative.split(node_path.default.sep)) {
|
|
15
|
+
current = node_path.default.join(current, segment);
|
|
16
|
+
if (create) try {
|
|
17
|
+
await (0, node_fs_promises.mkdir)(current, { mode: 448 });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
20
|
+
}
|
|
21
|
+
const info = await (0, node_fs_promises.lstat)(current);
|
|
22
|
+
if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== uid || (info.mode & 63) !== 0) throw new Error("Unsafe session temporary directory.");
|
|
23
|
+
if (await (0, node_fs_promises.realpath)(current) !== current) throw new Error("Session temporary directory changed during access.");
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
async function prepareSessionDirectory(platform, directory) {
|
|
28
|
+
return checkedSessionDirectory(platform, directory, true);
|
|
29
|
+
}
|
|
30
|
+
/** Remove only an existing session temp tree, never creating missing cleanup targets. */
|
|
31
|
+
async function removeSessionDirectory(platform, directory) {
|
|
32
|
+
try {
|
|
33
|
+
const checked = await checkedSessionDirectory(platform, directory, false);
|
|
34
|
+
await (0, node_fs_promises.rm)(checked, {
|
|
35
|
+
recursive: true,
|
|
36
|
+
force: true
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Persistent output roots may contain symlinks as files, but the owned root and parent must not redirect removal. */
|
|
43
|
+
async function removeSessionOutputDirectory(_platform, directory) {
|
|
44
|
+
try {
|
|
45
|
+
const parent = node_path.default.dirname(directory);
|
|
46
|
+
for (const target of [parent, directory]) {
|
|
47
|
+
const info = await (0, node_fs_promises.lstat)(target);
|
|
48
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe session output cleanup target.");
|
|
49
|
+
}
|
|
50
|
+
const realParent = await (0, node_fs_promises.realpath)(parent);
|
|
51
|
+
const realDirectory = await (0, node_fs_promises.realpath)(directory);
|
|
52
|
+
if (realDirectory !== node_path.default.join(realParent, node_path.default.basename(directory))) throw new Error("Session output cleanup target changed during access.");
|
|
53
|
+
await (0, node_fs_promises.rm)(realDirectory, {
|
|
54
|
+
recursive: true,
|
|
55
|
+
force: true
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
exports.prepareSessionDirectory = prepareSessionDirectory;
|
|
63
|
+
exports.removeSessionDirectory = removeSessionDirectory;
|
|
64
|
+
exports.removeSessionOutputDirectory = removeSessionOutputDirectory;
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=sessionDirectory.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionDirectory.cjs","names":["realpath","path","userInfo","mkdir","lstat","rm"],"sources":["../../../../src/services/sessionFiles/sessionDirectory.ts"],"sourcesContent":["import { lstat, mkdir, realpath, rm } from 'node:fs/promises';\nimport { userInfo } from 'node:os';\nimport path from 'node:path';\n\nimport type { PoPlatform } from '../../schemas/platform';\n\n/** Validate every Po-owned ancestor. OS temp itself may be shared. */\nasync function checkedSessionDirectory(platform: PoPlatform, directory: string, create: boolean): Promise<string> {\n const base = await realpath(platform.temporaryDirectory!);\n const relative = path.relative(platform.temporaryDirectory!, directory);\n if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('Unsafe session temp root.');\n let current = base;\n const uid = userInfo().uid;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n if (create) {\n try {\n await mkdir(current, { mode: 0o700 });\n } catch (error) {\n if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST')) throw error;\n }\n }\n const info = await lstat(current);\n if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== uid || (info.mode & 0o077) !== 0)\n throw new Error('Unsafe session temporary directory.');\n if ((await realpath(current)) !== current) throw new Error('Session temporary directory changed during access.');\n }\n return current;\n}\n\nexport async function prepareSessionDirectory(platform: PoPlatform, directory: string): Promise<string> {\n return checkedSessionDirectory(platform, directory, true);\n}\n\n/** Remove only an existing session temp tree, never creating missing cleanup targets. */\nexport async function removeSessionDirectory(platform: PoPlatform, directory: string): Promise<void> {\n try {\n const checked = await checkedSessionDirectory(platform, directory, false);\n await rm(checked, { recursive: true, force: true });\n } catch (error) {\n if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;\n }\n}\n\n/** Persistent output roots may contain symlinks as files, but the owned root and parent must not redirect removal. */\nexport async function removeSessionOutputDirectory(_platform: PoPlatform, directory: string): Promise<void> {\n try {\n const parent = path.dirname(directory);\n for (const target of [parent, directory]) {\n const info = await lstat(target);\n if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Unsafe session output cleanup target.');\n }\n const realParent = await realpath(parent);\n const realDirectory = await realpath(directory);\n if (realDirectory !== path.join(realParent, path.basename(directory)))\n throw new Error('Session output cleanup target changed during access.');\n await rm(realDirectory, { recursive: true, force: true });\n } catch (error) {\n if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;\n }\n}\n"],"mappings":";;;;;;;AAOA,eAAe,wBAAwB,UAAsB,WAAmB,QAAkC;CAChH,MAAM,OAAO,OAAA,GAAMA,iBAAAA,SAAAA,CAAS,SAAS,kBAAmB;CACxD,MAAM,WAAWC,UAAAA,QAAK,SAAS,SAAS,oBAAqB,SAAS;CACtE,IAAI,CAAC,YAAY,SAAS,WAAW,IAAI,KAAKA,UAAAA,QAAK,WAAW,QAAQ,GAAG,MAAM,IAAI,MAAM,2BAA2B;CACpH,IAAI,UAAU;CACd,MAAM,OAAA,GAAMC,QAAAA,SAAAA,CAAS,CAAC,CAAC;CACvB,KAAK,MAAM,WAAW,SAAS,MAAMD,UAAAA,QAAK,GAAG,GAAG;EAC9C,UAAUA,UAAAA,QAAK,KAAK,SAAS,OAAO;EACpC,IAAI,QACF,IAAI;GACF,OAAA,GAAME,iBAAAA,MAAAA,CAAM,SAAS,EAAE,MAAM,IAAM,CAAC;EACtC,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,WAAW,MAAM;EACrF;EAEF,MAAM,OAAO,OAAA,GAAMC,iBAAAA,MAAAA,CAAM,OAAO;EAChC,IAAI,CAAC,KAAK,YAAY,KAAK,KAAK,eAAe,KAAK,KAAK,QAAQ,QAAQ,KAAK,OAAO,QAAW,GAC9F,MAAM,IAAI,MAAM,qCAAqC;EACvD,IAAK,OAAA,GAAMJ,iBAAAA,SAAAA,CAAS,OAAO,MAAO,SAAS,MAAM,IAAI,MAAM,oDAAoD;CACjH;CACA,OAAO;AACT;AAEA,eAAsB,wBAAwB,UAAsB,WAAoC;CACtG,OAAO,wBAAwB,UAAU,WAAW,IAAI;AAC1D;;AAGA,eAAsB,uBAAuB,UAAsB,WAAkC;CACnG,IAAI;EACF,MAAM,UAAU,MAAM,wBAAwB,UAAU,WAAW,KAAK;EACxE,OAAA,GAAMK,iBAAAA,GAAAA,CAAG,SAAS;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACpD,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,WAAW,MAAM;CACrF;AACF;;AAGA,eAAsB,6BAA6B,WAAuB,WAAkC;CAC1G,IAAI;EACF,MAAM,SAASJ,UAAAA,QAAK,QAAQ,SAAS;EACrC,KAAK,MAAM,UAAU,CAAC,QAAQ,SAAS,GAAG;GACxC,MAAM,OAAO,OAAA,GAAMG,iBAAAA,MAAAA,CAAM,MAAM;GAC/B,IAAI,CAAC,KAAK,YAAY,KAAK,KAAK,eAAe,GAAG,MAAM,IAAI,MAAM,uCAAuC;EAC3G;EACA,MAAM,aAAa,OAAA,GAAMJ,iBAAAA,SAAAA,CAAS,MAAM;EACxC,MAAM,gBAAgB,OAAA,GAAMA,iBAAAA,SAAAA,CAAS,SAAS;EAC9C,IAAI,kBAAkBC,UAAAA,QAAK,KAAK,YAAYA,UAAAA,QAAK,SAAS,SAAS,CAAC,GAClE,MAAM,IAAI,MAAM,sDAAsD;EACxE,OAAA,GAAMI,iBAAAA,GAAAA,CAAG,eAAe;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CAC1D,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS,WAAW,MAAM;CACrF;AACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { userInfo } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { lstat, mkdir, realpath, rm } from "node:fs/promises";
|
|
4
|
+
//#region src/services/sessionFiles/sessionDirectory.ts
|
|
5
|
+
/** Validate every Po-owned ancestor. OS temp itself may be shared. */
|
|
6
|
+
async function checkedSessionDirectory(platform, directory, create) {
|
|
7
|
+
const base = await realpath(platform.temporaryDirectory);
|
|
8
|
+
const relative = path.relative(platform.temporaryDirectory, directory);
|
|
9
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Unsafe session temp root.");
|
|
10
|
+
let current = base;
|
|
11
|
+
const uid = userInfo().uid;
|
|
12
|
+
for (const segment of relative.split(path.sep)) {
|
|
13
|
+
current = path.join(current, segment);
|
|
14
|
+
if (create) try {
|
|
15
|
+
await mkdir(current, { mode: 448 });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
18
|
+
}
|
|
19
|
+
const info = await lstat(current);
|
|
20
|
+
if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== uid || (info.mode & 63) !== 0) throw new Error("Unsafe session temporary directory.");
|
|
21
|
+
if (await realpath(current) !== current) throw new Error("Session temporary directory changed during access.");
|
|
22
|
+
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
async function prepareSessionDirectory(platform, directory) {
|
|
26
|
+
return checkedSessionDirectory(platform, directory, true);
|
|
27
|
+
}
|
|
28
|
+
/** Remove only an existing session temp tree, never creating missing cleanup targets. */
|
|
29
|
+
async function removeSessionDirectory(platform, directory) {
|
|
30
|
+
try {
|
|
31
|
+
const checked = await checkedSessionDirectory(platform, directory, false);
|
|
32
|
+
await rm(checked, {
|
|
33
|
+
recursive: true,
|
|
34
|
+
force: true
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Persistent output roots may contain symlinks as files, but the owned root and parent must not redirect removal. */
|
|
41
|
+
async function removeSessionOutputDirectory(_platform, directory) {
|
|
42
|
+
try {
|
|
43
|
+
const parent = path.dirname(directory);
|
|
44
|
+
for (const target of [parent, directory]) {
|
|
45
|
+
const info = await lstat(target);
|
|
46
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe session output cleanup target.");
|
|
47
|
+
}
|
|
48
|
+
const realParent = await realpath(parent);
|
|
49
|
+
const realDirectory = await realpath(directory);
|
|
50
|
+
if (realDirectory !== path.join(realParent, path.basename(directory))) throw new Error("Session output cleanup target changed during access.");
|
|
51
|
+
await rm(realDirectory, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
force: true
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { prepareSessionDirectory, removeSessionDirectory, removeSessionOutputDirectory };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=sessionDirectory.mjs.map
|