Repository navigation
feat(antd-js): manifest API (REST) - #348
Conversation
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>
dirvine
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
optionalSizeat lines 141–149 now returns only non-negative safe integers. Unsafe positive integers throwInternalErrornamingsizeorexact_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
9007199254740993for each field, soJSON.stringifycannot 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+1andu64::MAXwere 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.
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:
-> BuiltManifest { manifest, link, entries }
decodeManifestLink(link, resolve = false) -> DecodedManifest
{ source, name, torrentInfoHashV1, torrentInfoHashV2, entries }
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
.antbytes are a Buffer both ways, and decodeManifest also takes aplain Uint8Array (encoded as a view, not copied). REST carries them as
standard base64 in
manifest; callers never see it. A build responsewhose 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