import { MemoryExecutionGrantStore, PostgresExecutionGrantStore } from "./execution-grant-store.js"; import { MemorySkillSelectionStore, PostgresSkillSelectionStore } from "./selection-store.js"; import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, PublishedSkillSelection, PublishedSkillSelectionState, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js"; import { type SkillLifecyclePatch } from "./types.js"; import { type SqliteStoreOptions } from "./sqlite-store.js"; export declare function createArtifactId(): string; export interface StoreOptions { databaseUrl?: string; bootstrapApiKey?: string; /** SQLite tuning, forwarded when the resolved target is SQLite. */ sqlite?: SqliteStoreOptions; } /** * Build the store an operator's configuration asks for. * * The old body was `options.databaseUrl ? Postgres : Memory`, which meant that the * single most common way to start this server - run it with nothing set - produced a * process that looked healthy and forgot everything on restart. There is no longer any * input that yields a non-durable store by accident: * * - a postgres:// URL -> Postgres, and a failure to reach it is fatal here * - a sqlite path / file: URL -> SQLite at that path, migrated on open * - nothing at all -> SQLite at ~/.hasna/skills/server.db, migrated on open * - "memory:" or ":memory:" -> non-durable, and only because it was named * - anything else -> throws, naming what is supported */ export declare function createStore(options?: StoreOptions): Promise; export declare class MemorySkillsStore implements SkillsProductStore { readonly selectionStore: MemorySkillSelectionStore; readonly executionGrantStore: MemoryExecutionGrantStore; readonly backend: StoreBackendInfo; private apiKeys; private runs; private logs; private artifacts; private idempotency; private skills; private bundles; private versions; private pins; private operatorEnrollments; constructor(apiKeys?: Array<{ token: string; principal?: Partial; }>); addApiKey(token: string, principal?: Partial): ApiPrincipal; ensureBootstrapApiKey(token: string, principal?: Partial): Promise; authenticateApiKeyHash(hash: string): Promise; updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise; enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise; inspectOperatorScopeTarget(keyId: string, orgId: string): Promise; createRun(input: CreateRunInput): Promise; listRuns(principal: ApiPrincipal, limit: number): Promise; getRun(principal: ApiPrincipal, runId: string): Promise; /** * Exclusive only by accident: nothing here takes a lock, and it is safe purely * because the read and the write happen in one synchronous turn of a single event * loop. That is not a claiming strategy, it is a property of there being exactly one * process and one Map. It is left as-is because this store is now explicitly * non-durable and test-only - the durable backends implement claiming properly * (Postgres via FOR UPDATE SKIP LOCKED, SQLite via BEGIN IMMEDIATE plus a conditional * claim by id). Do not use this as the model for a new backend. * * startedAt is preserved on re-claim to match both durable backends' COALESCE. */ claimNextRun(_input: ClaimRunInput): Promise; updateRun(runId: string, patch: Partial>): Promise; /** * Fenced transition. The memory store's exclusivity is one event-loop turn, * which makes the read-then-check atomic by construction; the generation * predicate is still re-asserted so the semantics match the SQL backends. */ transitionRun(runId: string, patch: RunTransitionPatch, expectedGeneration: number): Promise; appendLog(runId: string, orgId: string, level: ServerRunLog["level"], message: string): Promise; listLogs(principal: ApiPrincipal, runId: string): Promise; addArtifact(artifact: Omit): Promise; listArtifacts(principal: ApiPrincipal, runId: string): Promise; getArtifact(principal: ApiPrincipal, runId: string, id: string): Promise; publishSkill(input: PublishSkillInput): Promise; listSkillVersions(principal: ApiPrincipal, slug: string): Promise; getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise; getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise; listSkills(principal: ApiPrincipal): Promise; getSkill(principal: ApiPrincipal, slug: string): Promise; setSkillLifecycle(principal: ApiPrincipal, slug: string, patch: SkillLifecyclePatch, expectedRevisionId?: string): Promise; updateSkill(principal: ApiPrincipal, slug: string, patch: UpdateSkillPatch, expectedRevisionId?: string): Promise; deleteSkill(principal: ApiPrincipal, slug: string, tombstoneWindowMs: number): Promise; purgeExpiredTombstones(principal: ApiPrincipal): Promise; getSkillBundle(principal: ApiPrincipal, sha256: string): Promise; pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record): Promise; unpinSkill(principal: ApiPrincipal, slug: string): Promise; listPins(principal: ApiPrincipal): Promise; listTags(principal: ApiPrincipal): Promise; listSkillsByTag(principal: ApiPrincipal, tag: string): Promise; listPinsByTag(principal: ApiPrincipal, tag: string): Promise; listPublishedSlugs(principal: ApiPrincipal): Promise; private collectOrphanBundle; private patchRun; } export declare class PostgresSkillsStore implements SkillsProductStore { get selectionStore(): PostgresSkillSelectionStore; get executionGrantStore(): PostgresExecutionGrantStore; readonly backend: StoreBackendInfo; private sql; constructor(databaseUrl: string); /** * Bun's SQL client connects lazily, so constructing this store proves nothing about * the database being there. Without an explicit probe, a wrong host or a down * instance produced a server that started, answered /health with ok:true, and 500ed * on the first API call. * * The error deliberately carries no URL: a Postgres URL is a credential, the driver's * own messages sometimes echo it, and this string ends up in logs. */ verifyConnectivity(): Promise; /** * Give rows written before migration 0004 a real content revision id. * * The migration adds revision_id with DEFAULT '', which would make If-Match vacuous * for legacy rows (every stale client matches the same empty string). This replaces * the marker with a content sha, idempotently: new code always writes a full id, so * the marker never reappears and the sweep costs one index scan on later opens. */ private backfillLegacyRevisions; close(): Promise; ensureBootstrapApiKey(token: string, principal?: Partial): Promise; authenticateApiKeyHash(hash: string): Promise; updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise; private casUpdateApiKeyScopes; enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise; inspectOperatorScopeTarget(keyId: string, orgId: string): Promise; /** * Run a callback under an RLS tenant or worker context, on one pooled * connection, for the duration of one transaction only. * * Migration 0003 arms RLS on skills_runs and skills_artifacts: a statement * whose session has no `app.skills_org_id` and no `app.skills_claim_context` * sees zero rows. SET LOCAL (the `true` third argument) confines the setting * to this transaction, which is what keeps one pooled connection from * carrying tenant A's context into tenant B's next request - a session-wide * SET would be exactly the cross-tenant leak RLS exists to prevent. */ private withContext; createRun(input: CreateRunInput): Promise; listRuns(principal: ApiPrincipal, limit: number): Promise; getRun(principal: ApiPrincipal, runId: string): Promise; claimNextRun(input: ClaimRunInput): Promise; updateRun(runId: string, patch: Partial>): Promise; /** * Generation-fenced transition. Same semantics as the SQLite twin: the WHERE * re-asserts lease_generation, and a stale worker gets StaleLeaseGenerationError * instead of silently overwriting a cancelled or re-claimed run. */ transitionRun(runId: string, patch: RunTransitionPatch, expectedGeneration: number): Promise; /** * Append a log line, retrying when another writer took the sequence number first. * * This was `SELECT MAX(sequence)+1` and then a separate `INSERT`, with an await in * between. Measured with five concurrent appendLog calls on one run: one succeeded and * four threw `duplicate key value violates unique constraint`, which executeRun's catch * turns into a failed run - a skill killed by its own logging. * * Folding the MAX into the INSERT helps but does not fix it: under READ COMMITTED each * statement takes its own snapshot, so concurrent inserts still compute the same MAX * (measured: 2 of 5 succeeded). SQLite has one writer and needs no retry; Postgres does, * so the loop lives here rather than in shared code. Bounded, and only ever retried for * a uniqueness conflict - any other error propagates on the first attempt. */ appendLog(runId: string, orgId: string, level: ServerRunLog["level"], message: string): Promise; listLogs(principal: ApiPrincipal, runId: string): Promise; addArtifact(artifact: Omit): Promise; listArtifacts(principal: ApiPrincipal, runId: string): Promise; getArtifact(principal: ApiPrincipal, runId: string, artifactId: string): Promise; publishSkill(input: PublishSkillInput): Promise; listSkills(principal: ApiPrincipal): Promise; getSkill(principal: ApiPrincipal, slug: string): Promise; setSkillLifecycle(principal: ApiPrincipal, slug: string, patch: SkillLifecyclePatch, expectedRevisionId?: string): Promise; updateSkill(principal: ApiPrincipal, slug: string, patch: UpdateSkillPatch, expectedRevisionId?: string): Promise; deleteSkill(principal: ApiPrincipal, slug: string, tombstoneWindowMs: number): Promise; purgeExpiredTombstones(principal: ApiPrincipal): Promise; getSkillBundle(principal: ApiPrincipal, sha256: string): Promise; listSkillVersions(principal: ApiPrincipal, slug: string): Promise; getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise; getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise; pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record): Promise; unpinSkill(principal: ApiPrincipal, slug: string): Promise; listPins(principal: ApiPrincipal): Promise; listTags(principal: ApiPrincipal): Promise; listSkillsByTag(principal: ApiPrincipal, tag: string): Promise; listPinsByTag(principal: ApiPrincipal, tag: string): Promise; listPublishedSlugs(principal: ApiPrincipal): Promise; /** Drop a bundle no remaining skill in the org points at. See the SQLite twin. */ private collectOrphanBundle; }