import { type Identity, type DidDocument } from "./identity.js"; import { type Sphere } from "./did.js"; import { type Mandate, type Revocation, type SectionScope } from "./mandate.js"; import { type GammaEntry } from "./gamma.js"; import { type Author } from "./author.js"; export declare function ethosDir(handle: string): string; export declare function ethosZoneDir(handle: string, zone: Sphere): string; export declare function ethosZoneFile(handle: string, zone: Sphere): string; export declare function ethosHistoryDir(handle: string): string; export declare function ethosManifestPath(handle: string): string; /** * A section in the live ethos document. Spec §2.5.1. * * No embedded revision history — every mutation is a signed entry in the * gamma log (spec §10). The `gamma_ref` field names the latest gamma entry * that produced the current state of this section, so readers can always * trace a visible field back to the signed mutation that authored it. */ export interface Section { id: string; title: string; body: string; tags?: string[]; gamma_ref: string; } export interface ZoneDoc { sections: Section[]; } export type Zones = Record; /** * Signature carried by a zone manifest entry or the top-level manifest. * * `key` is either a sphere DID URL (owner signature) or a multibase-encoded * Ed25519 public key (delegate signature). When signed by a delegate, * `authorized_by` is set to the issuing mandate's id so verifiers can * resolve the delegate pubkey against a local mandate file. */ export interface ZoneSignature { alg: "ed25519"; key: string; value: string; authorized_by?: string; } export interface ManifestSignature { alg: "ed25519"; key: string; value: string; authorized_by?: string; } export interface ZoneManifest { file: string; encrypted: boolean; sha256_of_plaintext: string; section_titles: string[]; cipher?: ZoneCipher; signature: ZoneSignature; } export interface ZoneWrap { recipient: string; alg: "x25519-hkdf-sha256-aead"; ephemeral_public: string; wrap_nonce: string; wrapped_key: string; } export interface ZoneCipher { alg: "xchacha20poly1305-ietf"; nonce: string; wraps: ZoneWrap[]; } /** * Anchor to the gamma deep-memory log (spec §10). * * Each new edition snapshots the log's current head hash and length so the * signed manifest commits to the state of the log (spec §10.3.5 / §10.7). * Off-box delivery (e.g. an S3 URL for the encrypted .jsonl.enc) can * populate `url`; otherwise the log lives purely local. */ /** * A party authorized to read gamma entries in the v0.3 format. * * On every v0.3 append, the writer seals the entry's per-entry symmetric key * to each reader in this list (using each reader's X25519 public key). Readers * added after an entry was appended cannot decrypt that entry — seal is * forward-only. Subject sphere keys are always present; delegate readers are * added by grant when a mandate carries `gamma.read`. */ export interface GammaReader { /** Stable identifier — `did:aithos:*#` for subject, `urn:aithos:agent:*` for delegates. */ recipient: string; /** Multibase-encoded X25519 public key this reader decrypts with. */ pubkey: string; /** Mandate id that authorized this delegate reader. Absent for subject spheres. */ via_mandate?: string; /** ISO-8601 timestamp the reader was added. Informative. */ added_at: string; } export interface GammaManifestAnchor { head: string | null; count: number; url?: string; /** * v0.3+ — recipients sealed into every future entry's envelopes list. * OPTIONAL for backward compatibility with v0.2 manifests. A v0.3 writer * populates this on every edition. */ readers?: GammaReader[]; } /** * Manifest version. Bumped to 0.2.0 for the gamma cutover: sections no * longer carry embedded revisions[], and `gamma` is REQUIRED as soon as the * ethos has any section (every section is born from a gamma entry). */ export declare const AITHOS_VERSION: "0.2.0"; export type AithosVersion = typeof AITHOS_VERSION; /** * v0.3 bundle marker (per-section encryption, spec draft * `bundle-v0.3-per-section-encryption.md`). v0.2 remains the write default; * v0.3 is opt-in via the per-section path in `bundle-v03.ts`, selected at the * manifest level by `aithos: "0.3.0"` + per-zone `format_version: "v2"`. */ export declare const AITHOS_VERSION_V03: "0.3.0"; export type AithosVersionV03 = typeof AITHOS_VERSION_V03; export interface Manifest { aithos: AithosVersion; bundle_id: string; subject_did: string; subject_handle: string; display_name: string; edition: { version: string; created_at: string; supersedes: string | null; prev_hash: string | null; height: number; }; zones: Record; gamma?: GammaManifestAnchor; integrity: { sha256_of_did_json: string; manifest_signature: ManifestSignature; }; } export declare function ensureEthosLayout(handle: string): void; export declare function readManifest(handle: string): Manifest; export declare function writeManifest(handle: string, m: Manifest): void; export interface RenderContext { subjectDid: string; subjectHandle: string; editionVersion: string; createdAt: string; } /** * Document-form → markdown form, per spec §2.6.1 (revised for v0.2.0). * * Each section is a heading whose HTML comment carries the section id AND * its gamma_ref — the id of the latest gamma entry affecting the section. * The body follows as plain markdown. No per-revision blocks; the gamma * log holds the signed history. */ export declare function renderZoneMarkdown(zone: Sphere, doc: ZoneDoc, ctx: RenderContext): string; /** * Markdown form → document form. Parses the v0.2.0 layout: * `# <!-- <sec_id> · <gamma_ref> -->` * optional `<!-- tags: [...] -->` * body until the next `# ` heading. */ export declare function parseZoneMarkdown(markdown: string, expectedZone: Sphere): ZoneDoc; export interface EncryptedZone { ciphertext: Uint8Array; cipher: ZoneCipher; } export declare function encryptZone(plaintext: string, subjectDid: string, recipients: Array<{ did: string; x25519PublicKey: Uint8Array; }>): EncryptedZone; export declare function decryptZone(ciphertext: Uint8Array, cipher: ZoneCipher, subjectDid: string, myDidUrl: string, myX25519Secret: Uint8Array): string; /** * Seal a 32-byte DEK to a single recipient via X25519-HKDF-SHA256-AEAD * (spec §3.6). Exported so the v0.3 per-section path (`bundle-v03.ts`) reuses * the identical, audited wrap construction rather than re-deriving it — the * wrap shape is grain-agnostic (a DEK is a DEK, whether per-zone or * per-section), so the only thing that differs in v0.3 is the AEAD AAD over * the section body, not the key-wrapping. */ export declare function wrapDek(dek: Uint8Array, recipientDidUrl: string, recipientPk: Uint8Array): ZoneWrap; /** Inverse of {@link wrapDek}. Exported for reuse by the v0.3 per-section path. */ export declare function unwrapDek(wrap: ZoneWrap, mySk: Uint8Array): Uint8Array; export declare function subjectRecipientFor(identity: Identity, zone: "circle" | "self"): { did: string; x25519PublicKey: Uint8Array; x25519Secret: Uint8Array; }; /** * Stable wrap-list label for a delegate's DEK entry. * * The `did` field on a ZoneWrap / GammaWrap is just a unique key for the * recipient — lookup is by string equality, not DID resolution. For owner * wraps we use `did:aithos:...#kex-<zone>`. For delegate wraps we use * `<grantee.id>#<delegate-pubkey-multibase>`, which is stable across * editions while remaining distinguishable from any sphere wrap. */ export declare function delegateWrapDid(granteeId: string, pubkeyMultibase: string): string; /** * Recipients to include when an author writes (and re-encrypts) an encrypted * zone. * * - Owner: their own sphere X25519 pubkey (subject-as-recipient, §3.5.1) * PLUS every active (non-revoked) delegate whose mandate covers this zone. * - Delegate: BOTH the owner's sphere X25519 pubkey (so the owner can * decrypt when the bundle returns) AND the delegate's own X25519 pubkey. * Other delegates are not re-added by a delegate-side write: the delegate * can't reconstruct their pubkeys without consulting their mandate files, * which exist on disk only for authors who ran `issueMandateWithRewrap`. * * Revoked mandates are filtered out so a post-revocation re-render excludes * the delegate from the recipient set — which is exactly what * `repinAfterRevocation` leverages. */ export declare function authorZoneWriteRecipients(subject: Identity | Author, zone: "circle" | "self", subjectDid?: string): Array<{ did: string; x25519PublicKey: Uint8Array; }>; /** * Enumerate delegate recipients authorised to read/write a given zone of a * specific subject. Returns { did: "<granteeId>#<pubkeyMb>", x25519PublicKey } * pairs, excluding any mandate that has a matching local revocation. * * This is the cornerstone of `issueMandateWithRewrap` / `repinAfterRevocation`: * the live mandate directory is the source of truth for who can decrypt the * current edition. */ export declare function activeDelegatesForZone(subjectDid: string, zone: Sphere): Array<{ did: string; x25519PublicKey: Uint8Array; }>; /** * A delegate's recipient grant on a zone, carrying its optional `section_scope` * (companion draft `bundle-v0.3-section-level-mandates.md`). A grant with no * `sectionScope` covers the whole zone; a scoped grant covers only the sections * matching it. Used by the v0.3 author path to wrap each section's DEK to the * delegates entitled to that specific section (§3.5.4′). */ export interface DelegateGrant { did: string; x25519PublicKey: Uint8Array; /** The mandate's full scope set — lets the caller evaluate per-section * readership via {@link coversRead} (v0.5 per-scope selectors). */ scopes: string[]; /** Legacy top-level `section_scope` (§4.7′). When present it narrows the * whole-zone read/write scopes uniformly; combined with `scopes` by the * caller (recipient iff coversRead AND sectionMatchesScope). */ sectionScope?: SectionScope; } /** * Like {@link activeDelegatesForZone} but also returns each grant's * `section_scope`, so the caller can decide per-section who is entitled. */ export declare function activeDelegateGrantsForZone(subjectDid: string, zone: Sphere): DelegateGrant[]; /** * Recipient (did label + X25519 secret) an author uses to DECRYPT a zone * they're allowed to read. Owner → sphere secret. Delegate → their own * Ed25519-derived X25519 secret. */ export declare function authorZoneDecryptRecipient(subject: Identity | Author, zone: "circle" | "self"): { did: string; x25519Secret: Uint8Array; }; /** * Allocate a fresh `YYYY.MM.DD-N` version given the current manifest (if any). * Scans history/ to avoid colliding with prior editions. */ export declare function allocateEditionVersion(handle: string, now?: Date): string; /** * Sign the top-level manifest. * * Owner path: signed with the subject's public sphere key (unchanged). * Delegate path: signed with the delegate Ed25519 seed; the resulting * `manifest_signature` carries `authorized_by = mandate.id` so verifiers can * resolve the delegate pubkey against the issuing mandate. * * The canonical bytes include the `key` and `authorized_by` fields (with * `value` blanked), so the signature binds to both the signer identity and * the mandate it claims authority under — an attacker cannot swap * `authorized_by` post-facto without invalidating the signature. */ export declare function signManifest(subject: Identity | Author, m: Manifest): Manifest; export interface VerifySignatureOpts { /** * Resolver for delegate public keys when the signature carries * `authorized_by`. Returns the raw 32-byte Ed25519 public key or throws. * The resolver is expected to also validate the mandate (signature + time * window + scope) before returning; if any of those checks fail, it must * throw. */ resolveDelegatePubkey?: (keyId: string, mandateId: string) => Uint8Array; } export declare function verifyManifestSignature(m: Manifest, didDoc: DidDocument, opts?: VerifySignatureOpts): { ok: boolean; error?: string; }; /** sha256 hex of the canonical form of the manifest with blanked sig — the prev_hash anchor. */ export declare function canonicalManifestHashHex(m: Manifest): string; /** * Sign a zone document. * * Owner path: the sphere key matching the zone. Delegate path: the delegate * Ed25519 seed; the returned signature carries `authorized_by = mandate.id`. * Enforces `ethos.write.<zone>` scope + validity window before signing. */ export declare function signZone(subject: Identity | Author, zone: Sphere, doc: ZoneDoc): ZoneSignature; export declare function verifyZoneSignature(doc: ZoneDoc, sig: ZoneSignature, didDoc: DidDocument, opts?: VerifySignatureOpts): { ok: boolean; error?: string; }; export declare function newSectionId(): string; export declare function snapshotDidJson(handle: string): { path: string; hashHex: string; content: string; }; export declare function subjectHandleFromManifest(m: Manifest): string; export declare function loadZoneDoc(handle: string, zone: Sphere, who?: Identity | Author, manifest?: Manifest): ZoneDoc; /** * Return the raw plaintext markdown for a zone (decrypted if necessary), * without parsing it into a `ZoneDoc`. Used by `verifyEthos` to hash the * exact bytes that were written at edition-creation time, instead of * re-rendering them — re-rendering embeds the current manifest's edition * version in the frontmatter, which breaks hash equality for zones that * were carried forward from a previous edition. * * Returns "" for a zone that has no on-disk file (empty ethos). */ export declare function loadZonePlaintext(handle: string, zone: Sphere, who?: Identity | Author, manifest?: Manifest): string; export declare function writeZoneToDisk(handle: string, zone: Sphere, doc: ZoneDoc, subject: Identity | Author, ctx: RenderContext, subjectDid: string): { sha256Hex: string; cipher?: ZoneCipher; signature: ZoneSignature; sectionTitles: string[]; }; /** * Persist a new edition: re-render the zone(s) the author is authorised on, * rebuild the manifest, sign it, archive the previous manifest under history/. * * Owner path: all three zones are re-rendered every edition (previous * behaviour). Delegate path: only `author.mandate.actor_sphere` is re-rendered * and re-signed — other zones carry their previous manifest entry forward * unchanged, because the delegate has no authority to re-sign them. */ export declare function persistEdition(handle: string, subject: Identity | Author, zones: Zones, opts?: { now?: Date; prevManifest?: Manifest | null; /** * v0.3 — override the `gamma.readers` recorded in the new manifest * anchor. Use cases: * - `issueMandateWithRewrap`: add a delegate reader when the mandate * carries `gamma.read`. * - `repinAfterRevocation`: filter out revoked delegate readers. * * When omitted, the new anchor carries forward `prevManifest.gamma.readers` * (or bootstraps from the owner's sphere keys on a fresh v0.3 identity). */ gammaReadersOverride?: GammaReader[]; }): Manifest; /** * Shared shape for delegate-signer args across add / modify / delete. * A delegate is an agent's Ed25519 keypair authorized by a write mandate * (`ethos.write.<zone>`). The mandate's grantee.pubkey MUST equal * `keyMultibase`; the seed is used to sign the gamma entry directly. */ export interface DelegateSigner { mandateId: string; keySeed: Uint8Array; keyMultibase: string; } export interface AddSectionArgs { handle: string; /** * Owner identity (legacy shape) OR Author (v0.2.1). Exactly one of * `identity` / `author` must be set; if both are set, `author` wins. * The `delegate` signer is the v0.2.0-style shim and still works for * owner-local callers that already hold their own delegate keypair. */ identity?: Identity; author?: Author; zone: Sphere; title: string; body: string; tags?: string[]; delegate?: DelegateSigner; at?: Date; } /** * Append a new section to a zone. * * Flow: * 1. Emit a signed `section.add` gamma entry carrying the full title/body/tags. * 2. Use that entry's id as the section's `gamma_ref`. * 3. Add the section to the in-memory zone doc and persist a new edition. * * The gamma entry is appended FIRST so the edition's signed manifest already * commits to the updated `gamma.head` (spec §10.3.5). A crash between steps * 2 and 3 leaves the log ahead of the live doc — the next edition will * catch up. */ export declare function addSection(args: AddSectionArgs): { section: Section; manifest: Manifest; gammaEntry: GammaEntry; }; export interface ModifySectionArgs { handle: string; /** Owner identity (legacy) OR v0.2.1 Author. Exactly one must be set. */ identity?: Identity; author?: Author; zone: Sphere; sectionId: string; /** New title. Omit to keep the existing title. */ title?: string; /** New body. Omit to keep the existing body. */ body?: string; /** * New tag set. Omit to keep existing tags. To CLEAR tags, pass []. * Any array (including []) is treated as the authoritative replacement. */ tags?: string[]; delegate?: DelegateSigner; at?: Date; } /** * Apply an in-place modification to a section. * * Semantics (spec §10.6.1, option (a)): * - The payload of the emitted `section.modify` entry carries the FULL * new value of each field being changed — not a diff. Readers replay * the log by applying each payload as a straight replacement. * - At least one of {title, body, tags} MUST be provided. * - The section's `gamma_ref` is updated to point at the new entry. * * The prior `section.add` (and any earlier `section.modify` entries) remain * immutable in the log — that's the audit trail. */ export declare function modifySection(args: ModifySectionArgs): { section: Section; manifest: Manifest; gammaEntry: GammaEntry; }; export interface DeleteSectionArgs { handle: string; /** Owner identity (legacy) OR v0.2.1 Author. Exactly one must be set. */ identity?: Identity; author?: Author; zone: Sphere; sectionId: string; /** Free-text reason; stored in the gamma entry payload for audit. */ reason?: string; delegate?: DelegateSigner; at?: Date; } /** * Remove a section from its zone AND record the removal as a `section.delete` * entry in the gamma log. * * After this call: * - the current edition no longer contains the section (pack/install sees * it as if it never existed in the live doc), * - the gamma log retains the original `section.add` entry AND a new * `section.delete` entry, both signed and hash-chained, * - `manifest.gamma.head` is updated to the new delete entry's hash. * * Past editions archived under `ethos/history/` are unchanged — they still * reference the section by its titles and by the prior `gamma.head`, so the * edition chain remains byte-identical to what it was when that manifest was * signed. */ export declare function deleteSection(args: DeleteSectionArgs): { manifest: Manifest; gammaEntry: GammaEntry; deletedTitle: string; }; /** * Load every zone the author can see. * * Owner: all three zones (public + circle + self). * * Delegate: only the mandate's `actor_sphere` is decrypted through the * delegate wrap. The other two zones come back as empty docs — we do NOT * decrypt them under the delegate key (they aren't wrapped for the delegate * anyway) because a delegate operating on a tracked install has no * authority to re-sign them. `persistEdition` carries those zones' manifest * entries forward from the previous manifest untouched. */ export interface IssueMandateWithRewrapArgs { handle: string; /** Owner of the subject — must hold the sphere seeds of the mandate's zone. */ identity: Identity; /** Freshly-issued mandate. Assumed to already be on disk (writeMandate). */ mandate: Mandate; at?: Date; } /** * Make a freshly-issued mandate effective on the current encrypted state. * * The mandate alone is only a signed grant on paper — it doesn't change the * zone ciphertext. A delegate handed the bundle as-is couldn't decrypt * their zone because their X25519 pubkey isn't on the DEK wrap list yet. * This function repairs that: it re-renders the subject's zones under a * recipient set that now includes the delegate. * * v0.3 (gamma): the gamma log is NOT rewrapped. Every gamma entry is sealed * per-entry to a fixed recipient set at append time, and the rewrap idea * from v0.2 (single DEK for the whole log → every delegate gets full * history) is gone by design. Instead, if the mandate carries `gamma.read`, * we add the grantee to `manifest.gamma.readers` so that FUTURE gamma * entries will be sealed to them. Past entries remain unreadable — the * protocol only grants forward-looking read access. * * Without `gamma.read` in the mandate's scopes, the grantee never appears * on any gamma envelope. A write-only delegate can append correct, signed * entries using only `manifest.gamma.readers` (the public reader list) + * their Ed25519 seed — no plaintext ever crosses their process. * * Owner-only: the subject must hold the sphere seeds. Call AFTER * `writeMandate(mandate)` so that `activeDelegatesForZone` can see the * live mandate. */ export declare function issueMandateWithRewrap(args: IssueMandateWithRewrapArgs): Manifest; export interface RepinAfterRevocationArgs { handle: string; identity: Identity; revocation: Revocation; at?: Date; } /** * Rotate the DEKs so a revoked delegate can no longer decrypt the current * edition. The revocation must already be on disk (writeRevocation) so that * `activeDelegatesForZone` / the gamma rewrap helper omits the revoked key * from the new recipient set. * * Owner-only. Note this does NOT affect previously-shipped bundles — those * ciphertexts remain decryptable by the delegate. The protocol's safety * boundary is "from this edition onward". */ export declare function repinAfterRevocation(args: RepinAfterRevocationArgs): Manifest; export interface PackEthosToDirArgs { handle: string; /** Owner identity (legacy shape) OR v0.2.1 Author. `author` wins if both set. */ identity?: Identity; author?: Author; /** Destination directory — will be created if it doesn't exist. */ outDir: string; } /** * Copy the installed-ethos layout into the flat bundle layout that * `installBundleFromDir` and `verifyBundleAtPath` understand: * * <outDir>/ * ├── manifest.json * ├── did.json * ├── public.md * ├── circle.md.enc (if present) * ├── self.md.enc (if present) * └── gamma.jsonl.enc (if present) * * `author` is accepted for API symmetry with the mutation calls; today the * pack operation only touches on-disk bytes (no re-signing), so any valid * author is fine. We just need exactly one of `identity` / `author` so the * call shape matches the rest of the Author-taking APIs. */ export declare function packEthosToDir(args: PackEthosToDirArgs): void; export interface InstallBundleFromDirArgs { bundleDir: string; /** Local handle to install under. */ as: string; /** * Overwrite an existing install when true. Preserves `*.sealed.json` seed * files so an owner can re-install their own bundle without losing keys. */ force?: boolean; } /** * Import a flat bundle directory (as produced by `packEthosToDir`) into the * local keystore as handle `as`. Result is a tracked install: `did.json` and * `ethos/*` present, but no sealed seed files — unless an owner install * already exists at that handle and `force: true` was passed, in which case * the existing sealed seeds are preserved. */ export declare function installBundleFromDir(args: InstallBundleFromDirArgs): void; export declare function loadAllZones(handle: string, who: Identity | Author): Zones; export interface VerifyEthosResult { ok: boolean; errors: string[]; warnings: string[]; } /** * Build a delegate-key resolver that consults the local keystore. Looks up the * mandate keyed by `authorized_by`, re-verifies its signature against the DID * doc, enforces the local revocation list, and returns the raw Ed25519 public * key to verify against. Signatures from an unknown, revoked, or mis-scoped * mandate cause the resolver to throw, which the calling verify-* function * surfaces as a "delegate key resolution failed" error. * * Pass this to {@link verifyManifestSignature} / {@link verifyZoneSignature} * (and {@link verifyBundleAtPath}) any time the receiving keystore has the * mandates installed locally. */ export declare function keystoreDelegateResolver(didDoc: DidDocument): (keyId: string, mandateId: string) => Uint8Array; /** * Verify an installed ethos. * * In v0.2.0 the history of a section lives in the gamma log, not in the * live document. So section-level integrity has two layers: * * - **Live view**: the rendered zone markdown must hash to the value * declared in the manifest, and every section in the live doc must * name a `gamma_ref` that exists in the gamma log with a matching * section id. * - **Mutation history**: the gamma log must be self-consistent (every * entry's hash/signature verifies, every link chains to the previous * entry's hash) and the manifest's `gamma.head` / `gamma.count` must * agree with the log's actual tail. * * The deeper gamma-log walk (per-entry signature + chain) is performed by * `verifyGammaLog` from `gamma.ts`; this function only checks the light * anchor-vs-log consistency required by spec §10.7 (light tier). The CLI * wires in the full walk separately. */ export declare function verifyEthos(handle: string, identity: Identity | null, didDoc: DidDocument): VerifyEthosResult;