zip-peek
v0.6.7
Published
Service worker based ZIP asset loader for browser applications.
Keywords
Readme
zip-peek
zip-peek lets browser applications load files from a remote .zip archive by only requesting the needed range of bytes without downloading and extracting the full ZIP file.
It registers a service worker that intercepts requests like:
https://cdn.example.com/packages/session.zip/path/to/asset.pngThe service worker fetches only the byte range needed for the requested ZIP entry, inflates the file when required, caches the extracted response, and returns it to the browser as a normal asset response.
When Is This Package Useful?
Use this package when your app receives a base path and later appends folders and file names to fetch assets from that path.
For example, your app may normally treat the base path as a directory:
const basePath = "https://cdn.example.com/packages/session";
const imageUrl = `${basePath}/slides/slide-1/image.png`;If the same assets are sometimes delivered as a ZIP file instead:
const basePath = "https://cdn.example.com/packages/session.zip";
const imageUrl = `${basePath}/slides/slide-1/image.png`;zip-peek lets the app keep using the same URL-building pattern. The app does not need to know whether the base path points to a normal directory or a ZIP file, and the ZIP-specific work stays inside the service worker.
What It Does
The package provides two pieces:
- A browser API,
initZipPeek(), used by your application. - A standalone service worker file,
zipServiceWorker.js, that must be copied into your app's public output and served by your app. You will find a webpack sample below.
At runtime, the package:
- Registers the ZIP service worker with the configured
workerUrlandscopeUrl. - Reloads once after first service worker install when needed, so the page becomes controlled.
- Intercepts matching
.zip/...asset requests within the service worker scope. - Loads and caches the ZIP central directory on first use, or during init when
allowedZipUrlsis provided. - Streams the entry byte span (
offsetthroughnextOffset − 1), parses the local file header, and incrementally inflates deflated entries usingfflate. - Starts returning extracted bytes as a normal streaming
Response(or206when the client sent aRangeheader) before the complete entry has downloaded. - Tees network streams so fully extracted assets are cached asynchronously in the browser Cache API without delaying the client response.
Installation
pnpm add zip-peekor:
npm install zip-peekBasic Usage
import { initZipPeek } from "zip-peek";
const zipPeekInit = await initZipPeek({
workerUrl: new URL("/zipServiceWorker.js", window.location.origin).href,
scopeUrl: new URL("/", window.location.origin).href,
});
if (zipPeekInit.reloaded) {
return;
}After initialization, your app can continue building asset URLs under the ZIP URL:
const zipUrl = "https://cdn.example.com/packages/zipfile.zip";
const imgUrl = `${zipUrl}/slides/slide-1/image.png`;
// This request will work normally and zip-peek will do the magic in the background:
fetch(`${imgUrl}`);The browser requests that URL normally. The service worker intercepts it and serves slides/slide-1/image.png from inside the ZIP.
API
initZipPeek(options)
type InitZipPeekOptions = {
workerUrl: string;
scopeUrl: string;
reloadOnFirstInstall?: boolean;
cacheClearingStrategy?: ZipCacheClearingStrategy;
allowedZipUrls?: string[];
requireExactManifestPath?: boolean;
zipAssetCacheName?: string;
assetCacheTtlMs?: number;
logPrefix?: string;
onError?: (error: Error, info?: string) => void;
};
type ZipCacheClearingStrategy = "keep-all" | "keep-allowed-urls" | "clear-all";Returns:
type InitZipPeekResult = {
reloaded: boolean;
initialized: boolean;
};Options
workerUrl
The URL where the browser can download the service worker JavaScript file. The service worker file must be served from the same origin as the page.
Example:
workerUrl: new URL("./zipServiceWorker.js", window.location.href).href;In production, you will usually prefer hashed file to give it a long Cache-Control duration:
zipServiceWorker.a1b2c3d4.jsscopeUrl
The service worker scope. The worker can only intercept requests inside this scope. See Service Worker API for more details.
Examples:
scopeUrl: new URL("/", window.location.origin).href;
scopeUrl: new URL("/app/", window.location.origin).href;Choose the smallest scope that includes the pages and asset requests you want the service worker to intercept. For local development, the scope often differs from production because the app may be served from a different path.
reloadOnFirstInstall
Defaults to true. When a service worker is installed for the first time, the current page may not yet be controlled by it. With this option enabled, the package reloads once and returns { reloaded: true, initialized: false }.
cacheClearingStrategy
Defaults to 'keep-all'. Controls which zip-peek cache entries are cleared during initialization.
'keep-all': do not clear cached ZIP manifests or extracted assets.'keep-allowed-urls': requires non-emptyallowedZipUrls; clears cached ZIP data for URLs outside the allow-list.'clear-all': clears all zip-peek cached manifests and extracted assets for a fresh start.
allowedZipUrls
Restricts zip-peek to a known set of ZIP package URLs. When omitted, zip-peek lazily serves any matching .zip/... request inside the service worker scope. When provided, the array must contain at least one ZIP URL; requests to ZIP URLs outside the list receive 403 from the service worker.
The allow-list also acts as a warmup list: zip-peek loads and caches each allowed ZIP manifest during initialization.
Examples:
allowedZipUrls: ["https://cdn.example.com/packages/zipfile.zip"];
allowedZipUrls: [
"https://d3jfe.cloudfront.net/somedir/zipfile.zip?Expires=1798761599&Signature=K2CJW4Z7B3DCM4&Key-Pair-Id=K2CJW4Z7B3DCM4",
];requireExactManifestPath
Defaults to false. Controls how the service worker resolves a requested inner path against the ZIP manifest.
false(default): try an exact manifest key match first; if that fails, retry with{zipBasename}/{requestedPath}wherezipBasenameis the ZIP filename without the.zipextension (e.g. forsession.zip, requestslides/image.pngfalls back to manifest keysession/slides/image.png).true: only exact manifest key matches are accepted.
When fallback resolution succeeds, the asset is still served normally, but onError is invoked informationally with details about the missed exact key and the resolved fallback key.
onError
Optional error handler for zip-peek failures and informational reports from the service worker. Receives (error, info?) where info carries additional context when available.
Besides initialization and fetch failures, onError is also called when exact manifest lookup fails but zip-basename folder fallback succeeds (see requireExactManifestPath). Use this to log or telemetry mismatches between request paths and manifest layout without blocking the response.
zipAssetCacheName
Overrides the browser Cache API bucket used by the service worker for ZIP manifests and extracted assets.
Default:
zip-cache-v1assetCacheTtlMs
Controls how long extracted assets remain valid in the Cache API.
Default:
2 * 60 * 60 * 1000; // 2 hourslogPrefix
Changes the service worker log prefix.
Default:
[zipSW]Required Server Configuration
1. The ZIP server must support range requests
The ZIP package URL must support HTTP byte range requests. The package reads the ZIP central directory and individual files using Range headers.
The ZIP server should return:
Accept-Ranges: bytesFor range requests, it must return:
HTTP/1.1 206 Partial Content
Content-Range: bytes start-end/totalIf the ZIP server does not support range requests, zip-peek cannot stream individual files.
2. Configure CORS on the ZIP origin (required for cross-origin apps)
When your app and the ZIP URL are on different origins (for example, the app on https://app.example.com and the ZIP on https://cdn.example.com), the service worker performs cross-origin fetch() calls with a Range header on every manifest and entry read. That triggers a CORS preflight (OPTIONS). If CORS is misconfigured, the browser blocks the fetch and zip-peek logs NetworkError when attempting to fetch resource.
A normal CDN asset load (simple GET without Range) may work even when zip-peek fails — zip-peek has stricter requirements.
S3 bucket CORS
In the S3 console, open the ZIP bucket → Permissions → Cross-origin resource sharing (CORS) and add a rule like:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["https://your-app.example.com", "http://localhost:3000"],
"ExposeHeaders": ["Content-Range", "Accept-Ranges"]
}
]Required for zip-peek:
| Setting | Why |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| AllowedHeaders: ["*"] | Allows the Range request header (triggers preflight). |
| AllowedMethods: GET, HEAD | Manifest and asset reads. |
| AllowedOrigins | Must include your app origin(s). Use * only for quick local testing. |
| ExposeHeaders: Content-Range, Accept-Ranges | The service worker reads Content-Range when parsing the ZIP index. These two headers are required in ExposeHeaders. |
S3 answers OPTIONS preflight automatically when CORS is configured; you do not list OPTIONS in AllowedMethods.
CloudFront (when the ZIP is served through CloudFront)
S3 bucket CORS alone is often not enough when a CloudFront distribution sits in front of the bucket. CloudFront settings apply per distribution and per behavior — a working bucket on another path or origin does not guarantee the ZIP path is configured the same way.
On the CloudFront behavior that serves your ZIP objects, configure:
Allowed HTTP methods:
GET,HEAD,OPTIONSOPTIONSis required so CORS preflight forRangerequests succeeds.Origin request policy: CORS-S3Origin (AWS managed)
Forwards theOriginheader (and related CORS preflight headers) to S3 so the bucket can return the correct CORS response.Response headers policy: CORS with preflight enabled
EnsuresAccess-Control-Allow-Originand related headers are present on206 Partial Contentresponses. Match your app origin(s) in the policy.
After changing CORS or CloudFront settings, invalidate the CloudFront cache for the ZIP path so cached responses without CORS headers are not served.
Verify from your app origin:
# Preflight — must not return 403
curl -sI -X OPTIONS \
-H "Origin: https://your-app.example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: range" \
"https://cdn.example.com/packages/session.zip"
# Range GET — must return 206 with Access-Control-Allow-Origin
curl -sI -X GET \
-H "Origin: https://your-app.example.com" \
-H "Range: bytes=0-100" \
"https://cdn.example.com/packages/session.zip"3. The service worker file must be same-origin
Browsers require service worker scripts to be served from the same origin as the page.
Valid when the page is also served from https://app.example.com:
https://app.example.com/zipServiceWorker.a1b2c3d4.jsInvalid if the page is on another origin:
https://static-assets.example.com/zipServiceWorker.a1b2c3d4.js4. Configure Service-Worker-Allowed
By default, a service worker can only control pages under its own directory. If the worker file is served from a nested folder but needs to control a wider scope, the response for the worker file must include Service-Worker-Allowed.
For an app under /app/, configure:
Service-Worker-Allowed: /app/If the worker needs to control the full origin, configure:
Service-Worker-Allowed: /The header must be returned on the zipServiceWorker.js or zipServiceWorker.<hash>.js response itself.
5. Scope and worker location must match
The registered scope must be allowed by both:
- the worker file location
- the
Service-Worker-Allowedheader
Example:
navigator.serviceWorker.register("/app/zipServiceWorker.a1b2c3d4.js", {
scope: "/app/",
});This requires:
Service-Worker-Allowed: /app/or:
Service-Worker-Allowed: /Bundler Configuration
The package ships the worker as:
zip-peek/zipServiceWorker.jsYour application must copy this file into its public build output. The browser registers service workers by URL, so the worker must exist as a real served JavaScript file.
Production with a hashed worker filename
When your app emits hashed assets, copy the worker with a content hash and pass the final worker URL to initZipPeek().
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{
from: require.resolve("zip-peek/zipServiceWorker.js"),
to: "zipServiceWorker.[contenthash:8].js",
},
],
}),
],
};You then need a way for your runtime code to know the emitted hashed filename. Common approaches include:
- generating an asset manifest
- injecting the filename at build time
- using your framework or bundler's asset URL mechanism
For example, after resolving the emitted filename:
await initZipPeek({
workerUrl: new URL(`/assets/${zipServiceWorkerFilename}`, window.location.origin).href,
scopeUrl: new URL("/", window.location.origin).href,
});where zipServiceWorkerFilename is the final emitted file name, such as:
zipServiceWorker.a1b2c3d4.jsDevelopment configuration
In development, it is usually simpler to copy the worker without a hash:
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{
from: require.resolve("zip-peek/zipServiceWorker.js"),
to: "zipServiceWorker.js",
},
],
}),
],
};Then use:
await initZipPeek({
workerUrl: new URL("/zipServiceWorker.js", window.location.origin).href,
scopeUrl: new URL("/", window.location.origin).href,
});If your app is served from a subpath, adjust both workerUrl and scopeUrl to match that path.
Caching Behavior
The service worker stores:
- parsed ZIP manifests (in
zip-cache-v1by default, keyed per ZIP URL) - fully extracted and decompressed assets (populated in the background from a tee of the client stream, with
X-ZipSW-Cached-AtTTL metadata) - persisted
allowedZipUrlsin a separate config cache (zip-peek-config-v1)
The default asset/manifest cache bucket is:
zip-cache-v1Extracted assets expire after two hours by default. Set assetCacheTtlMs to customize this.
Use cacheClearingStrategy to control initialization-time cleanup. keep-all leaves existing entries untouched, keep-allowed-urls removes cached ZIP data outside allowedZipUrls, and clear-all removes all zip-peek cached ZIP data before warming any allowed manifests.
Internal Flow
For a detailed explanation of the modular service worker (src/zipServiceWorker.ts + src/worker/*), request interception, manifest fetching, entry extraction, allow-list behavior, and caching, see the Zip Service Worker Flow documentation.
Limitations
- The ZIP URL must end with
.zip. - The ZIP server must support HTTP range requests.
- Cross-origin ZIP URLs require CORS: S3
ExposeHeadersmust exposeContent-RangeandAccept-Ranges; CloudFront (if used) must allowGET/HEAD/OPTIONSand use CORS-S3Origin plus a CORS response headers policy with preflight. - The service worker file must be same-origin.
- The service worker can only intercept requests inside its registered scope.
- Supported ZIP compression methods are stored (
0) and deflated (8). - ZIP64 is not currently supported.
- Manifest entry fallback prepends the ZIP basename only (e.g.
session/icon.pngforsession.zip); generic*/filenamesuffix matching is not used. - Multi-range client requests are not supported (416).
- The package is browser-only and depends on Service Worker, Cache API, and HTTP range request support.
