Skip to content

feat(antd-js): manifest API (REST) - #348

Merged
Nic-dorman merged 5 commits into
mainfrom
nic/v2-1580-manifests-js
Oct 19, 2026
Merged

Nic-dorman merged 5 commits into
mainfrom
nic/v2-1580-manifests-js

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Part of the manifest API fan-out. Stacked on #346 (the manifest endpoints in antd and antd-go), which sits on the pointer API #326; review those first.

RestClient gains antd's ADR-0006 manifest surface, mirroring antd-go:

  • buildManifest(entries, { name?, torrentInfoHashes?, embedRoots? })
    -> BuiltManifest { manifest, link, entries }
  • decodeManifest(manifest, resolve = false) and
    decodeManifestLink(link, resolve = false) -> DecodedManifest
    { source, name, torrentInfoHashV1, torrentInfoHashV2, entries }
  • ManifestEntryInput { path?, size?, dataMap?, address? } and
    ManifestEntryInfo { effectiveName, path, size, kind, contentAddress,
    dataMap, exactSize, error }

The types and wire conversion live in a new src/manifests.ts; RestClient
only adds the three methods after the Files section, and index.ts exports
the types. The SDK is REST-only, so there is no gRPC side.

The .ant bytes are a Buffer both ways, and decodeManifest also takes a
plain Uint8Array (encoded as a view, not copied). REST carries them as
standard base64 in manifest; callers never see it. A build response
whose manifest is not padded standard base64 throws InternalError rather
than letting Node's lenient decoder return the wrong bytes.

Sizes are number, the SDK's size type. Unset is null, never 0: REST
omits an unset size or exact_size, and both read as null, while a 0 is
sent and read as 0. A size the daemon sends that is not a non-negative
integer throws InternalError. Every other unset value is null as well:
path, name, the torrent hashes, error, the link when it would pass 1,500
bytes, and dataMap on public entries and on every built entry.

embedRoots defaults to true and is always sent as embed_roots, so an
empty options object embeds roots. An entry with both dataMap and
address or neither (an empty string counts as unset), or a size that is
not a non-negative safe integer, throws BadRequestError naming
entries[i] before anything is sent, as antd-go's checkManifestEntries
does; so do empty manifest bytes and an empty link. The daemon's 400 is
the existing BadRequestError with its message kept, and a 404 or 502
while resolving a shrunk DataMap keeps its NotFoundError or NetworkError.
No new error classes. Nothing here uploads or pays; the README says so,
and that build and resolve read the network and are safe to retry.

Tests: 32 unit tests over a mocked fetch in src/manifests.test.ts cover
the wire body, the base64 round trip (including + and /, a Uint8Array
view and malformed base64), unset sizes reading as null with 0 kept, the
both-or-neither check and size check before sending, the embed_roots
default, a file link decode, and 400/404/502 mapping. The suite is 140
passing, with typecheck and build clean on Node 18. The example also
typechecks against a scratch tsconfig.

