diff --git a/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts b/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts index bd2bf362b20..0d58d2fad52 100644 --- a/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts +++ b/mobile/src/transport/mobile-web-bundle-fetch-range.test.ts @@ -12,12 +12,11 @@ import type { RpcClient } from './rpc-client' import type { RpcResponse } from './types' type Inflation = { readonly outLength: number; readonly resultLength: number } +type LoggedInflation = Inflation & { readonly body: Uint8Array } /** Every inflation in the process, keyed by the gzip body it was handed; a host reads back only * the bodies it sent, so a read left running by an earlier test's fetch never lands in its sink. */ -const inflationLog = vi.hoisted( - () => [] as { body: Uint8Array; outLength: number; resultLength: number }[] -) +const inflationLog = vi.hoisted((): LoggedInflation[] => []) // Observes the bound the decoder hands fflate, and what fflate hands back inside it. vi.mock('fflate', async (importOriginal) => { diff --git a/mobile/src/transport/mobile-web-bundle-reply-schemas.ts b/mobile/src/transport/mobile-web-bundle-reply-schemas.ts index e8bcb3c3230..ea68e7dbd14 100644 --- a/mobile/src/transport/mobile-web-bundle-reply-schemas.ts +++ b/mobile/src/transport/mobile-web-bundle-reply-schemas.ts @@ -130,7 +130,7 @@ export const MobileWebBundleManifestReplySchema = z.looseObject({ /** Self-describing on purpose: `buildId`, `path` and `offset` are echoed so a reassembler cannot * misplace a reply, and `sha256`/`assetByteLength` describe the whole asset rather than this * window, which is what lets the fetch verify without a second index. Shared by both read replies. */ -const windowHeaderShape = { +const windowHeaderFields = { buildId: z.string().regex(SHA256_PATTERN), path: MobileWebBundleAssetPathSchema, offset: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), @@ -140,7 +140,7 @@ const windowHeaderShape = { } export const MobileWebBundleChunkReplySchema = z.looseObject({ - ...windowHeaderShape, + ...windowHeaderFields, dataBase64: z.string().max(MAX_DATA_BASE64_LENGTH) }) @@ -148,7 +148,7 @@ export const MobileWebBundleChunkReplySchema = z.looseObject({ * enum: an encoding this build cannot decode is a typed refusal at the decoder, which names it, * rather than a reply-shape failure that names nothing. */ export const MobileWebBundleRangeReplySchema = z.looseObject({ - ...windowHeaderShape, + ...windowHeaderFields, encoding: z.string().min(1).max(32), dataBase64: z.string().max(MOBILE_WEB_BUNDLE_RANGE_MAX_DATA_BASE64_LENGTH) }) diff --git a/src/shared/mobile-web-bundle/bundle-rpc-contract.ts b/src/shared/mobile-web-bundle/bundle-rpc-contract.ts index d740e78fee9..9541985b5a8 100644 --- a/src/shared/mobile-web-bundle/bundle-rpc-contract.ts +++ b/src/shared/mobile-web-bundle/bundle-rpc-contract.ts @@ -64,7 +64,7 @@ export const MobileWebBundleManifestResultSchema = z }) .strict() -const windowParamsShape = { +const windowParamsFields = { buildId: z.string().regex(SHA256_PATTERN), path: MobileWebBundleAssetPathSchema, offset: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES) @@ -72,14 +72,14 @@ const windowParamsShape = { /** No `multipleOf` pin on `offset`: alignment is against the host's advertised `chunkBytes`, which * may be smaller than the constant, so the host rejects a misaligned offset instead. */ -export const MobileWebBundleChunkParamsSchema = z.object(windowParamsShape).strict() +export const MobileWebBundleChunkParamsSchema = z.object(windowParamsFields).strict() /** The chunk params exactly, on the `rangeBytes` grid the manifest reply advertised. */ export const MobileWebBundleRangeParamsSchema = MobileWebBundleChunkParamsSchema /** What every chunk or range reply restates about the window it answers. */ -const windowHeaderShape = { - ...windowParamsShape, +const windowHeaderFields = { + ...windowParamsFields, /** The whole asset, not this window: named for it so a reassembler cannot misread the two, and * paired with `sha256` it describes the asset without a second index. */ assetByteLength: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), @@ -90,14 +90,14 @@ const windowHeaderShape = { /** Strict, so a later `contentEncoding` is only a Rule 1 optional-field addition for clients whose * own reply readers are not strict. */ export const MobileWebBundleChunkResultSchema = z - .object({ ...windowHeaderShape, dataBase64: z.string().max(MAX_DATA_BASE64_LENGTH) }) + .object({ ...windowHeaderFields, dataBase64: z.string().max(MAX_DATA_BASE64_LENGTH) }) .strict() /** A chunk result plus the encoding of `dataBase64`, whose decoded length is * `min(rangeBytes, assetByteLength - offset)`. */ export const MobileWebBundleRangeResultSchema = z .object({ - ...windowHeaderShape, + ...windowHeaderFields, encoding: z.enum(MOBILE_WEB_BUNDLE_RANGE_ENCODINGS), dataBase64: z.string().max(MOBILE_WEB_BUNDLE_RANGE_MAX_DATA_BASE64_LENGTH) })