import type { KeyState, SignedEvent } from '@kokuin/controller'; import type { DIDMethodResolver, Identity } from '@kokuin/token'; import type { KubunDB, StoreProvider } from '@kubun/db'; import type { CatalogUpstreamLookup, ExtensionResolvers } from '@kubun/graphql'; import type { HLC } from '@kubun/hlc'; import type { Logger } from '@kubun/logger'; import type { DocumentNode } from '@kubun/protocol'; import type { Runtime } from '@sozai/runtime'; import type { ExecutionResult } from 'graphql'; import type { ControllerHeadHistory, ControllerHeadState, IngestOutcome, IngestSource } from './controller-head.js'; import type { ControllerHeadOnlineResult } from './controller-log-resolver.js'; import type { EngineEvents } from './engine-events.js'; import type { AccessDefaultEvent, ApplyVerifiedMutationParams, ApplyVerifiedMutationResult, ApplyVerifiedMutationsParams, ApplyVerifiedMutationsResult, ContextFactory, ExecuteParams, MutateDocumentsParams } from './engine-types.js'; import type { EngineEventBus } from './events.js'; import type { Engine } from './executor.js'; import type { PolicyGateMap } from './policies.js'; import type { ProviderImpl } from './providers.js'; import type { ReplayCache } from './replay-cache.js'; export type MutationPreflightParams = { fieldName: string; args: Record; stores: StoreProvider; viewerDID: string | null; }; export type SchemaExtension = { sdl: string; resolvers: ExtensionResolvers; mutationPreflight?: (params: MutationPreflightParams) => Promise>; retryableMutationFields?: ReadonlyArray; /** * Names of `Mutation` fields whose resolvers must run OUTSIDE `mutateGraph`'s * write transaction. Side-effecting operations (peer connect/sync, dance * round-trips) perform network I/O and/or open their own per-step * transactions; wrapping them in the outer write transaction would hold the * DB connection across a network round-trip and, on single-connection * SQLite, deadlock a nested apply against the ambient transaction. * * The opt-out affects ONLY the DB transaction. Such resolvers still run * through `mutateGraph`'s owner-signed authorization path and still receive * the signed mutation operations (`executeCreateMutation`, etc.), which * autocommit each write instead of sharing one ambient transaction. Store * reads and writes route through the root provider, so nothing joins a * transaction that is not open. * * A single mutation operation must not mix transactional and * non-transactional root fields — the engine rejects such an operation. */ nonTransactionalMutationFields?: Array; }; /** * Low-level graph operations available only to plugins. * These bypass the public Engine API to allow plugins (RPC, sync, etc.) * to execute GraphQL and apply pre-verified mutations directly. */ export type GraphInternals = { execute = Record>(params: ExecuteParams): Promise>; subscribe = Record>(params: ExecuteParams): Promise> | ExecutionResult>; applyVerifiedMutation(params: ApplyVerifiedMutationParams): Promise; applyVerifiedMutations(params: ApplyVerifiedMutationsParams): Promise; /** * Apply a batch of document writes signed by the engine identity — optionally * on behalf of `owner` — within one signed write transaction. No GraphQL text * is parsed or executed. */ mutateDocuments(params: MutateDocumentsParams): Promise>; }; /** * Parameters provided by the engine to plugin factories. * Shared resources flow from the engine — plugins must use these * rather than creating their own instances. */ export type PluginFactoryParams = { engine: Engine; graph: GraphInternals; db: KubunDB; runtime: Runtime; identity: Identity; eventBus: EngineEventBus; hlc: HLC; /** * How far into the future a stamp arriving from a peer may sit, in * milliseconds — the engine's own bound, so a plugin ingesting peer data * bounds it the same way the graph lane does rather than configuring a second * one of its own. */ maxDriftMS: number; /** The engine's `gcGraceMS`: how long a marked document waits before it may be swept. */ gcGraceMS: number; getLogger: (name: string) => Logger; /** Pull from networked providers, excluding local and direct sources, before a transaction. */ fetchControllerHeadOnline: (did: string, stores?: StoreProvider) => Promise; /** * The checked controller head: `ok` with the folded state, or `missing`. */ controllerHead: (stores: StoreProvider, did: string) => Promise; /** `controllerHead` under the row lock; needs a transaction-bound provider. */ controllerHeadForUpdate: (stores: StoreProvider, did: string) => Promise; /** * The accepted stored controller log, for building recovery candidates. */ controllerHistory: (stores: StoreProvider, did: string) => Promise; /** * Arbitrate candidate logs against the stored head. Refused candidates leave * the stored log unchanged. External evidence (`network`, `handoff`, * `service`) must be ingested on a base provider; only `local` appends may run * in the caller's transaction. */ ingestControllerLog: (stores: StoreProvider, did: string, candidates: Array>, source: IngestSource) => Promise; /** * The engine's `did:kokuin:` controller resolver, scoped to a store provider. * A plugin wrapping credentials to a `did:kokuin:` recipient must resolve that * controller through the request's transaction provider — building its own * resolver over the base db would read outside the tx. * * `localOnly` returns a resolver that never pulls inline: it reads only the * cached authoritative log and fails closed on a miss. An expired * entry still answers, and the engine refreshes it in the background once the * caller's transaction ends. */ controllerResolverFor: (stores: StoreProvider, opts?: { localOnly?: boolean; }) => DIDMethodResolver & { resolveHead?: (did: string) => Promise; }; /** * Register a cross-engine service provider under a kind. Collected during * construction; the engine exposes them via `getProviders`/`getProvider` * once every factory has run. `kind` is the registry key; `impl` carries the * provider envelope (name/transport/priority) plus its kind-specific methods. */ registerProvider: = Record>(kind: string, impl: ProviderImpl) => void; /** * The engine's persisted replay ledger. A plugin standing up an authenticated * Enkaku server passes it as `replay: { cache }`, so a signed request replayed * after a restart, or into another server on the device, is refused rather * than run again. */ replayCache: ReplayCache; }; /** Groups selected by this device's compiled push or pull routes. */ export type GroupsWhoseRoutesMatchDocument = (document: DocumentNode) => ReadonlyArray; /** The own-agents view custody decisions are made against, read through the caller's stores. */ export type CustodyAuthority = { /** The controller whose custody this device manages, or null with none or several. */ custodyOwner(): Promise; /** Whether `deviceDID` is a current bound own device of `owner`. */ isOwnDevice(owner: string, deviceDID: string): Promise; }; /** * Plugin descriptor returned by a plugin factory. * * Plugins expose APIs via the `api` field. Other plugins access them * via `engine.getAPI(name)`. Lifecycle coordination uses lazy promises * on plugin APIs (e.g. `httpAPI.listening`) rather than a central registry. */ export type KubunPlugin = { catalogUpstreamLookup?: (stores: StoreProvider, viewerDID: string) => CatalogUpstreamLookup; ready?: () => Promise; accessDefaultOwner?: (stores: StoreProvider) => Promise; custodyAuthority?: (stores: StoreProvider) => CustodyAuthority; recomputeEffectiveAccessDefaults?: (controllerDID: string, stores: StoreProvider) => Promise; recordAccessDefault?: (event: AccessDefaultEvent, stores: StoreProvider) => Promise; /** Compile outside write transactions; the returned matcher performs no I/O. */ prepareGroupsWhoseRoutesMatchDocument?: () => Promise; name: string; policies?: PolicyGateMap; schemaExtension?: (config: Record) => SchemaExtension; events?: Record; /** Plugin's public API, available to other plugins via engine.getAPI(name) */ api?: unknown; createContextFactory?: () => ContextFactory; dispose?: () => Promise | void; };