src/live-manifests.test.ts is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES. Run on its own against a manifest-enabled
daemon on a 10-node devnet, it passed 4 of 4: a build from the two
fixture addresses and a DataMap (kinds, sorted effective names, content
addresses, null sizes, the embedded root's exact size, a link); resolving
decodes of the bytes and the link agreeing, with the fixture sizes and an
embedded DataMap that dataGet downloads; the fixture link decoding, and
rebuilding it from its decoded entries giving the same link; and
BadRequestError for both sources, for "../x" (the daemon's message names
entries[1]) and for bytes that are not a manifest.

examples/09-manifests.ts (optional ANTD_BASE_URL) ran clean against the
same daemon: two public uploads, a 184-byte manifest named example with a
link, notes/one.txt and notes/two.txt resolving to 31 and 48 bytes.

Overlap with this SDK's pointer PR #332: a trial merge is clean.

Version bumps happen at release time; nothing here bumps a package version.

🤖 Generated with Claude Code

Nic-dorman and others added 4 commits October 15, 2026 14:13
RestClient gains antd's ADR-0006 manifest surface, mirroring antd-go:
- buildManifest(entries, { name?, torrentInfoHashes?, embedRoots? })
  -> BuiltManifest { manifest, link, entries }
- decodeManifest(manifest, resolve = false) and
  decodeManifestLink(link, resolve = false) -> DecodedManifest
  { source, name, torrentInfoHashV1, torrentInfoHashV2, entries }
- ManifestEntryInput { path?, size?, dataMap?, address? } and
  ManifestEntryInfo { effectiveName, path, size, kind, contentAddress,
  dataMap, exactSize, error }

The types and wire conversion live in a new src/manifests.ts; RestClient
only adds the three methods after the Files section, and index.ts exports
the types. The SDK is REST-only, so there is no gRPC side.

The `.ant` bytes are a Buffer both ways, and decodeManifest also takes a
plain Uint8Array (encoded as a view, not copied). REST carries them as
standard base64 in `manifest`; callers never see it. A build response
whose manifest is not padded standard base64 throws InternalError rather
than letting Node's lenient decoder return the wrong bytes.

Sizes are number, the SDK's size type. Unset is null, never 0: REST
omits an unset size or exact_size, and both read as null, while a 0 is
sent and read as 0. A size the daemon sends that is not a non-negative
integer throws InternalError. Every other unset value is null as well:
path, name, the torrent hashes, error, the link when it would pass 1,500
bytes, and dataMap on public entries and on every built entry.

embedRoots defaults to true and is always sent as embed_roots, so an
empty options object embeds roots. An entry with both dataMap and
address or neither (an empty string counts as unset), or a size that is
not a non-negative safe integer, throws BadRequestError naming
entries[i] before anything is sent, as antd-go's checkManifestEntries
does; so do empty manifest bytes and an empty link. The daemon's 400 is
the existing BadRequestError with its message kept, and a 404 or 502
while resolving a shrunk DataMap keeps its NotFoundError or NetworkError.
No new error classes. Nothing here uploads or pays; the README says so,
and that build and resolve read the network and are safe to retry.

Tests: 32 unit tests over a mocked fetch in src/manifests.test.ts cover
the wire body, the base64 round trip (including + and /, a Uint8Array
view and malformed base64), unset sizes reading as null with 0 kept, the
both-or-neither check and size check before sending, the embed_roots
default, a file link decode, and 400/404/502 mapping. The suite is 140
passing, with typecheck and build clean on Node 18. The example also
typechecks against a scratch tsconfig.

src/live-manifests.test.ts is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES. Run on its own against a manifest-enabled
daemon on a 10-node devnet, it passed 4 of 4: a build from the two
fixture addresses and a DataMap (kinds, sorted effective names, content
addresses, null sizes, the embedded root's exact size, a link); resolving
decodes of the bytes and the link agreeing, with the fixture sizes and an
embedded DataMap that dataGet downloads; the fixture link decoding, and
rebuilding it from its decoded entries giving the same link; and
BadRequestError for both sources, for "../x" (the daemon's message names
entries[1]) and for bytes that are not a manifest.

examples/09-manifests.ts (optional ANTD_BASE_URL) ran clean against the
same daemon: two public uploads, a 184-byte manifest named example with a
link, notes/one.txt and notes/two.txt resolving to 31 and 48 bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refusal the client makes before sending and the daemon's 400 for the
same mistake are now indistinguishable: the same error, the same status
and the same message.

buildManifest collects every bad entry and throws BadRequestError with
statusCode 400 and the daemon's aggregated text: "{n} invalid entries:
entries[i]: give data_map or address, not both; entries[j]: needs a
data_map or an address", 0-based, joined with "; ", at most 20 listed
and then "; and {k} more". The wording is the daemon's (data_map, not
dataMap) with no SDK prefix, and an empty string counts as unset. The
SDK's own size check stays, with the same type, reported in the same
form (entries[i]: size N is not a non-negative integer) and counted in
the same list.

decodeManifest with empty bytes and decodeManifestLink with an empty link
throw BadRequestError (400) with "needs a manifest (base64 .ant bytes) or
a link". Response-parsing failures (bad base64, malformed sizes) are
unchanged and still InternalError.

Tests: the unit tests now assert type, status and exact message,
including two bad entries at once, the 20-entry cap and both empty-decode
cases; 142 pass with typecheck and build clean. The live test, run on its
own, passes 4 of 4 and now sends the both-sources request past the client
check to the daemon and checks its 400 reads "Bad request: " plus the
client's message exactly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brings up the CI fixes already on the repin branch (#325): rustls 0.23.46 from main (#344, RUSTSEC-2026-0346..0349) and the notices generator reading .licenses/ (c226153, for granit-parser). No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings up #325's move from the rc.1 pin to the released ant-core 0.13.0 (ant-cli-v0.3.11) and ant-protocol 5.0.0, via #326. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman marked this pull request as ready for review October 16, 2026 09:07
@Nic-dorman
Nic-dorman changed the base branch from nic/v2-1570-antd-manifest-endpoints to main October 16, 2026 14:33

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE: #348 — JavaScript REST

Reviewed head: 4dcde18c0821b62a00b4e7e0833bd37465c6a9dd. Original base: 4cf292a4761fe07e0db82ba270c177382a4ef7b5.

No material blocker remains after the source-backed adjudication below.

Actual panel coverage: 4/6 scoped opinions — partial panel

Independent first passes used GLM 5.3/OpenRouter, local Qwen 3.8 and local DS4 Flash, two roles per evidence scope. A narrowed retry replaces a missing opinion; it is not an extra vote. This is not full-team consensus.

  • source-1 / correctness: GLM 5.3 UNCERTAIN; Qwen 3.8 APPROVE; DS4 Flash unavailable after same-model retry (Incomplete termination; unavailable seat)
  • source-1 / adversarial: GLM 5.3 CHANGES_REQUIRED; Qwen 3.8 APPROVE; DS4 Flash unavailable after same-model retry (Incomplete termination; unavailable seat)

Evidence and dissent adjudication

GLM correctness was UNCERTAIN and adversarial requested changes because it assumed proto-JSON string sizes. This endpoint does not use proto-JSON: axum Json serializes the Serde DTO with Option fields (types.rs:909-933; rest/manifests.rs:97-114). The parent compiled the actual DTO from #346 and verified absent, zero, 123 and u64-max output: sizes are JSON numbers, not strings. The DTO is identical in this checkout. That refutes the proposed type mismatch; it does not turn GLM into an approval. Empty/malformed-response leniency is nonblocking against this server contract. JavaScript number precision is not a full-u64 guarantee.

Source authority: actual daemon DTO and REST serializer. These are Serde REST fields; a proto uint64 declaration alone does not establish proto-JSON serialization on this endpoint.

Verified checks and limits

Re-ran Vitest: 142 passed, 4 live tests skipped; tsc --noEmit exited 0. No paid/live daemon test.
Head CI: {"SUCCESS": 5}. The live head was rechecked unchanged. The base moved to ea389c79c2ac40601c574e93031a66a9791db1d2: the reviewed diff is byte-identical, no moved-base path overlaps the PR, merge base is unchanged, and git merge-tree succeeds. This proves a clean trial merge, not integrated runtime behaviour.

Scope

Panel production/example fragments: antd-js/examples/09-manifests.ts; antd-js/src/index.ts; antd-js/src/manifests.ts; antd-js/src/rest-client.ts.
Not sent as full diff fragments to the panel: antd-js/README.md [docs]; antd-js/src/live-manifests.test.ts [tests]; antd-js/src/manifests.test.ts [tests]. Separate source-worker review covers the full recorded file inventory; that worker is not a named-model vote. Test execution above is separate from panel inspection. Checked wire base64/raw-byte boundaries, unset versus zero, default root embedding, input validation, endpoint paths, exports and transport limits where applicable. No live payment, deployment, merge or Linear state change.

The daemon sends manifest entry `size` and `exact_size` as u64 JSON
numbers. JSON parsing rounds any integer past Number.MAX_SAFE_INTEGER, so
`optionalSize`, which only checked Number.isInteger, returned
9007199254740993 as 9007199254740992 with no error. The recorded size is
the creator's claim, so no real petabyte file is needed to hit it.

Accept only safe integers, and throw InternalError naming the field for
anything larger, as the build-input path already does. Regression: a raw
response body (not JSON.stringify, which would round first) with
9007199254740993 in `size`, then in `exact_size`, must throw. 0,
9007199254740991, and absent or null sizes keep reading exactly.

Requested by Hermes on #367 (P2, data integrity).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — focused re-review of fcdf824

Reviewed exact head: fcdf824235d7ba8ab6d40f5268ff0e4e5e50d6ef. Follow-up delta: 4dcde18c0821b62a00b4e7e0833bd37465c6a9dd..fcdf824235d7ba8ab6d40f5268ff0e4e5e50d6ef; only antd-js/src/manifests.ts and antd-js/src/manifests.test.ts changed.

The JavaScript manifest-size blocker is fixed. No material blocker remains in this follow-up. This approves the code change, not merge readiness; the inherited security-audit gate below remains unresolved.

Verified fix

  • optionalSize at lines 141–149 now returns only non-negative safe integers. Unsafe positive integers throw InternalError naming size or exact_size, rather than returning the rounded JSON number.
  • Both fields use that guard through entryFromJson; build, byte-decode and link-decode all use this shared response conversion. 0, Number.MAX_SAFE_INTEGER, omitted and null values retain the expected results.
  • The committed regressions use raw response text containing 9007199254740993 for each field, so JSON.stringify cannot round the fixture before the test. Explicit rejection satisfies the previously requested remedy; this is not full-u64/BigInt support.
  • Non-blocking note from DS4: the comment overstates rounding above the safe range; some values, including 2^53, are exactly representable. Rejecting them is still the deliberate safe-integer policy, because an unsafe parsed number cannot establish the original wire value. This wording nuance does not reopen the blocker.

Actual review participation: 3/3 focused reviews

GLM 5.3 (z-ai/glm-5.3, OpenRouter), local Qwen 3.8 (qwen38-27b) and local DS4 Flash (deepseek-v4-flash) independently reviewed the same frozen follow-up evidence. All three returned APPROVE; no material dissent or missing seat. Each reviewed correctness and adversarial boundary cases in one focused follow-up role. This is not a new six-role audit of the whole PR, and old reviews are not counted here.

Checks I ran at this head

  • git diff --check, npm ci --ignore-scripts --no-audit --no-fund, TypeScript typecheck and tsup build: exit 0.
  • Vitest on Node v22.22.3/macOS: 144 passed, 4 live tests skipped, 0 failed.
  • 48 additional loopback HTTP cases passed against the built SDK, across all three manifest methods and both size fields. Raw 2^53, 2^53+1 and u64::MAX were rejected; safe boundaries and missing/null values were preserved. Invalid negative, fractional and string values were rejected too. These are separate review probes, not extra repository-test counts.
  • A scratch in-memory bundle control reverting only the acceptance guard reproduced 9007199254740993 → 9007199254740992. This is a causal control, not a full parent build.

Separate inherited CI/security gate — not waived

Current exact-head checks: {"FAILURE": 1, "SUCCESS": 4}. The JavaScript CI job passes, but the Security audit fails: cargo audit reports three vulnerabilities in zip 2.4.2 (RUSTSEC-2026-0369, RUSTSEC-2026-0367, RUSTSEC-2026-0368), including a critical symlink/permissions advisory. This is not a flaky or green check.

antd/Cargo.lock is byte-identical in the target base, previously reviewed parent and this head (Git blob 1f1e0acb2d947802bae11ae876b550f833276381). The JS-only follow-up did not introduce that dependency state. This review neither establishes exploit reachability nor authorises an audit waiver. Do not merge based on this review while that gate remains unresolved.

Limits and integration scope

No live daemon/network, paid operations, full multi-SDK rerun, publication or ZIP reachability assessment. This closes the JS finding raised on #367 for the reviewed #348 code only. It does not update #367's review or verify that integration branch contains this fix. No branch, dependency, merge or release changes were made.

@Nic-dorman
Nic-dorman merged commit 02b6705 into main Oct 19, 2026
4 of 5 checks passed
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