import { type OwnedBytes } from "../lib/skill-bundle.js"; import type { SkillMeta } from "../lib/registry-types.js"; import type { ArtifactStorage } from "./artifact-storage.js"; import type { SkillsServerConfig } from "./config.js"; import { type ApiPrincipal, type PublishSkillInput, type ServerPin, type ServerSkillRecord, type ServerSkillVersion, type SkillLifecyclePatch, type SkillsProductStore } from "./types.js"; /** Wire shape of a pin: the client-facing facts, without the storage columns. */ export declare function pinPayload(pin: ServerPin): Record; /** * The metadata field of a pin body. * * Absent means an empty object (matching the schema default), present-and-not-an- * object is refused rather than coerced: a client that sends `metadata: "team"` * sent a bug, and storing it as `{}` would make the round-trip silently lose * what the caller wrote. The route calls this on the parsed JSON body before * touching the store, so a malformed body is a 400 and never a row. */ export declare function pinMetadataField(body: Record): Record; export declare class SkillRequestError extends Error { readonly status: number; readonly code: string; constructor(status: number, code: string, message: string); } export declare function skillLifecyclePatch(body: Record): SkillLifecyclePatch; /** SkillMeta shape for a published row, so a client can treat both kinds alike. */ export declare function publishedSkillMeta(record: ServerSkillRecord): SkillMeta; /** * SkillMeta shape for a published row, plus a payload-level "alreadyPublished" flag: * true when the accepted publish found the version already recorded with the same * digest (an idempotent re-push), so the CLI can distinguish "published as X" from * "already published as X" (hasna/apps#1671). */ export declare function publishedPayload(record: ServerSkillRecord, opts?: { alreadyPublished?: boolean; }): Record; /** * The HTTP ETag for a published row: the revision id, quoted exactly as RFC 9110 wants. * The quotes are load-bearing — a client that echoes the whole header value back as * If-Match must send the quotes too. */ export declare function revisionEtag(revisionId: string): string; /** * Parse an If-Match header value into the revision id it names. * * Accepts the quoted form the server itself issues (RFC 9110) and a bare id for * tolerance. `*` is refused: "any revision" would license exactly the silent overwrite * the optimistic-concurrency guard exists to refuse. Malformed values are a 400 * statement about the request, never a fallback to "no guard". */ export declare function parseIfMatch(value: string | null): string | undefined; /** * Resolve a stored row that may be a tombstone (todos d061fcda). * * A tombstoned row answers 410 with the marker while its window is open, so a client's * pull can reconcile (remove the local copy). Once the window has passed the tombstone * is purged and the slug is simply gone (404). Returns "purged" when the caller should * answer 404, a tombstone payload when it should answer 410, or "live" for a live row. * * Purging drops the store's bundle row and, for every purged record, the S3 object * behind it — the same getSkillBundle-then-delete dance the publish/delete paths use, * so an object whose digest another row still references survives. */ export declare function tombstoneStatus(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, record: ServerSkillRecord): Promise<"live" | "purged" | Record>; /** List only records belonging to the authenticated organization. */ export declare function listMergedSkills(store: SkillsProductStore, principal: ApiPrincipal): Promise[]>; /** * What a read of one slug resolves to: the org's published row (live or tombstoned) or * nothing. Tombstones retain their 410 response until expiry; no machine-local * content can substitute for an absent or deleted organization record. */ export type SkillReadResolution = { kind: "published"; record: ServerSkillRecord; } | { kind: "tombstone"; payload: Record; } | { kind: "absent"; }; export declare function resolvePublishedSkill(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, slug: string): Promise; /** Distinct, non-empty tags in the authenticated organization's published catalog. */ export declare function listOrgTags(store: SkillsProductStore, principal: ApiPrincipal): Promise; /** The same organization catalog filtered through the store's indexed tag query. */ export declare function listMergedSkillsByTag(store: SkillsProductStore, principal: ApiPrincipal, tag: string): Promise[]>; /** * The minimal per-skill wire row the tag/sync summary routes serve: the client's * RemoteSkillSummary contract ({ slug, name?, version?, updatedAt? }). */ export declare function skillSummary(skill: Record): Record; /** Only published skills in this organization can contribute tags to its pins. */ export declare function listPinsByTag(store: SkillsProductStore, principal: ApiPrincipal, tag: string): Promise[]>; export declare function getMergedSkill(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, slug: string): Promise | null>; /** * The SKILL.md document for one slug, or null when nothing is served under it. * * A tombstoned slug returns null here; the caller that needs the 410 marker resolves * the tombstone itself via resolvePublishedSkill before calling. */ export declare function getMergedSkillMd(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, slug: string): Promise; interface ParsedPublish { input: Omit; bundleBytes?: OwnedBytes; } /** * Read a publish request. * * Two accepted encodings, both deliberate: * - multipart/form-data with a `manifest` JSON part and a `bundle` file part. This is * the one `skills push` uses. The bundle never passes through readJson(), so the 1 MB * JSON cap is neither raised nor bypassed - the bundle has its own, larger cap. * - application/json for a metadata-only publish (an instruction skill with no files). * Subject to the ordinary JSON cap, because it is an ordinary JSON body. * * On what the size checks here can and cannot do. Content-Length is a claim: a chunked * request need not send one, so the header check is an early-out, never the guarantee. * The real ceiling is Bun.serve's `maxRequestBodySize`, set from the same config value in * app.ts, which refuses at the socket before this function is reached. The checks below * are what remains meaningful once the body is in hand: the per-part limits, and the * refusal of any part this endpoint did not ask for - without which a request could carry * a compliant `manifest`, a compliant `bundle`, and unbounded extra parts that nothing * measured. */ export declare function parsePublishRequest(request: Request, config: SkillsServerConfig): Promise; export declare function storePublishedSkill(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, parsed: ParsedPublish, expectedRevisionId?: string): Promise<{ record: ServerSkillRecord; alreadyPublished: boolean; }>; /** * Tombstone a published skill (todos d061fcda): the row survives with a tombstone * marker for `tombstoneWindowMs`, so reads answer 410 and a pulling client can * reconcile, and the bundle is retained until the purge. The stored object is NOT * discarded here — the tombstoned row still references it; the purge discards it. * Returns the tombstoned record, or null when the org has no row by that slug. */ export declare function deletePublishedSkill(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, slug: string, tombstoneWindowMs: number): Promise; /** * Read a published bundle back, refusing to serve bytes that no longer hash to the digest * they were stored under. * * This is what the sha256 column buys. Storage drift - a truncated blob, an S3 object * replaced out of band, a bad restore - is otherwise completely silent: the client * receives a shorter tarball, extraction fails somewhere unrelated, and nothing points at * storage. Verifying on read turns that into one error naming the digest. */ export declare function readPublishedBundle(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, slug: string): Promise<{ record: ServerSkillRecord; bytes: OwnedBytes; }>; export declare function assertPublishableSlug(slug: string): void; export declare function assertSha256(value: string): void; /** * Versions surface (hasna/apps#1630). * * GET /api/v1/skills/:slug/versions -> { slug, current, versions: [...] } * GET /api/v1/skills/:slug/versions/:version -> one version's manifest * GET /api/v1/skills/:slug/versions/:version/bundle -> the exact bytes of that version * * The registry row is the "current" pointer; a version's bytes come from the * content-addressed bundle store so db-mode instances serve history exactly like S3 ones. */ export declare function skillVersionPayload(version: ServerSkillVersion, currentSha?: string): Record; export declare function listSkillVersionsPayload(store: SkillsProductStore, principal: ApiPrincipal, slug: string): Promise>; export declare function readSkillVersion(store: SkillsProductStore, principal: ApiPrincipal, slug: string, version: string): Promise; export declare function readSkillVersionBundle(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, slug: string, version: string): Promise<{ version: ServerSkillVersion; bytes: OwnedBytes; }>; export {};