/** * S3-compatible segment store (SPEC.md §5.1 cache semantics, §5.4 * delegated presign) — AWS S3, Cloudflare R2, MinIO. Zero dependencies: * hand-rolled SigV4 over `fetch` (sigv4.ts). * * Key layout (deterministic — every lookup is a GET/HEAD, never a LIST): * * {keyPrefix}seg/sha256/{hex} * The segment bytes, verbatim (the object body MUST be exactly the * content-addressed bytes so presigned GETs serve them directly and * the client's §5.1 hash check passes). The full `SegmentRecord` * (minus bytes) rides along as object user metadata * `x-amz-meta-syncular-record` = base64url(JSON), so `get` is a * single GET. * * {keyPrefix}find/{sha256Hex(canonical reuse key)}.json * Whole-table reuse pointer (§5.3): written only when * `rowCursor === null`, body = the record JSON. The reuse key is the * canonical JSON array `[partition, table, schemaVersion, mediaType, * scopeDigest, asOfCommitSeq]`, so `find` is one GET (plus a HEAD to * confirm the segment object itself still exists — lifecycle GC may * remove objects independently of pointers). * * TTL mapping: expiry is **store-side and authoritative** — `expiresAtMs` * (put-time + `ttlMs`, default 24 h) is recorded in the object metadata * and the pointer; `get` returns expired records so §5.5 can answer * `sync.segment_expired`, and `find` filters them out itself. S3 lifecycle * expiration is garbage collection only: configure it comfortably ABOVE * `ttlMs` (e.g. 2 days for the 24 h default) so clients normally see the * precise, retryable `sync.segment_expired` and hit `sync.not_found` only * long after. Never set lifecycle below `ttlMs`. */ import type { SegmentFindKey, SegmentMetadata, SegmentRecord, SegmentStore, SegmentStoreStats } from './segment-store.js'; import type { DelegatedPresignConfig, SegmentUrlIssue } from './signed-url.js'; export interface S3SegmentStoreConfig { /** * Endpoint origin, no bucket, no trailing slash: * AWS `https://s3..amazonaws.com`, * R2 `https://.r2.cloudflarestorage.com`, * MinIO `http://127.0.0.1:9000`. Requests are path-style * (`{endpoint}/{bucket}/{key}`), which all three accept. */ readonly endpoint: string; /** AWS region; R2 uses `auto`. */ readonly region: string; readonly bucket: string; readonly accessKeyId: string; readonly secretAccessKey: string; /** Optional STS session token. */ readonly sessionToken?: string; /** Key namespace inside the bucket, e.g. `syncular/`. Default: none. */ readonly keyPrefix?: string; /** Segment TTL (§5.1 cache semantics). Default 24 h. */ readonly ttlMs?: number; } export declare class S3SegmentStore implements SegmentStore { #private; constructor(config: S3SegmentStoreConfig); /** `{keyPrefix}seg/sha256/{hex}` — the §5.4 "key embeds the segmentId". */ objectKeyFor(segmentId: string): string; put(metadata: SegmentMetadata, bytes: Uint8Array, nowMs: number): Promise; get(segmentId: string): Promise<{ record: SegmentRecord; bytes: Uint8Array; } | undefined>; find(key: SegmentFindKey, nowMs: number): Promise; /** * Store-wide counters from the pointer-object accumulator (never a LIST). * Marked `approximate: true`: the counters are maintained by a best-effort * ETag-CAS read-modify-write on `put`, so a crash between the segment PUT * and the accumulator write, or lifecycle GC that deletes objects the * accumulator still counts, can drift them. Exact within a single writer; * a health gauge under concurrency. See the README "S3 stats". */ stats(): Promise; /** * SigV4 presigned GET for a segment object (§5.4 delegated presign). * The signed key embeds the `segmentId` (equivalence rule) and * `urlExpiresAtMs` is the provider-enforced expiry * (`X-Amz-Date + X-Amz-Expires`). TTL SHOULD be ≤ 15 minutes (§5.4); * default 900 s. */ presignSegmentGet(segmentId: string, options?: { readonly ttlSeconds?: number; readonly nowMs?: number; }): Promise; } /** * Wire an `S3SegmentStore` into `SyncServerConfig.signedUrls` as the §5.4 * delegated-presign scheme: * `signedUrls: s3PresignedUrls(store, { ttlSeconds: 900 })`. */ export declare function s3PresignedUrls(store: S3SegmentStore, options?: { readonly ttlSeconds?: number; }): DelegatedPresignConfig;