/** * RemoteCatalogSource — `ICatalogSource` HTTP client for the public skaile.store * Catalog backend (see `store/backend`). * * Wire shape: * * GET /trpc/catalog.get?input= → { result: { data: AssetDefinition | null } } * GET /trpc/catalog.list?input= → { result: { data: AssetDefinition[] } } * GET /tarball/ → 307 → (binary tarball) * * The tRPC server uses the standalone fastify adapter at `prefix: /trpc` * (`store/backend/libs/router-trpc/src/trpc.plugin.ts`). The transformer is * superjson, but our queries take only plain primitives + objects so the * `meta` envelope is empty in practice; we still send the canonical * `{ "json": ... }` shape so the server's superjson decoder is happy. * * The REST `GET /tarball/:sha256` controller lives at * `store/backend/apps/api/src/tarball.controller.ts` and 307-redirects to the * storage tier (presigned S3 URL or public CDN URL). The client is responsible * for following the redirect, downloading the bytes, and **verifying SHA256 * client-side** — mismatch is a hard fail (spec invariant: "Hash pinning ... * Mismatch = hard fail"). * * Cache: see {@link CatalogCache}. Stale-while-revalidate on metadata: a * stale entry serves the read while a background fetch refreshes the slot. * Tarballs are content-addressed and never expire — once cached on disk by * SHA256 they remain valid forever (until {@link CatalogCache.invalidateEverything}). * * Air-gapped: `cacheTtlMs: 0` disables network reads entirely. Every method * serves from cache or throws {@link OfflineError}; only `skaile update` * (which calls {@link RemoteCatalogSource.refresh}) flips the network on * for one bulk refresh. * * @docLink packages/library/api-reference#remote-catalog-source */ import type { CatalogAsset, CatalogAssetFilter, CatalogDomain, CatalogDomainFilter, CatalogSourceInfo, ICatalogSource } from "@skaile/workspaces/plugins"; import type { InstallManifest } from "@skaile/workspaces/types"; import { CatalogCache } from "./cache.js"; /** * A `fetch`-shaped function. Injected for tests; defaults to global `fetch`. * * @docLink packages/library/api-reference#remote-catalog-source */ export type HttpClient = (url: string, init?: { method?: string; headers?: Record; redirect?: "follow" | "manual" | "error"; }) => Promise; /** * Constructor options for {@link RemoteCatalogSource}. * * @docLink packages/library/api-reference#remote-catalog-source */ export interface RemoteCatalogSourceOptions { /** Base URL of the Catalog backend, e.g. `https://api.skaile.store`. No trailing slash required. */ baseUrl: string; /** * Metadata cache TTL in milliseconds. * Default `86_400_000` (24h). `0` disables network — air-gapped mode. */ cacheTtlMs?: number; /** Override `fetch` for tests. */ httpClient?: HttpClient; /** Override the cache directory. Default is `~/.skaile/cache/catalog/`. */ cacheDir?: string; /** Override clock for tests. */ now?: () => number; /** Pre-built cache instance (test injection). When provided, `cacheTtlMs`/`cacheDir`/`now` are ignored. */ cache?: CatalogCache; /** * If true, suppresses the background revalidation `unhandledRejection` log when * a stale-while-revalidate fetch fails. Default false. */ silentBackgroundErrors?: boolean; } /** * Thrown when the SHA256 of a downloaded tarball does not match the expected * value. This is a **hard fail** per the spec — the bytes are tampered or * corrupt and must not be installed. * * @docLink packages/library/api-reference#remote-catalog-source */ export declare class TarballHashMismatchError extends Error { readonly expected: string; readonly actual: string; constructor(expected: string, actual: string); } /** * Thrown when the client is in air-gapped mode (`cacheTtlMs === 0`) and the * requested data is not in the cache. The caller can resolve by running * `skaile update` to perform a one-shot bulk refresh. * * @docLink packages/library/api-reference#remote-catalog-source */ export declare class OfflineError extends Error { constructor(message: string); } /** * Thrown when the Catalog backend returns a non-2xx (and non-307 for tarball * fetch) response. Carries the HTTP status for debug. * * @docLink packages/library/api-reference#remote-catalog-source */ export declare class CatalogHttpError extends Error { readonly status: number; readonly url?: string | undefined; constructor(message: string, status: number, url?: string | undefined); } /** * HTTP client implementation of `ICatalogSource` for the public skaile.store * Catalog. See module docstring for the wire contract. * * @docLink packages/library/api-reference#remote-catalog-source */ export declare class RemoteCatalogSource implements ICatalogSource { readonly id: string; readonly baseUrl: string; readonly cache: CatalogCache; private readonly cacheTtlMs; private readonly httpClient; private readonly silentBackgroundErrors; /** Tracks in-flight background revalidations so callers can `await` them in tests. */ private readonly inFlight; constructor(opts: RemoteCatalogSourceOptions); /** * Resolve a single asset by canonical ref (`/@`). * * Cache: hit returns immediately. Miss fetches `/trpc/catalog.get`. Stale * entry serves the cached value and triggers a background refresh. * * Air-gapped: throws {@link OfflineError} on cache miss. */ resolve(ref: string): Promise; /** * List assets matching `filter`. * * Cache: hit returns immediately. Miss fetches `/trpc/catalog.list`. Stale * entry serves the cached array and triggers a background refresh. * * Air-gapped: throws {@link OfflineError} on cache miss. */ listAssets(filter?: CatalogAssetFilter): Promise; /** * Fetch the content-addressed tarball for an asset. The bytes are downloaded * via the REST endpoint `GET /tarball/:sha256` (which 307-redirects to the * storage tier), then verified against `sha256` on the client side. * * Cache: tarballs are content-addressed and cached forever once written. * * Air-gapped: throws {@link OfflineError} on cache miss. * * @throws {@link TarballHashMismatchError} when the downloaded SHA256 does not match. */ fetchTarball(_ref: string, sha256: string): Promise; /** * Enumerate available versions for a version-less ref. The Catalog has no * dedicated version-list route, so this derives versions from `listAssets` * (filtered to the publisher) and keeps only the matching kind+name. Reuses * the cached list path, so repeated calls are cheap. */ listVersions(ref: string): Promise; /** * Single synthetic Source pointing at the configured Catalog URL. Phase 2 * is single-Source per spec; multi-source federation is Phase 4.8. */ listSources(): Promise; /** * List navigational domains via `GET /trpc/catalog.listDomains`. Display-only, * so this **never throws**: a missing endpoint (older store → 404), a transport * error, or a malformed payload all degrade to `[]` rather than jeopardize the * asset feed this call accompanies. Air-gapped mode returns `[]` (domains are * not part of the offline cache in v1). * * Not cached — domains are read once per `skaile manage` open and are cheap. */ listDomains(filter?: CatalogDomainFilter): Promise; /** * Force a metadata-cache refresh. Wipes all resolve/list entries and * re-fetches `listAssets()` (no filter) to pre-warm the canonical query. * * Driven by `skaile update`. Tarballs (content-addressed) are not touched * — they remain valid across refreshes. * * In air-gapped mode this method **does** make a network call (bypassing * the TTL=0 gate); it is the only way to repopulate the cache. */ refresh(): Promise<{ assetsCached: number; }>; /** * Fetch the pointer-only install manifest for an asset ref. * * The install manifest is the Pointer Triple: the immutable upstream * `source.url` + `source.commitSha`, the per-file `files[].sha256` list, a * composite `sha256` rollup, plus `kind` and `tier`. Callers fetch the asset * bytes directly from the upstream repo at that commit and verify each file * against the recorded hashes — the Catalog itself never serves asset bytes. * * Wire surface: `GET /trpc/catalog.getInstallManifest?input=`. * * This is a `RemoteCatalogSource`-only method — it is not part of the * `ICatalogSource` contract. `LocalCatalogSource` has the asset bytes on disk * and has no pointer-triple concept, so adding it to the shared interface * would force a meaningless implementation there. * * Not cached: the install path is a one-shot read driven by `skaile install`. * * @throws {@link CatalogHttpError} on a non-2xx response or malformed envelope. */ getInstallManifest(ref: string): Promise; /** * Cheap probe used during cross-check when both a source-side and a store-side * candidate at the same version need to be verified for sha256 equality. * Returns only the composite sha256 — no source URL, no file list. * * Wire: `GET /trpc/catalog.getCanonicalDigest?input=`. * * Not cached: one-shot read during install resolution. * * @param ref - Canonical asset ref `/:@`. * @returns `{ sha256 }` on a 200, or `null` on a 404. * @throws {@link CatalogHttpError} on a non-2xx/non-404 response or malformed payload. * @docLink packages/library/api-reference#remote-catalog-source */ getCanonicalDigest(ref: string): Promise<{ sha256: string; } | null>; /** * Wait for any in-flight stale-while-revalidate background fetches to settle. * Test-only escape hatch. */ waitForBackground(): Promise; private scheduleBackground; private fetchAndStoreResolve; private fetchAndStoreList; /** Raw network call: GET /trpc/catalog.get?input= */ private fetchResolveNetwork; /** Raw network call: GET /trpc/catalog.list?input= */ private fetchListNetwork; } /** * Build a tRPC v11 GET URL with superjson-encoded input. * * Format: `/trpc/?input=})>` * * superjson encodes plain values as `{ "json": }`. The `meta` block is * only present for non-JSON-native types (Date, Map, BigInt, etc.) — our inputs * are plain strings/objects so we never emit `meta`. * * Exported for tests. * * @docLink packages/library/api-reference#remote-catalog-source */ export declare function trpcGetUrl(baseUrl: string, procedure: string, input: unknown): string; //# sourceMappingURL=remote-catalog-source.d.ts.map