diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7a7bc9..a5b310e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,15 @@ name: Release -# Pushing a `v` tag on main publishes that version to npm. -# Running the workflow manually performs every release check and packs the tarball -# without publishing it. +# Merging a change to main that sets an untagged `package.json` version publishes +# that version to npm and tags the merge commit `v`; pushes to main whose +# version is already tagged release nothing. Pushing a `v` tag on main +# publishes that version too. Running the workflow manually, on a branch or a tag, +# performs every release check and packs the tarball without publishing it. +# scripts/release-decision.sh decides which of these a run is. on: push: + branches: + - main tags: - "v*" workflow_dispatch: @@ -17,13 +22,29 @@ concurrency: cancel-in-progress: false jobs: - package: - name: Verify and pack + decide: + name: Determine the release runs-on: ubuntu-latest outputs: name: ${{ steps.meta.outputs.name }} version: ${{ steps.meta.outputs.version }} + tag: ${{ steps.meta.outputs.tag }} dist_tag: ${{ steps.meta.outputs.dist_tag }} + publish: ${{ steps.meta.outputs.publish }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine version and dist-tag + id: meta + run: bash scripts/release-decision.sh + package: + name: Verify and pack + needs: decide + if: needs.decide.outputs.publish == 'true' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + outputs: tarball: ${{ steps.pack.outputs.tarball }} steps: - uses: actions/checkout@v4 @@ -34,48 +55,6 @@ jobs: node-version: 22 cache: npm - - name: Determine version and dist-tag - id: meta - shell: bash - run: | - name="$(node -p 'require("./package.json").name')" - version="$(node -p 'require("./package.json").version')" - - if [ "$GITHUB_REF_TYPE" = "tag" ]; then - if [ "$GITHUB_REF_NAME" != "v${version}" ]; then - echo "::error title=Tag does not match package.json::Tag ${GITHUB_REF_NAME} does not match package.json version ${version}. Tag the release as v${version}." - exit 1 - fi - if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then - echo "::error title=Tag is not on main::${GITHUB_REF_NAME} points at ${GITHUB_SHA}, which is not on main." - exit 1 - fi - fi - - # Same dist-tag policy as @withautonomi/ant: the first dot-separated pre-release - # identifier decides, and an unrecognised one fails rather than moving `latest`. - pre="" - if [ "$version" != "${version%%-*}" ]; then - pre="${version#*-}" - fi - if [ -z "$pre" ]; then - dist_tag="latest" - else - case "${pre%%.*}" in - beta) dist_tag="beta" ;; - rc) dist_tag="rc" ;; - *) - echo "::error title=Unrecognised pre-release::Version ${version} has pre-release identifier '${pre%%.*}', which maps to no npm dist-tag." - exit 1 - ;; - esac - fi - - echo "name=${name}" >> "$GITHUB_OUTPUT" - echo "version=${version}" >> "$GITHUB_OUTPUT" - echo "dist_tag=${dist_tag}" >> "$GITHUB_OUTPUT" - echo "Releasing ${name}@${version} to dist-tag ${dist_tag}" - - run: npm ci # The published WASM must be a clean build of a commit on ant-client main. @@ -112,19 +91,21 @@ jobs: publish: name: Publish to npm - needs: package - if: github.event_name == 'push' + needs: [decide, package] + # The decision publishes only for a push; checked here too, since a manual run must never publish. + if: github.event_name == 'push' && needs.decide.outputs.publish == 'true' runs-on: ubuntu-latest environment: npm permissions: - # Create the GitHub release. + # Create the GitHub release, and its tag after a merge to main. contents: write # npm provenance and trusted publishing exchange this token with the registry. id-token: write env: - NAME: ${{ needs.package.outputs.name }} - VERSION: ${{ needs.package.outputs.version }} - DIST_TAG: ${{ needs.package.outputs.dist_tag }} + NAME: ${{ needs.decide.outputs.name }} + VERSION: ${{ needs.decide.outputs.version }} + TAG: ${{ needs.decide.outputs.tag }} + DIST_TAG: ${{ needs.decide.outputs.dist_tag }} TARBALL_NAME: ${{ needs.package.outputs.tarball }} steps: - uses: actions/download-artifact@v4 @@ -180,17 +161,24 @@ jobs: shell: bash run: | set -euo pipefail - if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - echo "::notice title=Release exists::GitHub release ${GITHUB_REF_NAME} already exists; skipping." + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "::notice title=Release exists::GitHub release ${TAG} already exists; skipping." exit 0 fi prerelease=() if [ "$DIST_TAG" != "latest" ]; then prerelease=(--prerelease) fi - gh release create "$GITHUB_REF_NAME" "$RUNNER_TEMP/package/$TARBALL_NAME" \ + # A pushed tag already exists; after a merge, the release creates the tag + # on the merge commit. Tags made with this token start no other workflow. + if [ "$GITHUB_REF_TYPE" = "tag" ]; then + target=(--verify-tag) + else + target=(--target "$GITHUB_SHA") + fi + gh release create "$TAG" "$RUNNER_TEMP/package/$TARBALL_NAME" \ --repo "$GITHUB_REPOSITORY" \ - --verify-tag \ + "${target[@]}" \ --generate-notes \ "${prerelease[@]}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6611d7a..1fa7625 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,8 +20,8 @@ binary and notices checksums. Run `npm run build:examples` and `npm pack --dry-run` to check the shipped assets, and `npm run verify:wasm` to check the WASM release requirements. -Maintainers release by pushing a `v` tag on main; see "Releasing to npm" -in the [README](README.md#releasing-to-npm). +Maintainers release by merging a change that sets a new `package.json` version to +main; see "Releasing to npm" in the [README](README.md#releasing-to-npm). ## Architecture decisions diff --git a/README.md b/README.md index dcd8fca..a8c83f9 100644 --- a/README.md +++ b/README.md @@ -489,7 +489,7 @@ const payment: PaymentProvider = { totalAmount: submission.totalAmount, } }; }); - context.submitted(submission); + await context.submitted(submission); const settlement = await submission.wait(); if (settlement.status === "failed") throw new Error(settlement.reason); return settlement.receipt; @@ -509,7 +509,9 @@ check it immediately before submitting. After submitting storage payment, keep observing its receipt and resolve with that receipt even if the signal aborts; the SDK stops the upload waiting promptly and records the later receipt for recovery. Call `context.submitted()` immediately after broadcast, even if cancellation -occurred while the wallet was submitting. The observer must reject on an unknown +occurred while the wallet was submitting, and await it before observing the receipt. +It settles once the SDK has journaled the broadcast, or after at most 30 seconds, +and never rejects. The observer must reject on an unknown outcome (such as an RPC timeout) and return `failed` only for a definitive failure. Successful repricing may return a different confirmed transaction hash. @@ -586,8 +588,26 @@ admission when eight-response windows show sustained GET CPU load above half the event loop and repeated processing or scheduling stalls above 50 ms. Isolated slow responses and background timer clamping do not trigger that reduction. These bounds include speculative GETs and can keep actual concurrency below your ceiling. -Complete downloads are memory-bound. Use a random-access reader for large media -or range-oriented formats. +Streaming downloads and media reads no longer have a fixed 1 GB cap; byte +positions are safe JavaScript integers, including positions beyond 4 GiB. +Complete downloads remain memory-bound, so `download()` and `downloadAndSave()` +reject a file larger than `maxMemoryBytes` before content transfer. It defaults +to `SDK_LIMITS.defaultDownloadMemoryBytes` (1,000,000,000 bytes, the former cap). +Pass a smaller budget to suit your application, a larger one to buffer bigger +files deliberately, or use `streamAndSave()` to write them to disk: + +```ts +const result = await client.download(address, { + maxMemoryBytes: 256 * 1024 * 1024, + signal: controller.signal, +}); +``` + +This budget covers the returned Uint8Array only. The Blob snapshot, resolved +DataMap, caches and transport buffers need additional memory. Allocation can +still fail below the budget. Cancellation stops the core download as well as the +SDK caller; admitted transport responses may still drain to preserve the session. +Concurrent reads on one client share in-flight fetches of the same chunk. For each record, the Rust client can try known or newly discovered candidates while discovery continues, accepting only verified content. Early misses do not @@ -630,6 +650,51 @@ await client.downloadAndSave(address, { fileHandle }); await saveDownload(download, { fileHandle }); ``` +### Stream directly to disk + +`downloadAndSave()` retains its buffered bytes/Blob result. For large files, open +a reader and use `pipeTo()` with a browser `WritableStream`, including a +`FileSystemWritableFileStream`. Obtain a destination from a user action before +awaiting network work; feature-detect `showSaveFilePicker` in your application: + +```ts +// Inside a click handler in a browser supporting the File System Access API: +const handle = await window.showSaveFilePicker({ suggestedName: "archive.bin" }); +const reader = await client.openFile(address, { signal: controller.signal }); +try { + const writable = await handle.createWritable(); + if (controller.signal.aborted) { + await writable.abort(controller.signal.reason); + throw controller.signal.reason; + } + // pipeTo owns the destination once called; it closes or aborts it. + const result = await reader.pipeTo(writable, { + signal: controller.signal, + onProgress: (bytesWritten, totalBytes) => console.log(bytesWritten, totalBytes), + }); + console.log(result.bytesWritten, result.hash); +} finally { + reader.close(); +} +``` + +Options are passed as the second argument to `pipeTo`. Optional `start` and +`end` select a half-open range; the returned BLAKE3 covers exactly those bytes. +A failed read or verification rejects with `OPEN_FILE_FAILED`; an error raised by +the destination's own write or close rejects with `SAVE_FAILED`, its cause intact. +The core fetches concurrently and writes in order, awaiting each write and holding +up to 32 MiB of pipeline plaintext, with at most 4 MiB per write. The resident +DataMap/index, shared cache and transport reservations are additional memory. +The destination's own buffering and disk quota also apply. + +The destination closes on success and aborts on transfer failure; its writer lock +is released in both cases. Invalid arguments leave it untouched. An already +aborted signal rejects before acquiring the destination, so the caller must +abort a writable it has just created in that case. Cancellation during the final +close can reject after the destination has committed. Closing the reader cancels +active pipes; otherwise it remains reusable after a completed or failed pipe. +The [all-in-one example](examples/all-in-one) includes a disk-download cancel button. + ## Manifests and `ant://` links A manifest (ant-client ADR-0006) describes a set of files the way a `.torrent` @@ -674,7 +739,14 @@ client reads for an entry, `manifestFileName(entry)` its last path component, an is rejected. Entry `size` is a hint from the manifest's creator and decides nothing. `knownSize`, when present, is the plaintext size the core derives from an embedded root DataMap; public entries and shrunk maps omit it. Downloads and -readers verify the actual content through the core. +readers verify the actual content through the core. `sha256`, when present, is +the hex SHA-256 of the plaintext as the manifest's creator recorded it; the ant +CLI records one for every file it uploads. As the CLI does when extracting, +`download()` and `downloadAndSave()` check it against the bytes before returning +or saving them, and fail with `DOWNLOAD_FAILED` on a mismatch. The check uses +WebCrypto, which browsers offer only in secure contexts; without it such an entry +fails to download rather than arriving unchecked. `streamAndSave()`, `openFile()` +and `createMediaSource()` never hold the whole file, so they do not check it. The bundled core reads the finalized ADR-0006 v1 encoding from `ant-client` main. Links and files created by the earlier draft builds using the @@ -685,6 +757,159 @@ See the [main sync validation](docs/audits/2026-10-08-ant-client-main-sync.md) for production WASM and real-browser devnet evidence. The earlier streaming timings above were measured with a draft build. +## Creating manifests and share links + +`createManifest()` builds canonical `.ant` bytes and an `ant://manifest/...` +link offline through Rust. It accepts existing upload results; it does not +upload another file or publish the manifest. The core validates portable paths, +rejects collisions, derives embedded addresses and sorts entries canonically. + +```ts +import { createManifest, createFileLink } from "@withautonomi/ant-browser-sdk"; + +const exported = await createManifest({ + name: "photos", + entries: [ + { kind: "public", address: publicUpload.file.address, path: "public.jpg" }, + { kind: "embedded", dataMap: privateUpload.file.dataMap, path: "private.jpg" }, + ], +}); +const manifestBlob = new Blob([exported.bytes.slice()], { type: "application/octet-stream" }); +// Save manifestBlob as photos.ant through your application's file UI. +console.log(exported.link, exported.linkRecommended); +console.log(await createFileLink(publicUpload.file.address)); +``` + +Embedded DataMaps in both the exported file and link grant read access to private +files. `linkRecommended` is false when the encoded link payload exceeds the core's +1,500-byte recommendation; offer the `.ant` file then. A file may retain a root +DataMap while its link uses the compact published form, with the same identity. +Optional `torrent.infoHashV1` and `infoHashV2` are hex metadata, validated by Rust. +An entry's optional `sha256` is the hex SHA-256 of its plaintext, recorded as your +claim as the CLI records its own; the SDK computes none. + +## Pointers and payment recovery + +Pointers are owner-signed mutable references. `pointerAddress(ownerSeed)` computes +an address offline from an application-owned 32-byte FIPS 204 seed. Keep that seed +securely for later updates; the SDK never includes it in a checkpoint. + +```ts +import { pointerAddress, PointerError } from "@withautonomi/ant-browser-sdk"; + +const address = await pointerAddress(ownerSeed); +// Use an application-owned file or database transaction for durable checkpoints. +const onCheckpoint = async (checkpoint: string) => { + const writable = await journalFileHandle.createWritable(); + await writable.write(checkpoint); + await writable.close(); +}; + +try { + await client.createPointer({ ownerSeed, kind: "chunk", target: fileAddress, onCheckpoint }); + await client.updatePointer({ ownerSeed, kind: "chunk", target: nextFileAddress, onCheckpoint }); +} catch (error) { + if (error instanceof PointerError && error.checkpoint) { + // Retain the original checkpoint. The configured payment provider's recover() + // observes an existing transaction; this call never submits a new payment. + await client.resumePointer(error.checkpoint, { onCheckpoint }); + } else throw error; +} +const record = await client.getPointer(address); // Pointer or null +const target = await client.resolvePointer(address); +``` + +Each write requires `onCheckpoint`; resolve it only after durable storage. Keep +pending checkpoints across reloads and serialize writes for each owner in your +application. Every checkpoint is the complete current state, so store each one +over the previous one for that write. The SDK first persists the original signed +state, verified plan and quotes immediately before calling the wallet, then +retains broadcast evidence and, once Rust has accepted the receipt for that plan, +the receipt with the paid record and proof. Calls never overlap: a state produced +while a write is in progress is coalesced into the next call. + +`PointerError.checkpoint` is set once the wallet may have been asked to pay. It +holds the latest evidence even if persisting it failed; retain it even if the +operation was cancelled. When it is `undefined`, no payment was requested: delete +any checkpoint you persisted for that attempt and write again. After payment, a +rejected or unresponsive callback (timed out after 30 seconds) does not stop Rust +from storing the paid state, and a write the network stored resolves normally; +delete its checkpoint then. A write settles only after its last checkpoint call +has finished or timed out. + +If the wallet reported a broadcast but its own receipt poll failed, the write +observes that transaction again through the submission's `wait()`. Receipts are +checked by Rust against the plan before they are journaled; a rejected receipt's +transaction hashes are kept as evidence so recovery can observe them again. + +`resumePointer()` first re-observes broadcasts reported on this page through their +`wait()`, then uses the client's payment provider, or an explicit `payment` +override, only through its observation-only `recover()` method. Ethers and Wagmi +adapters support this. A paid proof needs no wallet; it is stored even if +persisting the recovered proof fails. Storers accept a paid proof only while its +quotes are under a day old; an older one cannot be stored without paying again. +`resumePointer()` then fails with the storer's error, which also says the proof +holds a quote older than storers accept when this device's clock agrees. Recovery +never signs a new state, refreshes quotes or calls `pay()`. If a crash hid the +transaction hash, reconcile the original wallet externally; an empty journal is +not proof that no transaction was submitted. Do not retry a fresh write while +payment is unresolved. +Checkpoints are application-owned, versioned opaque strings; do not edit them or +accept them from untrusted sources. They contain signed state and payment evidence. + +```ts +await client.transferPointer({ ownerSeed, recipient: recipientPointerAddress, onCheckpoint }); +const finality = await client.pointerFinality(address); +if (finality.status === "final") { + console.log(finality.majority?.transferredTo); +} +``` + +Transfer permanently redirects control to an existing recipient pointer. A write +result alone does not establish finality: retain all five statuses (`open`, +`settling`, `unconfirmed`, `final`, `forked`) and their holder counts. Only `final` +means the core observed a majority with every peer answering and no rival final +state. Pointer counters are decimal strings, including the final counter +`18446744073709551615`; never convert them to `number`. Unknown target kinds retain +their numeric `kindTag`. + +Pointer operations accept `signal` and `onProgress`. Cancellation before payment +prevents submission. After submission, the write continues observing settlement, +persisting evidence and storing the paid state. `client.close()` shuts the +transport immediately unless such a write is still storing its paid state, in +which case it waits for that write; reads and other operations are cancelled. This +cannot outlive page termination; resume from the durable checkpoint on the next +page load. + +## Streaming saves + +`client.streamAndSave(source, options)` accepts a public file/address, private +DataMap reference or manifest entry. Invoke it directly from a click handler to +open the browser save picker before network work: + +```ts +const saved = await client.streamAndSave(entry, { + suggestedName: "movie.mp4", + signal: controller.signal, + onProgress: event => console.log(event.completed, event.total), +}); +console.log(saved.name, saved.bytesWritten, saved.hash); +``` + +Pass `fileHandle` to reuse a selected file or OPFS handle, or `destination` for an +application-owned `WritableStream`. Supply one destination. The method +uses the core's bounded pipeline, closes its reader and destination on success, +and aborts acquired destinations on failure/cancellation. Cancellation during close +may arrive after the file has committed. It returns byte count and BLAKE3 without +a whole-file Blob. Without a supplied destination or browser save picker, it +rejects with `SAVE_FAILED`. Read and verification failures reject with +`OPEN_FILE_FAILED`; destination write failures, such as a full disk, with +`SAVE_FAILED`. A failed save leaves the file chosen in the save picker in place, +possibly empty: the SDK cannot tell a file the picker created from an existing +file the user chose, so it deletes neither; `downloadAndSave()` does the same. +`downloadAndSave()` retains its buffered return value and ordinary +browser-download fallback. + ## Random-access reads `openFile()` avoids reconstructing the entire file. Each `read()` call can request @@ -733,7 +958,8 @@ source.close(); For audio and video content types, and common media extensions, creating the source also reads the first `SDK_LIMITS.mediaPrimeHeadBytes` (1 MiB) and the -last `SDK_LIMITS.mediaPrimeTailBytes` (64 KiB) at once, in the background. A +last `SDK_LIMITS.mediaPrimeTailBytes` (64 KiB) at once, in the background, as +soon as its reader is open and while the service worker is still installing. A player reads the header, then seeks to the index MP4 files keep at the end, and only then plays; with both already in the reader's cache, playback starts as soon as the element is attached instead of after two sequential network @@ -746,17 +972,26 @@ page-owned authenticated reader; it does not connect to Autonomi nodes itself. The page and client must therefore remain open while the media URL is in use. Multiple clients can serve independent media sources on the same page. -A media source's reader streams: from wherever playback starts or seeks to, the -shared core fetches up to four records of the next 32 MiB in parallel. Playback -therefore does not wait for each record's discovery. It also fetches the file's -last record when playback starts, because MP4 and WebM usually keep their index -there. It keeps at most about 24 of the file's records cached, releasing those -behind playback. Readers from `openFile()` read ahead only once a read -continues a previous read, so a single header read does not fetch the window. +A media source's reader is an ordinary random-access reader, like one from +`openFile()`: it reads ahead once a read continues a previous one, so playback +fetches the following records while it plays, but the first block a player +waits for has the connections to itself. The core's streaming mode, which +fetches up to four records of the next 32 MiB and the file's last record as soon +as playback starts, delayed the first frame on the live network, so media +sources no longer use it; priming reads the head and the tail instead. See +[ADR-0003](docs/adr/ADR-0003-adaptive-browser-downloads.md). + +While a media source is open, the page messages the service worker every 10 +seconds. Firefox stops a worker that has had no new event for 30 seconds, even +one still answering a request, which otherwise cut off a block that took longer +or a response the player read for that long. The worker waits up to two minutes +for a block, longer than the core's retries of a missing record, after 15 and 45 +seconds. If the application already has a root-scoped service worker, merge the `autonomi-file-range` fetch and message-handling logic from the packaged worker -into it and pass that worker's URL: +into it, including its `message` listener, which the keep-awake messages need, +and pass that worker's URL: ```ts const source = await client.createMediaSource(address, { @@ -767,7 +1002,9 @@ const source = await client.createMediaSource(address, { A site can have only one controlling service worker per scope. Media sources refuse to replace a different worker already registered for their exact scope. They also require a secure context (HTTPS or localhost), and the packaged bridge -accepts files up to 1,000,000,000 bytes (1 GB). +accepts sizes and offsets through `Number.MAX_SAFE_INTEGER`. Deploy the updated +service worker with the SDK to remove the previous 1 GB media limit. Browser +media support, storage and resident DataMap memory remain practical constraints. ## Public metadata names @@ -897,25 +1134,29 @@ console.log(SDK_LIMITS.maxFileBytes, SDK_LIMITS.maxRangeBytes); | Limit | Value | | --- | --- | -| `minFileBytes` / `maxFileBytes` | 3 / 1,000,000,000 bytes | +| `minFileBytes` / `maxUploadBytes` | 3 / 1,000,000,000 bytes for uploads | +| `maxFileBytes` | Alias of the upload limit, retained for compatibility | | `maxRangeBytes` | 4 MiB per read or stream chunk | | `defaultStreamChunkBytes` | 1 MiB | | `downloadConcurrency.min` / `.max` / `.default` | 1 / 256 / `"auto"` | -| `mediaMaxFileBytes` | 1,000,000,000 bytes | +| `defaultDownloadMemoryBytes` | 1,000,000,000 bytes, the default buffered-download `maxMemoryBytes` | +| `mediaMaxFileBytes` | `Number.MAX_SAFE_INTEGER` (9,007,199,254,740,991) | The limits and capability reports are immutable. Unsupported input sizes are -rejected before copying byte arrays, staging Blobs, or opening files from supplied -metadata. Address-only reads learn the size from the core; media setup checks it -before registering a worker or creating a URL. These are protocol ceilings; -whole-file transfers still need sufficient memory, and staging needs quota for at -least one encrypted record. +rejected before copying upload byte arrays or staging Blobs. Reads learn the +verified size from the core; media setup checks it before creating a URL. Its +service worker installs while the reader opens. Uploads retain the upstream 1 GB +cap. Buffered downloads need sufficient memory and default to a 1 GB output +budget; disk streams need destination capacity, and upload staging needs quota +for at least one encrypted record. `getBrowserCapabilities()` safely runs outside a browser and makes no network requests, allocations of workers, or permission prompts. `features` reports each API's presence. `operations` reports requirements for `connect`, `uploadBytes`, -`uploadBlob`, `download`, `read`, `stream`, `media`, `saveWithPicker`, and -`saveWithDownload`, including missing features for each. Call it again to get a -fresh snapshot. +`uploadBlob`, `download`, `read`, `stream`, `pipeTo`, `streamAndSave`, `media`, `saveWithPicker`, and +`saveWithDownload`, including missing features for each. `streamAndSave` reports +its default path, which opens the save picker; with your own `destination` or +`fileHandle`, check `pipeTo` instead. Call it again to get a fresh snapshot. `available` means required APIs are present. It does not probe permissions, storage quota, CSP, media codecs, network access, or wallet availability. In @@ -960,6 +1201,14 @@ and traffic policy remain deployment responsibilities. | `client.resumeUpload()` | Resume retained input with explicit authorization for new payments | | `recovery.discard()` | Release retained input after pending work settles | | `parseManifestLink()` | Decode an `ant://manifest/...` or `ant://
` link without network access | +| `createManifest()` | Create canonical `.ant` bytes and a compact share link offline | +| `createFileLink()` | Format a public file share link | +| `pointerAddress()` | Derive a pointer address from its owner seed offline | +| `client.getPointer()` / `resolvePointer()` | Read a verified pointer or resolve its chain | +| `client.createPointer()` / `updatePointer()` | Pay for and store a state with durable recovery checkpoints | +| `client.transferPointer()` / `pointerFinality()` | Transfer control permanently and inspect finality | +| `client.resumePointer()` | Recover the original paid state without repayment | +| `client.streamAndSave()` | Save through the bounded streaming pipeline | | `decodeManifest()` | Decode the bytes of a `.ant` manifest file | | `client.download()` | Fetch, reconstruct, and verify a complete public file, private file or manifest entry | | `client.downloadAndSave()` | Download and open a browser save flow | @@ -1034,17 +1283,16 @@ recorded hashes and the binary is a clean build of a commit on `ant-client` main ### Releasing to npm The [release workflow](.github/workflows/release.yml) publishes -`@withautonomi/ant-browser-sdk` when a `v` tag is pushed: - -1. Merge a change that sets `version` in `package.json` (for example with - `npm version 0.2.0 --no-git-tag-version`). -2. Tag that commit on main and push the tag: - `git tag v0.2.0 && git push origin v0.2.0`. - -The workflow requires the tag to match `package.json` and to be on main, verifies -the WASM provenance, runs `npm run check`, builds the examples, and packs the -tarball. It publishes that tarball with npm provenance and attaches it to a GitHub -release. Stable versions go to the `latest` dist-tag; `-beta.N` and `-rc.N` +`@withautonomi/ant-browser-sdk` when a change that sets a new `version` in +`package.json` (for example with `npm version 0.2.0 --no-git-tag-version`) is +merged to main, and tags the merge commit `v`. A push to main whose +version is already tagged releases nothing. Pushing a `v` tag on main +also publishes that version, for example to release a commit already merged. + +The workflow requires a pushed tag to match `package.json` and to be on main, +verifies the WASM provenance, runs `npm run check`, builds the examples, and packs +the tarball. It publishes that tarball with npm provenance and attaches it to a +GitHub release. Stable versions go to the `latest` dist-tag; `-beta.N` and `-rc.N` versions go to `beta` and `rc`, and cannot be the package's first publish. Re-running a release skips steps that already completed. Run the workflow manually to perform every check without publishing. diff --git a/docs/adr/ADR-0003-adaptive-browser-downloads.md b/docs/adr/ADR-0003-adaptive-browser-downloads.md index f18dc5b..bf5de62 100644 --- a/docs/adr/ADR-0003-adaptive-browser-downloads.md +++ b/docs/adr/ADR-0003-adaptive-browser-downloads.md @@ -143,3 +143,41 @@ attached afterwards is served from the reader's cache. The option `primePlayback` forces or disables it. The reads are never awaited and never fail the media source; a failure is left for the player to report. No core or wire change: the reads use the ordinary streaming reader. + +## Media readers open without streaming mode (2026-10-22) + +The first frame waits on one fetch: the record holding the start of the +file. The core's streaming mode starts fetching up to four records of the +next 32 MiB, and the file's last record, at the same moment, so those +compete with it for the same connections. `createMediaSource()` now opens +an ordinary reader, which reads ahead only once a read continues a previous +one, and keeps priming the head and the tail. It also stops waiting on +itself: the media service worker installs while the reader opens, and +priming starts as soon as the reader is open rather than once the worker +controls the page. Errors surface in the same order as before. + +Against the live network, ten alternating rounds in fresh Chromium contexts +with the same 54.9 MB video gave a median first frame of 6.6 s against +9.1 s, faster in 7 of 10 pairs; one baseline sample failed to load. Two +later batches kept the plain reader ahead by less, 7.8 s against 9.7 s and +then 8.8 s against 9.2 s: the first frame mostly waits for the first +record's own fetch, which varied from 4 to 11 s, so the competing fetches +cost less than the first batch suggested. Seeking to the middle after ten +seconds of playback was slower in the first batch, median 6.5 s against +3.4 s, and faster in a later one, 2.8 s against 5.1 s, so no seek +difference is claimed. A fourth batch, with the `ccd4106` core, showed no +first-frame gain either, 6.1 s against 5.6 s over 12 rounds, so none is +claimed for the reader mode. The plain reader is kept because it does no +worse, fetches less ahead of the player, and is what try.autonomi.com's +player uses: with this change the SDK started the video as soon as that +player, a median 4.5 s against 4.1 s over 20 rounds, doing the same network +work. + +The page also messages the worker every 10 s while a media source is open, +because Firefox stops a worker that has had no new event for 30 s even while +it answers a request. The worker waits up to 120 s for a block, beyond the +core's retries of a missing record after 15 and 45 s, and marks its +responses `nosniff` and sandboxed, since their type comes from the file's +metadata. No core or wire change. Evidence is in the +[2026-10-22 audit](../audits/2026-10-22-media-first-frame.md) and the +[2026-10-26 comparison with try.autonomi.com](../audits/2026-10-26-sdk-vs-try-streaming.md). diff --git a/docs/adr/ADR-0007-manifests-in-the-browser-sdk.md b/docs/adr/ADR-0007-manifests-in-the-browser-sdk.md index bdb08e1..d30a682 100644 --- a/docs/adr/ADR-0007-manifests-in-the-browser-sdk.md +++ b/docs/adr/ADR-0007-manifests-in-the-browser-sdk.md @@ -46,10 +46,23 @@ exactly the latter, and a public entry exactly the former. The SDK exports `parseManifestLink(link)` and `decodeManifest(bytes)`, both asynchronous because they initialise the WASM module, returning frozen `ManifestLink` and `Manifest` values whose entries carry `name`, optional -`path` and `size`, optional `knownSize`, `kind`, hex `address`, and for embedded entries the -canonical DataMap bytes; a manifest also exposes the torrent info hashes its -creator recorded, as hex, which the SDK only surfaces. Failures carry the -`INVALID_SOURCE` code. +`path` and `size`, optional `knownSize`, optional hex `sha256`, `kind`, hex +`address`, and for embedded entries the canonical DataMap bytes; a manifest +also exposes the torrent info hashes its creator recorded, as hex, which the +SDK only surfaces. Failures carry the `INVALID_SOURCE` code. + +`sha256` is the creator's whole-file hash of the plaintext (ant-client +ADR-0006), which the CLI checks when extracting. `download()` and +`downloadAndSave()` check it too, before returning or saving the bytes, and +fail with `DOWNLOAD_FAILED` on a mismatch; a malformed `sha256` is refused with +`INVALID_SOURCE` before anything is fetched. The bytes are already whole there, +and the core's browser build exports no SHA-256, so WebCrypto hashes them, +before the result's Blob copies them so the check does not raise peak memory. +This is a check of the creator's claim, not of integrity, which the core still +verifies by content address. Without WebCrypto, outside a secure context, such +an entry fails to download rather than arriving unchecked. `streamAndSave()`, +`openFile()` and `createMediaSource()` never hold the whole file and WebCrypto +cannot hash incrementally, so they do not check it. The core derives an embedded entry's address by hashing its published DataMap form, so a root map and its shrunk map identify the same file. `knownSize` is @@ -66,6 +79,15 @@ and `isManifestEntry` expose that mapping. Entry `size` is never used for anything but display. No extraction to a filesystem is added: the browser has none, and applications own how files are saved. + +`createManifest(input)` also creates canonical `.ant` bytes, the normalized +manifest view, a compact link and a `linkRecommended` flag through Rust. +`createFileLink(address)` formats public links through the core grammar. Inputs +contain public addresses or canonical embedded DataMaps, optional relative paths, +size hints, `sha256` claims and torrent metadata. No TypeScript format encoder +or network upload is added. Exported embedded maps and links grant private-file +access. + ## Consequences ### Positive @@ -98,6 +120,12 @@ none, and applications own how files are saved. exposed as `knownSize`, distinct from the creator's `size` on a public entry. Draft DataMap layouts and unsupported format versions reject with `INVALID_SOURCE`. +- The ant-client golden v1 fixture with a creator's `sha256` decodes with it + as hex; `createManifest` records a `sha256` through to the decoded bytes and + link, and rejects a malformed one. `download()` returns an entry's bytes only + when they match its `sha256`; `downloadAndSave()` writes nothing on a + mismatch; a malformed `sha256` is refused before any fetch, and a missing + WebCrypto fails the download. - Client tests: an embedded entry routes to the private download and reader with the entry's file name and no public DataMap fetch; a public entry routes to the public download; an embedded entry without DataMap bytes is diff --git a/docs/adr/ADR-0008-large-file-downloads-and-writable-streams.md b/docs/adr/ADR-0008-large-file-downloads-and-writable-streams.md new file mode 100644 index 0000000..4e212ee --- /dev/null +++ b/docs/adr/ADR-0008-large-file-downloads-and-writable-streams.md @@ -0,0 +1,115 @@ +# ADR-0008: Large-file downloads and writable streams + +- **Status:** Proposed +- **Date:** 2026-10-15 +- **Decision owners:** Engineering team +- **Reviewers:** Pending human engineering review +- **Supersedes:** none +- **Superseded by:** none +- **Related:** ADR-0003; ADR-0007; ant-client ADR-0004 large-file download destinations + +## Context + +ant-client main supports 64-bit file positions, bounded plaintext decryption, +direct writable destinations, cancellable downloads and shared in-flight GETs. +The SDK still caps media at 1 GB and does not expose the new download options +or writable pipeline. Its existing download result includes both bytes and a +Blob, so changing that method to return only disk metadata would break callers. + +## Decision Drivers + +- Support files beyond 1 GB and seeks beyond 4 GiB without whole-file buffering. +- Preserve Rust ownership of integrity, scheduling, decryption and read policy. +- Preserve existing download results, upload/payment behavior and error codes. +- Make cancellation, destination ownership and memory limits explicit. + +## Considered Options + +1. Remove only the media limit. This leaves disk downloads using file-sized buffers. +2. Change downloadAndSave to return disk-only results. This breaks its byte/Blob API. +3. Expose the core's pipeTo on the existing reader and retain buffered methods. + +## Decision + +Choose option 3. `PublicFileReader.pipeTo(writable, options)` accepts public, +private and manifest-backed readers, an optional half-open byte range, +AbortSignal and byte-progress callback. It returns the bytes written and their +BLAKE3 hash. The core fetches concurrently, writes in order with backpressure, +and holds up to 32 MiB of pipeline plaintext; the resident map/index, shared +chunk cache and transport reservations are additional memory. Each write is +at most 4 MiB. Concurrent reads share in-flight GETs through the core. + +The core closes the destination on success, aborts on transfer failure, and +releases the writer lock. Invalid calls leave the destination untouched. +Cancellation during destination close can reject after the data is committed. +Closing a reader cancels its active pipes. The SDK defers freeing the WASM +reader until raw calls settle, including cancelled range calls. + +`download({ maxMemoryBytes, signal })` forwards both options to Rust. The budget +limits the output Uint8Array, not total process memory; the Blob snapshot and +core resources remain additional allocations. It defaults to +`SDK_LIMITS.defaultDownloadMemoryBytes`, the former 1,000,000,000-byte cap, so a +caller that never chose a budget still gets a typed rejection instead of a +file-sized allocation; larger buffers are an explicit choice. downloadAndSave +keeps its existing buffered return value. Applications use streamAndSave or pipeTo +for large disk downloads; the all-in-one example demonstrates this with a cancel +button. + +The SDK hands the core a forwarding stream rather than the caller's destination, +so a failure raised by the destination's own write or close rejects with +SAVE_FAILED while read and verification failures keep OPEN_FILE_FAILED. The +forwarding stream takes the destination's writer only on first use, preserving +"invalid calls leave it untouched", and adds no buffering: each write still +awaits the destination. + +Media and service-worker positions accept safe JavaScript integers through +Number.MAX_SAFE_INTEGER. Uploads remain limited by upstream to 1,000,000,000 +bytes. SDK_LIMITS.maxFileBytes retains that upload limit for compatibility; +maxUploadBytes names it explicitly. No network, DataMap or payment format changes. + + +`client.streamAndSave(source, options)` owns destination selection, reader lifetime +and `pipeTo` orchestration. It accepts a supplied WritableStream, a file handle or +a picker invoked before network awaits, returns byte count/hash, closes readers, +and aborts acquired destinations on failure. It never falls back to full-file +buffering. `downloadAndSave` keeps its existing buffered contract. A browser save +picker creates or truncates its file before any network work; when a save that +opened the picker fails before the core could close (and so commit) the +destination, the SDK removes that file if it is still empty, once the destination +is released. Capability reports describe streamAndSave's default picker path; +applications supplying their own destination check pipeTo. + +## Consequences + +### Positive + +- Large files can be saved and played with bounded content buffering. +- Existing buffered consumers remain compatible. +- Verified fetching and backpressure use one shared core implementation. + +### Negative / Trade-offs + +- The destination must be a WritableStream; browser file-picker support varies. +- The resolved map/index still grows with chunk count. Large JS allocations, + destination quota and browser limits can fail below the safe-integer ceiling. +- Buffered downloads still need memory proportional to file size, and files over + the default budget need an explicit maxMemoryBytes or a streaming save. + +### Neutral / Operational + +- Regenerate bindings, binary, provenance and notices together from clean main. +- Deploy the updated media service worker alongside the SDK. +- No new npm dependency. Rust dependency changes are recorded in generated notices. + +## Validation + +Run SDK checks, production-WASM tests, example builds, package/provenance checks +and governance. Verify real Chromium against isolated compatible WebRTC nodes: +public/private uploads, manifest reads, disk output, cancellation, media ranges, +and a native-produced file larger than 4 GiB with matching size and BLAKE3. +Record exact revisions and limitations in a dated audit. A compressible large +fixture validates correctness, not representative network throughput. + +## Notes for AI-assisted work + +This ADR remains Proposed pending human review. Accepted ADRs are immutable. diff --git a/docs/adr/ADR-0009-browser-api-completion.md b/docs/adr/ADR-0009-browser-api-completion.md new file mode 100644 index 0000000..da67712 --- /dev/null +++ b/docs/adr/ADR-0009-browser-api-completion.md @@ -0,0 +1,58 @@ +# ADR-0009: Typed pointers and durable payment recovery + +- **Status:** Proposed +- **Date:** 2026-10-15 +- **Decision owners:** Mick +- **Reviewers:** Pending human engineering review +- **Supersedes:** none +- **Superseded by:** none +- **Related:** ADR-0005; ADR-0007; ant-client ADR-0007 + +## Context + +Browser applications need shared-core manifest exports and pointer operations with recoverable payments. Losing a page after broadcast must not authorize another payment. + +## Decision Drivers + +- Keep protocol validation and payment proofs in Rust. +- Retain durable evidence across failures and cancellation. +- Preserve compatibility with existing readers and wallet adapters. + +## Considered Options + +1. Expose raw bindings and leave each application to reconstruct the protocol. +2. Persist only paid proofs, leaving broadcast-to-confirmation failures unresolved. +3. Typed wrappers and pre-payment checkpoints with observation-only recovery (chosen). + +## Decision + +Expose typed pointer address, read, resolve, create, update, transfer, finality and recovery methods. Counters remain decimal strings and all five finality states are preserved. Require applications to durably persist each opaque checkpoint through onCheckpoint. The first checkpoint is the payment intent (signed state, original payment plan, quotes and network identity), persisted immediately before the wallet is invoked, so every durable checkpoint may have a payment. Later checkpoints add broadcast evidence and, only once Rust has accepted a receipt for the plan, the receipt with the paid record and proof (base64). A rejected receipt is never journaled as one; its transaction hashes are kept as evidence. Never journal the owner seed. Each checkpoint is a complete state; calls are serialized and states produced during a call are coalesced into the next. + +A wallet awaits `PaymentContext.submitted()` before observing its receipt, as the core's wallets await `onSubmission`: it settles once the broadcast evidence is durable, in the pointer checkpoint or the core's upload journal, or after at most 30 seconds, and never rejects. The bundled Ethers and Wagmi providers do so. The page therefore keeps the transaction before anything waits on its confirmation, and a slow or failed journal write never stops the wallet observing a transaction already broadcast. + +PointerError carries a checkpoint exactly when the wallet may have been asked to pay; without one, no payment was requested and a fresh write is safe. After payment, persistence failures and timeouts do not stop Rust from storing the paid state, and a write the network stored resolves. Resume and in-page recovery only observe the original transaction (first through the submission's wait() observer on the same page, then PaymentProvider.recover) or store an existing paid state; they never invoke PaymentProvider.pay. Cancellation before submission stops payment; after submission settlement and paid-proof storage continue, and client.close() keeps the transport open for that write only. Applications retain checkpoints until they have confirmed the desired state, and serialize writes for each owner. Transfer is permanent and callers must inspect finality. + +## Consequences + +### Positive + +- Applications can recover interrupted writes without an automatic second payment. +- Native and browser formats and pointer policy remain shared. + +### Negative / Trade-offs + +- Applications must supply durable persistence and an observation-only recovery provider for unresolved payments. A crash before submission may require explicit verification that nothing was broadcast. +- Checkpoints contain signed records and transaction evidence and must be treated as sensitive application data. + +### Neutral / Operational + +- Additive APIs; no network protocol change. New checkpoint schemas are versioned. +- SDK releases require the companion Rust changes on ant-client main. + +## Validation + +Test malformed manifests, canonical round trips, pending and paid recovery, callback failures, wallets waiting for broadcast evidence to be journaled, network mismatches, cancellation and real-browser paid pointer writes and streaming saves. Record exact revisions and limitations in a dated audit. Human engineering review is required. + +## Notes for AI-assisted work + +This ADR remains Proposed until human engineering review. diff --git a/docs/audits/2026-10-15-large-file-streaming.md b/docs/audits/2026-10-15-large-file-streaming.md new file mode 100644 index 0000000..47fcc76 --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming.md @@ -0,0 +1,112 @@ +# Current ant-client core and large-file streaming — 2026-10-15 + +SDK PR #6 was checked against current upstream main and merged as +`29ab1758e1188fc445b82465949dcc655345935b`; see the +[manifest review](2026-10-15-manifest-main-review.md). This follow-up starts from +that merge on `feat/large-file-streaming-20261015`. The implementation tested is +SDK commit `456dbb7b6b6ccbcce0603bbae2fd7b1664b68975`. + +## Changes + +- Rebuilt production WASM from clean ant-client main + `225a6768589ecbb6a0c77d70df329f343b22776f`, including large-file downloads, + shared in-flight chunk fetches, current pointer fixes and dependency updates. + High-level pointer APIs are outside this SDK change. +- Added typed `PublicFileReader.pipeTo(destination, options)` over the core's + ordered, concurrent writable pipeline, with ranges, byte progress, BLAKE3, + cancellation, backpressure and destination cleanup. Closing a reader cancels + active pipes; WASM readers are freed only after their raw calls settle. +- Added `DownloadOptions.maxMemoryBytes` and forwarded download cancellation to + Rust. Avoided an extra file-sized intermediate copy before Blob construction. +- Removed the 1 GB media cap in both the SDK and standalone service worker; + byte positions are exact JavaScript safe integers. Added capability reporting + for writable streaming and a disk-download/cancel flow to the all-in-one demo. +- Retained upstream's 1,000,000,000-byte browser upload limit. Exposed it as + `SDK_LIMITS.maxUploadBytes`, keeping `maxFileBytes` as the compatible alias. + `downloadAndSave()` retains its existing buffered bytes/Blob return value. +- Added [Proposed ADR-0008](../adr/ADR-0008-large-file-downloads-and-writable-streams.md). + No Accepted ADR was changed. No new npm dependency. + +## Revisions and provenance + +- WASM and native CLI: ant-client + `225a6768589ecbb6a0c77d70df329f343b22776f`, clean sources and committed lockfile. +- WASM SHA-256: + `9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352`. +- Cargo.lock SHA-256: + `64a762b99db03f64e20bdb6699e5e576417dedbd6172049834c64fd27a068314`. +- Nodes: clean ant-node `26129a2611cac08f9f53a557f91fb8806b40a1cc`, upstream's + browser integration pin. Built with `cargo build --locked --bin ant-devnet`. +- Seven isolated WebRTC-enabled nodes, three bootstrap nodes, ephemeral node + ports, API port 39870, demo port 39871 and a local Anvil chain. Only task-owned + servers and data were used. +- Node v22.23.1, Rust 1.99.0, wasm-pack 0.15.0, Python 3.11.15, + Playwright 1.62.1, Chromium 151.0.7922.34. +- Exact generated provenance and WASM/JS/native-binary checksums are in the + [evidence directory](2026-10-15-large-file-streaming/). + +## Validation + +- `npm run check`: build, TypeScript and **211 tests in 22 files passed**. + Production WASM validates descriptors beyond 4 GiB/1024 chunks, the new + download options, pre-aborted signals and the unchanged manifest fixtures. + SDK regressions cover cancellation propagation, raw-call lifetime, caller + edits to signal options, Blob view boundaries and service-worker ranges. +- `npm run verify:wasm`: passed main ancestry, binary and licence provenance. +- `npm run build:examples`: all five applications passed. +- `npm pack --dry-run --json`: passed prepack checks and packaged both root + licences, the updated service worker and generated WASM notices. +- `python3 scripts/adr-governance.py`: passed. +- `python3 .github/scripts/test_check_pr.py`: 52/52 passed. + +Real Chromium exercised the production SDK and WASM, without RTC/RPC mocks: + +- Public/private Blob uploads through the all-in-one demo, saved downloads and + private DataMap save/reload each preserved **13,056 bytes**. Embedded/public + manifest entries, manifest links, three reader ranges and two HTTP 206 media + ranges per visibility passed. Root-derived size remained correct despite a + creator size hint of 1. Every observed WASM hash matched provenance. +- The new demo disk-download button wrote the public sample to a real OPFS + `FileSystemWritableFileStream`, preserving all bytes and its hash. The picker + was replaced by a function returning that handle for automation; the native + picker dialog was not exercised. +- The native CLI uploaded the upstream **4,303,347,835-byte** fixture as + **1,032 records**, with zero failed records, in about 50 seconds. Its address + matched the fixture. Only the history record created by this probe was moved + into the task directory; existing user upload history was preserved. +- Public and private readers returned correct bytes at the head, beyond 1 GB, + across 4 GiB, and at EOF. Both buffered download paths rejected a 64 MiB + output budget with `DOWNLOAD_FAILED` and streaming guidance. +- The whole large file streamed into OPFS with its exact size and BLAKE3 + `1b382a68936c50a2be8f4caa7f579c90cf649544a46f07ce5adeeffc20f5a3cd`. + Progress reached the exact file size after 1,028 callbacks. Stored bytes + around 4 GiB and at EOF matched; a private range also streamed correctly. +- A stalled destination write was cancelled both by the caller's signal and by + closing the reader. Each pipe rejected promptly, released its writer lock and + aborted the destination once the pending write settled. +- A media request crossing 4 GiB returned HTTP 206 and + `Content-Range: bytes 4294967288-4294967303/4303347835`, with 16 correct bytes. +- Successful runs reported no page or console errors. Final browser summaries + retain the exact SDK revision, WASM hash, timings and results. + +## Limitations and operational notes + +This is Chromium correctness evidence on a local devnet, not a mainnet or +cross-browser performance comparison. The large fixture repeats one byte and +compresses to about 67 KB of records; its disk-stream timing is not a realistic +network-throughput measurement. No Safari/Firefox playback or codec test, native +vs browser performance parity, pointer operation, or multi-gigabyte browser +upload is claimed. Shared-fetch and bounded-pipeline algorithms are upstream +implementations; this audit does not independently measure their GET counts or +peak memory. Resident map/index, cache, transport and destination memory are +additional to the pipeline's 32 MiB plaintext bound. + +The first notice-generation attempt hit GitHub's unauthenticated rate limit and +installed no artifacts. The authenticated retry completed and passed provenance +verification. A final browser rerun initially hit Vite's dependency-optimization +page reload; the retained probe waits for network idle before starting evaluation. +This harness failure and the successful rerun are recorded separately. + +Deploy the updated service worker with the SDK. Reverting requires reverting +the bindings, binary, provenance and licence notices together with the wrappers. +ADRs remain Proposed pending human engineering review. diff --git a/docs/audits/2026-10-15-large-file-streaming/adr.log.gz b/docs/audits/2026-10-15-large-file-streaming/adr.log.gz new file mode 100644 index 0000000..bcb51cf Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/adr.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/browser-check.log.gz b/docs/audits/2026-10-15-large-file-streaming/browser-check.log.gz new file mode 100644 index 0000000..0de2c0c Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/browser-check.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/browser-check.mjs.gz b/docs/audits/2026-10-15-large-file-streaming/browser-check.mjs.gz new file mode 100644 index 0000000..ab1195c Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/browser-check.mjs.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/browser-summary.json b/docs/audits/2026-10-15-large-file-streaming/browser-summary.json new file mode 100644 index 0000000..7fa4f62 --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming/browser-summary.json @@ -0,0 +1,42 @@ +{ + "browser": "151.0.7922.34", + "sdkBase": "29ab175", + "sdkRevision": "456dbb7b6b6ccbcce0603bbae2fd7b1664b68975", + "wasm": "225a6768589ecbb6a0c77d70df329f343b22776f", + "wasmSha256": "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352", + "node": "26129a2611cac08f9f53a557f91fb8806b40a1cc", + "phases": [ + { + "visibility": "public", + "address": "eccac1618853ecee64ab24d56de20a149b1702838f77c5a5cdf4c630e07df55e", + "knownSize": 13056, + "creatorSize": 1, + "downloadBytes": 13056, + "rangeReads": 3, + "mediaRanges": 2, + "sha256": "f4d99b7cad3937caccb331974609f53b4eabb2fa944f6f39d768be4f2e26d91b", + "wasmHashes": [ + "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352", + "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352" + ], + "pageErrors": [], + "consoleErrors": [] + }, + { + "visibility": "private", + "address": "f2853ce4176147f432464dd9059bf6c74901ef0aeaf5f032985d268f152e39dd", + "knownSize": 13056, + "creatorSize": 1, + "downloadBytes": 13056, + "rangeReads": 3, + "mediaRanges": 2, + "sha256": "ea713b9f6096ac909d6166c705b4d9e139547b972bd471467e31f2445b12fffa", + "wasmHashes": [ + "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352", + "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352" + ], + "pageErrors": [], + "consoleErrors": [] + } + ] +} diff --git a/docs/audits/2026-10-15-large-file-streaming/checksums.json b/docs/audits/2026-10-15-large-file-streaming/checksums.json new file mode 100644 index 0000000..405a5a3 --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming/checksums.json @@ -0,0 +1,8 @@ +{ + "src/wasm/ant_core_bg.wasm": "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352", + "src/wasm/ant_core.js": "1f3fa3ca1ff5b871cdf97b35493f784cce044571b17b7bc0d40cde16f7aa0c1f", + "src/wasm/ant_core.d.ts": "5fcea753514fd95f745fcac63386cb266b31b4ded6869a11011b53b13fc25a86", + "src/wasm/THIRD-PARTY-NOTICES.txt": "96342e4398d82820d5e1fcd89b05910ff621765a78ed1edf8537ee9ef9f13845", + "ant-devnet": "6315c3bc07a54898ff3b486d9e5f4b265cc089f9a5c93410c98228465d04fea3", + "ant": "d346fef802b00324b7d764f09425dcf4aefcb2b5d44ad670e86a71f63ffdccc4" +} diff --git a/docs/audits/2026-10-15-large-file-streaming/cli-build.log.gz b/docs/audits/2026-10-15-large-file-streaming/cli-build.log.gz new file mode 100644 index 0000000..10ef091 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/cli-build.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/commands.txt b/docs/audits/2026-10-15-large-file-streaming/commands.txt new file mode 100644 index 0000000..75c027e --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming/commands.txt @@ -0,0 +1,31 @@ +# SDK worktree: /Users/mick/RustroverProjects/ant-browser-sdk-pr6-20261015 +# Branch: feat/large-file-streaming-20261015 from merged main 29ab1758e1188fc445b82465949dcc655345935b +# Tested SDK implementation: 456dbb7b6b6ccbcce0603bbae2fd7b1664b68975 +npm ci +RUSTUP_TOOLCHAIN=1.99.0 PYTHON=/Users/mick/.local/bin/python3.11 npm run sync:wasm +# The successful retry supplied GITHUB_TOKEN privately from gh auth token through +# subprocess environment, to avoid the unauthenticated GitHub API rate limit. +npm run check +npm run verify:wasm +npm run build:examples +npm pack --dry-run --json +python3 scripts/adr-governance.py +python3 .github/scripts/test_check_pr.py +# Clean ant-client 225a6768589ecbb6a0c77d70df329f343b22776f: +RUSTUP_TOOLCHAIN=1.99.0 cargo build --locked --release --bin ant +# Clean ant-node 26129a2611cac08f9f53a557f91fb8806b40a1cc: +RUSTUP_TOOLCHAIN=1.99.0 cargo build --locked --bin ant-devnet +target/debug/ant-devnet --nodes 7 --bootstrap-count 3 --base-port 0 --webrtc-direct --webrtc-direct-base-port 0 --serve-port 39870 --enable-evm --data-dir /private/tmp/sdk-20261015-devnet-data --manifest /private/tmp/sdk-20261015-devnet-manifest.json +# SDK: +node node_modules/vite/bin/vite.js --config examples/all-in-one/vite.config.ts --port 39871 --strictPort +# Native fixture: 4,303,347,835 repetitions of byte 90, written in 4 MiB blocks; +# expected address/hash from ant-core/wasm-tests/fixtures/large-file.json. +# SECRET_KEY supplied privately from the verified local Anvil devnet manifest. +ant --json --devnet-manifest /private/tmp/sdk-20261015-large/devnet-manifest.json --allow-loopback --evm-network local file upload /private/tmp/sdk-20261015-large/large-file.bin --public +# Only this command's newly created manifest_id record was moved from the +# macOS upload-history directory into the task's temporary directory. +# Exact probes are retained as .mjs.gz; they use the real production SDK/WASM. +node /private/tmp/sdk-20261015-browser-check.mjs +node /private/tmp/sdk-20261015-disk-demo.mjs +node /private/tmp/sdk-20261015-large-browser.mjs +# Stop only the two task-owned servers through their PTY sessions with SIGINT. diff --git a/docs/audits/2026-10-15-large-file-streaming/devnet-build.log.gz b/docs/audits/2026-10-15-large-file-streaming/devnet-build.log.gz new file mode 100644 index 0000000..25b3416 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/devnet-build.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/disk-demo-summary.json b/docs/audits/2026-10-15-large-file-streaming/disk-demo-summary.json new file mode 100644 index 0000000..fc02399 --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming/disk-demo-summary.json @@ -0,0 +1,10 @@ +{ + "browser": "151.0.7922.34", + "result": { + "size": 13056, + "matched": true, + "cancelDisabled": true, + "log": "Verified and saved 13056 bytes to demo-disk.bin; BLAKE3 cb0ef44162971fee6b6098e8b0b55efe23593a2ab56711324e71fc6b67e4b78d" + }, + "errors": [] +} diff --git a/docs/audits/2026-10-15-large-file-streaming/disk-demo.log.gz b/docs/audits/2026-10-15-large-file-streaming/disk-demo.log.gz new file mode 100644 index 0000000..c116179 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/disk-demo.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/disk-demo.mjs.gz b/docs/audits/2026-10-15-large-file-streaming/disk-demo.mjs.gz new file mode 100644 index 0000000..3f06d32 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/disk-demo.mjs.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/example-builds.log.gz b/docs/audits/2026-10-15-large-file-streaming/example-builds.log.gz new file mode 100644 index 0000000..7712e86 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/example-builds.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/large-browser-vite-reload.log.gz b/docs/audits/2026-10-15-large-file-streaming/large-browser-vite-reload.log.gz new file mode 100644 index 0000000..50854eb Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/large-browser-vite-reload.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/large-browser.log.gz b/docs/audits/2026-10-15-large-file-streaming/large-browser.log.gz new file mode 100644 index 0000000..fc85c04 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/large-browser.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/large-browser.mjs.gz b/docs/audits/2026-10-15-large-file-streaming/large-browser.mjs.gz new file mode 100644 index 0000000..a92efc8 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/large-browser.mjs.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/large-summary.json b/docs/audits/2026-10-15-large-file-streaming/large-summary.json new file mode 100644 index 0000000..ffa7734 --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming/large-summary.json @@ -0,0 +1,98 @@ +{ + "browser": "151.0.7922.34", + "sdkBase": "29ab1758e1188fc445b82465949dcc655345935b", + "sdkRevision": "456dbb7b6b6ccbcce0603bbae2fd7b1664b68975", + "wasm": "225a6768589ecbb6a0c77d70df329f343b22776f", + "node": "26129a2611cac08f9f53a557f91fb8806b40a1cc", + "wasmSha256": "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352", + "result": { + "size": 4303347835, + "ranges": [ + { + "visibility": "public", + "start": 0, + "bytes": 16 + }, + { + "visibility": "public", + "start": 1000000001, + "bytes": 16 + }, + { + "visibility": "public", + "start": 4294967288, + "bytes": 16 + }, + { + "visibility": "public", + "start": 4303347819, + "bytes": 16 + }, + { + "visibility": "private", + "start": 0, + "bytes": 16 + }, + { + "visibility": "private", + "start": 1000000001, + "bytes": 16 + }, + { + "visibility": "private", + "start": 4294967288, + "bytes": 16 + }, + { + "visibility": "private", + "start": 4303347819, + "bytes": 16 + } + ], + "budgets": [ + { + "visibility": "public", + "code": "DOWNLOAD_FAILED", + "message": "Public file download failed: file exceeds the download memory budget; use openPublicFile/openPrivateFile and reader.pipeTo(writable) to stream to disk" + }, + { + "visibility": "private", + "code": "DOWNLOAD_FAILED", + "message": "Private file download failed: file exceeds the download memory budget; use openPublicFile/openPrivateFile and reader.pipeTo(writable) to stream to disk" + } + ], + "cancellation": [ + { + "method": "signal", + "unlocked": true, + "aborted": true + }, + { + "method": "close", + "unlocked": true, + "aborted": true + } + ], + "disk": { + "bytesWritten": 4303347835, + "hash": "1b382a68936c50a2be8f4caa7f579c90cf649544a46f07ce5adeeffc20f5a3cd", + "storedSize": 4303347835, + "seconds": 91.96030000007153, + "progressCalls": 1028, + "lastProgress": 4303347835 + }, + "privatePipe": { + "bytesWritten": 16, + "hash": "808b5770416f3e23c40032cdde085915ab1d2bad36b59bffc377bfa7ab1ee39d" + }, + "media": { + "status": 206, + "contentRange": "bytes 4294967288-4294967303/4303347835", + "bytes": 16 + } + }, + "errors": [], + "hashes": [ + "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352" + ] +} diff --git a/docs/audits/2026-10-15-large-file-streaming/native-upload.json b/docs/audits/2026-10-15-large-file-streaming/native-upload.json new file mode 100644 index 0000000..05710ec --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming/native-upload.json @@ -0,0 +1 @@ +{"address":"bdf831b9fd46588ecd85bd7563732d44430ccf6fce27e81514a00057881bca37","mode":"public","chunks":1032,"total_chunks":1032,"chunks_stored":1032,"chunks_failed":0,"size":4303347835,"storage_cost_atto":"12093750000000000000","gas_cost_wei":"40694041665767275","elapsed_secs":49.989094834,"chunk_attempts_total":1032,"store_durations_ms":[240,262,259,770,897,925,961,878,913,1044,984,1092,1013,1104,1032,1141,1109,1123,1156,1125,1262,1186,1280,1283,1237,1189,1270,1245,1221,1318,1225,1380,1282,1230,1282,1305,1326,1341,1279,1418,1427,1423,1532,1443,1504,1509,1560,1538,1604,1591,1532,1499,1609,1533,1594,1630,1589,1598,1624,1570,1639,1631,1472,1640,231,624,658,759,730,728,846,771,897,921,875,879,915,881,957,1020,940,919,1056,1001,1001,982,1166,1114,1159,1210,1202,1209,1149,1055,1141,1263,1214,1265,1146,1316,1263,1310,1348,1314,1370,1332,1332,1336,1215,1291,1226,1392,1356,1296,1277,1278,1377,1337,1288,1391,1337,1377,1434,1354,1344,1271,1469,1462,216,665,812,810,811,822,892,969,1028,905,957,1023,969,1032,1011,1016,1160,1030,1122,1037,1078,1166,1095,1206,1076,1064,1133,1149,1153,1182,1220,1200,1242,1257,1250,1281,1253,1136,1221,1266,1314,1164,1342,1268,1187,1380,1299,1379,1397,1354,1406,1395,1429,1386,1361,1467,1459,1440,1462,1376,1344,1372,1337,1303,227,567,550,528,624,594,626,726,647,634,672,765,755,844,887,829,840,990,872,875,998,942,1076,1045,987,1084,1122,1129,1114,1207,1111,1143,1103,1224,1154,1135,1258,1180,1212,1303,1359,1381,1328,1215,1377,1287,1411,1422,1280,1417,1343,1410,1361,1305,1374,1487,1345,1464,1487,1439,1396,1500,1434,1400,227,514,674,622,570,537,702,672,644,736,870,819,807,841,801,847,845,941,904,896,1002,1055,945,1187,1132,1157,1183,1178,1201,1247,1248,1122,1124,1136,1267,1169,1261,1128,1252,1264,1202,1167,1157,1320,1250,1279,1369,1363,1375,1218,1347,1264,1297,1382,1366,1321,1306,1346,1330,1285,1236,1433,1310,1414,252,292,416,488,682,481,511,610,612,747,869,811,811,883,939,911,939,904,937,883,1035,992,945,913,1006,1060,974,1123,1031,1058,1133,1051,1107,1177,1203,1027,1256,1271,1089,1058,1262,1243,1161,1115,1294,1291,1256,1233,1288,1312,1177,1305,1188,1287,1123,1285,1255,1356,1312,1386,1312,1402,1214,1319,229,435,548,549,656,635,667,648,746,700,679,877,730,770,850,918,751,868,957,912,960,853,969,1086,1079,951,985,1088,959,1132,1057,984,995,1123,1009,1082,1021,1088,1126,1153,1192,1200,1227,1252,1219,1154,1124,1243,1134,1224,1269,1298,1148,1274,1328,1144,1148,1265,1303,1279,1299,1249,1333,1245,232,582,621,647,475,685,781,758,702,652,687,772,730,758,793,761,723,754,745,871,898,944,806,971,971,918,998,972,944,908,891,1047,1092,1047,1142,1066,1083,1154,1153,1138,1070,1070,1116,1072,1217,1168,1195,1203,1072,1238,1137,1083,1103,1157,1185,1218,1131,1115,1210,1233,1146,1176,1227,1121,236,417,449,520,562,493,476,617,558,522,586,551,603,576,600,589,586,686,696,735,687,787,862,1026,994,948,892,884,1093,1076,1125,1147,1081,1179,1212,1222,1235,1095,1177,1141,1168,1133,1137,1161,1198,1288,1235,1241,1292,1219,1257,1302,1218,1276,1250,1305,1268,1291,1336,1351,1309,1320,1170,1233,315,461,441,517,570,507,558,659,681,724,629,659,741,683,687,718,810,693,676,766,735,794,844,794,899,793,763,1063,1007,964,1014,1040,1065,956,976,1118,1100,932,1135,1156,1096,1112,1162,1072,1150,1193,1204,1198,1217,1140,1140,1078,1106,1222,1150,1047,1189,1229,1226,1215,1191,1180,1262,1158,213,429,487,463,548,525,589,611,654,619,655,702,696,823,704,804,806,828,847,878,828,825,823,860,864,940,887,917,1013,988,1047,1037,980,1003,1004,1029,1081,1036,1124,992,1092,1086,1187,1160,1180,1232,1193,1195,1251,1260,1237,1148,1106,1298,1279,1297,1281,1314,1265,1353,1342,1335,1289,1180,223,535,502,534,614,537,557,540,712,597,757,695,889,795,713,713,762,848,813,915,802,801,863,859,893,882,959,869,960,1001,923,902,1015,941,1083,1007,1060,1092,1149,1117,1080,1138,983,1026,1175,1155,1168,1203,1197,1259,1282,1264,1250,1161,1190,1261,1282,1288,1302,1331,1216,1337,1190,1352,235,261,346,316,516,448,542,565,613,545,573,593,640,684,726,901,814,874,797,966,999,1061,1101,1107,1105,990,984,1125,1100,1193,1177,1162,1144,1167,1196,1163,1195,1201,1071,1235,1174,1250,1103,1100,1207,1098,1102,1200,1185,1096,1146,1185,1205,1178,1222,1174,1212,1196,1171,1244,1240,1246,1229,1240,230,483,461,530,520,512,543,565,646,671,702,696,763,838,877,857,859,857,826,832,777,844,856,904,993,1065,958,906,968,1103,1103,1097,996,986,966,973,1150,1143,1152,989,974,1133,1000,1142,1130,1094,1165,1104,1159,1127,1156,1195,1206,1249,1223,1234,1235,1200,1182,1171,1163,1225,1223,1207,193,466,454,512,557,599,621,606,670,707,670,721,719,766,734,723,745,796,809,885,855,843,888,957,911,940,969,973,981,1009,1097,976,943,988,1139,1072,1100,1100,1073,1144,1173,1158,1206,1119,1138,1147,1119,1196,1166,1258,1266,1264,1236,1300,1269,1312,1287,1317,1303,1295,1321,1296,1284,1249,235,238,387,475,502,513,553,728,627,800,707,708,696,736,914,819,810,915,906,1052,1075,1032,1055,1084,1087,1016,1121,1121,1151,1143,1053,1067,1084,1201,1163,1162,1131,1065,1217,1266,1265,1300,1270,1209,1322,1325,1242,1241,1335,1225,1342,1368,1326,1234,1282,1250,1324,1290,1289,1316,1289,1343,1269,1207,60,117,155,161,166,159,165,177],"retries_histogram":[1032,0,0,0],"manifest_id":"20261015T092114Z-large-file.bin"} diff --git a/docs/audits/2026-10-15-large-file-streaming/pack-dry-run.log.gz b/docs/audits/2026-10-15-large-file-streaming/pack-dry-run.log.gz new file mode 100644 index 0000000..a15f2fd Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/pack-dry-run.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/pr-checker.log.gz b/docs/audits/2026-10-15-large-file-streaming/pr-checker.log.gz new file mode 100644 index 0000000..fe854c6 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/pr-checker.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/sdk-check.log.gz b/docs/audits/2026-10-15-large-file-streaming/sdk-check.log.gz new file mode 100644 index 0000000..ad70a69 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/sdk-check.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/source.json b/docs/audits/2026-10-15-large-file-streaming/source.json new file mode 100644 index 0000000..6891974 --- /dev/null +++ b/docs/audits/2026-10-15-large-file-streaming/source.json @@ -0,0 +1,18 @@ +{ + "repository": "https://github.com/WithAutonomi/ant-client", + "revision": "225a6768589ecbb6a0c77d70df329f343b22776f", + "dirty": false, + "target": "wasm32-unknown-unknown", + "features": [ + "browser-wasm" + ], + "defaultFeatures": false, + "cargoLockSha256": "64a762b99db03f64e20bdb6699e5e576417dedbd6172049834c64fd27a068314", + "wasmSha256": "9298d1564f1809f97f06b3bacee3a674040ff74c6af1aa3366d1f0ba542a8352", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "wasmPack": "wasm-pack 0.15.0", + "noticesSha256": "96342e4398d82820d5e1fcd89b05910ff621765a78ed1edf8537ee9ef9f13845", + "rustStdCopyrightSha256": "5647be074c8edf7339fd863055923a8fc80bc5610a8d4661ec3b767b9d392c27", + "noticesGeneratorSha256": "fbae032f6ae379d81311ceeabb2d23cd9d40af1a69ca0b422a50b632d7b31bd7", + "noticesConfigSha256": "1d5154cdb0705f6eed668288a0e31f31f273dace0a404f97c091c3d2bdd72cf3" +} diff --git a/docs/audits/2026-10-15-large-file-streaming/verify-wasm.log.gz b/docs/audits/2026-10-15-large-file-streaming/verify-wasm.log.gz new file mode 100644 index 0000000..43e78dc Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/verify-wasm.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/wasm-sync-rate-limit.log.gz b/docs/audits/2026-10-15-large-file-streaming/wasm-sync-rate-limit.log.gz new file mode 100644 index 0000000..451b9a4 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/wasm-sync-rate-limit.log.gz differ diff --git a/docs/audits/2026-10-15-large-file-streaming/wasm-sync.log.gz b/docs/audits/2026-10-15-large-file-streaming/wasm-sync.log.gz new file mode 100644 index 0000000..636db82 Binary files /dev/null and b/docs/audits/2026-10-15-large-file-streaming/wasm-sync.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save.md b/docs/audits/2026-10-15-pointer-manifest-save.md new file mode 100644 index 0000000..091aa61 --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save.md @@ -0,0 +1,121 @@ +# Pointer recovery, manifest export and streaming saves — 2026-10-15 + +SDK implementation `461dc2f8f17080eb8100edd8fa5063e1df1ae7a8` on `feat/large-file-streaming-20261015` +adds the three follow-ups to the [large-file work](2026-10-15-large-file-streaming.md). +Chromium revalidated the committed runtime. The recovery fixture was captured +in an earlier successful run with the same production WASM build. + +## Changes and compatibility + +- Typed pointer address, read, resolve, create, update, transfer, finality and + resume APIs. Counters remain decimal u64 strings, and all five finality states + remain visible. Long-running operations expose cancellation and progress. +- Required durable `onCheckpoint` persistence before pointer payment. Checkpoints + retain the original signed state, verified payment plan, pinned network, + submissions, receipt and paid proof, never the owner seed. Recovery observes an + existing transaction or reuses its proof; it never calls `pay`. Client closure + retains the network until pending pointer work settles. Stalled persistence + callbacks are bounded without preventing storage of a state already paid for. +- Rust-backed `createManifest` and `createFileLink`, canonical `.ant` bytes, + compact links, validated paths/torrent metadata and link-size guidance. The demo + exports a manifest and displays its share link. +- `streamAndSave` owns picker selection, bounded piping and reader cleanup, accepts + file handles or supplied writable streams, and returns bytes written plus hash. + The demo uses this method. Existing `downloadAndSave` results remain compatible. +- Additive SDK APIs and a new versioned, application-owned opaque pointer checkpoint. + No changes to network records, manifest v1 encoding, file storage or node policy. + No new npm dependency. Proposed ADRs 0007/0008 were extended and 0009 was added; + no Accepted ADR was edited or accepted autonomously. + +## Revisions and release dependency + +- ant-client companion branch `feat/browser-pointer-recovery-manifest-export-20261015`: + clean `8316468b7f5f813457f195bccb16a452426d5f35`, based on main + `225a6768589ecbb6a0c77d70df329f343b22776f`. It adds the manifest construction + exports, awaited pre-payment pointer checkpoint and offline paid-proof recovery. +- WASM SHA-256: `6b2e4499fafaca8502be7b8a4447cfae78cf044b2b181e1527fb3d21b07edfd9`. +- Cargo.lock SHA-256: `64a762b99db03f64e20bdb6699e5e576417dedbd6172049834c64fd27a068314`. +- Node: clean browser integration pin + `26129a2611cac08f9f53a557f91fb8806b40a1cc`. +- Native manifest decoder: the previously built CLI from ant-client main + `225a6768589ecbb6a0c77d70df329f343b22776f`; exact binary hash is retained. +- Node.js 22.23.1; Rust 1.99.0; wasm-pack 0.15.0; Python 3.11.15; + Playwright 1.62.1; Chromium 151.0.7922.34. + +`npm run verify:wasm` **fails the expected main-ancestry gate**: the companion +commit is on its feature branch, not ant-client main. Binary and licence hashes +match; the source is clean. Do not release this SDK until the Rust changes are +merged to main and WASM is rebuilt from that main revision. The gate was not +weakened. Both feature branches are intended for review, not a published release. + +## Automated validation + +- `npm run check`: build, TypeScript and **230 tests in 23 files passed**. +- `npm pack --dry-run --json`: prepack repeated those checks successfully; + 97 files include both licences, pointer declarations, WASM and notices. +- `npm run build:examples`: all five Vite applications passed. +- SDK and ant-client ADR governance passed. PR-checker tests: 52/52 passed. +- `cargo test -p ant-core --lib`: **839 tests passed**. +- Native all-target/all-feature ant-core clippy and browser-WASM clippy passed + with `-D warnings`; formatting passed. The dependency future-incompatibility + notice for proc-macro-error2 is unchanged upstream. +- Production-WASM tests round-trip mixed manifests, reject unsafe paths, + collisions, malformed maps/hashes and inexact size hints. A real devnet + checkpoint rebuilds the exact original proof offline; changed signatures, + state/payment-plan mismatches, unsupported versions and missing transactions + reject. This fixture contains public signed records and local-chain evidence, + not owner seeds. +- SDK regressions cover checkpoint failure before payment, failure/timeouts after + payment, interrupted confirmation, no repayment, wrong-network recovery, + cancellation, reader cleanup and deferred network freeing during settlement. + +## Real-browser and native evidence + +Seven isolated WebRTC nodes and a local Anvil chain used ephemeral node ports, +API port 39970 and Vite port 39971. The script checks the local-chain identity +before using the public Anvil development key. Only task-owned processes and +storage were used. + +- Public and private uploads each round-tripped **13,056 bytes** through + browser-created manifests and compact links, then `streamAndSave` into real + OPFS writable files. Size and BLAKE3 matched. The private root map derived size + 13,056 despite its deliberately incorrect creator hint of 1. +- The unchanged native CLI decoded both browser-created `.ant` files with the + same address, kind, path and size information. +- Pointer creation returned counter `0`, update `1`, reads and chain resolution + matched. Permanent transfer returned counter `18446744073709551615`; finality + reported **7 answered / 7 holders**, one final state, with the correct recipient. + Replaying the retained paid state preserved its state ID. +- A payment adapter deliberately threw after a real confirmed transaction. The + checkpoint retained broadcast evidence, and `resumePointer` recovered through + Ethers receipt observation and stored the original signed state. The payment + submission count stayed **exactly one**. Superseded: since `a4b6a53` the write + confirms such a broadcast on the same page itself; the + [2026-10-20 audit](2026-10-20-sha256-check-and-journal-wait.md) covers that + and recovery from a lost page. +- Cancellation of a supplied streaming destination rejected with AbortError, + aborted it once and released its writer lock. +- The actual all-in-one export button downloaded a valid `.ant` file and displayed + its equivalent link. The Stream to disk button saved 13,056 correct bytes to + OPFS. Automated pickers returned real file handles; native picker dialogs were + not automated. +- Successful browser runs had no page errors and observed the exact recorded + WASM hash. Full scripts, summaries, commands, hashes and compressed logs are in + [the evidence directory](2026-10-15-pointer-manifest-save/). + +## Limits and observations + +An initial rapid run stopped before payment after a lookup found only two of +seven required peers; another closed a DataChannel during a pointer read. +Upstream's browser harness documents a 16-request/second node session limit and +paces instant-Anvil pointer operations by 1.5 seconds. Applying the same pacing +and connecting through all devnet seeds passed. No node admission rule or SDK +network policy was bypassed; this does not establish sustained burst performance. + +This is local Chromium correctness evidence, not a mainnet/cross-browser or +throughput claim. The preceding audit covers the 4.3 GB pipeline; this follow-up +validates the new convenience API with small real files and does not repeat that +large fixture. Checkpoint durability depends on the application's callback and +storage; page termination can still hide a transaction hash between broadcast +and persistence. Such an outcome remains unresolved and never authorizes an +automatic second payment. Future SDK releases must first satisfy main provenance. diff --git a/docs/audits/2026-10-15-pointer-manifest-save/adr.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/adr.log.gz new file mode 100644 index 0000000..c5a9247 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/adr.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/browser-rate-limit.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/browser-rate-limit.log.gz new file mode 100644 index 0000000..1ff18bf Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/browser-rate-limit.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/browser-summary.json b/docs/audits/2026-10-15-pointer-manifest-save/browser-summary.json new file mode 100644 index 0000000..adad5a8 --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/browser-summary.json @@ -0,0 +1,85 @@ +{ + "browser": "151.0.7922.34", + "wasm": { + "repository": "https://github.com/WithAutonomi/ant-client", + "revision": "8316468b7f5f813457f195bccb16a452426d5f35", + "dirty": false, + "target": "wasm32-unknown-unknown", + "features": [ + "browser-wasm" + ], + "defaultFeatures": false, + "cargoLockSha256": "64a762b99db03f64e20bdb6699e5e576417dedbd6172049834c64fd27a068314", + "wasmSha256": "6b2e4499fafaca8502be7b8a4447cfae78cf044b2b181e1527fb3d21b07edfd9", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "wasmPack": "wasm-pack 0.15.0", + "noticesSha256": "96342e4398d82820d5e1fcd89b05910ff621765a78ed1edf8537ee9ef9f13845", + "rustStdCopyrightSha256": "5647be074c8edf7339fd863055923a8fc80bc5610a8d4661ec3b767b9d392c27", + "noticesGeneratorSha256": "fbae032f6ae379d81311ceeabb2d23cd9d40af1a69ca0b422a50b632d7b31bd7", + "noticesConfigSha256": "1d5154cdb0705f6eed668288a0e31f31f273dace0a404f97c091c3d2bdd72cf3" + }, + "node": "26129a2611cac08f9f53a557f91fb8806b40a1cc", + "result": [ + { + "visibility": "public", + "bytes": 13056, + "hash": "ddea49284d5219f1e7ce825bacd9926a3dcfe68f5bb3d90b3597bcaf1ead76e0", + "linkRecommended": true, + "manifestBytes": 116 + }, + { + "visibility": "private", + "bytes": 13056, + "hash": "ddea49284d5219f1e7ce825bacd9926a3dcfe68f5bb3d90b3597bcaf1ead76e0", + "linkRecommended": true, + "manifestBytes": 300 + }, + { + "pointers": { + "address": "66315ec5b564bc1cd8e5c8e00930184396480aa59c7fd64a62bff316b600588f", + "created": "0", + "updated": "1", + "transferred": "18446744073709551615", + "finality": { + "address": "66315ec5b564bc1cd8e5c8e00930184396480aa59c7fd64a62bff316b600588f", + "group": 7, + "answered": 7, + "status": "final", + "states": [ + { + "stateId": "d708222bd7cd2a1d2ca02eaf5c0d02767c6ddce894279303e904c62c9bea01a5", + "kind": "pointer", + "kindTag": 1, + "target": "170ce0eefec9925845b6d9b75bad36f45504b315b08a15ab68ac07ead647028f", + "holders": 7, + "transferredTo": "170ce0eefec9925845b6d9b75bad36f45504b315b08a15ab68ac07ead647028f" + } + ], + "majority": { + "stateId": "d708222bd7cd2a1d2ca02eaf5c0d02767c6ddce894279303e904c62c9bea01a5", + "kind": "pointer", + "kindTag": 1, + "target": "170ce0eefec9925845b6d9b75bad36f45504b315b08a15ab68ac07ead647028f", + "holders": 7, + "transferredTo": "170ce0eefec9925845b6d9b75bad36f45504b315b08a15ab68ac07ead647028f" + } + }, + "paidRetry": "d708222bd7cd2a1d2ca02eaf5c0d02767c6ddce894279303e904c62c9bea01a5", + "recovered": "8f56bdf3eacfeec70a51d7299332fae2c0dc7c470f44f7a8c8116c2c88695f14", + "payCount": 1 + } + }, + { + "streamCancellation": { + "cancelled": true, + "aborts": 1, + "locked": false + } + } + ], + "pageErrors": [], + "wasmHashes": [ + "6b2e4499fafaca8502be7b8a4447cfae78cf044b2b181e1527fb3d21b07edfd9" + ], + "sdkRevision": "461dc2f8f17080eb8100edd8fa5063e1df1ae7a8" +} diff --git a/docs/audits/2026-10-15-pointer-manifest-save/browser.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/browser.log.gz new file mode 100644 index 0000000..74d537b Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/browser.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/browser.mjs.gz b/docs/audits/2026-10-15-pointer-manifest-save/browser.mjs.gz new file mode 100644 index 0000000..ee816f8 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/browser.mjs.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/checksums.json b/docs/audits/2026-10-15-pointer-manifest-save/checksums.json new file mode 100644 index 0000000..b8da339 --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/checksums.json @@ -0,0 +1,8 @@ +{ + "/Users/mick/RustroverProjects/ant-browser-sdk-pr6-20261015/dist/client.js": "541e3846d78f4f2e3f6417550ff9c7bb427a9fd43ef7a2aa697944b314bef182", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr6-20261015/dist/pointers.js": "f0657a00ac0fd3fa0aab2709b0773bcd71b4883727bbad7c40ee5128b625fd76", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr6-20261015/dist/manifest.js": "7417362d42fff81d5c4d2d9d39dde84acb5953a8faa3248a8bf0b92f675ab59e", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr6-20261015/src/wasm/ant_core_bg.wasm": "6b2e4499fafaca8502be7b8a4447cfae78cf044b2b181e1527fb3d21b07edfd9", + "/Users/mick/RustroverProjects/ant-client-sdk-sync-20261015/target/release/ant": "d346fef802b00324b7d764f09425dcf4aefcb2b5d44ad670e86a71f63ffdccc4", + "/Users/mick/RustroverProjects/ant-node-sdk-devnet-20261015/target/debug/ant-devnet": "6315c3bc07a54898ff3b486d9e5f4b265cc089f9a5c93410c98228465d04fea3" +} diff --git a/docs/audits/2026-10-15-pointer-manifest-save/clippy-native.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/clippy-native.log.gz new file mode 100644 index 0000000..3784a49 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/clippy-native.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/clippy-wasm.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/clippy-wasm.log.gz new file mode 100644 index 0000000..64c9824 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/clippy-wasm.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/commands.txt b/docs/audits/2026-10-15-pointer-manifest-save/commands.txt new file mode 100644 index 0000000..59aaffb --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/commands.txt @@ -0,0 +1,24 @@ +ant-client worktree: /Users/mick/RustroverProjects/ant-client-sdk-sync-20261015 +cargo fmt --all -- --check +RUSTUP_TOOLCHAIN=1.99.0 cargo test -p ant-core --lib +RUSTUP_TOOLCHAIN=1.99.0 cargo clippy -p ant-core --all-targets --all-features -- -D warnings +RUSTUP_TOOLCHAIN=1.99.0 cargo clippy -p ant-core --target wasm32-unknown-unknown --no-default-features --features browser-wasm -- -D warnings +python3 scripts/adr-governance.py + +SDK worktree: /Users/mick/RustroverProjects/ant-browser-sdk-pr6-20261015 +ANT_CLIENT_DIR=/Users/mick/RustroverProjects/ant-client-sdk-sync-20261015 RUSTUP_TOOLCHAIN=1.99.0 PYTHON=/Users/mick/.local/bin/python3.11 npm run sync:wasm +# Sync and verify used GITHUB_TOKEN obtained from gh auth token without logging it. +npm run check +npm run build:examples +npm pack --dry-run --json +npm run verify:wasm +python3 scripts/adr-governance.py +python3 .github/scripts/test_check_pr.py +npx vite --config examples/all-in-one/vite.config.ts --host 127.0.0.1 --port 39971 --strictPort +node /tmp/sdk-api-browser.mjs +node /tmp/sdk-api-demo.mjs +/Users/mick/RustroverProjects/ant-client-sdk-sync-20261015/target/release/ant --json manifest show /tmp/sdk-api-public.ant +/Users/mick/RustroverProjects/ant-client-sdk-sync-20261015/target/release/ant --json manifest show /tmp/sdk-api-private.ant + +Node worktree: /Users/mick/RustroverProjects/ant-node-sdk-devnet-20261015 +./target/debug/ant-devnet --nodes 7 --bootstrap-count 3 --base-port 0 --webrtc-direct --webrtc-direct-base-port 0 --serve-port 39970 --enable-evm --data-dir /private/tmp/sdk-20261015-api-devnet-data --manifest /private/tmp/sdk-20261015-api-devnet.json diff --git a/docs/audits/2026-10-15-pointer-manifest-save/core-adr.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/core-adr.log.gz new file mode 100644 index 0000000..bcb51cf Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/core-adr.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/demo-summary.json b/docs/audits/2026-10-15-pointer-manifest-save/demo-summary.json new file mode 100644 index 0000000..cf86177 --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/demo-summary.json @@ -0,0 +1,10 @@ +{ + "exportedManifestBytes": 91, + "linkMatchesFile": true, + "disk": { + "size": 13056, + "matches": true + }, + "errors": [], + "browser": "151.0.7922.34" +} \ No newline at end of file diff --git a/docs/audits/2026-10-15-pointer-manifest-save/demo.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/demo.log.gz new file mode 100644 index 0000000..619d872 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/demo.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/demo.mjs.gz b/docs/audits/2026-10-15-pointer-manifest-save/demo.mjs.gz new file mode 100644 index 0000000..878ca9a Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/demo.mjs.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/example-builds.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/example-builds.log.gz new file mode 100644 index 0000000..88be458 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/example-builds.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/native-private.json b/docs/audits/2026-10-15-pointer-manifest-save/native-private.json new file mode 100644 index 0000000..d619fe2 --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/native-private.json @@ -0,0 +1,15 @@ +{ + "entries": [ + { + "address": "f2853ce4176147f432464dd9059bf6c74901ef0aeaf5f032985d268f152e39dd", + "kind": "embedded", + "known_size": 13056, + "name": "folder/private.bin", + "path": "folder/private.bin", + "size": 1 + } + ], + "name": "exports", + "torrent": null, + "total_size": 1 +} diff --git a/docs/audits/2026-10-15-pointer-manifest-save/native-public.json b/docs/audits/2026-10-15-pointer-manifest-save/native-public.json new file mode 100644 index 0000000..d5fe2a8 --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/native-public.json @@ -0,0 +1,15 @@ +{ + "entries": [ + { + "address": "f2853ce4176147f432464dd9059bf6c74901ef0aeaf5f032985d268f152e39dd", + "kind": "public", + "known_size": null, + "name": "folder/public.bin", + "path": "folder/public.bin", + "size": 1 + } + ], + "name": "exports", + "torrent": null, + "total_size": 1 +} diff --git a/docs/audits/2026-10-15-pointer-manifest-save/pack.json.gz b/docs/audits/2026-10-15-pointer-manifest-save/pack.json.gz new file mode 100644 index 0000000..66fa024 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/pack.json.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/pack.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/pack.log.gz new file mode 100644 index 0000000..025be2c Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/pack.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/pr-checker.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/pr-checker.log.gz new file mode 100644 index 0000000..fe854c6 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/pr-checker.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/private.ant b/docs/audits/2026-10-15-pointer-manifest-save/private.ant new file mode 100644 index 0000000..fa8aceb Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/private.ant differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/public.ant b/docs/audits/2026-10-15-pointer-manifest-save/public.ant new file mode 100644 index 0000000..432acdc --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/public.ant @@ -0,0 +1,2 @@ +ÁANT‚¤name§exports§entries‘ƒ¤path±folder/public.bin¤size¦source�¦Public�§addressÄ ò…<äaGô2FMÙ›öÇIï +êõð2˜]&�.9Ý \ No newline at end of file diff --git a/docs/audits/2026-10-15-pointer-manifest-save/rust-tests.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/rust-tests.log.gz new file mode 100644 index 0000000..9d673a0 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/rust-tests.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/sdk-check.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/sdk-check.log.gz new file mode 100644 index 0000000..3fe2c40 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/sdk-check.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/source.json b/docs/audits/2026-10-15-pointer-manifest-save/source.json new file mode 100644 index 0000000..944d167 --- /dev/null +++ b/docs/audits/2026-10-15-pointer-manifest-save/source.json @@ -0,0 +1,18 @@ +{ + "repository": "https://github.com/WithAutonomi/ant-client", + "revision": "8316468b7f5f813457f195bccb16a452426d5f35", + "dirty": false, + "target": "wasm32-unknown-unknown", + "features": [ + "browser-wasm" + ], + "defaultFeatures": false, + "cargoLockSha256": "64a762b99db03f64e20bdb6699e5e576417dedbd6172049834c64fd27a068314", + "wasmSha256": "6b2e4499fafaca8502be7b8a4447cfae78cf044b2b181e1527fb3d21b07edfd9", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "wasmPack": "wasm-pack 0.15.0", + "noticesSha256": "96342e4398d82820d5e1fcd89b05910ff621765a78ed1edf8537ee9ef9f13845", + "rustStdCopyrightSha256": "5647be074c8edf7339fd863055923a8fc80bc5610a8d4661ec3b767b9d392c27", + "noticesGeneratorSha256": "fbae032f6ae379d81311ceeabb2d23cd9d40af1a69ca0b422a50b632d7b31bd7", + "noticesConfigSha256": "1d5154cdb0705f6eed668288a0e31f31f273dace0a404f97c091c3d2bdd72cf3" +} diff --git a/docs/audits/2026-10-15-pointer-manifest-save/verify-wasm.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/verify-wasm.log.gz new file mode 100644 index 0000000..2ce2892 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/verify-wasm.log.gz differ diff --git a/docs/audits/2026-10-15-pointer-manifest-save/wasm-sync.log.gz b/docs/audits/2026-10-15-pointer-manifest-save/wasm-sync.log.gz new file mode 100644 index 0000000..ab4c757 Binary files /dev/null and b/docs/audits/2026-10-15-pointer-manifest-save/wasm-sync.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait.md b/docs/audits/2026-10-20-sha256-check-and-journal-wait.md new file mode 100644 index 0000000..450c9d2 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait.md @@ -0,0 +1,126 @@ +# Manifest SHA-256 checks and wallets awaiting the journal — 2026-10-20 + +SDK `317a634b2fedddc8322c6fbf04b85ab73bbdbfc7` on `feat/large-file-streaming-20261015` +makes `download()` and `downloadAndSave()` check a manifest entry's recorded +`sha256`, and has wallets await `PaymentContext.submitted()` until the broadcast +is journaled. This run repeats the real-Chromium checks of the +[pointer, manifest and save audit](2026-10-15-pointer-manifest-save.md) against +the current bundle, the first real-browser run since `a4b6a53`, and adds checks +for both changes. + +## Revisions + +- SDK `317a634b2fedddc8322c6fbf04b85ab73bbdbfc7`, clean. +- WASM from ant-client `404fa3440c7529d2ab0e10ef200d960d619c0fd4` + (WithAutonomi/ant-client#230 rebased onto main), SHA-256 + `e28ec0cda529ed404f0f22fb260aedbe6d9278e85484fd3e28658669bff08497`; + provenance in [`source.json`](2026-10-20-sha256-check-and-journal-wait/source.json). +- Native CLI: release `ant` built from the same clean `404fa34`, SHA-256 + `49210762ede36e1bfdcd8215ddd1bcdfbdf3e4002b705c5f43203f5415d1bff0`. +- Node: clean browser integration pin `26129a2611cac08f9f53a557f91fb8806b40a1cc`, + `ant-devnet` built with default features (including `webrtc-direct`), SHA-256 + `b605a66b8068c5f69984bdb96922207fd221da75c7033a1074f6df3c09438c03`. +- Node.js 22.23.1; Rust 1.99.0; Playwright 1.62.1; Chromium 151.0.7922.34; + Anvil 1.7.1. + +`npm run verify:wasm` **fails the expected main-ancestry gate**: `404fa34` is +not on ant-client main. Do not release until the WASM is rebuilt from main. + +## Automated validation + +- `npm run check`: build, TypeScript and **258 tests in 23 files passed**. +- `npm run build:examples`: all five Vite applications passed. +- `npm pack --dry-run --json`: prepack repeated the checks; 97 files. +- ADR governance (8 ADRs) and all 52 PR-checker cases passed. + +## Real-browser and native evidence + +Seven isolated WebRTC nodes and a local Anvil chain used ephemeral node ports, +API port 39970 and Vite port 39971. The script checks the local-chain identity +before using the public Anvil development key, and paces network operations by +1.5 seconds as before. Each 13,056-byte file had a fresh random 16-byte prefix, +so every upload paid rather than finding records an earlier run had stored. + +Manifest SHA-256: + +- Browser-created manifests for a public and an embedded private upload recorded + the plaintext SHA-256. It was given in upper case and decoded in lower case + from both the `.ant` bytes and the link. +- `download()` returned the bytes when the entry's claim matched them. For the + same entry with a well-formed wrong claim, it failed with `DOWNLOAD_FAILED` + naming both hashes. +- `downloadAndSave()` saved matching bytes through the picker. With the wrong + claim it failed with `DOWNLOAD_FAILED`, opened no writable, and removed the + empty picked file. +- `streamAndSave()` wrote all bytes under the wrong claim too, as documented: + streaming saves do not check it. Their BLAKE3 matched the upload. +- The native CLI at `404fa34` decoded all four browser-created `.ant` files with + the same `sha256`. `ant manifest download` from the devnet wrote both files, + hashing to the recorded values, and refused both wrong claims with "SHA-256 + mismatch", writing nothing. + +Wallets awaiting the journal: + +- An observed Ethers wallet paid for the public upload, the private upload and a + pointer creation. Each journal callback took 1.5 seconds to persist the + checkpoint carrying the broadcast: in hex MessagePack for the upload, in JSON + for the pointer. `submitted()` settled 1,507 to 1,509 ms after the broadcast, + within 1 ms of that checkpoint persisting, and the wallet returned 13 to 17 ms + later. + +Pointers and recovery: + +- Creation returned counter `0`, update `1` and transfer + `18446744073709551615`. Finality reported **7 answered / 7**, one final state + transferred to the recipient. Replaying the paid state kept its state ID. +- Same page: a wallet that threw after a real confirmed transaction no longer + fails the write. Since `a4b6a53` the write observes the broadcast through its + `wait()` and stores it. The pointer read back at counter `0`, its checkpoint + holds the paid state, and one payment was made. +- Lost page: page A's wallet broadcast, awaited `submitted()`, then never + returned, and page A was closed. Its checkpoint already held the transaction + and no paid state. A fresh page B, whose wallet failed any payment attempt, + resumed from that checkpoint through the Ethers provider's `recover()`, then + stored the pointer at counter `0` and read it back. Payments: **one on page A, + none on page B**. +- Cancellation of a supplied streaming destination rejected with AbortError, + aborted it once and released its writer lock. + +Demo: the all-in-one export button downloaded a valid 91-byte `.ant` file +matching its link, and the Stream to disk button saved 13,056 correct bytes to +OPFS. Pickers were automated; native dialogs were not. + +Every page had no errors, and every WASM response matched the bundled hash. +Scripts, summaries, logs, the four `.ant` files and native outputs are in +[the evidence directory](2026-10-20-sha256-check-and-journal-wait/). The `dist` +hashes there are of the build `npm run check` produced from the same clean +source after the run. + +## Changes from the 2026-10-15 script + +- The embedded entry carries no size hint. Since ant-client `973ae93` the core + refuses one contradicting the root map, such as that script's deliberate hint + of 1. +- The recovery check is split into same-page confirmation and a lost page, + because the write now confirms a same-page broadcast itself. +- Each file has fresh content, and three wallets are observed with a slow journal. + +## Runs and limits + +Six runs used the same devnet; the sixth passed every check. The first, third, +fourth and fifth stopped at the script issues above: the size hint, the +same-page recovery, uploads that found their records already stored and did not +pay, and looking for the upload checkpoint's hash as plain hex. The second +stopped when a paid pointer update was stored on 4 of 5 close-group peers after +a WebRTC dial dropped ("close-group PUT shortfall (dial churn)"); its checkpoint +was not resumed. That failure is in the core's store after payment, like the +DataChannel closure in the earlier audit; its log is +[kept](2026-10-20-sha256-check-and-journal-wait/browser-dial-churn.log.gz). +This run does not establish its rate. + +This is local Chromium correctness evidence, not a mainnet, cross-browser or +throughput claim. Only the Ethers provider ran; the Wagmi provider, Merkle +payments, the 30-second bound and a missing WebCrypto are covered by unit tests +only. The pages ran on `http://127.0.0.1`, a secure context. The 4.3 GB +pipeline of the [large-file audit](2026-10-15-large-file-streaming.md) was not +repeated. diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/adr.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/adr.log.gz new file mode 100644 index 0000000..0ec7fd3 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/adr.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser-dial-churn.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser-dial-churn.log.gz new file mode 100644 index 0000000..d7070d9 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser-dial-churn.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser-summary.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser-summary.json new file mode 100644 index 0000000..1099535 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser-summary.json @@ -0,0 +1,181 @@ +{ + "browser": "151.0.7922.34", + "sdkRevision": "317a634b2fedddc8322c6fbf04b85ab73bbdbfc7", + "wasm": { + "repository": "https://github.com/WithAutonomi/ant-client", + "revision": "404fa3440c7529d2ab0e10ef200d960d619c0fd4", + "dirty": false, + "target": "wasm32-unknown-unknown", + "features": [ + "browser-wasm" + ], + "defaultFeatures": false, + "cargoLockSha256": "d3682d218f167a7e216cacbe9fe189b926ff7ed685bba95a3c02b0e6d85ee373", + "wasmSha256": "e28ec0cda529ed404f0f22fb260aedbe6d9278e85484fd3e28658669bff08497", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "wasmPack": "wasm-pack 0.15.0", + "noticesSha256": "60ff80092cfb004811f2b4bc3292f12e56bd9a60b3960007ae8ff5236e3485e0", + "rustStdCopyrightSha256": "5647be074c8edf7339fd863055923a8fc80bc5610a8d4661ec3b767b9d392c27", + "noticesGeneratorSha256": "fbae032f6ae379d81311ceeabb2d23cd9d40af1a69ca0b422a50b632d7b31bd7", + "noticesConfigSha256": "1d5154cdb0705f6eed668288a0e31f31f273dace0a404f97c091c3d2bdd72cf3" + }, + "node": "26129a2611cac08f9f53a557f91fb8806b40a1cc", + "sha256s": { + "public": "904a3873e93109e3c3f84856be8a082da63c28dedba1f03688b6b71e95fcbac2", + "private": "ab9a1202e187d81f055e61e0658e40d3a37030c8e222b140183b3adc6529af42" + }, + "wrongSha256s": { + "public": "004a3873e93109e3c3f84856be8a082da63c28dedba1f03688b6b71e95fcbac2", + "private": "0b9a1202e187d81f055e61e0658e40d3a37030c8e222b140183b3adc6529af42" + }, + "prefixes": { + "public": "c65f816209f5c871eebcf59336dcc6bb", + "private": "0b514109838fdc7ec1b285e428414357" + }, + "result": [ + { + "visibility": "public", + "bytes": 13056, + "hash": "fe4211228c6cdfee62aace2752530eb725c50d0071d944b3c1c35f254b784d5b", + "sha256": "904a3873e93109e3c3f84856be8a082da63c28dedba1f03688b6b71e95fcbac2", + "linkRecommended": true, + "manifestBytes": 157, + "download": { + "bytes": 13056, + "matchesClaim": true + }, + "downloadMismatch": { + "code": "DOWNLOAD_FAILED" + }, + "downloadAndSave": { + "method": "file-picker" + }, + "downloadAndSaveMismatch": { + "code": "DOWNLOAD_FAILED", + "writables": 0, + "refusedFileLeft": false + }, + "streamAndSaveIgnoresClaim": { + "bytesWritten": 13056 + } + }, + { + "visibility": "private", + "bytes": 13056, + "hash": "4b891d822ea88a54a34238945c77a09bf2e992936c0b84eae9603858103340f8", + "sha256": "ab9a1202e187d81f055e61e0658e40d3a37030c8e222b140183b3adc6529af42", + "linkRecommended": true, + "manifestBytes": 335, + "download": { + "bytes": 13056, + "matchesClaim": true + }, + "downloadMismatch": { + "code": "DOWNLOAD_FAILED" + }, + "downloadAndSave": { + "method": "file-picker" + }, + "downloadAndSaveMismatch": { + "code": "DOWNLOAD_FAILED", + "writables": 0, + "refusedFileLeft": false + }, + "streamAndSaveIgnoresClaim": { + "bytesWritten": 13056 + } + }, + { + "pointers": { + "fileAddress": "56d383f4a837fd7e5dd79f9d6be821852e59b45f38e396ba8d184fb9e1a09798", + "address": "20928dcf861fad8a2e8f90ee1faffbc1a0d3725f0e58370ffe91752de85026de", + "created": "0", + "updated": "1", + "transferred": "18446744073709551615", + "finality": { + "address": "20928dcf861fad8a2e8f90ee1faffbc1a0d3725f0e58370ffe91752de85026de", + "group": 7, + "answered": 7, + "status": "final", + "states": [ + { + "stateId": "ab532131ceac59b2e24e7a886dde0ab12c104f25a2515d85734ae1b17380bc3b", + "kind": "pointer", + "kindTag": 1, + "target": "c9bb4399fe00c8e820f375a7f9d15c155a72a09bd5ac52c3cde62b464dec23df", + "holders": 7, + "transferredTo": "c9bb4399fe00c8e820f375a7f9d15c155a72a09bd5ac52c3cde62b464dec23df" + } + ], + "majority": { + "stateId": "ab532131ceac59b2e24e7a886dde0ab12c104f25a2515d85734ae1b17380bc3b", + "kind": "pointer", + "kindTag": 1, + "target": "c9bb4399fe00c8e820f375a7f9d15c155a72a09bd5ac52c3cde62b464dec23df", + "holders": 7, + "transferredTo": "c9bb4399fe00c8e820f375a7f9d15c155a72a09bd5ac52c3cde62b464dec23df" + } + }, + "paidRetry": "ab532131ceac59b2e24e7a886dde0ab12c104f25a2515d85734ae1b17380bc3b", + "samePageConfirmed": "74637e69046a3f832461069540f51ce9e1185939b5d70fa55b929cb69c5815cb", + "payCount": 1 + } + }, + { + "streamCancellation": { + "cancelled": true, + "aborts": 1, + "locked": false + } + }, + { + "journalWaits": { + "upload public": [ + { + "checkpointsWithoutHash": 0, + "transactionHash": "0x6d9c2092c79bf5e82b8f1295f12af0ee9b020c40c6b44961532e1de862a39d03", + "broadcastMs": 1756, + "journalPersistedMs": 3264, + "submittedSettledMs": 3265, + "walletReturnedMs": 3282 + } + ], + "upload private": [ + { + "checkpointsWithoutHash": 0, + "transactionHash": "0x4f0dab76ade583b05b9af6cca0d9acca1b53bf8de8a768e0ddf92ce8c0cb6c07", + "broadcastMs": 14334, + "journalPersistedMs": 15842, + "submittedSettledMs": 15842, + "walletReturnedMs": 15855 + } + ], + "pointer create": [ + { + "checkpointsWithoutHash": 0, + "transactionHash": "0xfb240476ffa5cf170a4e9b13c964c0a98ce47a9f00e6524102c17e70c008756c", + "broadcastMs": 28333, + "journalPersistedMs": 29840, + "submittedSettledMs": 29840, + "walletReturnedMs": 29854 + } + ] + } + }, + { + "lostPageRecovery": { + "address": "b97428a33f15d1bd8c292f040a59007f1b0479720b5ee43c8d52c9b947ba83b2", + "transactionHash": "0x795560b1e630348f9310f12b1bd29f96631ad03d8dd9c266a2ad1c547c6caece", + "lostPagePayments": 1, + "recoveryPagePayments": 0, + "stateId": "f188607bf1d6fd601db273cb1da9e629e99ba00ca17919c76ccc50e63a957e70" + } + } + ], + "pageErrors": [], + "wasmHashes": [ + "e28ec0cda529ed404f0f22fb260aedbe6d9278e85484fd3e28658669bff08497", + "e28ec0cda529ed404f0f22fb260aedbe6d9278e85484fd3e28658669bff08497", + "e28ec0cda529ed404f0f22fb260aedbe6d9278e85484fd3e28658669bff08497" + ] +} \ No newline at end of file diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser.log.gz new file mode 100644 index 0000000..4c04e95 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser.mjs.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser.mjs.gz new file mode 100644 index 0000000..4689811 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/browser.mjs.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/checksums.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/checksums.json new file mode 100644 index 0000000..e30da14 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/checksums.json @@ -0,0 +1,9 @@ +{ + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/client.js": "1b62355d096b83ab69e70b5f85670a16957646cd8ae2d114a4ee0fe590161d8b", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/pointers.js": "12a5b98e1b506211de210069e0e079db7c00a3d3c3ac039655e9e7f0780e2ac1", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/manifest.js": "94db7389d4ca612b59fa77b4a3672521732680fcbc7f3b55543536fc0fa481f8", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/ethers.js": "241060deb5f758b01c0a058ef0323bb7a602d7b2134308da49c680f11e7dccf8", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/src/wasm/ant_core_bg.wasm": "e28ec0cda529ed404f0f22fb260aedbe6d9278e85484fd3e28658669bff08497", + "/Users/mick/RustroverProjects/ant-client-pr230-bab6beb-sdk/target/release/ant": "49210762ede36e1bfdcd8215ddd1bcdfbdf3e4002b705c5f43203f5415d1bff0", + "/Users/mick/RustroverProjects/ant-node-sdk-devnet-20261015/target/debug/ant-devnet": "b605a66b8068c5f69984bdb96922207fd221da75c7033a1074f6df3c09438c03" +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/commands.txt b/docs/audits/2026-10-20-sha256-check-and-journal-wait/commands.txt new file mode 100644 index 0000000..2180e8c --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/commands.txt @@ -0,0 +1,23 @@ +Node worktree: /Users/mick/RustroverProjects/ant-node-sdk-devnet-20261015 (clean 26129a2611cac08f9f53a557f91fb8806b40a1cc) +RUSTUP_TOOLCHAIN=1.99.0 cargo build --bin ant-devnet +./target/debug/ant-devnet --nodes 7 --bootstrap-count 3 --base-port 0 --webrtc-direct --webrtc-direct-base-port 0 --serve-port 39970 --enable-evm --data-dir /data --manifest /devnet.json + +ant-client worktree: /Users/mick/RustroverProjects/ant-client-pr230-bab6beb-sdk (clean, detached at 404fa3440c7529d2ab0e10ef200d960d619c0fd4) +RUSTUP_TOOLCHAIN=1.99.0 cargo build --release --bin ant + +SDK worktree: /Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb (clean 317a634b2fedddc8322c6fbf04b85ab73bbdbfc7) +npm run build +npx vite --config examples/all-in-one/vite.config.ts --host 127.0.0.1 --port 39971 --strictPort +node /browser.mjs /evidence +node /demo.mjs /evidence +# The CLI ran under a throwaway HOME so nothing reached the user's data directory. +HOME=/cli-home ant --json manifest show .ant +HOME=/cli-home ant --json --devnet-manifest /devnet.json --allow-loopback manifest download .ant -o /native/out- +# for public, private, public-wrong-sha256 and private-wrong-sha256 +npm run check +npm run build:examples +npm pack --dry-run --json +python3 scripts/adr-governance.py +python3 .github/scripts/test_check_pr.py +# verify:wasm used GITHUB_TOKEN obtained from gh auth token without logging it. +npm run verify:wasm diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo-summary.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo-summary.json new file mode 100644 index 0000000..cf86177 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo-summary.json @@ -0,0 +1,10 @@ +{ + "exportedManifestBytes": 91, + "linkMatchesFile": true, + "disk": { + "size": 13056, + "matches": true + }, + "errors": [], + "browser": "151.0.7922.34" +} \ No newline at end of file diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo.log.gz new file mode 100644 index 0000000..83e4a86 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo.mjs.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo.mjs.gz new file mode 100644 index 0000000..202ab55 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/demo.mjs.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/example-builds.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/example-builds.log.gz new file mode 100644 index 0000000..347cd56 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/example-builds.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-private-wrong-sha256.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-private-wrong-sha256.json new file mode 100644 index 0000000..0347884 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-private-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/private.bin", + "status": { + "error": "SHA-256 mismatch: the manifest records 0b9a1202e187d81f055e61e0658e40d3a37030c8e222b140183b3adc6529af42, the downloaded file hashes to ab9a1202e187d81f055e61e0658e40d3a37030c8e222b140183b3adc6529af42", + "status": "failed" + } + } + ], + "failed": 1, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/devnet-20261020/native/out-private-wrong-sha256", + "written": 0 +} +Error: 1 entr(y/ies) failed to download diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-private.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-private.json new file mode 100644 index 0000000..cf1f714 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-private.json @@ -0,0 +1,15 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/private.bin", + "status": { + "bytes": 13056, + "status": "written" + } + } + ], + "failed": 0, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/devnet-20261020/native/out-private", + "written": 1 +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-public-wrong-sha256.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-public-wrong-sha256.json new file mode 100644 index 0000000..a83cb85 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-public-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/public.bin", + "status": { + "error": "SHA-256 mismatch: the manifest records 004a3873e93109e3c3f84856be8a082da63c28dedba1f03688b6b71e95fcbac2, the downloaded file hashes to 904a3873e93109e3c3f84856be8a082da63c28dedba1f03688b6b71e95fcbac2", + "status": "failed" + } + } + ], + "failed": 1, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/devnet-20261020/native/out-public-wrong-sha256", + "written": 0 +} +Error: 1 entr(y/ies) failed to download diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-public.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-public.json new file mode 100644 index 0000000..547e889 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-download-public.json @@ -0,0 +1,15 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/public.bin", + "status": { + "bytes": 13056, + "status": "written" + } + } + ], + "failed": 0, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/devnet-20261020/native/out-public", + "written": 1 +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-private-wrong-sha256.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-private-wrong-sha256.json new file mode 100644 index 0000000..acfff8a --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-private-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "c71029d31da183fa2725c7b939cbacb33ad5060ec953c2f99cb5b31c9fba7a84", + "kind": "embedded", + "known_size": 13056, + "name": "folder/private.bin", + "path": "folder/private.bin", + "sha256": "0b9a1202e187d81f055e61e0658e40d3a37030c8e222b140183b3adc6529af42", + "size": null + } + ], + "name": "exports", + "torrent": null, + "total_size": null +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-private.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-private.json new file mode 100644 index 0000000..ccb37b5 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-private.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "c71029d31da183fa2725c7b939cbacb33ad5060ec953c2f99cb5b31c9fba7a84", + "kind": "embedded", + "known_size": 13056, + "name": "folder/private.bin", + "path": "folder/private.bin", + "sha256": "ab9a1202e187d81f055e61e0658e40d3a37030c8e222b140183b3adc6529af42", + "size": null + } + ], + "name": "exports", + "torrent": null, + "total_size": null +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-public-wrong-sha256.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-public-wrong-sha256.json new file mode 100644 index 0000000..faabd17 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-public-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "56d383f4a837fd7e5dd79f9d6be821852e59b45f38e396ba8d184fb9e1a09798", + "kind": "public", + "known_size": null, + "name": "folder/public.bin", + "path": "folder/public.bin", + "sha256": "004a3873e93109e3c3f84856be8a082da63c28dedba1f03688b6b71e95fcbac2", + "size": null + } + ], + "name": "exports", + "torrent": null, + "total_size": null +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-public.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-public.json new file mode 100644 index 0000000..7a24797 --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/native-show-public.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "56d383f4a837fd7e5dd79f9d6be821852e59b45f38e396ba8d184fb9e1a09798", + "kind": "public", + "known_size": null, + "name": "folder/public.bin", + "path": "folder/public.bin", + "sha256": "904a3873e93109e3c3f84856be8a082da63c28dedba1f03688b6b71e95fcbac2", + "size": 1 + } + ], + "name": "exports", + "torrent": null, + "total_size": 1 +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/pack.json.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/pack.json.gz new file mode 100644 index 0000000..3fb40a6 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/pack.json.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/pack.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/pack.log.gz new file mode 100644 index 0000000..40568f3 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/pack.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/pr-checker.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/pr-checker.log.gz new file mode 100644 index 0000000..fec1219 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/pr-checker.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/private-wrong-sha256.ant b/docs/audits/2026-10-20-sha256-check-and-journal-wait/private-wrong-sha256.ant new file mode 100644 index 0000000..36077c9 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/private-wrong-sha256.ant differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/private.ant b/docs/audits/2026-10-20-sha256-check-and-journal-wait/private.ant new file mode 100644 index 0000000..36e80e5 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/private.ant differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/public-wrong-sha256.ant b/docs/audits/2026-10-20-sha256-check-and-journal-wait/public-wrong-sha256.ant new file mode 100644 index 0000000..3b9e26b Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/public-wrong-sha256.ant differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/public.ant b/docs/audits/2026-10-20-sha256-check-and-journal-wait/public.ant new file mode 100644 index 0000000..5e38bfd --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/public.ant @@ -0,0 +1 @@ +ÁANT‚¤name§exports§entries‘„¤path±folder/public.bin¤size¦sha256Ä �J8sé1 ãÃøHV¾Š-¦<(Þۡð6ˆ¶·•üº¦source�¦Public�§addressÄ VӃô¨7ý~]ן�kè!….Y´_8㖺�O¹᠗˜ \ No newline at end of file diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/sdk-check.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/sdk-check.log.gz new file mode 100644 index 0000000..f05d602 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/sdk-check.log.gz differ diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/source.json b/docs/audits/2026-10-20-sha256-check-and-journal-wait/source.json new file mode 100644 index 0000000..0827a0c --- /dev/null +++ b/docs/audits/2026-10-20-sha256-check-and-journal-wait/source.json @@ -0,0 +1,18 @@ +{ + "repository": "https://github.com/WithAutonomi/ant-client", + "revision": "404fa3440c7529d2ab0e10ef200d960d619c0fd4", + "dirty": false, + "target": "wasm32-unknown-unknown", + "features": [ + "browser-wasm" + ], + "defaultFeatures": false, + "cargoLockSha256": "d3682d218f167a7e216cacbe9fe189b926ff7ed685bba95a3c02b0e6d85ee373", + "wasmSha256": "e28ec0cda529ed404f0f22fb260aedbe6d9278e85484fd3e28658669bff08497", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "wasmPack": "wasm-pack 0.15.0", + "noticesSha256": "60ff80092cfb004811f2b4bc3292f12e56bd9a60b3960007ae8ff5236e3485e0", + "rustStdCopyrightSha256": "5647be074c8edf7339fd863055923a8fc80bc5610a8d4661ec3b767b9d392c27", + "noticesGeneratorSha256": "fbae032f6ae379d81311ceeabb2d23cd9d40af1a69ca0b422a50b632d7b31bd7", + "noticesConfigSha256": "1d5154cdb0705f6eed668288a0e31f31f273dace0a404f97c091c3d2bdd72cf3" +} diff --git a/docs/audits/2026-10-20-sha256-check-and-journal-wait/verify-wasm.log.gz b/docs/audits/2026-10-20-sha256-check-and-journal-wait/verify-wasm.log.gz new file mode 100644 index 0000000..af834b8 Binary files /dev/null and b/docs/audits/2026-10-20-sha256-check-and-journal-wait/verify-wasm.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14.md b/docs/audits/2026-10-22-ant-client-main-8b19c14.md new file mode 100644 index 0000000..d7fe633 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14.md @@ -0,0 +1,98 @@ +# WASM rebuilt from ant-client main `8b19c14` — 2026-10-22 + +WithAutonomi/ant-client#230 merged into main as `8b19c1499360ebd8166a1ef3dd65b88db914dd7f`. +The SDK's bundled WASM is rebuilt from that clean main revision for the 0.1.3 +release, and the checks of the +[2026-10-20 audit](2026-10-20-sha256-check-and-journal-wait.md) were repeated +against it with the same scripts. + +## What the rebuild brings + +The previous bundle came from `404fa34`, which reached main through #230. Main +adds, for the browser build: + +- `93f3bd2`: the core checks unknown keys on any JavaScript object, including + one without `Object.prototype` or from another frame. The SDK already passes + its own option objects and `structuredClone` copies, so its behavior does not + change. +- WithAutonomi/ant-client#225: hot-record reads spread across the close group + and back off briefly from nodes that decline. +- `a205d06`: a `[patch.crates-io]` pinning `saorsa-transport` and `ant-node` to + git revisions for ADR-0023 testnet builds, to be removed once those crates are + published. `cargo tree` for the browser target shows `saorsa-transport` as a + normal dependency, so the bundle now contains `saorsa-transport 0.38.1` from + `https://github.com/WithAutonomi/saorsa-transport` at + `520dd3ef7540b1743e644ecdd8a74f2b58ec43a9` instead of the crates.io release. + The licence notices record that source. `ant-node` is only a dev-dependency + there and is not bundled. + +The JS glue gains an import reading a peer connection's ICE state and loses the +`instanceof Object` import `93f3bd2` no longer uses; the rest is renumbered +closures and reordered internal exports. The bindings' exports and the SDK's +public API are unchanged. + +## Revisions + +- SDK `317a634b2fedddc8322c6fbf04b85ab73bbdbfc7` with the rebuilt `src/wasm` and + the 0.1.3 version, as committed together with this audit. +- WASM from clean ant-client main `8b19c1499360ebd8166a1ef3dd65b88db914dd7f`, + SHA-256 `0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8`; + provenance in [`source.json`](2026-10-22-ant-client-main-8b19c14/source.json). +- Native CLI: release `ant` built from the same main revision, SHA-256 + `50d4916d5a8a42dbdd761f5d7a498a7587a503df1ede66acec5e0696589322ec`. +- Node: clean browser integration pin `26129a2611cac08f9f53a557f91fb8806b40a1cc`, + which main's browser tests still pin; the same `ant-devnet` binary as the + 2026-10-20 audit. +- Node.js 22.23.1; Rust 1.99.0; wasm-pack 0.15.0; Python 3.11; Playwright 1.62.1; + Chromium 151.0.7922.34; Anvil 1.7.1. + +## Automated validation + +- `npm run verify:wasm` **passed**: `8b19c14` is on main, and the binary and + licence hashes match. +- `npm run check`: build, TypeScript and **258 tests in 23 files passed**. +- `npm run build:examples`: all five Vite applications passed. +- `npm pack --dry-run --json`: `withautonomi-ant-browser-sdk-0.1.3.tgz`, 97 files. +- ADR governance (8 ADRs) and all 52 PR-checker cases passed. + +## Real-browser and native evidence + +Seven isolated WebRTC nodes and a local Anvil chain, set up and paced as in the +2026-10-20 audit. The first run passed every check: + +- Manifest SHA-256, for a public and an embedded private 13,056-byte upload: + `download()` returned bytes meeting the recorded claim and failed a wrong claim + with `DOWNLOAD_FAILED`; `downloadAndSave()` saved matching bytes and, for a + wrong claim, opened no writable and removed the empty picked file; + `streamAndSave()` wrote all bytes under either claim, as documented. +- The native CLI at `8b19c14` decoded all four browser-created `.ant` files with + the same `sha256`, downloaded both files from the devnet hashing to the + recorded values, and refused both wrong claims with "SHA-256 mismatch". +- Wallets awaiting the journal: for the public upload, the private upload and a + pointer creation, `submitted()` settled 1,506 to 1,509 ms after the broadcast, + when the deliberately slow checkpoint carrying it persisted, and the wallet + returned 14 or 15 ms later. +- Pointers: counters `0`, `1` and `18446744073709551615`; finality **7 answered / + 7**, one final state; paid replay kept its state ID. +- A wallet throwing after a real transaction: the write confirmed it on the same + page, with one payment. A page lost after its broadcast was journaled: a fresh + page recovered the pointer through Ethers `recover()`, with **one payment on + the lost page and none on the recovering page**. +- Streaming cancellation rejected with AbortError, aborted once and released its + lock. The all-in-one demo exported a valid 91-byte `.ant` matching its link and + streamed 13,056 correct bytes to OPFS. + +No page errors; every WASM response matched the bundled hash. Summaries, logs, +`.ant` files and native outputs are in +[the evidence directory](2026-10-22-ant-client-main-8b19c14/); the scripts are +those of the 2026-10-20 evidence directory. + +## Limits + +The run checks correctness against nodes at the browser integration pin, not +against the ADR-0023 node tip the transport pin targets, nor mainnet. It does not +measure the effect of #225 on read performance. Only the Ethers provider ran in +a browser; the Wagmi provider, Merkle payments, the 30-second journal bound and a +missing WebCrypto are covered by unit tests. The release bundles a transport +revision that is not a crates.io release; a later main without the patch should +be released with a rebuilt bundle. diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/adr.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/adr.log.gz new file mode 100644 index 0000000..3e94692 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/adr.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/browser-summary.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/browser-summary.json new file mode 100644 index 0000000..5e1b03b --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/browser-summary.json @@ -0,0 +1,181 @@ +{ + "browser": "151.0.7922.34", + "sdkRevision": "317a634b2fedddc8322c6fbf04b85ab73bbdbfc7", + "wasm": { + "repository": "https://github.com/WithAutonomi/ant-client", + "revision": "8b19c1499360ebd8166a1ef3dd65b88db914dd7f", + "dirty": false, + "target": "wasm32-unknown-unknown", + "features": [ + "browser-wasm" + ], + "defaultFeatures": false, + "cargoLockSha256": "68a8483073d0e6681bfa47abe84e811a4e782379e459e0cb80625567a76d4478", + "wasmSha256": "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "wasmPack": "wasm-pack 0.15.0", + "noticesSha256": "2c7c966b1bc09d09d0632018663ce2170abfd9a00cefdc8901824251deab421c", + "rustStdCopyrightSha256": "5647be074c8edf7339fd863055923a8fc80bc5610a8d4661ec3b767b9d392c27", + "noticesGeneratorSha256": "fbae032f6ae379d81311ceeabb2d23cd9d40af1a69ca0b422a50b632d7b31bd7", + "noticesConfigSha256": "1d5154cdb0705f6eed668288a0e31f31f273dace0a404f97c091c3d2bdd72cf3" + }, + "node": "26129a2611cac08f9f53a557f91fb8806b40a1cc", + "sha256s": { + "public": "b3c6483f461810aa56ef3b2a30fb6dc31efb00bb83c7e0d5048c6d1f1169f9e6", + "private": "72f690916557f0313ea0b0da3be6d4072b25649f67c51a50d45d564b42100c91" + }, + "wrongSha256s": { + "public": "03c6483f461810aa56ef3b2a30fb6dc31efb00bb83c7e0d5048c6d1f1169f9e6", + "private": "02f690916557f0313ea0b0da3be6d4072b25649f67c51a50d45d564b42100c91" + }, + "prefixes": { + "public": "330f97d143095d4043f19360db251e73", + "private": "a184cc63012fca12bf0d0c280ec57169" + }, + "result": [ + { + "visibility": "public", + "bytes": 13056, + "hash": "3726a5932dcf4859f2957fe67f54a15f12962b2ba4d3fb135a20f4c296bad8bd", + "sha256": "b3c6483f461810aa56ef3b2a30fb6dc31efb00bb83c7e0d5048c6d1f1169f9e6", + "linkRecommended": true, + "manifestBytes": 157, + "download": { + "bytes": 13056, + "matchesClaim": true + }, + "downloadMismatch": { + "code": "DOWNLOAD_FAILED" + }, + "downloadAndSave": { + "method": "file-picker" + }, + "downloadAndSaveMismatch": { + "code": "DOWNLOAD_FAILED", + "writables": 0, + "refusedFileLeft": false + }, + "streamAndSaveIgnoresClaim": { + "bytesWritten": 13056 + } + }, + { + "visibility": "private", + "bytes": 13056, + "hash": "b34998e70c7d2b09322f5733f222f52d06b3a21eef05663ef27fc6e9db7d5508", + "sha256": "72f690916557f0313ea0b0da3be6d4072b25649f67c51a50d45d564b42100c91", + "linkRecommended": true, + "manifestBytes": 335, + "download": { + "bytes": 13056, + "matchesClaim": true + }, + "downloadMismatch": { + "code": "DOWNLOAD_FAILED" + }, + "downloadAndSave": { + "method": "file-picker" + }, + "downloadAndSaveMismatch": { + "code": "DOWNLOAD_FAILED", + "writables": 0, + "refusedFileLeft": false + }, + "streamAndSaveIgnoresClaim": { + "bytesWritten": 13056 + } + }, + { + "pointers": { + "fileAddress": "2b4dd2223e6c2bce66c211f40ed8b0f35d9ad35afa37c281449bc56db3285f63", + "address": "5cbaf95194bd809dbe50617f0e52f0b7a73cd9efde1a116c8e0a7f1a7d0c46bf", + "created": "0", + "updated": "1", + "transferred": "18446744073709551615", + "finality": { + "address": "5cbaf95194bd809dbe50617f0e52f0b7a73cd9efde1a116c8e0a7f1a7d0c46bf", + "group": 7, + "answered": 7, + "status": "final", + "states": [ + { + "stateId": "3627ca31398dc1a81a4e032ace0bf206a7cdfddfd02645d81d80ed7e292d9df6", + "kind": "pointer", + "kindTag": 1, + "target": "aa5b20e76f2e5088420a4118693c9be7c84d26898f5c8f8a03be12fd877a4001", + "holders": 7, + "transferredTo": "aa5b20e76f2e5088420a4118693c9be7c84d26898f5c8f8a03be12fd877a4001" + } + ], + "majority": { + "stateId": "3627ca31398dc1a81a4e032ace0bf206a7cdfddfd02645d81d80ed7e292d9df6", + "kind": "pointer", + "kindTag": 1, + "target": "aa5b20e76f2e5088420a4118693c9be7c84d26898f5c8f8a03be12fd877a4001", + "holders": 7, + "transferredTo": "aa5b20e76f2e5088420a4118693c9be7c84d26898f5c8f8a03be12fd877a4001" + } + }, + "paidRetry": "3627ca31398dc1a81a4e032ace0bf206a7cdfddfd02645d81d80ed7e292d9df6", + "samePageConfirmed": "ac6e2bab0bef3f5cb897be1fd274ef3b657dbf7deb2b8e38e5ad36f8c7efbcf5", + "payCount": 1 + } + }, + { + "streamCancellation": { + "cancelled": true, + "aborts": 1, + "locked": false + } + }, + { + "journalWaits": { + "upload public": [ + { + "checkpointsWithoutHash": 0, + "transactionHash": "0xd1af337d415a4a6335b66fa9f26c449a53b06e2025f45f7c57d574e181a63a0a", + "broadcastMs": 1848, + "journalPersistedMs": 3357, + "submittedSettledMs": 3357, + "walletReturnedMs": 3372 + } + ], + "upload private": [ + { + "checkpointsWithoutHash": 0, + "transactionHash": "0x1fb7a76884023aa463ee16c00fa5f51546dff3d7b098501b1673493c9dee5a20", + "broadcastMs": 14429, + "journalPersistedMs": 15937, + "submittedSettledMs": 15937, + "walletReturnedMs": 15951 + } + ], + "pointer create": [ + { + "checkpointsWithoutHash": 0, + "transactionHash": "0x21e7bc249133fe95c105dcfe0177a1f5ad9c7e2895f17fdbe0c4d28fd2f5865c", + "broadcastMs": 28473, + "journalPersistedMs": 29979, + "submittedSettledMs": 29979, + "walletReturnedMs": 29993 + } + ] + } + }, + { + "lostPageRecovery": { + "address": "e3007b9b4610a70574b22992064edb99bcb4be8c4dd0d7b0ba1d474c6ec9f5a1", + "transactionHash": "0x0d96ece8f153e11bf411412229fa7d293e3eb3d9f045e090a712ce79afbfa10e", + "lostPagePayments": 1, + "recoveryPagePayments": 0, + "stateId": "757bd24e105329e097f960e270d30eda8aea1baa03d807dc617564b2d45b7973" + } + } + ], + "pageErrors": [], + "wasmHashes": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8", + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8", + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] +} \ No newline at end of file diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/browser.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/browser.log.gz new file mode 100644 index 0000000..53eba4a Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/browser.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/checksums.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/checksums.json new file mode 100644 index 0000000..8e726a4 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/checksums.json @@ -0,0 +1,11 @@ +{ + "/browser.mjs": "82c025e53854f1a0094fcc9887a3f8fb2dc609d299b0cb24ef84a7e506434c28", + "/demo.mjs": "1ab3628f2d9cc4db0cd0864cb1f408df3c39ef11307609438b032253954a488c", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/client.js": "1b62355d096b83ab69e70b5f85670a16957646cd8ae2d114a4ee0fe590161d8b", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/pointers.js": "12a5b98e1b506211de210069e0e079db7c00a3d3c3ac039655e9e7f0780e2ac1", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/manifest.js": "94db7389d4ca612b59fa77b4a3672521732680fcbc7f3b55543536fc0fa481f8", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/dist/ethers.js": "241060deb5f758b01c0a058ef0323bb7a602d7b2134308da49c680f11e7dccf8", + "/Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb/src/wasm/ant_core_bg.wasm": "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8", + "/Users/mick/RustroverProjects/ant-client-pr230-bab6beb-sdk/target/release/ant": "50d4916d5a8a42dbdd761f5d7a498a7587a503df1ede66acec5e0696589322ec", + "/Users/mick/RustroverProjects/ant-node-sdk-devnet-20261015/target/debug/ant-devnet": "b605a66b8068c5f69984bdb96922207fd221da75c7033a1074f6df3c09438c03" +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/commands.txt b/docs/audits/2026-10-22-ant-client-main-8b19c14/commands.txt new file mode 100644 index 0000000..ee44c6b --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/commands.txt @@ -0,0 +1,26 @@ +ant-client worktree: /Users/mick/RustroverProjects/ant-client-pr230-bab6beb-sdk (clean, detached at main 8b19c1499360ebd8166a1ef3dd65b88db914dd7f) +RUSTUP_TOOLCHAIN=1.99.0 cargo tree -p ant-core --target wasm32-unknown-unknown --no-default-features --features browser-wasm -i saorsa-transport --depth 1 +RUSTUP_TOOLCHAIN=1.99.0 cargo tree -p ant-core --target wasm32-unknown-unknown --no-default-features --features browser-wasm -i ant-node --depth 1 +RUSTUP_TOOLCHAIN=1.99.0 cargo build --release --bin ant + +SDK worktree: /Users/mick/RustroverProjects/ant-browser-sdk-pr7-bab6beb (317a634b2fedddc8322c6fbf04b85ab73bbdbfc7) +# Sync and verify used GITHUB_TOKEN obtained from gh auth token without logging it. +RUSTUP_TOOLCHAIN=1.99.0 PYTHON=/Users/mick/.local/bin/python3.11 ANT_CLIENT_DIR=/Users/mick/RustroverProjects/ant-client-pr230-bab6beb-sdk npm run sync:wasm +npm run check +npm run build:examples +npm run verify:wasm +python3 scripts/adr-governance.py +python3 .github/scripts/test_check_pr.py +npm version 0.1.3 --no-git-tag-version +npm pack --dry-run --json +npx vite --config examples/all-in-one/vite.config.ts --host 127.0.0.1 --port 39971 --strictPort +# The scripts of the 2026-10-20 audit, unchanged (hashes in checksums.json). +node /browser.mjs /evidence +node /demo.mjs /evidence +# The CLI ran under a throwaway HOME so nothing reached the user's data directory. +HOME=/cli-home ant --json manifest show .ant +HOME=/cli-home ant --json --devnet-manifest /devnet.json --allow-loopback manifest download .ant -o /native/out- +# for public, private, public-wrong-sha256 and private-wrong-sha256 + +Node worktree: /Users/mick/RustroverProjects/ant-node-sdk-devnet-20261015 (clean 26129a2611cac08f9f53a557f91fb8806b40a1cc, ant-devnet as built for the 2026-10-20 audit) +./target/debug/ant-devnet --nodes 7 --bootstrap-count 3 --base-port 0 --webrtc-direct --webrtc-direct-base-port 0 --serve-port 39970 --enable-evm --data-dir /data --manifest /devnet.json diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/demo-summary.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/demo-summary.json new file mode 100644 index 0000000..cf86177 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/demo-summary.json @@ -0,0 +1,10 @@ +{ + "exportedManifestBytes": 91, + "linkMatchesFile": true, + "disk": { + "size": 13056, + "matches": true + }, + "errors": [], + "browser": "151.0.7922.34" +} \ No newline at end of file diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/demo.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/demo.log.gz new file mode 100644 index 0000000..facaf4e Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/demo.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/example-builds.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/example-builds.log.gz new file mode 100644 index 0000000..889a584 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/example-builds.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-private-wrong-sha256.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-private-wrong-sha256.json new file mode 100644 index 0000000..897f543 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-private-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/private.bin", + "status": { + "error": "SHA-256 mismatch: the manifest records 02f690916557f0313ea0b0da3be6d4072b25649f67c51a50d45d564b42100c91, the downloaded file hashes to 72f690916557f0313ea0b0da3be6d4072b25649f67c51a50d45d564b42100c91", + "status": "failed" + } + } + ], + "failed": 1, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/main-8b19c14/native/out-private-wrong-sha256", + "written": 0 +} +Error: 1 entr(y/ies) failed to download diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-private.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-private.json new file mode 100644 index 0000000..44c0c99 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-private.json @@ -0,0 +1,15 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/private.bin", + "status": { + "bytes": 13056, + "status": "written" + } + } + ], + "failed": 0, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/main-8b19c14/native/out-private", + "written": 1 +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-public-wrong-sha256.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-public-wrong-sha256.json new file mode 100644 index 0000000..f737804 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-public-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/public.bin", + "status": { + "error": "SHA-256 mismatch: the manifest records 03c6483f461810aa56ef3b2a30fb6dc31efb00bb83c7e0d5048c6d1f1169f9e6, the downloaded file hashes to b3c6483f461810aa56ef3b2a30fb6dc31efb00bb83c7e0d5048c6d1f1169f9e6", + "status": "failed" + } + } + ], + "failed": 1, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/main-8b19c14/native/out-public-wrong-sha256", + "written": 0 +} +Error: 1 entr(y/ies) failed to download diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-public.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-public.json new file mode 100644 index 0000000..8bbaf26 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-download-public.json @@ -0,0 +1,15 @@ +{ + "cancelled": 0, + "entries": [ + { + "name": "folder/public.bin", + "status": { + "bytes": 13056, + "status": "written" + } + } + ], + "failed": 0, + "output": "/private/tmp/claude-501/-Users-mick-RustroverProjects-ant-client/85270b78-be92-44ed-ac92-ef9a6b6b79d0/scratchpad/main-8b19c14/native/out-public", + "written": 1 +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-private-wrong-sha256.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-private-wrong-sha256.json new file mode 100644 index 0000000..bd015c3 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-private-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "620d47a84eeedf7af769727fdd6f9e4896d80998420d92e5f0ac94069f34540f", + "kind": "embedded", + "known_size": 13056, + "name": "folder/private.bin", + "path": "folder/private.bin", + "sha256": "02f690916557f0313ea0b0da3be6d4072b25649f67c51a50d45d564b42100c91", + "size": null + } + ], + "name": "exports", + "torrent": null, + "total_size": null +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-private.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-private.json new file mode 100644 index 0000000..7a9523f --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-private.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "620d47a84eeedf7af769727fdd6f9e4896d80998420d92e5f0ac94069f34540f", + "kind": "embedded", + "known_size": 13056, + "name": "folder/private.bin", + "path": "folder/private.bin", + "sha256": "72f690916557f0313ea0b0da3be6d4072b25649f67c51a50d45d564b42100c91", + "size": null + } + ], + "name": "exports", + "torrent": null, + "total_size": null +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-public-wrong-sha256.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-public-wrong-sha256.json new file mode 100644 index 0000000..04313d6 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-public-wrong-sha256.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "2b4dd2223e6c2bce66c211f40ed8b0f35d9ad35afa37c281449bc56db3285f63", + "kind": "public", + "known_size": null, + "name": "folder/public.bin", + "path": "folder/public.bin", + "sha256": "03c6483f461810aa56ef3b2a30fb6dc31efb00bb83c7e0d5048c6d1f1169f9e6", + "size": null + } + ], + "name": "exports", + "torrent": null, + "total_size": null +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-public.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-public.json new file mode 100644 index 0000000..bc3a657 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/native-show-public.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "address": "2b4dd2223e6c2bce66c211f40ed8b0f35d9ad35afa37c281449bc56db3285f63", + "kind": "public", + "known_size": null, + "name": "folder/public.bin", + "path": "folder/public.bin", + "sha256": "b3c6483f461810aa56ef3b2a30fb6dc31efb00bb83c7e0d5048c6d1f1169f9e6", + "size": 1 + } + ], + "name": "exports", + "torrent": null, + "total_size": 1 +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/pack.json.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/pack.json.gz new file mode 100644 index 0000000..d7c3cc0 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/pack.json.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/pack.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/pack.log.gz new file mode 100644 index 0000000..a787e45 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/pack.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/pr-checker.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/pr-checker.log.gz new file mode 100644 index 0000000..67ca3a2 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/pr-checker.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/private-wrong-sha256.ant b/docs/audits/2026-10-22-ant-client-main-8b19c14/private-wrong-sha256.ant new file mode 100644 index 0000000..5a8a258 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/private-wrong-sha256.ant differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/private.ant b/docs/audits/2026-10-22-ant-client-main-8b19c14/private.ant new file mode 100644 index 0000000..8a31740 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/private.ant differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/public-wrong-sha256.ant b/docs/audits/2026-10-22-ant-client-main-8b19c14/public-wrong-sha256.ant new file mode 100644 index 0000000..013b3f3 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/public-wrong-sha256.ant differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/public.ant b/docs/audits/2026-10-22-ant-client-main-8b19c14/public.ant new file mode 100644 index 0000000..ea3c378 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/public.ant differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/sdk-check.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/sdk-check.log.gz new file mode 100644 index 0000000..02acd2a Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/sdk-check.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/source.json b/docs/audits/2026-10-22-ant-client-main-8b19c14/source.json new file mode 100644 index 0000000..7985507 --- /dev/null +++ b/docs/audits/2026-10-22-ant-client-main-8b19c14/source.json @@ -0,0 +1,18 @@ +{ + "repository": "https://github.com/WithAutonomi/ant-client", + "revision": "8b19c1499360ebd8166a1ef3dd65b88db914dd7f", + "dirty": false, + "target": "wasm32-unknown-unknown", + "features": [ + "browser-wasm" + ], + "defaultFeatures": false, + "cargoLockSha256": "68a8483073d0e6681bfa47abe84e811a4e782379e459e0cb80625567a76d4478", + "wasmSha256": "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "wasmPack": "wasm-pack 0.15.0", + "noticesSha256": "2c7c966b1bc09d09d0632018663ce2170abfd9a00cefdc8901824251deab421c", + "rustStdCopyrightSha256": "5647be074c8edf7339fd863055923a8fc80bc5610a8d4661ec3b767b9d392c27", + "noticesGeneratorSha256": "fbae032f6ae379d81311ceeabb2d23cd9d40af1a69ca0b422a50b632d7b31bd7", + "noticesConfigSha256": "1d5154cdb0705f6eed668288a0e31f31f273dace0a404f97c091c3d2bdd72cf3" +} diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/verify-wasm.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/verify-wasm.log.gz new file mode 100644 index 0000000..70e3801 Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/verify-wasm.log.gz differ diff --git a/docs/audits/2026-10-22-ant-client-main-8b19c14/wasm-sync.log.gz b/docs/audits/2026-10-22-ant-client-main-8b19c14/wasm-sync.log.gz new file mode 100644 index 0000000..1f3a8ee Binary files /dev/null and b/docs/audits/2026-10-22-ant-client-main-8b19c14/wasm-sync.log.gz differ diff --git a/docs/audits/2026-10-22-media-first-frame.md b/docs/audits/2026-10-22-media-first-frame.md new file mode 100644 index 0000000..d655375 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame.md @@ -0,0 +1,135 @@ +# Media first frame: plain reader, no serial waits, Firefox keep-awake — 2026-10-22 + +[try.autonomi.com](https://github.com/WithAutonomi/try/pull/18), which uses the +same core directly, started its video sooner than `createMediaSource()`. This +change brings what it does differently into the SDK, measured with the SDK's +own media path. It was first proposed separately (PR #8) and is part of the +0.1.3 release. + +## Changes + +- `createMediaSource()` opens its reader without the core's streaming mode. + That mode starts fetching up to four records of the next 32 MiB, and the + file's last record, alongside the first block a player waits for. The plain + reader still reads ahead once a read continues a previous one. +- The media service worker installs while the reader opens, and the head and + tail are primed as soon as the reader is open, instead of each waiting for + the one before. An open failure still surfaces first; a worker failure + surfaces once the reader has opened, which is then closed. +- While any media source is open the page messages the worker every 10 s, and + the worker listens. Firefox stops a worker that has had no new event for + 30 s, even one still answering a request. +- The worker waits up to 120 s for a block instead of 45 s: the core retries a + missing record after 15 and after 45 s. Its responses carry + `X-Content-Type-Options: nosniff` and `Content-Security-Policy: sandbox; + default-src 'none'`, since their type comes from the file's metadata. + +No public type, core or wire change. Applications that merged the packaged +worker into their own should merge its new `message` listener too. + +## Revisions + +- Baseline: SDK `8c00e820feea25d344c9b83bad8466740c52fbdb`, the 0.1.3 + release commit before this change. +- Candidate: the same commit plus this change; the built `client.js`, + `internal/media.js` and worker hashes in + [`checksums.json`](2026-10-22-media-first-frame/checksums.json) are those + measured. +- WASM in both: ant-client `8b19c1499360ebd8166a1ef3dd65b88db914dd7f`, + `0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8`. The + release now bundles the WASM from `ccd4106`, whose browser, data and + read-engine source is unchanged from `8b19c14` + ([2026-10-26 audit](2026-10-26-ant-client-main-ccd4106.md)); these timings + were not repeated on it. +- Video: try's featured 54,903,201-byte MP4, read from its manifest + (`content/manifests/video.ant`, embedded root map), so no DataMap is fetched. +- macOS 26.5.2 arm64; Node 22.23.1; Playwright 1.62.1; Chromium + 151.0.7922.34, Firefox 153.0, WebKit 26.5. + +## First frame on the live network + +[`harness.js`](2026-10-22-media-first-frame/harness.js), identical in both +builds, connects with `AutonomiClient.connect()` on mainnet defaults when the +page opens, waits 3 s, then creates a media source for the video, plays it +muted, and records the first decoded frame (`requestVideoFrameCallback`), ten +seconds of playback, a seek to the middle and every read the reader served. +[`drive.mjs`](2026-10-22-media-first-frame/drive.mjs) ran ten alternating rounds, +each in a fresh Chromium context. + +| Build | First frame (s) | Median | Seek to middle, median | +| --- | --- | ---: | ---: | +| Baseline | 6.1, 11.0, 5.8, 9.1, 7.3, 10.6, failed, 11.3, 8.4, 9.6 | 9.1 | 3.4 s | +| Candidate | 7.2, 4.5, 13.3, 5.2, 5.6, 6.7, 9.6, 6.5, 7.7, 5.4 | 6.6 | 6.5 s | + +The candidate was faster in 7 of 10 paired rounds. One baseline sample failed: +the player reported no supported source. Every other sample played at least +ten seconds in ten, with two `waiting` events each. + +In every sample the first frame is the read of the first 1 MiB, which waits +for the file's first record; every block after it came from the reader's cache +in about 30 ms. The spread is that one record's fetch on the live network, +which was slower in the afternoon than the morning's try runs, plausibly +because the day's runs made it hot for its holders. + +The seek was slower in this batch: streaming mode had, after ten seconds, +already fetched the window holding the middle of this file, which the plain +reader fetches when asked. + +Two later batches of the same two builds, run alternately with variants of a +core read-ahead change that was not adopted, put the plain reader ahead by +less: median first frame 7.8 s against +9.7 s, then 8.8 s against 9.2 s over 15 rounds. In that 15-round batch the +plain reader also sought faster, median 2.8 s against 5.1 s, so seek times +swing too much between batches to show a difference either way. Their raw +results were not kept. A trace of the core read on 2026-10-26 (ant-client +`ccd4106`) puts most of a first record's time in discovery reaching the +holder, a median 1.4 s, and its handshake, 0.6 s, which neither reader mode +changes. + +A fourth batch on 2026-10-26, with both builds rebuilt on the `ccd4106` core, +showed no gain: median first frame 6.1 s with the plain reader against 5.6 s +in streaming mode, sooner in 6 of 12 rounds. In the same run, and in a traced +one, the SDK with this change started the video as soon as try's own player, +a median 4.5 s against 4.1 s over 20 rounds, doing the same network work +([audit](2026-10-26-sdk-vs-try-streaming.md)). + +The same comparison in try's own page, with the same core, gave a median +first frame of 5.0 s with a plain reader against 8.4 s in streaming mode, +three rounds each. + +## Firefox + +[`firefox-keepalive.mjs`](2026-10-22-media-first-frame/firefox-keepalive.mjs) +([results](2026-10-22-media-first-frame/firefox-keepalive.jsonl)) lowers +Firefox's worker idle limits to 15 s and 1 s and serves, through the +SDK's `MediaBridge` and packaged worker, a reader that takes 22 s per block. The +baseline answered `206` with an empty body after 15 s: the worker was stopped +mid-answer. The candidate answered the ten requested bytes after 22 s, after +two keep-awake messages, and sent none once the source closed. + +[`playback.mjs`](2026-10-22-media-first-frame/playback.mjs) plays a test-pattern +MP4 through the same bridge and worker in Chromium, Firefox and WebKit +([results](2026-10-22-media-first-frame/playback.jsonl)): each +played past half a second, with the new response headers present. + +## Automated validation + +- `npm run check`: build, TypeScript and **262 tests in 23 files passed**. New + tests: the worker installs while the reader opens and priming starts before + it is ready, with a plain reader; a failed worker surfaces once the reader has + opened and closes it; the bridge does nothing more once its worker controls + the page; keep-awake messages every 10 s only while a source is open; the + worker's message listener, timeout and headers. Five of them fail against the + baseline source; the failed-worker ordering test passes on both and guards + the new parallel setup. +- `npm run build:examples`, `npm pack --dry-run` (97 files), `npm run + verify:wasm`, ADR governance (8 ADRs) and all 52 PR-checker cases passed. + +## Limits + +Ten recorded samples per build, and the later batches' medians only, from one +machine and network; Chromium only for live timings, one video. Live node load +and versions are unknown. The plain reader led the first frame in the first +three batches, by 0.4 to 2.5 s at the median, and not in the fourth, so no +first-frame gain from the reader mode is claimed, and no seek difference. The +change does start playback as soon as try.autonomi.com's player does. diff --git a/docs/audits/2026-10-22-media-first-frame/adr.log.gz b/docs/audits/2026-10-22-media-first-frame/adr.log.gz new file mode 100644 index 0000000..71c935d Binary files /dev/null and b/docs/audits/2026-10-22-media-first-frame/adr.log.gz differ diff --git a/docs/audits/2026-10-22-media-first-frame/bridge.html b/docs/audits/2026-10-22-media-first-frame/bridge.html new file mode 100644 index 0000000..977aca6 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/bridge.html @@ -0,0 +1,2 @@ + +Media bridge only diff --git a/docs/audits/2026-10-22-media-first-frame/checksums.json b/docs/audits/2026-10-22-media-first-frame/checksums.json new file mode 100644 index 0000000..2320a6a --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/checksums.json @@ -0,0 +1,11 @@ +{ + "/site-baseline/sdk/client.js": "1b62355d096b83ab69e70b5f85670a16957646cd8ae2d114a4ee0fe590161d8b", + "/site-baseline/sdk/internal/media.js": "59f151a25423dc74065bb9243df47f5fb862d4786bf56bf7b82928d0e10e2581", + "/site-baseline/autonomi-stream-sw.js": "7862bcd999b201383224313a675c512299399a1a10d3c428468617033d359683", + "/site-candidate/sdk/client.js": "8fce06261e2eb86e5fba64a7df1ca66dce73c085ae26a81725909cb2ad63bb33", + "/site-candidate/sdk/internal/media.js": "b3fc5174f1d5af8ed0696278639edc93c52a243fbb3b8cd535f98f489686d6e4", + "/site-candidate/autonomi-stream-sw.js": "d92e3d61913e6433103e54ee82aaf4910395ca74c5759e95ab342239a5e0ea06", + "/site-candidate/sdk/wasm/ant_core_bg.wasm": "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8", + "/site-candidate/video.ant": "cea3ab028b454d6ff00e26119e3f46f0a57f3bcbbf9b9794c67fbc37d1aba7f4", + "/site-baseline/harness.js": "33d49a9672c2f3e206f4ffb5552e429fba4955e71e0cf98307d9f98f5c0208ce" +} diff --git a/docs/audits/2026-10-22-media-first-frame/comparison-1.json b/docs/audits/2026-10-22-media-first-frame/comparison-1.json new file mode 100644 index 0000000..26cb3b0 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/comparison-1.json @@ -0,0 +1,478 @@ +{ + "startedAt": "2026-10-22T15:12:34.351Z", + "browser": "151.0.7922.34", + "variants": [ + { + "name": "baseline", + "url": "http://127.0.0.1:39830" + }, + { + "name": "candidate", + "url": "http://127.0.0.1:39831" + } + ], + "runs": 5, + "scriptSha256": "78b1f3be7b8b1573e35be54fb3a368427a527fa47e5951ac0012fdb876689e93", + "samples": [ + { + "round": 1, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 17.400000005960464, + "firstFrameMs": 6077.39999999702, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 9, + "ms": 5961 + }, + { + "start": 54837665, + "length": 65536, + "at": 9, + "ms": -1 + }, + { + "start": 0, + "length": 1048576, + "at": 21, + "ms": 5949 + }, + { + "start": 1048576, + "length": 1048576, + "at": 5971, + "ms": 27 + }, + { + "start": 2097152, + "length": 1048576, + "at": 6005, + "ms": 29 + }, + { + "start": 3145728, + "length": 1048576, + "at": 6044, + "ms": -1 + } + ], + "playedIn10s": 9.958324, + "seekMs": 3345.699999997021, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 1, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 17.299999997019768, + "firstFrameMs": 7189.299999991059, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 3, + "ms": 7127 + }, + { + "start": 54837665, + "length": 65536, + "at": 3, + "ms": 2203 + }, + { + "start": 0, + "length": 1048576, + "at": 19, + "ms": 7111 + }, + { + "start": 1048576, + "length": 1048576, + "at": 7132, + "ms": 31 + }, + { + "start": 2097152, + "length": 1048576, + "at": 7166, + "ms": 28 + } + ], + "playedIn10s": 9.990967999999999, + "seekMs": 9631.49999999702, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 2, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 17, + "firstFrameMs": 4464.79999999404, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 3, + "ms": 4404 + }, + { + "start": 54837665, + "length": 65536, + "at": 3, + "ms": 2884 + }, + { + "start": 0, + "length": 1048576, + "at": 19, + "ms": 4388 + }, + { + "start": 1048576, + "length": 1048576, + "at": 4408, + "ms": 30 + }, + { + "start": 2097152, + "length": 1048576, + "at": 4440, + "ms": 28 + } + ], + "playedIn10s": 9.986528999999999, + "seekMs": 2636.7000000029802, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 2, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 17.400000005960464, + "firstFrameMs": 11030.900000002981, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 10, + "ms": 10965 + }, + { + "start": 54837665, + "length": 65536, + "at": 10, + "ms": 3919 + }, + { + "start": 0, + "length": 1048576, + "at": 21, + "ms": 10954 + }, + { + "start": 1048576, + "length": 1048576, + "at": 10975, + "ms": 29 + }, + { + "start": 2097152, + "length": 1048576, + "at": 11006, + "ms": 29 + } + ], + "playedIn10s": 9.986566, + "seekMs": 1559.199999994038, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 3, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 21.399999991059303, + "firstFrameMs": 5830.199999997019, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 12, + "ms": 5763 + }, + { + "start": 54837665, + "length": 65536, + "at": 12, + "ms": 3238 + }, + { + "start": 0, + "length": 1048576, + "at": 25, + "ms": 5750 + }, + { + "start": 1048576, + "length": 1048576, + "at": 5776, + "ms": 27 + }, + { + "start": 2097152, + "length": 1048576, + "at": 5805, + "ms": 29 + } + ], + "playedIn10s": 9.989638999999999, + "seekMs": 7418.4000000059605, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 3, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 23.200000002980232, + "firstFrameMs": 13271.29999999106, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 6, + "ms": 13208 + }, + { + "start": 54837665, + "length": 65536, + "at": 6, + "ms": 3499 + }, + { + "start": 0, + "length": 1048576, + "at": 25, + "ms": 13189 + }, + { + "start": 1048576, + "length": 1048576, + "at": 13215, + "ms": 29 + }, + { + "start": 2097152, + "length": 1048576, + "at": 13245, + "ms": 26 + } + ], + "playedIn10s": 9.984762, + "seekMs": 7879, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 4, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 23, + "firstFrameMs": 5232.700000000001, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 6, + "ms": 5210 + }, + { + "start": 54837665, + "length": 65536, + "at": 6, + "ms": 2794 + }, + { + "start": 0, + "length": 1048576, + "at": 25, + "ms": 5190 + }, + { + "start": 1048576, + "length": 1048576, + "at": 5216, + "ms": 31 + } + ], + "playedIn10s": 9.959088, + "seekMs": 2456.400000002981, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 4, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 18.700000002980232, + "firstFrameMs": 9135.399999994039, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 10, + "ms": 9068 + }, + { + "start": 54837665, + "length": 65536, + "at": 11, + "ms": 3538 + }, + { + "start": 0, + "length": 1048576, + "at": 22, + "ms": 9056 + }, + { + "start": 1048576, + "length": 1048576, + "at": 9079, + "ms": 29 + }, + { + "start": 2097152, + "length": 1048576, + "at": 9113, + "ms": 27 + } + ], + "playedIn10s": 9.985812000000001, + "seekMs": 4147.199999988079, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 5, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 21.399999991059303, + "firstFrameMs": 7326.799999999999, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 10, + "ms": 7256 + }, + { + "start": 54837665, + "length": 65536, + "at": 11, + "ms": 4314 + }, + { + "start": 0, + "length": 1048576, + "at": 26, + "ms": 7240 + }, + { + "start": 1048576, + "length": 1048576, + "at": 7271, + "ms": 29 + }, + { + "start": 2097152, + "length": 1048576, + "at": 7302, + "ms": 28 + } + ], + "playedIn10s": 9.982589999999998, + "seekMs": 4634.300000008941, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 5, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 19.5, + "firstFrameMs": 5565.6, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 4, + "ms": 5507 + }, + { + "start": 54837665, + "length": 65536, + "at": 4, + "ms": 1274 + }, + { + "start": 0, + "length": 1048576, + "at": 21, + "ms": 5489 + }, + { + "start": 1048576, + "length": 1048576, + "at": 5511, + "ms": 29 + }, + { + "start": 2097152, + "length": 1048576, + "at": 5542, + "ms": 27 + } + ], + "playedIn10s": 9.993248000000001, + "seekMs": 4231.5999999940395, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + } + ] +} diff --git a/docs/audits/2026-10-22-media-first-frame/comparison-2.json b/docs/audits/2026-10-22-media-first-frame/comparison-2.json new file mode 100644 index 0000000..f09cb43 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/comparison-2.json @@ -0,0 +1,448 @@ +{ + "startedAt": "2026-10-22T15:18:43.796Z", + "browser": "151.0.7922.34", + "variants": [ + { + "name": "candidate", + "url": "http://127.0.0.1:39831" + }, + { + "name": "baseline", + "url": "http://127.0.0.1:39830" + } + ], + "runs": 5, + "scriptSha256": "78b1f3be7b8b1573e35be54fb3a368427a527fa47e5951ac0012fdb876689e93", + "samples": [ + { + "round": 1, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 20.299999997019768, + "firstFrameMs": 6709.10000000298, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 4, + "ms": 6589 + }, + { + "start": 54837665, + "length": 65536, + "at": 4, + "ms": -1 + }, + { + "start": 0, + "length": 1048576, + "at": 22, + "ms": 6570 + }, + { + "start": 1048576, + "length": 1048576, + "at": 6593, + "ms": 30 + }, + { + "start": 2097152, + "length": 1048576, + "at": 6624, + "ms": 28 + }, + { + "start": 3145728, + "length": 1048576, + "at": 6653, + "ms": -1 + } + ], + "playedIn10s": 9.958454, + "seekMs": 14960.79999999702, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 1, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 17.599999994039536, + "firstFrameMs": 10609.799999991059, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 8, + "ms": 10546 + }, + { + "start": 54837665, + "length": 65536, + "at": 8, + "ms": 8222 + }, + { + "start": 0, + "length": 1048576, + "at": 21, + "ms": 10533 + }, + { + "start": 1048576, + "length": 1048576, + "at": 10555, + "ms": 27 + }, + { + "start": 2097152, + "length": 1048576, + "at": 10584, + "ms": 27 + } + ], + "playedIn10s": 9.989044999999999, + "seekMs": 8765.899999994042, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 2, + "variant": "baseline", + "status": "failed", + "error": "page.evaluate: NotSupportedError: Failed to load because no supported source was found.", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 2, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 16.299999997019768, + "firstFrameMs": 9634.09999999404, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 3, + "ms": 9571 + }, + { + "start": 54837665, + "length": 65536, + "at": 3, + "ms": 9337 + }, + { + "start": 0, + "length": 1048576, + "at": 18, + "ms": 9556 + }, + { + "start": 1048576, + "length": 1048576, + "at": 9576, + "ms": 31 + }, + { + "start": 2097152, + "length": 1048576, + "at": 9609, + "ms": 30 + } + ], + "playedIn10s": 9.980536, + "seekMs": 11478.299999994037, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 3, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 20.400000005960464, + "firstFrameMs": 6483.800000002981, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 4, + "ms": 6423 + }, + { + "start": 54837665, + "length": 65536, + "at": 4, + "ms": 3557 + }, + { + "start": 0, + "length": 1048576, + "at": 23, + "ms": 6405 + }, + { + "start": 1048576, + "length": 1048576, + "at": 6428, + "ms": 28 + }, + { + "start": 2097152, + "length": 1048576, + "at": 6458, + "ms": 25 + } + ], + "playedIn10s": 9.987957999999999, + "seekMs": 4106.79999999404, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 3, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 22.299999997019768, + "firstFrameMs": 11295.70000000894, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 12, + "ms": 11227 + }, + { + "start": 54837665, + "length": 65536, + "at": 13, + "ms": 2954 + }, + { + "start": 0, + "length": 1048576, + "at": 26, + "ms": 11213 + }, + { + "start": 1048576, + "length": 1048576, + "at": 11240, + "ms": 28 + }, + { + "start": 2097152, + "length": 1048576, + "at": 11270, + "ms": 29 + } + ], + "playedIn10s": 9.986059000000001, + "seekMs": 65, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 4, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 22.299999997019768, + "firstFrameMs": 8426.20000000596, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 12, + "ms": 8359 + }, + { + "start": 54837665, + "length": 65536, + "at": 12, + "ms": 4328 + }, + { + "start": 0, + "length": 1048576, + "at": 26, + "ms": 8344 + }, + { + "start": 1048576, + "length": 1048576, + "at": 8372, + "ms": 27 + }, + { + "start": 2097152, + "length": 1048576, + "at": 8401, + "ms": 26 + } + ], + "playedIn10s": 9.987123, + "seekMs": 3379.800000002979, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 4, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 22.600000008940697, + "firstFrameMs": 7738.20000000298, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 6, + "ms": 7674 + }, + { + "start": 54837665, + "length": 65536, + "at": 6, + "ms": 2597 + }, + { + "start": 0, + "length": 1048576, + "at": 25, + "ms": 7655 + }, + { + "start": 1048576, + "length": 1048576, + "at": 7680, + "ms": 31 + }, + { + "start": 2097152, + "length": 1048576, + "at": 7713, + "ms": 28 + } + ], + "playedIn10s": 9.988992, + "seekMs": 5214.9999999970205, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 5, + "variant": "candidate", + "waiting": 2, + "sourceReadyMs": 21.799999997019768, + "firstFrameMs": 5396.5, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 4, + "ms": 5335 + }, + { + "start": 54837665, + "length": 65536, + "at": 4, + "ms": 2784 + }, + { + "start": 0, + "length": 1048576, + "at": 24, + "ms": 5315 + }, + { + "start": 1048576, + "length": 1048576, + "at": 5340, + "ms": 29 + }, + { + "start": 2097152, + "length": 1048576, + "at": 5371, + "ms": 27 + } + ], + "playedIn10s": 9.989819, + "seekMs": 7699.20000000596, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + }, + { + "round": 5, + "variant": "baseline", + "waiting": 2, + "sourceReadyMs": 21.599999994039536, + "firstFrameMs": 9580.70000000298, + "readsBeforeFirstFrame": [ + { + "start": 0, + "length": 1048576, + "at": 12, + "ms": 9491 + }, + { + "start": 54837665, + "length": 65536, + "at": 12, + "ms": 6105 + }, + { + "start": 0, + "length": 1048576, + "at": 25, + "ms": 9477 + }, + { + "start": 1048576, + "length": 1048576, + "at": 9522, + "ms": 31 + }, + { + "start": 2097152, + "length": 1048576, + "at": 9570, + "ms": 28 + } + ], + "playedIn10s": 9.997207000000001, + "seekMs": 2686.4000000059605, + "status": "passed", + "pageErrors": [], + "wasm": [ + "0c01cd4377a4b4846ae989691317738b909a83ce95e81ea62ce1833391a794a8" + ] + } + ] +} diff --git a/docs/audits/2026-10-22-media-first-frame/drive.mjs b/docs/audits/2026-10-22-media-first-frame/drive.mjs new file mode 100644 index 0000000..0b03b44 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/drive.mjs @@ -0,0 +1,49 @@ +// Alternating fresh-context samples of the harness on each served variant. +// Usage: node drive.mjs --out results.json --runs 3 --variant name=url ... +import { chromium } from "/Users/mick/RustroverProjects/try-autonomi-manifests/node_modules/playwright/index.mjs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { dirname, resolve } from "node:path"; +import { parseArgs } from "node:util"; + +const { values } = parseArgs({ options: { variant: { type: "string", multiple: true }, runs: { type: "string", default: "3" }, out: { type: "string" }, browser: { type: "string", default: "chromium" } } }); +const variants = values.variant.map(item => { const [name, url] = item.split("="); return { name, url }; }); +const browser = await chromium.launch({ headless: true, args: ["--force-fieldtrials=WebRTC-NoSdpMangleUfrag/Enabled/", "--autoplay-policy=no-user-gesture-required"] }); +const report = { startedAt: new Date().toISOString(), browser: browser.version(), variants, runs: Number(values.runs), + scriptSha256: createHash("sha256").update(await readFile(new URL(import.meta.url))).digest("hex"), samples: [] }; +const out = resolve(values.out); +await mkdir(dirname(out), { recursive: true }); +try { + for (let round = 1; round <= report.runs; round++) { + for (const variant of round % 2 ? variants : [...variants].reverse()) { + const context = await browser.newContext(); + const page = await context.newPage(); + const errors = []; + const wasm = new Set(); + page.on("pageerror", error => errors.push(error.message)); + page.on("response", async response => { if (new URL(response.url()).pathname.endsWith(".wasm")) wasm.add(createHash("sha256").update(await response.body()).digest("hex")); }); + const sample = { round, variant: variant.name }; + try { + await page.goto(variant.url); + await page.waitForFunction(() => window.__harness && window.__harness.state !== "connecting", null, { timeout: 60_000 }); + if (await page.evaluate(() => window.__harness.state) !== "connected") throw Error(await page.evaluate(() => window.__harness.error)); + // As a visitor reads the page: the remaining seeds connect in the background. + await page.waitForTimeout(3000); + Object.assign(sample, await page.evaluate(() => window.__harness.run({ manifestUrl: "./video.ant" }))); + sample.status = sample.playedIn10s >= 5 ? "passed" : "failed"; + } catch (error) { + sample.status = "failed"; + sample.error = String(error.message ?? error); + } finally { + sample.pageErrors = errors; + sample.wasm = [...wasm]; + await context.close(); + } + console.log(JSON.stringify(sample)); + report.samples.push(sample); + await writeFile(out, JSON.stringify(report, null, 2) + "\n"); + } + } +} finally { + await browser.close(); +} diff --git a/docs/audits/2026-10-22-media-first-frame/example-builds.log.gz b/docs/audits/2026-10-22-media-first-frame/example-builds.log.gz new file mode 100644 index 0000000..9ea9a9b Binary files /dev/null and b/docs/audits/2026-10-22-media-first-frame/example-builds.log.gz differ diff --git a/docs/audits/2026-10-22-media-first-frame/firefox-keepalive.jsonl b/docs/audits/2026-10-22-media-first-frame/firefox-keepalive.jsonl new file mode 100644 index 0000000..f27d2b7 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/firefox-keepalive.jsonl @@ -0,0 +1,2 @@ +{"name":"baseline","browser":"153.0","answer":{"status":206,"body":[],"seconds":15.009},"whileOpen":0,"afterClose":0} +{"name":"candidate","browser":"153.0","answer":{"status":206,"body":[100,101,102,103,104,105,106,107,108,109],"seconds":22.041},"whileOpen":2,"afterClose":0} diff --git a/docs/audits/2026-10-22-media-first-frame/firefox-keepalive.mjs b/docs/audits/2026-10-22-media-first-frame/firefox-keepalive.mjs new file mode 100644 index 0000000..cec6819 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/firefox-keepalive.mjs @@ -0,0 +1,56 @@ +// Firefox stops a service worker 30 s after its last event, even one still +// answering a request. With those limits lowered to 15 s and 1 s, a block that +// takes 22 s outlasts them. The SDK's own MediaBridge and packaged worker serve +// it from a reader that is slow on purpose; no network is involved. +// Usage: node firefox-keepalive.mjs name=url ... +import { firefox } from "/Users/mick/RustroverProjects/try-autonomi-manifests/node_modules/playwright/index.mjs"; + +const results = []; +for (const item of process.argv.slice(2)) { + const [name, url] = item.split("="); + const browser = await firefox.launch({ + firefoxUserPrefs: { "dom.serviceWorkers.idle_timeout": 15_000, "dom.serviceWorkers.idle_extended_timeout": 1_000 }, + }); + try { + const page = await browser.newPage(); + await page.goto(`${url}/bridge.html`); + const result = await page.evaluate(async () => { + const { MediaBridge } = await import("./sdk/internal/media.js"); + const bytes = Uint8Array.from({ length: 4096 }, (_, index) => index & 255); + let awake = 0; + const post = ServiceWorker.prototype.postMessage; + ServiceWorker.prototype.postMessage = function (message, ...rest) { + if (message?.type === "autonomi-stream-awake") awake++; + return post.call(this, message, ...rest); + }; + const reader = { + address: "ab".repeat(32), name: "slow.mp4", size: bytes.length, contentType: "video/mp4", + async read(start, length) { + await new Promise(resolve => setTimeout(resolve, 22_000)); + return bytes.slice(start, start + length); + }, + close() {}, + }; + const bridge = new MediaBridge(); + const source = await bridge.attach(reader, {}); + const started = performance.now(); + let answer; + try { + const response = await fetch(source.url, { headers: { Range: "bytes=100-109" } }); + answer = { status: response.status, body: [...new Uint8Array(await response.arrayBuffer())] }; + } catch (error) { + answer = { error: String(error) }; + } + answer.seconds = (performance.now() - started) / 1000; + source.close(); + const whileOpen = awake; + await new Promise(resolve => setTimeout(resolve, 12_000)); + bridge.close(); + return { answer, whileOpen, afterClose: awake - whileOpen }; + }); + results.push({ name, browser: browser.version(), ...result }); + console.log(JSON.stringify(results.at(-1))); + } finally { + await browser.close(); + } +} diff --git a/docs/audits/2026-10-22-media-first-frame/harness.js b/docs/audits/2026-10-22-media-first-frame/harness.js new file mode 100644 index 0000000..0c4b79c --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/harness.js @@ -0,0 +1,56 @@ +// The SDK's own createMediaSource() against the live network: connect on load, +// then on request stream a manifest entry and time its first frame, playback +// and a seek. window.__harness drives it from Playwright. +import { AutonomiClient, PublicFileReader, decodeManifest } from "./sdk/index.js"; + +// Every read the reader serves, for the timeline. +const reads = []; +const read = PublicFileReader.prototype.read; +PublicFileReader.prototype.read = async function (start, length, ...rest) { + const entry = { start, length, at: performance.now() }; + reads.push(entry); + try { return await read.call(this, start, length, ...rest); } finally { entry.ms = performance.now() - entry.at; } +}; + +const harness = window.__harness = { state: "connecting", error: null }; +const video = document.getElementById("player"); +let client; +try { + client = await AutonomiClient.connect(); + harness.state = "connected"; +} catch (error) { + harness.state = "failed"; + harness.error = String(error?.message ?? error); +} + +harness.run = async ({ manifestUrl }) => { + const entry = (await decodeManifest(new Uint8Array(await (await fetch(manifestUrl)).arrayBuffer()))).entries[0]; + const result = { waiting: 0 }; + reads.length = 0; + video.addEventListener("waiting", () => result.waiting++); + const started = performance.now(); + const source = await client.createMediaSource(entry); + result.sourceReadyMs = performance.now() - started; + const first = new Promise(resolve => video.requestVideoFrameCallback(at => resolve(at - started))); + video.src = source.url; + await video.play(); + result.firstFrameMs = await first; + result.readsBeforeFirstFrame = reads.map(entry => ({ start: entry.start, length: entry.length, at: Math.round(entry.at - started), ms: Math.round(entry.ms ?? -1) })); + const playbackStart = video.currentTime; + await new Promise(resolve => setTimeout(resolve, 10_000)); + result.playedIn10s = video.currentTime - playbackStart; + const target = video.duration / 2; + const seekStarted = performance.now(); + const seek = new Promise(resolve => { + const onFrame = (at, metadata) => (Math.abs(metadata.mediaTime - target) < 1 ? resolve(at - seekStarted) : video.requestVideoFrameCallback(onFrame)); + video.requestVideoFrameCallback(onFrame); + }); + video.currentTime = target; + await video.play(); + result.seekMs = await Promise.race([seek, new Promise((_, reject) => setTimeout(() => reject(new Error("seek frame deadline")), 60_000))]); + video.pause(); + video.removeAttribute("src"); + video.load(); + source.close(); + return result; +}; diff --git a/docs/audits/2026-10-22-media-first-frame/index.html b/docs/audits/2026-10-22-media-first-frame/index.html new file mode 100644 index 0000000..72da4af --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/index.html @@ -0,0 +1,8 @@ + + +SDK media first frame + + + + + diff --git a/docs/audits/2026-10-22-media-first-frame/pack.json.gz b/docs/audits/2026-10-22-media-first-frame/pack.json.gz new file mode 100644 index 0000000..dd37e6d Binary files /dev/null and b/docs/audits/2026-10-22-media-first-frame/pack.json.gz differ diff --git a/docs/audits/2026-10-22-media-first-frame/pack.log.gz b/docs/audits/2026-10-22-media-first-frame/pack.log.gz new file mode 100644 index 0000000..be5ffaf Binary files /dev/null and b/docs/audits/2026-10-22-media-first-frame/pack.log.gz differ diff --git a/docs/audits/2026-10-22-media-first-frame/playback.jsonl b/docs/audits/2026-10-22-media-first-frame/playback.jsonl new file mode 100644 index 0000000..1bb44e5 --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/playback.jsonl @@ -0,0 +1,3 @@ +{"browser":"chromium","version":"151.0.7922.34","played":0.511293,"status":206,"nosniff":"nosniff","csp":"sandbox; default-src 'none'"} +{"browser":"firefox","version":"153.0","played":0.528215,"status":206,"nosniff":"nosniff","csp":"sandbox; default-src 'none'"} +{"browser":"webkit","version":"26.5","played":0.5045102076654525,"status":206,"nosniff":"nosniff","csp":"sandbox; default-src 'none'"} diff --git a/docs/audits/2026-10-22-media-first-frame/playback.mjs b/docs/audits/2026-10-22-media-first-frame/playback.mjs new file mode 100644 index 0000000..942c89b --- /dev/null +++ b/docs/audits/2026-10-22-media-first-frame/playback.mjs @@ -0,0 +1,40 @@ +// A real