/** * gitvault D6 — named addressing resolution + id-pinning (repo-first-onramp * task 4.5). * * Two things live here, both Node-only (local git state): * * - PIN read/write: `r402.repoId` (+ `r402.repoAddress`, the human name it * was resolved from) in the repository's LOCAL git config * (`git config --local` — this checkout's own `.git/config`, never * global). "Local git state" per design D6's id-pinning paragraph. * - `resolveGitvaultAddress`: the orchestrator the remote helper and * `gitvault snapshot` drive — pinned id first, then dispatch on the * address's form (id vs slug — {@link gitvaultRemoteAddressForm}), then * (slug-form only, opt-in via `allow_create`) push-to-create on a miss. * * ID-PINNING RATIONALE (design D6, "resolved"): "on first contact the helper * resolves slug/name → repo_id and pins the immutable id in local git state. * Names are sugar for humans and first-time clones; every existing checkout * follows the id and survives any rename." Concretely: an id-form address * (`org_id`/`prj_...`) already survives a rename (project ids never change), * so pinning buys it nothing and this module never pins one. A SLUG-form * address re-resolves `org-slug/name → repo_id` over the network on every * open unless pinned — and a rename of either half would silently break that * resolution for an EXISTING checkout the moment it takes effect. Pinning * `repo_id` the first time a slug-form address resolves means every later * open on this checkout goes straight to `repo_id` (no resolution round-trip, * and no exposure to a later rename at all) — structurally better than a * redirect, per the design's own framing. */ import { type GitvaultRemoteAddress } from "../namespaces/gitvault.js"; import type { GitvaultTransport } from "./gitvault-publication.js"; import type { GitvaultCreationResult } from "./gitvault-creation-journal.js"; import type { GitvaultKeystore } from "./gitvault-keystore.js"; export interface GitvaultPinnedRepo { repo_id: string; /** `null` if the pin predates this field, or was written by hand. */ resolved_from: { org_slug: string; repo_name: string; } | null; /** `null` on a legacy pin that predates the id-carrying schema (or one written by hand) — resolution then self-upgrades it through one validation read. */ project_id: string | null; /** `null` on a legacy pin — see `project_id`. */ org_id: string | null; /** The coordination room key (`r402.room`) — `null` on a pin written before kygit-handoff. Same value as `project_id` for a project's default room. */ room: string | null; } /** Read this checkout's pinned `repo_id`, or `null` when nothing is pinned. */ export declare function readPinnedGitvaultRepo(repoDir: string): Promise; /** * Pin `repo_id` into this checkout's LOCAL git config — and, when resolved * from a slug-form address, the address it was resolved from (diagnostic * only). `resolvedFrom` is omitted for an id-form pin (gitvault-client- * round-trips design D4): an id-form address has no org-slug/name pair to * record, and — per {@link resolveGitvaultAddress}'s own doc comment — the * design's instruction is that pinning "only resolves names"; id-form is * already rename-proof, so this pin exists purely to skip the resolution * ROUND TRIP, not to survive a rename. */ export declare function pinGitvaultRepo(repoDir: string, repoId: string, resolvedFrom?: { org_slug: string; repo_name: string; }, ids?: { project_id: string; org_id: string; room_key?: string; }): Promise; /** * add-room-invite design D10: a bare `rooms join ` arrival standing * inside a git repository pins `r402.orgId`/`r402.room` in this checkout's * LOCAL git config — the SAME two keys {@link pinGitvaultRepo} writes * alongside a vault's `r402.repoId` — but WITHOUT one, since a room-only * join has no vault to pin. Read back by {@link readPinnedRoomBinding}, * independent of {@link readPinnedGitvaultRepo}'s own gate on `r402.repoId` * being present (a room-only checkout never sets it, so that reader would * otherwise see this pin as absent). */ export declare function pinRoomBinding(repoDir: string, ids: { org_id: string; room_key: string; }): Promise; /** Read `r402.orgId`/`r402.room` directly, with NO gate on `r402.repoId` (design D10) — the counterpart read for {@link pinRoomBinding}. Either field is `null` when unset (no repository, or never pinned). */ export declare function readPinnedRoomBinding(repoDir: string): Promise<{ org_id: string | null; room: string | null; }>; export interface GitvaultAddressResolution { repo_id: string; project_id: string; org_id: string; form: "id" | "slug"; /** * How this resolution happened: a local git-config pin, the local * keystore's own project→repo file (gitvault-offline-clone-resolve, * id-form only — {@link GitvaultKeystore.findRepoByProject}), a live * network resolution, or a fresh push-to-create. `"pin"` and `"keystore"` * are BOTH zero-transport-operation answers (see {@link offline}); they * are kept distinct only so a caller that cares about provenance can tell * them apart — {@link recoverStaleGitvaultPin}'s generic `offline` check * treats them identically. */ via: "pin" | "keystore" | "resolved" | "created"; /** The `org-slug/name` the address named, when slug-form; `null` for id-form. */ address: { org_slug: string; repo_name: string; } | null; /** Set only when THIS call push-to-created the vault (`via === "created"`); `null` otherwise. */ created: { deduplicated: boolean; recovery_receipt: GitvaultCreationResult["recovery_receipt"]; genesis_sha256: string; } | null; /** * `true` iff this resolution performed ZERO transport operations (an * id-carrying pin). Stale-pin recovery is then the caller's to arm on the * verb's first repo-scoped read — see {@link recoverStaleGitvaultPin}. */ offline: boolean; } export interface ResolveGitvaultAddressOptions { keystore: GitvaultKeystore; transport: GitvaultTransport; /** The parsed remote address (`parseGitvaultRemoteUrl`'s output) — id-form or slug-form, undiscriminated. */ address: GitvaultRemoteAddress; /** The local working tree, for reading/writing the pin. Omit for a repository-free read (e.g. `git ls-remote`) — pinning is then skipped, never a failure. */ repo_dir?: string; /** Push-to-create on a slug-form miss (D6). `false` (the default) makes a miss an ordinary not-found refusal — the read-only path (`list`/`fetch`, `gitvault status`). */ allow_create?: boolean; client_creation_id?: string; service_public_key?: Uint8Array | string; /** Fires once, only when THIS call allocated the vault (mirrors `Gitvault.push`'s `onVaultCreated`). */ onVaultCreated?: (created: NonNullable) => void | Promise; /** * gitvault-offline-clone-resolve (design D3): recovery-only. Skips BOTH * local answers — the git-config pin AND the id-form keystore consult — * forcing a genuine network resolution. Set exclusively by {@link * recoverStaleGitvaultPin}'s internal re-resolve call: a stale local * answer (a pin or a keystore file naming a repo that no longer resolves * or is no longer ours) must never be allowed to answer the SAME stale * fact back to itself, which is what would happen if recovery re-ran the * ordinary local-first path. Never set by an ordinary caller. */ bypass_local_answers?: boolean; } /** * Resolve a parsed remote address to `{repo_id, project_id, org_id}`, * pinning `repo_id` in local git state on the first successful resolution — * of EITHER form (gitvault-client-round-trips design D4 widens the * pre-existing slug-form pin, task 4.5, to id-form too) — and, when * `allow_create` is set and resolution misses, push-to-create it (task * 4.4/4.5, design D6). A pinned id that no longer resolves (404) clears the * pin and re-resolves ONCE through the ordinary form-dispatch path below, * rather than failing on a stale local pointer. */ export declare function resolveGitvaultAddress(options: ResolveGitvaultAddressOptions): Promise; /** * Stale-pin recovery for OFFLINE resolutions (client-surface spec, * "Id-form remotes pin repo_id like slug-form remotes"; widened by * gitvault-offline-clone-resolve design D3 to the keystore-sourced offline * answer too): a pin or a keystore hit resolves with no network read, so the * once-in-a-lifetime stale case (a vault deleted and re-allocated out from * under this checkout, or a keystore file left behind by an old allocation) * surfaces as the VERB's first repo-scoped read failing instead of a * resolution failure. * * Given that failure, answer the fresh resolution to retry the verb against — * or `null` when there is nothing to recover: * * - a non-vault-absent error (anything outside the not-found family and the * gateway's authorize-before-reveal `GITVAULT_ACCESS_DENIED` fold, which * deliberately makes "gone" and "not yours" one envelope) → `null`, * rethrow yours; * - re-resolution yielding the SAME `repo_id` → the local answer was fine * and the refusal is real: the pin is restored (re-resolution re-pins * it) and the answer is `null` — recovery never widens what an * unauthorized caller learns (both probes return the same refusal * family it already saw); * - a DIFFERENT `repo_id` → the fresh resolution, pin already rewritten; * the caller retries the verb exactly once. * * The re-resolve MUST bypass every local answer (`bypass_local_answers: * true`) — a stale keystore file (or, before this change, a stale pin) is * exactly the fact recovery exists to get past, so re-running the ordinary * local-first path would just consult the SAME stale local answer again and * never reach the network. This is why the retry always costs one real * transport operation, deliberately outside the counted budgets (D3, D5). * * Re-resolution failures are swallowed in favor of `null` (the caller's * original error is the truthful one to surface). */ export declare function recoverStaleGitvaultPin(options: { keystore: GitvaultKeystore; transport: GitvaultTransport; address: GitvaultRemoteAddress; repo_dir: string; /** The offline resolution the failing verb ran under. */ resolution: GitvaultAddressResolution; /** The verb's failure. */ error: unknown; }): Promise; //# sourceMappingURL=gitvault-address.d.ts.map