@saga-sync/core
v0.1.4
Published
Shared kernel for saga-sync: manifest schema, crypto, and dependency-free stores
Readme
@saga-sync/core
The shared kernel of saga-sync — the reference implementation of
privacy-protocol-state-distribution. It holds the manifest
schema, the crypto (sha256 digests + Ed25519 signing), and the dependency-free
Store abstraction that both the producer and the consumer build on.
Both sides depend on core so the manifest/chunk shape and the digest algorithm are defined exactly once:
client → core producer → core (no runtime edge client ↔ producer)Consumers install @saga-sync/client, which pulls this in; producers
install @saga-sync/producer. You rarely depend on core directly.
Entry points
@saga-sync/core— the browser-safe barrel (nonode:imports, noBuffer):Manifest,ChunkMeta/ManifestDatatypes,sha256Hex, the signing helpers,CanonicalEvent,Hex,Store,HttpStore.@saga-sync/core/node— re-exports the Node-onlyDiskStore(atomic local-filesystem persistence). Kept out of the browser bundle.
The Store seam
Store is the one abstraction for persistence. Keys are flat object names; the
interface is async because HTTP/S3 backends are inherently async.
interface Store {
put(key: string, data: Uint8Array): Promise<void>; // atomic — no partial reads
get(key: string): Promise<Uint8Array | null>; // null if absent
delete(key: string): Promise<void>; // no error if absent
list(prefix: string): Promise<string[]>; // keys under a prefix
}DiskStore(/node) — backed by a base directory.putwrites${file}.${pid}.tmpthenrenames over the target; the rename is atomic on a single filesystem, so a crash mid-write never leaves a partial object. Used by the producer (local output) and the client CLI (chunk cache).HttpStore— read-only, backed by a base URL:getfetches${baseUrl}/${key}, returningnullon 404;put/delete/listthrow. The consumer read-side; pairs with a CDN-fronted bucket. Fetch-based, so browser-safe.
The producer's GcsStore and the factory that selects a backend live in
@saga-sync/producer, not here — core stays dependency-light.
Manifest
Manifest wraps a Store and owns index.json — the index the producer writes
and the client reads. It holds the manifest in memory and persists atomically on
mutation. The normative on-the-wire schema is in the root
SPEC.md §3.1; this is the class API.
- Reads:
sealedChunks(id),hotHead(id),protocolIds(),firstCoveredBlock(id)/lastCoveredBlock(id),gaps(id), plus the metadata accessorsprotocolName/protocolMetadata/chainId/trackedAddresses/trackedEventTopics. - Mutations (serialized through an internal mutex):
appendChunk,setHotHead,clearHotHead, and the write-oncesetProtocolMeta(id, …)— it fills only fields still unset, soprotocolMetadatais immutable per stream once first written. - Format:
MANIFEST_VERSION = 1. The format is still in development, so theversionfield is informational —fromRawreads theavailableProtocolsshape and does not gate on the number (a manifest carrying an older development stamp is read and transparently re-stamped on the next write).persist()serializes keys sorted and coalesces/throttles writes (so the bytes stay order-independent and under a GCS object's write-rate limit);flush()forces any pending write to land. - Signing (optional): construct with a
signerand everypersist()also writes a detachedindex.json.sigover the exact serialized bytes.
Crypto
hash.ts—sha256Hex(bytes)— the one place the digest algorithm is named; producer and consumer both go through it so digests agree. Pure@noble- its own hex (no
Buffer), so it runs unchanged in a browser.
- its own hex (no
signing.ts— the one place Ed25519 lives.signManifest(bytes, secret)/verifyManifestSignatureover@noble/curves;signerFromEnv()builds a signer from theMANIFEST_SIGNING_KEYenv var (a 32-byte hex seed). A detached signature over the rawindex.jsonbytes authenticates the publisher; because the manifest holds every chunk's digest, one signature transitively authenticates the whole dataset. Opt-in on both ends.
Shared types
events.ts—CanonicalEvent— the persisted log shape: all-lowercase0x-hexcontractAddress,eventTopic(=topics[0]),topics[],data,blockNumber,logIndex. The producer'snormalize()writes it; the client reconstructs it.hex.ts—Hex— the`0x${string}`alias replacing viem'sHex, so core (and client) carry no viem dependency.
Dependencies
@noble/hashes + @noble/curves only — both pure-JS and isomorphic. No viem, no
@google-cloud/storage, no node: imports on the . entry.
