import type { Identity } from '@kokuin/token'; import type { CredentialAuthority } from '@kubun/credential'; import type { KubunDB, StoreProvider } from '@kubun/db'; import type { Adapter } from '@kubun/db-adapter'; import type { CatalogContext, PatchOperation } from '@kubun/graphql'; import type { Logger } from '@kubun/logger'; import type { MergeOutcome, PostStateGate } from '@kubun/mutation'; import type { DocumentData, DocumentMutation, DocumentNode } from '@kubun/protocol'; import type { GraphStoreAPI, ViewerReadAccess } from '@kubun/store-graph'; import type { P2PStoreAPI } from '@kubun/store-p2p'; import type { Runtime } from '@sozai/runtime'; import type { AccessChecker, AccessRule, DefaultAccessLevel } from './access-control.js'; import type { EngineEvents } from './engine-events.js'; import type { EngineEventBus } from './events.js'; import type { KubunPlugin, PluginFactoryParams } from './plugin.js'; /** * Per-request execution context provided to plugin operations. * Only carries what's per-request — shared resources (identity, hlc, logger, etc.) * are already captured in the plugin closure from PluginFactoryParams. */ export type ExecutionContext = { viewerDID: string; /** Delegation tokens the request carried, so a plugin can act under the same chain. */ delegationTokens?: Array; /** * Credential read/administer authority for this request, assembled by the engine * from the viewer and the request's delegation tokens. Required: every request * gets one (self-owned when no tokens are present), so plugins never fabricate it. */ credentialAuthority: CredentialAuthority; /** * Trusted provenance: `true` only when `viewerDID` was established from a * verified envelope `iss` at the transport boundary (e.g. the graph-service * handler), never inferred by the engine itself. Defaults to `false` for * direct/in-process callers and the `requireAuth:false` substituted-DID path. */ authenticated: boolean; /** * Server-derived group ID for this request, when the transport is bound to a * group tunnel (see `ServiceHandlerContext.groupID` in * `@kubun/plugin-service-api`). `undefined` outside a group context — never * inferred by the engine itself. */ groupID?: string; /** * Client-presented independent capability chains for this request. Unlike * `groupID`, this rides the wire (`MutateGraphParams.invocationCaps`) — the * graph-service handler forwards it only from its delegated `graph/mutate` * branch (see `EngineGraphParams.invocationCaps`). Verification of these * chains happens downstream of this rail, not here. */ invocationCaps?: Array>; /** * Connector operation-envelope ids for a credential-spend connector action * (`connectorCreate`/`connectorUpdate`). Unlike `groupID`, * these DO ride the wire (`MutateGraphParams.operationID`/ * `deterministicEventID`, see `EngineGraphParams.operationID`) — the * graph-service handler reads them from the request payload, not from the * transport. `@kubun/plugin-connector`'s connector-action context reads * them off this ExecutionContext (never a separate ad-hoc parameter) and * supplies them to `executeAction`'s `ExecuteActionParams`. */ operationID?: string; deterministicEventID?: string; }; /** * A function that produces per-request context fields. * Called by the engine for each execute()/subscribe() invocation. */ export type ContextFactory = (executionContext: ExecutionContext, stores: StoreProvider) => Record; export type EngineParams = { db: Adapter | KubunDB; identity: Identity; logger?: Logger; runtime?: Partial; /** Plugin factories — called with shared params, return plugin descriptors. */ plugins?: Array<(params: PluginFactoryParams) => KubunPlugin>; eventBus?: EngineEventBus; /** * Server default access level applied when a document carries no override and * its owner has set no model default. Defaults to open read * (`{ read: 'anyone', write: 'only_owner' }`) so existing callers keep their * current open-read behavior. */ defaultAccessLevel?: DefaultAccessLevel; /** * Maximum allowed future drift (in milliseconds) between an incoming * mutation's HLC wall-time and the local clock. A mutation whose HLC * wall-time exceeds `now + maxDriftMS` is rejected to guard against * last-writer-wins poisoning from future-dated writes. The past direction is * unbounded so offline-first writes always apply. Defaults to one hour. */ maxDriftMS?: number; /** * Maximum number of undelivered events buffered per subscription generator * before the oldest are dropped. Guards against a slow subscriber growing * memory without bound. Defaults to the GraphQL context default (1000). */ maxSubscriptionQueueSize?: number; /** * How long (in milliseconds) a cached `did:kokuin:` controller log is trusted * before a re-pull from a group peer is attempted. Bounds how stale a * controller's device-key revocation may be locally. Defaults to * 5 minutes. */ controllerLogTTLMs?: number; /** * How long a document must stay marked for collection, unretained and unchanged, before this * device may delete its local copy. Defaults to 7 days. */ gcGraceMS?: number; /** * Whether this device takes custody of its owner's catalogs, until its owner sets otherwise * through `setDeviceCustodyEligible`. An ineligible device accepts no assignment. Defaults to * true. */ custodyEligible?: boolean; }; /** * Optional pre-persist gate. Receives the synthesized post-apply * `DocumentNode` after the mutation's effect has been computed in memory but * before any write to the store (no `saveDocument`/`createDocument`/log * insert). Returning `false` causes the engine to skip persist + event emit * and report the mutation as `dropped: true`. Returning `true` (or no gate * provided) lets the existing apply flow proceed. * * The gate is intentionally generic — it is not access-control-aware. The * receive wiring composes `resolveAccessRule`/`checkAccess` and passes the * resulting decision through this hook. */ export type AccessGate = PostStateGate; /** * Transaction-scoped stores handed to an {@link AccessGateFactory} so the gate * resolves access state through the apply transaction's connection. */ export type AccessGateStores = { graphStore: GraphStoreAPI; p2pStore?: P2PStoreAPI; }; /** * Builds an {@link AccessGate} bound to the apply transaction's stores. * * The apply runs inside a DB transaction; a gate that reads access state must * read through that transaction, not a main-connection handle captured at * construction time (which deadlocks on single-connection SQLite and reads a * stale snapshot on Postgres). The engine invokes this factory with its * transaction-scoped stores and uses the returned gate. Returning `undefined` * skips the gate (apply proceeds). */ export type AccessGateFactory = (stores: AccessGateStores) => AccessGate | undefined; export type ApplyVerifiedMutationParams = { /** The signed mutation JWT token */ token: string; /** * Source of the mutation. * - `'local'` (default): authored on this peer — emits `engine:mutation:authored`. * - `'peer'`: received from another peer — emits `engine:mutation:received`. */ origin?: 'local' | 'peer'; /** * Optional gate factory evaluated after compute, before persist + event emit. * The engine invokes it with its transaction-scoped stores so gate reads run * inside the apply transaction. On deny: no DB write, no events, result * returns `dropped: true`. */ accessGate?: AccessGateFactory; /** * MLS group the mutation was delivered through, on the broadcast receive * path; absent on merkle/RPC where the doc's groups are resolved by walk. */ arrivalGroupID?: string; /** Complete a parked pending mutation in the apply transaction, before emitting events. */ completesPendingMutation?: boolean; }; export type ApplyVerifiedMutationResult = { accessNarrowed?: boolean; /** Effective signed read rule retained before this entry applied. */ previousReadRule?: AccessRule | null; /** The resulting document after mutation. Null when the gate denied — the * pre-state is preserved on disk and the post-state was discarded. */ document: DocumentNode | null; /** The document snapshot before the mutation was applied, or null if it did not exist */ previousDoc: DocumentNode | null; /** The verified mutation payload */ mutation: DocumentMutation; /** The DID of the mutation author (from JWT issuer) */ authorDID: string; /** BLAKE3 hash of the mutation JWT */ hash: string; /** The signed mutation JWT — retained so downstream consumers (e.g. broadcast * senders) can route the mutation to peers without re-signing. */ token: string; /** * `true` when an access gate denied the entry or final-gate mode dropped a permanent refusal. * Unverified payloads have no result entry and count only towards the batch dropped total. */ dropped: boolean; /** * Merge diagnostics, present together only for a non-null application against * a provider-merge-policy group's SET or tombstone path. A dropped result, an * ordinary-model application, and a CHANGE application carry none of the four. */ outcome?: MergeOutcome; /** The incoming candidate's own state digest. */ candidateDigest?: string; /** The state digest the register kept after this apply. */ winnerDigest?: string; /** Whether this application moved the register's winning state digest. */ winnerChanged?: boolean; }; export type ApplyVerifiedMutationsParams = { tokens: Array; /** * Source of the mutations in this batch. * - `'local'` (default): authored on this peer — emits `engine:mutation:authored`. * - `'peer'`: received from another peer — emits `engine:mutation:received`. */ origin?: 'local' | 'peer'; /** * Optional gate factory evaluated per entry. The engine invokes it once with * its transaction-scoped stores; each entry's gate decision is independent — * some entries may be applied while others are dropped. */ accessGate?: AccessGateFactory; /** Drop permanently refused entries, then check final documents before commit. * A final denial or transient fault rolls back the whole batch. */ finalAccessGate?: AccessGateFactory; }; export type ApplyVerifiedMutationsResult = { /** Verified entries only; permanently invalid tokens have no trusted payload to return. */ results: Array; /** Count of entries dropped via access gates or permanent refusal in final-gate mode. */ dropped: number; }; /** * A single document write for `mutateDocuments`. The engine signs each write * with its own identity, optionally on behalf of `owner`, and applies it inside * one signed write transaction — no GraphQL text is parsed or executed. */ export type DocumentWrite = { type: 'create'; modelID: string; data: DocumentData; } | { type: 'set'; modelID: string; unique: Uint8Array; data: DocumentData; /** * Fetch-order token for a merge-policy-aware SET. A model that declares a * `providerMergePolicy` rejects a tokenless write; ordinary models ignore * it. Optional, so every existing caller stays source-compatible. */ sourceFetchHLC?: string; } | { /** * A fetch-ordered deletion of a merge-policy document (a provider * reported the entity absent). Only a model that declares a * `providerMergePolicy` accepts it. */ type: 'tombstone'; modelID: string; unique: Uint8Array; sourceFetchHLC: string; } | { type: 'update'; docID: string; patch: Array; } | { type: 'remove'; docID: string; }; export type MutateDocumentsParams = { /** DID the written documents are owned by; defaults to the signer. */ owner?: string; /** Delegation tokens to attach, unioned with tokens this device holds. */ delegationTokens?: Array; writes: Array; /** Runs once on the signed write's scoped stores after all writes, before commit. */ afterWrite?: (context: { stores: StoreProvider; results: ReadonlyArray; cap?: ReadonlyArray; }) => Promise; }; export type ExecuteParams = { graphID: string; text: string; variables?: Record; viewerDID?: string; contextExtensions?: Record; /** Override the store provider for this execution (e.g. transactional provider). */ stores?: StoreProvider; /** * Delegation tokens the request carries, surfaced on the ExecutionContext so a * plugin's per-request credential authority can authorize a delegated chain. * Independent of the mutation `cap` auto-attach, which happens upstream. */ delegationTokens?: Array; /** * Trusted provenance for `viewerDID`, forwarded onto `ExecutionContext.authenticated`. * Set `true` only by a caller that verified `viewerDID` from a signed envelope * `iss` (the graph-service handler); every other caller omits it, defaulting to `false`. */ authenticated?: boolean; /** Forwarded onto `ExecutionContext.groupID`. See `EngineGraphParams.groupID`. */ groupID?: string; /** Forwarded onto `ExecutionContext.invocationCaps`. See `EngineGraphParams.invocationCaps`. */ invocationCaps?: Array>; /** * Forwarded onto `ExecutionContext.operationID`/`ExecutionContext.deterministicEventID`. * See `EngineGraphParams.operationID`. */ operationID?: string; deterministicEventID?: string; }; /** * A replication event for a model access-default write. Access-default writes * are local policy, not signed document mutations, so they never ride * `mutationResults`; the mutation driver collects them and the caller decides * when to emit (post-commit for the transactional path, immediately for the * autocommit path). */ export type AccessDefaultEvent = { name: 'engine:access-default:set'; data: EngineEvents['engine:access-default:set']; } | { name: 'engine:access-default:removed'; data: EngineEvents['engine:access-default:removed']; }; export type CreateContextParams = { store: GraphStoreAPI; catalog?: CatalogContext; viewerDID: string; eventBus: EngineEventBus; accessChecker?: AccessChecker; viewerReadAccess?: ViewerReadAccess; contextExtensions?: Record; maxSubscriptionQueueSize?: number; };