import { randomUUID } from 'node:crypto'; import type { LocalApplicationProductSurface, LocalApplicationProductSurfaceAuthority, } from '@qinglong/local-application'; import { LocalOwnerPepperKeyringFileProvider } from '@qinglong/local-owner-console/pepper-custody'; import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy'; import { createLocalApiAdmission } from '../admission/localApiAdmission'; import { createLocalApiCredentialAuthenticator } from '../authentication/credentialAuthenticator'; import { createLocalPresenceProofManager } from '../authentication/localPresenceProof'; import type { LocalApiProcessConfig } from '../production-process/config'; import { createLocalApiRunListRoute } from '../run/runListRoute'; import { createLocalApiRunReadRoute } from '../run/runReadRoute'; import { createLocalApiRunEventListRoute } from '../run/runEventListRoute'; import { createLocalApiRunStepListRoute } from '../run/runStepListRoute'; import { createLocalApiRunCancellationRoute } from '../run/runCancellationRoute'; import { createLocalApiRunAttemptLogReadRoute } from '../run/runAttemptLogReadRoute'; import { createLocalApiTaskListRoute } from '../task/taskListRoute'; import { createLocalApiTaskReadRoute } from '../task/taskReadRoute'; import { createLocalApiTaskStartRoute } from '../task/taskStartRoute'; import { createLocalApiTaskPutRoute } from '../task/taskPutRoute'; import { createLocalApiTaskAuthoringRoute } from '../task/taskAuthoringRoute'; import { createLocalApiTriggerListRoute, createLocalApiTriggerReadRoute, } from '../trigger/triggerReadRoutes'; import { createLocalApiTriggerPutRoute } from '../trigger/triggerPutRoute'; import { createLocalApiSecretListRoute, createLocalApiSecretPutRoute, } from '../secret/secretRoutes'; import { createPanelCronListRoute } from '../panel-compatibility/panelCronListRoute'; import { createPanelBootstrapRoute } from '../panel-compatibility/panelBootstrapRoute'; import { startLocalApiHttpSurface } from '../transport/httpSurface'; export interface LocalApiProductSurfaceEvent { readonly schemaVersion: 1; readonly component: 'qinglong3-local-api'; readonly level: 'info' | 'error'; readonly event: 'listening' | 'draining' | 'stopped'; readonly host: '127.0.0.1' | '::1'; readonly port: number; readonly stopResult?: 'stopped' | 'timed_out'; } export interface LocalApiProductSurfaceOptions { readonly emit?: ( event: Readonly, ) => void | Promise; readonly now?: () => number; readonly randomUuid?: () => string; } async function bestEffortEmit( emit: LocalApiProductSurfaceOptions['emit'], event: Readonly, ): Promise { if (!emit) return; try { await emit(event); } catch { // Diagnostics cannot replace listener or drain outcomes. } } function surfaceEvent( config: Readonly, event: LocalApiProductSurfaceEvent['event'], values: Readonly< Pick & Partial> >, ): Readonly { return Object.freeze({ schemaVersion: 1, component: 'qinglong3-local-api', event, host: config.listener.host, port: config.listener.port, ...values, }); } export function createLocalApiProductSurface( config: Readonly, options: LocalApiProductSurfaceOptions = {}, ): Readonly { if ( !config || typeof config !== 'object' || Array.isArray(config) || !options || typeof options !== 'object' || Array.isArray(options) || (options.emit !== undefined && typeof options.emit !== 'function') || (options.now !== undefined && typeof options.now !== 'function') || (options.randomUuid !== undefined && typeof options.randomUuid !== 'function') ) { throw new TypeError('Local API product surface options are invalid'); } return Object.freeze({ async start(authority: Readonly) { const provider = new LocalOwnerPepperKeyringFileProvider( config.ownerPepperKeyringDirectory, ); const authenticator = createLocalApiCredentialAuthenticator( authority, provider, options.now === undefined ? {} : { now: options.now }, ); const presenceProof = createLocalPresenceProofManager({ deploymentRoot: config.deploymentRoot, profile: authority.profile, ...(options.now === undefined ? {} : { now: options.now }), ...(options.randomUuid === undefined ? {} : { randomUuid: options.randomUuid }), }); const policy = new ProjectPolicyEngine(authority.projectPolicy); const runReadRoute = createLocalApiRunReadRoute(authority.runs); const runListRoute = createLocalApiRunListRoute(authority.runs); const runEventListRoute = createLocalApiRunEventListRoute(authority.runs); const runStepListRoute = createLocalApiRunStepListRoute( authority.runs, authority.stepRuns, ); const runCancellationRoute = createLocalApiRunCancellationRoute( authority.runCancellation, options.randomUuid ?? randomUUID, ); const runAttemptLogReadRoute = createLocalApiRunAttemptLogReadRoute( authority.runAttemptLogRead, ); const taskListRoute = createLocalApiTaskListRoute( authority.taskDefinitions, ); const taskReadRoute = createLocalApiTaskReadRoute( authority.taskDefinitions, ); const taskStartRoute = createLocalApiTaskStartRoute( authority.taskStart, options.randomUuid ?? randomUUID, ); const taskAuthoringRoute = createLocalApiTaskAuthoringRoute({ profile: authority.profile, projectPolicy: authority.projectPolicy, taskDefinitions: authority.taskDefinitions, securityAudit: authority.securityAudit, presenceProof, ...(options.now === undefined ? {} : { now: options.now }), ...(options.randomUuid === undefined ? {} : { randomUuid: options.randomUuid }), }); const taskPutRoute = createLocalApiTaskPutRoute({ projectPolicy: authority.projectPolicy, taskDefinitions: authority.taskDefinitions, taskDefinitionAdministrationForCredential: (fence) => { if (fence.subjectType !== 'user') { throw new TypeError('Task mutation requires a User credential'); } return authority.taskDefinitionAdministrationForCredential({ ...fence, subjectType: 'user', }); }, securityAudit: authority.securityAudit, presenceProof, taskAuthoringLeases: taskAuthoringRoute.leases, ...(options.now === undefined ? {} : { now: options.now }), ...(options.randomUuid === undefined ? {} : { randomUuid: options.randomUuid }), }); const triggerListRoute = createLocalApiTriggerListRoute( authority.triggers, ); const triggerReadRoute = createLocalApiTriggerReadRoute( authority.triggers, ); const triggerPutRoute = createLocalApiTriggerPutRoute({ projectPolicy: authority.projectPolicy, triggers: authority.triggers, triggerAdministrationForCredential: (fence) => { if (fence.subjectType !== 'user') { throw new TypeError('Trigger mutation requires a User credential'); } return authority.triggerAdministrationForCredential({ ...fence, subjectType: 'user', }); }, securityAudit: authority.securityAudit, presenceProof, ...(options.now === undefined ? {} : { now: options.now }), ...(options.randomUuid === undefined ? {} : { randomUuid: options.randomUuid }), }); const secretListRoute = createLocalApiSecretListRoute( authority.localSecretMetadata, ); const secretPutRoute = createLocalApiSecretPutRoute({ projectPolicy: authority.projectPolicy, secretAdministrationForCredential: (fence) => { if (fence.subjectType !== 'user') { throw new TypeError('Secret mutation requires a User credential'); } return authority.localSecretAdministrationForCredential({ ...fence, subjectType: 'user', }); }, securityAudit: authority.securityAudit, secretKeys: authority.localSecretKeys, presenceProof, ...(options.now === undefined ? {} : { now: options.now }), ...(options.randomUuid === undefined ? {} : { randomUuid: options.randomUuid }), }); const panelCronListRoute = createPanelCronListRoute({ tasks: authority.taskDefinitions, triggers: authority.triggers, }); const panelBootstrapRoute = createPanelBootstrapRoute(authority.profile); const admission = createLocalApiAdmission({ authenticator, policy, audit: authority.securityAudit, runReadRoute, runListRoute, runEventListRoute, runStepListRoute, runCancellationRoute, runAttemptLogReadRoute, taskListRoute, taskReadRoute, taskStartRoute, taskPutRoute, taskAuthoringRoute, triggerListRoute, triggerReadRoute, triggerPutRoute, secretListRoute, secretPutRoute, panelCronListRoute, panelBootstrapRoute, ...(options.now === undefined ? {} : { now: options.now }), ...(options.randomUuid === undefined ? {} : { randomUuid: options.randomUuid }), }); let active; try { active = await startLocalApiHttpSurface({ profile: authority.profile, host: config.listener.host, port: config.listener.port, admission, ...(options.randomUuid === undefined ? {} : { randomUuid: options.randomUuid }), }); } catch (error) { taskAuthoringRoute.close(); presenceProof.close(); throw error; } await bestEffortEmit( options.emit, surfaceEvent(config, 'listening', { level: 'info' }), ); let stopPromise: Promise<'stopped' | 'timed_out'> | undefined; return Object.freeze({ stopAndDrain() { if (stopPromise) return stopPromise; stopPromise = (async () => { await bestEffortEmit( options.emit, surfaceEvent(config, 'draining', { level: 'info' }), ); let stopResult = await active.stopAndDrain(); try { taskAuthoringRoute.close(); presenceProof.close(); } catch { stopResult = 'timed_out'; } await bestEffortEmit( options.emit, surfaceEvent(config, 'stopped', { level: stopResult === 'stopped' ? 'info' : 'error', stopResult, }), ); return stopResult; })(); return stopPromise; }, }); }, }); }