{"version":3,"file":"packageApi.cjs","names":[],"sources":["../../src/schemas/packageApi.ts"],"sourcesContent":["/**\n * The HTTP surface a package offers its host.\n *\n * A package declares one in package.json under `doompiApi`, naming an entry per\n * scope. Which host mounts it follows from that scope: a session-scoped API is\n * served by the session's own doompi-server, a hub-scoped one by the cockpit\n * hub. The package writes routes once and never learns which process ran them.\n *\n * The handler is a plain fetch callback so no host and no package is committed\n * to one HTTP framework; a Hono app satisfies it through `app.fetch`.\n */\n\nimport type { DoomMcpProjection } from './mcpProjection.ts';\n\n/** The host-owned sync view a repository-scoped package API may inspect. */\nexport interface DoomRepositorySyncView {\n  fresh: boolean;\n  reasons: string[];\n  mcpProjection?: DoomMcpProjection;\n}\n\n/** Where an API runs: inside one session's server, or in the machine-wide hub. */\nexport type DoomApiScope = 'session' | 'hub';\n\nexport const DOOM_API_SCOPES: readonly DoomApiScope[] = ['session', 'hub'];\n\n/** The segment an API is mounted under, below this prefix. */\nexport const DOOM_API_ROUTE_PREFIX = '/api/plugin';\n/** Selects the hub API bundle that owns the named cockpit session. */\nexport const DOOM_HUB_API_SESSION_QUERY_PARAM = 'hubSession';\n\n/** Absolute unix socket path exposed to processes inside a session server. */\nexport const DOOM_API_SOCKET_ENV = 'DOOMPI_SESSION_API_SOCKET';\n/** Bearer token for agent-only routes on a session API socket. */\nexport const DOOM_API_INTERNAL_TOKEN_ENV = 'DOOMPI_SESSION_API_INTERNAL_TOKEN';\n\n/** Trusted caller headers written only by the cockpit-to-session proxy. */\nexport const DOOM_API_CALLER_LOCALITY_HEADER = 'x-doompi-api-caller-locality';\nexport const DOOM_API_CALLER_DEVICE_ID_HEADER = 'x-doompi-api-caller-device-id';\nexport const DOOM_API_CALLER_STEP_UP_HEADER = 'x-doompi-api-caller-step-up';\nexport const DOOM_API_CALLER_HEADERS = [\n  DOOM_API_CALLER_LOCALITY_HEADER,\n  DOOM_API_CALLER_DEVICE_ID_HEADER,\n  DOOM_API_CALLER_STEP_UP_HEADER,\n] as const;\n\nexport type DoomApiCallerStepUp = 'not-required' | 'verified' | 'unavailable';\nexport type DoomApiCaller =\n  | { locality: 'local'; stepUp: 'not-required' }\n  | { locality: 'remote'; deviceId: string; stepUp: DoomApiCallerStepUp };\n\n/** Reads the proxy-authenticated caller identity, rejecting partial or contradictory stamps. */\nexport function doomApiCallerFrom(headers: Headers): DoomApiCaller | undefined {\n  const locality = headers.get(DOOM_API_CALLER_LOCALITY_HEADER);\n  const deviceId = headers.get(DOOM_API_CALLER_DEVICE_ID_HEADER);\n  const stepUp = headers.get(DOOM_API_CALLER_STEP_UP_HEADER);\n  if (locality === 'local') {\n    return deviceId === null && stepUp === 'not-required' ? { locality, stepUp } : undefined;\n  }\n  if (\n    locality === 'remote' &&\n    deviceId !== null &&\n    deviceId !== '' &&\n    (stepUp === 'not-required' || stepUp === 'verified' || stepUp === 'unavailable')\n  ) {\n    return { locality, deviceId, stepUp };\n  }\n  return undefined;\n}\n/**\n * A browser-reachable OAuth redirect the host serves on its own listener.\n *\n * A package brokering third-party OAuth cannot use a loopback redirect when the\n * operator's browser is on another machine: `127.0.0.1` resolves to whichever\n * machine the browser runs on. The host owns a listener that is reachable from\n * wherever the cockpit is being used, so it lends that instead of the package\n * opening a port of its own.\n */\nexport interface DoomOAuthRedirect {\n  /** Absolute URL to register as the redirect target, already origin-correct. */\n  readonly redirectUri: string;\n  /**\n   * Accept redirects carrying this state. Must complete before the\n   * authorization URL is surfaced, or the redirect races its own reservation.\n   */\n  reserve(state: string, timeoutMs?: number): Promise<void>;\n  /** Resolves once the redirect carrying this state arrives. */\n  wait(state: string, timeoutMs?: number): Promise<{ code: string; state: string }>;\n  /** Drops a reservation whose flow ended without a redirect. */\n  cancel(state: string): void;\n}\n\n/** What the host tells an API about itself when it starts. */\nexport interface DoomApiContext {\n  scope: DoomApiScope;\n  /** The session this host serves; absent for a hub-scoped API. */\n  sessionId?: string;\n  /** The session's working directory; absent for a hub-scoped API. */\n  cwd?: string;\n  /** Shared only with child processes in this session, never with remote API clients. */\n  internalToken?: string;\n  /** Shared only with the cockpit hub for privileged cross-session coordination. */\n  hubToken?: string;\n  /**\n   * Resolves a hub-issued opaque repository id to an admitted canonical root.\n   * Hub APIs must never accept a browser-supplied filesystem path instead.\n   */\n  resolveRepository?(repositoryId: string): string | undefined;\n  /** Reads the admitted repository's sync projection without exposing its state path. */\n  readRepositorySync?(repositoryId: string): DoomRepositorySyncView | undefined;\n  /**\n   * Borrows the hub's OAuth redirect surface. Undefined when the hub cannot\n   * currently serve one, in which case the package keeps its own behaviour.\n   */\n  oauthRedirect?(): DoomOAuthRedirect | undefined;\n  onNotice(message: string): void;\n}\n\n/**\n * One running surface. The host strips the mount prefix before calling, so\n * routes are declared relative ('/runners/:id/log') and a package never\n * repeats where it was mounted.\n */\nexport interface DoomApiHandler {\n  fetch(request: Request): Response | Promise<Response>;\n  close(): void;\n}\n\nexport interface DoomApi {\n  /** Segment under /api/plugin/; globally unique across loaded packages. */\n  basePath: string;\n  start(context: DoomApiContext): DoomApiHandler;\n}\n\n/** The named export a declared entry must provide, so a host can find it. */\nexport const DOOM_API_EXPORT = 'api';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Narrows a module's export to an API, so a broken package is a notice rather than a crash. */\nexport function isDoomApi(value: unknown): value is DoomApi {\n  if (!isRecord(value)) return false;\n  return typeof value.basePath === 'string' && value.basePath !== '' && typeof value.start === 'function';\n}\n\n/** The package.json field a package declares its API under. */\nexport const DOOM_API_MANIFEST_FIELD = 'doompiApi';\n\nconst BASE_PATH_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;\n\nexport interface DoomApiEntryDeclaration {\n  /** Package-relative ./path to the source entry. */\n  entry: string;\n  /** Package-relative ./path to the built entry, which is what a host imports. */\n  dist?: string;\n}\n\n/** One package's declaration, validated, with an entry per scope it offers. */\nexport interface DeclaredPackageApi {\n  basePath: string;\n  packageDir: string;\n  packageName: string;\n  session?: DoomApiEntryDeclaration;\n  hub?: DoomApiEntryDeclaration;\n}\n\nexport class DoomApiManifestError extends Error {\n  constructor(packageDir: string, message: string) {\n    super(`${DOOM_API_MANIFEST_FIELD} manifest in ${packageDir}: ${message}`);\n    this.name = 'DoomApiManifestError';\n  }\n}\n\nfunction normalizeEntry(packageDir: string, scope: DoomApiScope, value: unknown): DoomApiEntryDeclaration {\n  const raw = typeof value === 'string' ? { entry: value } : value;\n  if (!isRecord(raw)) throw new DoomApiManifestError(packageDir, `${scope} must be a path or {entry, dist}.`);\n  const { entry, dist } = raw;\n  if (typeof entry !== 'string' || !entry.startsWith('./') || entry.includes('..')) {\n    throw new DoomApiManifestError(packageDir, `${scope}.entry must be a package-relative ./path with no '..'.`);\n  }\n  if (dist !== undefined && (typeof dist !== 'string' || !dist.startsWith('./') || dist.includes('..'))) {\n    throw new DoomApiManifestError(packageDir, `${scope}.dist must be a package-relative ./path with no '..'.`);\n  }\n  if (typeof dist !== 'string') {\n    throw new DoomApiManifestError(\n      packageDir,\n      `${scope}.dist is required: a host imports the built entry the package ships, never its source.`,\n    );\n  }\n  return { entry, dist };\n}\n\n/**\n * Validates one package.json's `doompiApi` block. A block naming neither scope\n * is an error rather than an empty result: declaring an API that no host can\n * ever mount is a mistake worth reporting where it was made.\n */\nexport function declaredApisOf(packageDir: string, manifest: Record<string, unknown>): DeclaredPackageApi[] {\n  const declared = manifest[DOOM_API_MANIFEST_FIELD];\n  if (declared === undefined) return [];\n  const blocks = Array.isArray(declared) ? declared : [declared];\n  const apis: DeclaredPackageApi[] = [];\n  for (const block of blocks) {\n    if (!isRecord(block)) throw new DoomApiManifestError(packageDir, 'each block must be an object.');\n    const { basePath, session, hub } = block;\n    if (typeof basePath !== 'string' || !BASE_PATH_PATTERN.test(basePath)) {\n      throw new DoomApiManifestError(packageDir, `basePath '${String(basePath)}' must be kebab-case.`);\n    }\n    if (session === undefined && hub === undefined) {\n      throw new DoomApiManifestError(packageDir, `'${basePath}' names neither a session nor a hub entry.`);\n    }\n    apis.push({\n      basePath,\n      packageDir,\n      packageName: typeof manifest.name === 'string' ? manifest.name : packageDir,\n      ...(session === undefined ? {} : { session: normalizeEntry(packageDir, 'session', session) }),\n      ...(hub === undefined ? {} : { hub: normalizeEntry(packageDir, 'hub', hub) }),\n    });\n  }\n  return apis;\n}\n\n/**\n * The deterministic order hosts mount in, with cross-package collisions settled\n * the way every other shared name is: the first package keeps the base path and\n * the later one is dropped with a notice.\n */\nexport function orderDeclaredApis(\n  apis: readonly DeclaredPackageApi[],\n  onNotice: (message: string) => void = () => undefined,\n): DeclaredPackageApi[] {\n  const sorted = [...apis].sort(\n    (left, right) => left.basePath.localeCompare(right.basePath) || left.packageDir.localeCompare(right.packageDir),\n  );\n  const owners = new Map<string, DeclaredPackageApi>();\n  const kept: DeclaredPackageApi[] = [];\n  for (const api of sorted) {\n    const holder = owners.get(api.basePath);\n    if (holder !== undefined) {\n      onNotice(\n        `package API '${api.basePath}' from ${api.packageDir} is skipped: ${holder.packageDir} already claims it.`,\n      );\n      continue;\n    }\n    owners.set(api.basePath, api);\n    kept.push(api);\n  }\n  return kept;\n}\n"],"mappings":"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"}