import { AppendSegmentResult, type Checkpoint, type Manifest, type ReplicaHeads, type RevocationPins, type Segment } from '@cleocode/contracts/cloud'; import { type KeyPair } from './crypto.js'; import { type FetchLike, type Http, NexusError } from './http.js'; import { type TrustedSigners } from './keys.js'; import { type SegmentMetaFields } from './signing.js'; export type Uploader = (url: string, headers: Record, bytes: Buffer) => Promise; /** Download `url`, refusing (and no longer reading) once more than `maxBytes` arrive. */ export type Downloader = (url: string, maxBytes: number) => Promise; /** * Why the client refused what the server sent. Every refusal carries one in `details.reason`, so the * caller (and the tests) can tell the checks apart. None of them is retryable as is. */ export type RefusalReason = 'unknown-signer' | 'bad-signature' | 'hash-mismatch' | 'blob-hash' | 'blob-size' | 'decrypt' | 'payload' | 'seq-order' | 'replica-device' | 'replica-replay' | 'replica-gap' | 'page' | 'checkpoint-stream' | 'checkpoint-rollback' | 'checkpoint-replicas' | 'revoked-signer' | 'too-large'; /** * Whether `err` is this client refusing what the server sent (status 0, a {@link RefusalReason}), as * opposed to a server or network error. * * @param err - Anything thrown. * @returns True for a client refusal. */ export declare const isClientRefusal: (err: unknown) => err is NexusError; /** An attachment's ciphertext size is its plaintext size plus this ({@link SEAL_OVERHEAD}). */ export declare const AEAD_OVERHEAD: number; /** * Whether a refusal proves the recorded content wrong, not just its delivery: the ciphertext matched * its announced sha256 and still failed the AEAD or the plaintext sha256. A size or hash mismatch of the * download (a flaky proxy, a lying server) or a client cap proves nothing about the record. * * @param err - Anything thrown. * @returns True for `decrypt` and `hash-mismatch` refusals. */ export declare const provesContentWrong: (err: unknown) => boolean; /** The default downloader: https only, no redirects, and it stops reading past `maxBytes`. */ export declare function cappedDownloader(fetchImpl: FetchLike): Downloader; export type SegmentMeta = SegmentMetaFields; export interface PulledSegment { seq: number; replicaId: string; replicaSeq: number; deviceId: string; plaintext: Buffer; meta: SegmentMeta; } /** * Where a replica's pull stands. The caller persists it in the same local transaction that applies the * pulled ops, and passes it back to the next pull. It is what lets the client refuse replayed, * reordered, re-attributed and gapped segments across pages and restarts. */ export interface PullCursor { /** The last stream seq applied. The next pull asks for segments strictly after it. */ after: number; /** * True when `replicas` lists every replica with a segment at or below `after`: the cursor started at * genesis, or was seeded from a checkpoint's signed replica map (cursorFromCheckpoint). A replica first * seen later must then start at replicaSeq 0. False only for a cursor built some other way. */ knowsAllReplicas: boolean; /** Per replica: the device that signs its segments (pinned at first sight) and the last replicaSeq applied. */ replicas: Record; } export declare const initialPullCursor: () => PullCursor; /** A cursor that resumes right after a (verified) checkpoint, seeded from its signed replica map. */ export declare function cursorFromCheckpoint(cp: Pick): PullCursor; /** * The pins for revoking `deviceId`'s key: the last replicaSeq of each of its replicas in the given * cursors, and the highest coversSeq of its checkpoints in the given (already verified) lists. Compute * them from pulls this client verified itself, with the key trusted as pending (certifiedSigners with * includePending), before signing the revocation. */ export declare function revocationPins(deviceId: string, streams: readonly { streamId: string; cursor: PullCursor; checkpoints?: readonly Checkpoint[]; }[]): RevocationPins; export interface JournalOptions { http: Http; streamId: string; replicaId: string; deviceId: string; signing: KeyPair; /** The stream data key: a project data key (PDK) or the home stream key (HSK). */ key: Buffer; uploader?: Uploader; downloader?: Downloader; /** Used by the default uploader and downloader. Defaults to the global fetch. */ fetch?: FetchLike; /** The largest segment blob a pull will download. Default 256 MiB. */ maxSegmentBlobBytes?: number; } /** sha256 of the canonical segment metadata. Covered by the segment signature and the ciphertext AAD. */ export declare const segmentMetaHash: (m: SegmentMetaFields) => string; /** sha256 of the canonical manifest. Covered by the checkpoint signature. */ export declare const manifestHash: (m: Manifest) => string; /** sha256 of the canonical replica map. Covered by the checkpoint signature. */ export declare const replicasHash: (r: ReplicaHeads) => string; /** * One replica's view of one journal stream. The caller owns durable state (`nextReplicaSeq`, the * PullCursor and the highest checkpoint coversSeq seen) and must persist it in the same local * transaction that records what was sent or applied. */ export declare class Journal { private readonly o; private readonly up; private readonly down; private readonly base; private readonly maxSegmentBlobBytes; constructor(o: JournalOptions); /** * Encrypt, sign and append one segment of ops: segment/v2, or segment/v3 when `meta` carries * `txnDeltas` (journal spec §2.11). Safe to retry with the same replicaSeq, metadata and `sealed` * bytes (from sealSegment): re-encrypting would change the hash and defeat idempotency. */ push(replicaSeq: number, plaintext: Uint8Array, meta: SegmentMeta, sealed?: Buffer): Promise; /** * Encrypt a segment without sending it, so the caller can persist the exact bytes before pushing. * The AAD covers the metadata hash, so the same `meta` must be passed to push. */ sealSegment(replicaSeq: number, plaintext: Uint8Array, meta: SegmentMeta): Buffer; /** * Pull, verify and decrypt the segments after `cursor.after`. Nothing unverified is ever returned for * applying: the pull stops with a NexusError (see RefusalReason) when a segment * - is not strictly after the previous one (replayed or reordered: `seq-order`); * - is signed by a device outside `trustedSigners` (`unknown-signer`), or its signature fails over the * stream, replica, device, position, ciphertext hash and metadata hash (`bad-signature`); * - belongs to a replica pinned to another device (`replica-device`), repeats a replicaSeq * (`replica-replay`) or skips one (`replica-gap`); * - is signed by a revoked key past its revocation pin (`revoked-signer`); * - has bytes that do not match its hash (`hash-mismatch`, `blob-hash`, `blob-size`) or fail to decrypt. * Returns the next cursor; persist it with the applied ops, together with the TrustState returned by the * certifiedSigners call that built `trustedSigners`, in the same local transaction. Nothing in core does * this yet; the caller must. */ pull(cursor: PullCursor, trustedSigners: TrustedSigners, limit?: number): Promise<{ segments: PulledSegment[]; head: number; cursor: PullCursor; }>; private verifyAndOpen; private decrypt; /** * Encrypt, upload and sign a checkpoint bundle, then register it. The client mints the checkpoint id, * because the signature and the bundle's AAD both cover it. `cursor` is this replica's pull cursor at * the point the bundle captures: it gives coversSeq and the signed replica map, so it must know every * replica. The signature is checkpoint/v2, or checkpoint/v3 for a manifest with the §2.11 accounting * fields ({@link manifestVersion}). The server verifies the signature and the replica map, and refuses * lineage breaks and regressions. A retry of the same checkpoint returns it again. */ pushCheckpoint(args: { bundle: Uint8Array; manifest: Manifest; cursor: PullCursor; parentCheckpointId: string | null; /** * The id to register it under, minted by the caller so it can be recorded * before the upload (a genesis push adopts its own stored checkpoint after * a crash by this id, T13302). A fresh uuidv7 when absent. */ checkpointId?: string; }): Promise; /** * Check a checkpoint record: it belongs to this stream, and a trusted key signed every field that * matters (ids, lineage, coversSeq, manifest, replica map, bundle hash and size), under the domain its * manifest's format picks (checkpoint/v2 or /v3, so v3 fields can be neither dropped nor added after * signing). Accepted signers: * - the author, with a live key, or with a revoked key whose pin for this stream covers coversSeq; * - otherwise, any live device that endorsed (re-signed) it. */ verifyCheckpoint(cp: Checkpoint, trustedSigners: TrustedSigners): void; /** * Re-sign (endorse) a checkpoint with this device's key, after checking it against `trustedSigners`, * for example so that a checkpoint written by a device about to be revoked stays restorable without its pin. */ endorseCheckpoint(cp: Checkpoint, trustedSigners: TrustedSigners): Promise; /** * Download, verify and decrypt a checkpoint bundle (point-in-time restore). `minCoversSeq` is the * highest checkpoint coversSeq this replica has already seen (the caller persists it): an older * checkpoint is refused, so the server cannot roll the replica back. The returned cursor resumes the * pull after the checkpoint, seeded from its signed replica map. */ restoreCheckpoint(checkpointId: string, trustedSigners: TrustedSigners, opts?: { minCoversSeq?: number; }): Promise<{ bundle: Buffer; checkpoint: Checkpoint; cursor: PullCursor; }>; private uploadBlob; /** * Download a segment stored as a blob. The blob may have been uploaded by another member, so it is * fetched through the stream (`GET …/segments/:seq/blob`), not the caller's own blob scope. At most the * announced size is read, and the bytes must match that size and the segment's sha256. */ downloadSegmentBlob(s: Pick): Promise; /** The stream path of an attachment's ref: an HMAC of its plaintext sha256, so the server learns nothing. */ private attachmentPath; /** * Put a content-addressed attachment (a doc body) on the stream (T13721): encrypted under the stream * key, bound to the stream and its plaintext sha256, uploaded as an `attachment` blob and recorded * under its ref (E32). A ref already recorded is checked like a pull would check it: a good one is not * uploaded again; a junk one (a squat, a buggy client) is cleared (E34) and replaced with these bytes. * * @param sha256 - The plaintext sha256 the synced row names. * @param plaintext - The bytes; they must hash to `sha256`. * @returns `sent` when this call recorded it, `present` when the stream already had a good copy, * `contested` when the ref holds another blob this caller may not clear, or a racer recorded first, * and `unverified` when the recorded blob could not be checked (a size or hash fault of its * download): nothing is cleared, and the next push checks it again. */ pushAttachment(sha256: string, plaintext: Buffer): Promise<'sent' | 'present' | 'contested' | 'unverified'>; /** * Read a content-addressed attachment from the stream (T13721), uploaded by any member (E33). The * ciphertext must match the announced size and sha256, decrypt under the stream key in this stream * and sha256's context, and hash to `sha256`; anything else is refused before a byte is returned. * * @param sha256 - The plaintext sha256 the synced row names. * @param maxBytes - The largest ciphertext to download. * @returns The verified plaintext, or null when no member has put it on the stream yet. */ pullAttachment(sha256: string, maxBytes: number): Promise; /** * Whether some member has recorded a blob for an attachment (E33), without downloading it. * * @param sha256 - The plaintext sha256 the synced row names. * @returns True when a record exists. */ attachmentRecorded(sha256: string): Promise; /** * Clear an attachment's record (E34), so a holder of the real bytes can record them again. Allowed * for the member who recorded it and for a project owner; the server refuses anyone else. * * @param sha256 - The plaintext sha256 the synced row names. * @returns True when this call removed a record. */ clearAttachment(sha256: string): Promise; /** Download and verify the blob behind an attachment ref; any mismatch is a client refusal. */ private readAttachment; } //# sourceMappingURL=journal.d.ts.map