import { type IndexRow as IndexRowV03, type ManifestV03, type Section, type SignedMandate } from "@aithos/protocol-client"; import type { AithosAuth } from "./auth.js"; import type { AithosSdkEndpoints } from "./endpoints.js"; import type { DelegateActor } from "./internal/delegate-state.js"; import type { OwnerSigners } from "./internal/owner-signers.js"; import { AithosSDKError } from "./types.js"; /** * Raised whenever an operation touches an Ethos that is NOT in the latest bundle * format. This SDK is latest-format-only: a legacy Ethos must be migrated before * it can be read or written. The message carries the migration link so the error * is actionable even without a dedicated UI. */ export declare class EthosMigrationRequiredError extends AithosSDKError { constructor(subjectDid: string, detectedVersion?: string); } export type ZoneName = "public" | "circle" | "self"; export declare const ZONE_NAMES: readonly ZoneName[]; export interface AddSectionInput { readonly title: string; readonly body: string; readonly tags?: readonly string[]; /** * Explicit section id. Defaults to a generated `sec_` — pass one when * the id is part of the CONTRACT, e.g. an `ethos.append.#prefix=gmail:` * delegate MUST mint its sections under its fence (`gmail:`), and * idempotent writers may want deterministic ids. Allowed: letters, digits, * `: . _ -`, length ≤ 128. */ readonly id?: string; } export interface UpdateSectionPatch { readonly title?: string; readonly body?: string; readonly tags?: readonly string[]; } export type StagedChange = { readonly kind: "add"; readonly zone: ZoneName; readonly section: Section; } | { readonly kind: "update"; readonly zone: ZoneName; readonly sectionId: string; readonly patch: UpdateSectionPatch; } | { readonly kind: "delete"; readonly zone: ZoneName; readonly sectionId: string; }; export interface PublishResult { /** New manifest height after publish. */ readonly editionHeight: number; /** SHA-256 hex of the canonical manifest. */ readonly manifestHash: string; /** DID of the subject we published for. */ readonly subjectDid: string; /** Zones whose contents changed in this edition. */ readonly zonesPublished: readonly ZoneName[]; } /** * One row of a zone's section index, capability-annotated for the current actor. * Returned by {@link EthosZone.index} — unlike {@link EthosZone.sections} (which * yields only decrypted content), this lists EVERY section in the zone, including * the ones this actor can't open, so a UI can render them (e.g. greyed out). */ export interface SectionIndexEntry { readonly id: string; /** * Clear title when known: always for `public`/`circle` (plaintext index), and * for `self` only for sections this actor can decrypt (the `self` index is * sealed). Absent for a sealed section this actor can't open. */ readonly title?: string; readonly tags?: readonly string[]; /** This actor can decrypt the section's content (it appears in `sections()`). */ readonly readable: boolean; /** This actor's mandate authorizes editing this section (owner: always true). */ readonly writable: boolean; } type ActorOwner = { readonly kind: "owner"; readonly subjectDid: string; readonly signers: OwnerSigners; }; type ActorDelegate = { readonly kind: "delegate"; readonly subjectDid: string; readonly actor: DelegateActor; }; type ActorAnonymous = { readonly kind: "anonymous"; readonly subjectDid: string; }; type Actor = ActorOwner | ActorDelegate | ActorAnonymous; export declare class EthosClient { #private; readonly subjectDid: string; readonly mode: Actor["kind"]; constructor(actor: Actor); /** Return the per-zone proxy. */ zone(name: ZoneName): EthosZone; hasPendingChanges(): boolean; pendingChanges(): readonly StagedChange[]; discard(): void; /** * Build and publish a new edition with all staged mutations applied. * Throws if there's nothing staged. After a successful publish, the * mutation buffer is cleared and any cached snapshot is invalidated * so the next read picks up the fresh edition. */ publish(): Promise; /** * Re-author the current edition to pick up the owner's CURRENT delegate grants: * sections newly covered by a mandate get the delegate's wrap **added** (a cheap * re-wrap — body ciphertext unchanged, zero blob upload), and sections whose * grants were revoked are re-encrypted. No content change is staged. * * This is what makes "issue a mandate → the delegate can immediately read the * granted sections" work in one step: granting authorises, but only a (re)seal * adds the delegate as a cryptographic recipient. Owner-only; returns `null` * if the subject has no published edition yet. * * Pass the mandate(s) you JUST minted via `opts.includeMandates` (the * `mandate` field of {@link MintedMandate}): the server's `list_mandates` * index is eventually consistent, so a mint-then-immediately-reseal that * relies on the crawl alone can miss the brand-new mandate and silently not * seal the delegate in. In-hand mandates are merged over the crawled grants. * * `opts.mode` selects the recipient policy for carried sections: * - `"additive"` (default) — never removes a recipient: revoked residue is * kept (the server gates its reads), new grants are appended via a cheap * re-wrap. Zero re-encryption, can't jam on lingering revoked wraps. * - `"rotate"` — the explicit hard-cut ("Rotate keys"): sections whose * recipient set shrank are re-encrypted under a fresh DEK, cutting * removed delegates cryptographically. O(sections-to-rotate) reads. */ /** * EXPLICIT v0.3 → v0.4 migration (spec Partie II N10). One edition: blobs * carried by sha (zero re-upload/re-encryption), active zone grants carried * into the keyring, self titles re-sealed under their section DEKs. Owner * only. No-op (null) when the subject is already v0.4 or has no edition. * Irreversible: the platform refuses later v0.3 editions (-32045). */ migrateToV04(): Promise; reseal(opts?: { readonly includeMandates?: readonly SignedMandate[]; /** v0.4 adds "rotate-deep": new DEKs + re-encrypted bodies (the strong * cryptographic erasure). On v0.3 subjects it behaves like "rotate". */ readonly mode?: "additive" | "rotate" | "rotate-deep"; }): Promise; /** * Targeted seal — the cheap successor of the global {@link reseal} for the * mandate-creation flow: seals ONLY the given in-hand mandate(s) into the * sections their scopes cover, via the `grants:"extras-only"` fast path. * * - ZERO mandate crawl (no list_mandates / get_mandate round-trips — the * publish scales with the ethos, not with how many mandates exist); * - ZERO blob upload (additive wrap append, manifest-only edition); * - race-proof by construction (never depends on the eventually-consistent * mandates index seeing the fresh row). * * Returns `null` when there's nothing to do: no published edition yet, or * none of the mandates carries a sealable read grant (no grantee pubkey / * no read-bearing scope). Owner-only. */ sealGrant(mandates: SignedMandate | readonly SignedMandate[]): Promise; /** * Wrap pruning — the periodic metadata GC of the additive doctrine. Finds the * owner's DEAD mandates (revoked, or expired by `not_after`), and publishes a * manifest-only edition dropping their wraps from every carried section. * * Explicitly NOT a cryptographic cut (an ex-delegate may have memorised the * DEK; the server gate blocks its reads; `reseal({mode:"rotate"})` is the * crypto cut). What it buys: the manifest stops growing without bound and * stops advertising the full history of past delegates. * * Cheap by design: the dead-mandate crawl only fetches DEAD mandates' * details, the local manifest scan skips the publish entirely when no dead * wrap is present (returns `null` — the steady-state outcome), and the * publish itself uploads zero blobs. Fire-and-forget friendly (e.g. once * per owner session). Owner-only. */ pruneWraps(): Promise; /** * Idempotently ensure the subject's Ethos has at least one published * edition. Required because a **delegate** cannot bootstrap a first * edition (the first edition's manifest is signed with the owner's * public-sphere key, which delegates do not have). Without an initial * owner-published edition, subsequent delegate writes via * {@link publish} fail with `not found: edition for did:…`. * * Semantics: * - If an edition already exists (owner OR delegate OR anonymous mode), * this is a NO-OP and returns `{ alreadyInitialized: true }`. * - If no edition exists AND the actor is the owner, this stages and * publishes a height=1 edition containing a single sentinel section * `aithos-init` in the `public` zone, then returns * `{ alreadyInitialized: false, editionHeight: 1 }`. Any previously * staged mutations on this client are preserved and NOT auto-flushed. * - If no edition exists AND the actor is NOT the owner (delegate or * anonymous), throws `ethos_bootstrap_not_owner` — only the owner * can sign a first edition. * * Call site: typically the owner's dashboard, right after sign-in and * before any delegate-mode write (e.g. before triggering a backend * worker that holds a mandate). Idempotent ⇒ safe to call on every * mount. * * Implementation note: this routes through {@link #publishFirstEditionOwner} * which the SDK already uses internally when {@link publish} detects a * fresh Ethos with staged owner mutations. ensureInitialized() exposes * the same code path as an explicit primitive, so the caller doesn't * need to stage a mutation just to trigger first-edition logic. */ ensureInitialized(): Promise<{ alreadyInitialized: true; } | { alreadyInitialized: false; editionHeight: number; manifestHash: string; }>; _readZone(zone: ZoneName): Promise; _stageAdd(zone: ZoneName, input: AddSectionInput): void; _stageUpdate(zone: ZoneName, sectionId: string, patch: UpdateSectionPatch): void; _stageDelete(zone: ZoneName, sectionId: string): void; /** * Capability-annotated index for a zone: EVERY persisted section (including * ones this actor can't decrypt), each tagged readable/writable for the * current actor. Drives a UI that shows inaccessible sections (e.g. greyed). * Reflects the PERSISTED edition only — staged mutations are not applied here * (use `sections()` for the effective, staged view of readable content). */ _readIndex(zone: ZoneName): Promise; /** Read + decrypt ONE persisted section on demand (cached). Drives the lazy * "open a section to load it" UI. Staged edits are NOT applied — the editor * holds those in its own form state until publish. */ _readSection(zone: ZoneName, sectionId: string): Promise
; /** * INTERNAL (SdkStorage). The cached manifest + per-zone RAW index rows * (`section_id` / `title?` / `tags?` / `title_hidden` / `gamma_ref`) — the * protocol shape, unlike {@link _readIndex} which projects a UI-oriented * view. `null` when the subject has no edition yet. Same lazy snapshot the * other readers use: one network round-trip, invalidated by publish. */ _indexSnapshot(): Promise<{ readonly manifest: ManifestV03; readonly index: Record; } | null>; } export declare class EthosZone { #private; constructor(parent: EthosClient, name: ZoneName); get name(): ZoneName; /** Effective sections (persisted + staged mutations applied). */ sections(): Promise; /** * Capability-annotated index of EVERY persisted section in this zone — the ones * this actor can decrypt AND the ones it can't — each tagged `readable` / * `writable`. Use this (rather than {@link sections}) to render a complete * view where inaccessible sections appear greyed/locked. For `self`, sealed * sections this actor can't open come back without a `title`. */ index(): Promise; /** * Read + decrypt ONE section on demand (lazy) — `null` if it's absent from the * index or this actor can't decrypt it. Prefer this over {@link sections} to * avoid loading every body: render {@link index} first, then open sections as * the user clicks them. Bodies are cached until the next publish. */ section(sectionId: string): Promise
; addSection(input: AddSectionInput): void; updateSection(sectionId: string, patch: UpdateSectionPatch): void; deleteSection(sectionId: string): void; /** * Return every section in this zone whose `title` is exactly `title`. * * Match is exact and case-sensitive. The result is always an array — it * may be empty (no match), have one element (the typical case), or have * more than one element when the author has happened to publish two * sections with the same title. Section titles are not required by the * protocol to be unique within a zone. * * The order of returned sections is the zone's authored order * (`sections()` ordering, spec §2.5.2). * * @param title Section title to look up — exact, case-sensitive. */ findSectionsByTitle(title: string): Promise; /** * Stage an update for **every** section in this zone whose `title` * matches `title` exactly. Returns the list of section IDs that were * staged — empty when nothing matched. * * Apply with `client.publish()` like any other staged mutation. The * staged entries are identical to what `updateSection(id, patch)` would * produce, one per matched section, so `pendingChanges()` / * `discard()` behave normally. * * Note: this method does NOT throw when there is no match — it returns * `[]`. That's intentional: callers driven by an LLM frequently want to * upsert (try update, then fall back to add) and shouldn't have to * catch. * * @param title Section title to look up — exact, case-sensitive. * @param patch Same patch shape accepted by `updateSection`. * @returns Array of `section.id` strings whose updates were staged. */ updateSectionsByTitle(title: string, patch: UpdateSectionPatch): Promise; /** * Stage a delete for **every** section in this zone whose `title` * matches `title` exactly. Returns the list of section IDs that were * staged — empty when nothing matched. * * Same semantics as {@link updateSectionsByTitle}: silent on no-match, * apply with `client.publish()`. * * @param title Section title to look up — exact, case-sensitive. * @returns Array of `section.id` strings whose deletes were staged. */ deleteSectionsByTitle(title: string): Promise; } export interface EthosNamespaceDeps { readonly auth: AithosAuth; readonly endpoints: AithosSdkEndpoints; readonly fetch: typeof fetch; } export declare class EthosNamespace { #private; constructor(deps: EthosNamespaceDeps); /** * EthosClient for the currently signed-in owner. Throws if there is * no owner — callers should check `auth.canSignAsOwner()` first or * surface the error to the user as "please sign in". */ me(): EthosClient; /** * EthosClient for an arbitrary subject DID. The mode is resolved at * construction time: * - if `did` matches the currently signed-in owner → owner mode * - else if a mandate held by `auth` covers this subject → delegate mode * - else → anonymous read-only mode * * Async signature so future implementations may do an eager manifest * fetch (e.g. to fail fast on unknown DIDs); today resolution is sync * and the actual fetch happens on the first `sections()` / `publish()` * call. */ of(did: string): Promise; } export {}; //# sourceMappingURL=ethos.d.ts.map