Files
orca/src/shared/mobile-web-bundle/mobile-web-bundle-capability.ts
T
Jinwoo Hong f2ca3cbfb7 feat(mobile-web-bundle): manifest and RPC contract for the desktop-served mobile web bundle (OTA phase A, 1/5) (#21325)
* feat(mobile-web-bundle): add the manifest contract and content-addressed build id

The schema every later Phase A lane parses against: the ceilings that bound host
memory (256 assets, 32 MiB total, 10 MiB per asset), and a build id that is a
pure function of content so a client can use it as a cache key unconditionally.

The serializer sorts its input rather than trusting the caller, so a producer
that emits assets in any order still lands on the same id.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile-web-bundle): add the bundle RPC payload contract

Method names, capability name, the 48 KiB chunk size, params/result schemas for
both methods, and the six error codes as a closed union pinned by a coverage
record. Constants and data only; the host wiring and the capability push land in
later lanes.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile-web-bundle): hash the build id without node:crypto

Metro ships no Node core shims, so a value import from these modules would fail
to bundle on the phone. The pure-JS sha256 keeps both contract modules
runtime-neutral, which also lets a cached manifest be re-verified on device.

Verified digest parity against node:crypto across the 55/56/64-byte padding
boundaries before the swap.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile-web-bundle): reject a manifest whose buildId is not its content hash

A stale id passed every other check and would then serve the wrong bytes under a
cache key the client already trusts. Runs last of the invariants because it is
the only one that hashes.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile-web-bundle): require a lowercase content type

The pattern carried an `i` flag over lowercase character classes, so the same
bytes described as `Text/HTML` and `text/html` produced two different build ids.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-web-bundle): move the capability name to a zod-free module

A4 wires this constant into protocol-version.ts, which the phone reads on the
capability path. Leaving it in the schema module would have dragged zod along
with it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-web-bundle): name the chunk reply's length assetByteLength

It is the whole asset's length, not the chunk's, and sitting beside dataBase64
under the old name it read as the chunk's. Both are non-negative integers, so a
producer that emitted the wrong one would only surface at the final hash check.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile-web-bundle): reject asset paths that are not portable or that fold together

Two paths differing only in case are one file on macOS and Windows, so the host
would serve the same bytes under two entries and one of the two hashes could
never match. Windows-reserved segment names and trailing dots cannot be written
to the bundle root at all.

Both follow skill-package-manifest's checks, the folded-path Set and the
reserved-segment pattern.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile-web-bundle): accept one spelling of a parameterised content type

The optional space in `; ?charset=` let the same bytes carry two content types
and therefore two build ids. Pinned to the single-space form the bundle builder
emits.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* perf(mobile-web-bundle): stop hashing a manifest a cheaper invariant already rejected

zod runs superRefine even after the asset-array ceiling has failed, so a 257
asset manifest was still sorted and hashed. Each invariant now returns on its
own issue and the count is checked first, which is what the comment claimed.

The tests read the issue paths: an oversized or otherwise invalid manifest with
a deliberately wrong buildId reports no buildId issue, while the same wrong id
inside the ceiling does.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile-web-bundle): say that the manifest has no additive path

`.strict()` plus a literal schemaVersion closes the shape completely, so the
version bump is the only way to change it. The phone value-imports this schema,
so Phase B must read an unrecognised schemaVersion as a bundle to re-fetch
rather than as a parse crash.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-17 22:08:18 -04:00

5 lines
302 B
TypeScript

/** Negotiated, never inferred from the desktop version: a build can ship without a bundle.
* Zod-free and dependency-free so `protocol-version.ts` can name it without pulling a schema
* library into the phone's capability path. */
export const MOBILE_WEB_BUNDLE_CAPABILITY = 'mobileWeb.bundle.v1'