Skip to content

feat(payloads): streaming Payloads client + primitive payloads CLI#308

Open
poiley wants to merge 7 commits into
mainfrom
payloads/streaming-cli
Open

feat(payloads): streaming Payloads client + primitive payloads CLI#308
poiley wants to merge 7 commits into
mainfrom
payloads/streaming-cli

Conversation

@poiley

@poiley poiley commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

Adds first-class support for Primitive Payloads — large, content-addressed, end-to-end-encrypted objects (attachments up to ~1 TB) — to the Node SDK and CLI.

  • @primitivedotdev/sdk/payloads — a streaming client:
    • pushFile(path, opts) chunks (64 MiB), encrypts each chunk (per-chunk HKDF-derived AES-256-GCM), content-addresses it (SHA-256), commits a Merkle manifest, and uploads — in bounded memory (a multi-GB file never loads fully into RAM). Transient 429/5xx are retried with backoff.
    • pullFile(root, out, opts) streams down, verifies each chunk against its content address, and decrypts — one chunk at a time.
    • encodeManifest(bytes, opts) — pure, deterministic manifest computation (precompute a content address / convergent encoding / conformance testing).
  • CLI: primitive payloads push <file> and primitive payloads pull <root> --cek <hex> --out <file>.

These routes are openapi: false server-side, so this is hand-written rather than generated.

Design notes

  • The chunk/crypto/Merkle object model is carried in the SDK and bundled inline — the same pattern the SDK already uses for the private @primitivedotdev/api-core, so nothing new is published to npm and there's no low-level package next to the SDK/CLI. (For E2E encryption the client must run this code anyway.)
  • To guarantee the client can't silently drift from the server's object model, a conformance vector generated from the server reference implementation pins the SDK's output byte-for-byte (test-fixtures/payloads/conformance-vector.json + sdk-node/tests/payloads/conformance.test.ts). It fails on any change to the crypto/hashing/Merkle construction — verified it fails on a one-character KDF change.

Testing

  • Conformance: SDK encodeManifest reproduces the server-generated manifest byte-for-byte (per-chunk hashes + Merkle root).
  • Offline client tests (fake in-memory server): push/pull round-trip (multi-chunk, single-chunk, exact chunk-boundary, zero-byte), the transient-503 retry path, and download integrity (a corrupted chunk is rejected).
  • Live: a 5 GiB round-trip against staging (push → pull → SHA-256 match), completing with the V8 heap capped at 384 MiB (proves the object is never resident).
  • make node-check and make cli-check green locally (biome incl. the new module, typecheck, full suites: SDK 715, CLI 726). shared-check node fixtures unaffected.

Depends on

The server-side Payloads datapath (schema, routes, capability-token downloads, chunk-size guard) — merged to staging in the API mono-repo.

poiley added 5 commits July 14, 2026 12:45
…push/pull (CLI)

@primitivedotdev/sdk/payloads: streaming, bounded-memory push/pull for large
content-addressed, end-to-end-encrypted objects. Chunk/manifest construction is
byte-compatible with the server object model (64 MiB chunks, per-chunk HKDF
AES-256-GCM, SHA-256 content addressing, Merkle manifest). Two-pass push keeps
memory flat regardless of object size; transient 429/5xx are retried with
backoff. CLI adds payloads push/pull on top.

Local/unpublished: verified a 5 GiB round-trip against staging (integrity OK).
Exposes the chunk/crypto/Merkle construction as a pure function for precomputing
an object's content address, convergent encoding, and conformance-testing
against the server object model. A deterministic dual-encode (same cek/objectId)
against payloads-core produced a byte-identical manifest — the vendored client
primitives are verified byte-compatible with the reference implementation.
Golden vector generated from the server reference implementation
(@primitivedotdev/payloads-core in the mono-repo) pins the SDK's vendored
chunk/crypto/Merkle construction to the server's. encodeManifest must reproduce
the fixture manifest byte-for-byte; any drift in HKDF/AES-GCM, content hashing,
or Merkle layout fails the build — catching interop/dedup breakage before it
ships. Runs with the normal SDK test suite; verified it fails on a one-char KDF
change.
Fake in-memory server exercises the SDK client with no network: push/pull
round-trip (multi-chunk, single-chunk, exact chunk boundary, zero-byte), the
transient-503 retry path (previously unexercised), and the download integrity
check (a corrupted chunk rejects). Closes the retry/edge-case coverage gaps
flagged in the confidence review.
Comment thread sdk-node/src/payloads/index.ts Fixed
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge — the three issues flagged in earlier review rounds are all addressed and the crypto implementation is correct.

The streaming client handles the full lifecycle correctly: deterministic re-encryption makes the two-pass upload safe, the Merkle root check closes the content-address loop on download, write-stream errors are surfaced via a raced promise rather than left as uncaught events, and failed downloads leave no plaintext on disk. The conformance vector and offline test suite cover the critical paths thoroughly.

sdk-node/package.json — lint:fix script omits src/payloads/index.ts; all other files look good.

Important Files Changed

Filename Overview
sdk-node/src/payloads/index.ts Core streaming payloads client — chunk/crypto/Merkle implementation is byte-compatible with the server, previously flagged issues (manifest root consistency check, partial-file cleanup, and write-stream error surfacing) are all addressed in this revision.
cli-node/src/oclif/commands/payloads.ts New push/pull CLI commands; auth resolution, progress reporting, and JSON output look correct.
sdk-node/tests/payloads/client.test.ts Offline round-trip, retry, integrity, manifest-tamper, and write-error paths all covered with an in-memory server mock.
sdk-node/tests/payloads/conformance.test.ts Golden-vector test pins SDK's HKDF/AES-GCM/Merkle output byte-for-byte against the server reference implementation.
sdk-node/package.json Adds ./payloads export and updates lint; lint:fix omits src/payloads/index.ts (minor inconsistency).
test-fixtures/payloads/conformance-vector.json Server-generated golden vector for 200 KB input — well-documented, contains expected per-chunk hashes and Merkle root.

Reviews (3): Last reviewed commit: "fix(payloads): robust write-stream lifec..." | Re-trigger Greptile

Comment thread sdk-node/src/payloads/index.ts
Comment thread sdk-node/src/payloads/index.ts Outdated
…clean partial file on failed pull

- apiRoot trimmed trailing slashes with /\/+$/ on caller-supplied input
  (CodeQL js/polynomial-redos, high). Replace with a linear scan.
- pullFile now recomputes the Merkle root from the manifest's chunk hashes and
  rejects a manifest that doesn't match the requested content address, and
  removes a partial output file if the download fails mid-stream.
- Tests for the manifest-root-mismatch rejection and partial-file cleanup.
Comment thread sdk-node/src/payloads/index.ts Outdated
Attach an 'error' listener and race writes/end against it so an async disk-write
failure (disk full, EIO, bad path) surfaces as a rejection instead of an
uncaught error that crashes the process — and destroy the stream + remove the
partial file on any failure. Adds a write-error test (invalid output path).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants