/**
 * Client-side (zero-knowledge) encryption for session transcripts before they
 * leave this machine for R2.
 *
 * R2 encrypts objects at rest server-side (AES-256, Cloudflare default), but
 * that key is Cloudflare's — anyone with bucket-read access (or Cloudflare
 * itself) can read the plaintext. Transcripts carry secrets, tokens, and
 * absolute file paths, so "encrypted at rest by the provider" is not enough. We
 * seal each transcript BODY client-side with AES-256-GCM under a key that never
 * leaves the machines that share the sync bundle; Cloudflare only ever stores
 * ciphertext.
 *
 * The key is a 32-byte secret (`R2_SYNC_ENC_KEY`) held in the same
 * keychain-backed `r2.backups` bundle as the R2 credentials. Every machine in
 * the sync fabric shares that bundle, so every machine derives the identical key
 * and can decrypt its peers' objects. The key is deliberately SEPARATE from the
 * R2 access key so that rotating the R2 token (RUSH-1464) never orphans
 * transcripts already encrypted under the old one.
 *
 * Identity for CRDT merge stays over PLAINTEXT: the manifest hash is computed on
 * the cleartext transcript (sync.ts), and pull decrypts before the G-Set union
 * (crdt.ts) ever sees the bytes. Ciphertext is non-deterministic (a fresh random
 * IV per seal), so it is never usable as an identity — which is exactly why the
 * manifest, not the object body, carries the hash.
 */
import type { R2Config } from './config.js';
/** Serialized envelope stored as the R2 object body when encryption is on. */
export interface TranscriptEnvelope {
    /** Envelope format version. */
    v: 1;
    alg: 'aes-256-gcm';
    /** base64 12-byte GCM nonce, fresh per object. */
    iv: string;
    /** base64 ciphertext. */
    ct: string;
    /** base64 16-byte GCM auth tag. */
    tag: string;
}
/**
 * Decode the configured `R2_SYNC_ENC_KEY` into a 32-byte key, or null when the
 * bundle does not carry one (encryption off — see pushOwn's warning path).
 *
 * Accepts hex (64 chars) or base64; both must decode to exactly 32 bytes. A key
 * that is present but the wrong length THROWS rather than silently truncating —
 * a malformed key is a configuration bug, not a reason to fall back to a weaker
 * or wrong key.
 */
export declare function resolveSyncEncKey(cfg: Pick<R2Config, 'syncEncKey'>): Buffer | null;
/** Generate a fresh 32-byte transcript key, base64-encoded (for provisioning). */
export declare function generateSyncEncKey(): string;
/** Seal a transcript body. Returns the serialized envelope to store in R2. */
export declare function encryptTranscript(plaintext: string, key: Buffer): string;
/**
 * Parse a stored object body into an envelope, or null when it is not one.
 *
 * A plaintext transcript is NDJSON — many JSON objects, one per line — so it
 * never parses as a single object carrying our `v`/`alg`/`ct`/`tag` fields. That
 * makes envelope-vs-plaintext detection unambiguous and lets a puller read BOTH
 * encrypted objects and any legacy plaintext already in the bucket (the beta
 * uploaded plaintext before this landed). This is format-version handling for a
 * real migration, not a "just in case" fallback.
 */
export declare function parseEnvelope(body: string): TranscriptEnvelope | null;
/** True when a stored object body is one of our encryption envelopes. */
export declare function isTranscriptEnvelope(body: string): boolean;
/** Open a sealed envelope. Throws on a wrong key / tampered body (GCM tag mismatch). */
export declare function decryptEnvelope(envelope: TranscriptEnvelope, key: Buffer): string;
/**
 * Return the plaintext transcript for a fetched object body, transparently
 * decrypting when it is an envelope.
 *
 *  - Envelope + key  → decrypted plaintext.
 *  - Envelope + no key → throws (the object is encrypted but this machine has no
 *    key to read it — surfacing that beats silently mis-merging ciphertext).
 *  - Plaintext body  → returned verbatim (legacy/unencrypted object).
 */
export declare function decryptTranscriptBody(body: string, key: Buffer | null): string;
