zip-peek 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -251
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/types.d.ts +1 -0
- package/dist/zip-utils.d.ts +2 -0
- package/dist/zip-utils.js +14 -0
- package/dist/zipServiceWorker.js +1 -1
- package/dist/zipServiceWorker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,256 +1,34 @@
|
|
|
1
1
|
# zip-peek
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`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.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It registers a service worker that intercepts requests like:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## The setup every platform team knows
|
|
10
|
-
|
|
11
|
-
At our company, we serve the same packaged content to two clients: a **mobile app** and a **web app**. For a long time, that meant **two identical buckets** on our CDN—same assets, same structure, duplicated storage and ops:
|
|
12
|
-
|
|
13
|
-
| Bucket | Consumer | Why it existed |
|
|
14
|
-
|--------|----------|----------------|
|
|
15
|
-
| **Zipped** | Mobile | Users can download the package once and open it offline later. A single archive is the right shape for “take this home.” |
|
|
16
|
-
| **Unzipped** | Web | Loose files under a normal folder path—`base/slides/slide-1/image.png`—because that’s what browsers and our frontend already expected. |
|
|
17
|
-
|
|
18
|
-
Same content. Two pipelines. Two bills. Two places for drift to creep in (“did we deploy the zip *and* the folder?”).
|
|
19
|
-
|
|
20
|
-
Nobody loved it, but it felt *correct*. Mobile gets archives. Web gets directories. That’s just how the web works, isn’t it?
|
|
21
|
-
|
|
22
|
-
### Before → After
|
|
23
|
-
|
|
24
|
-
| **Before** | **After** |
|
|
25
|
-
|------------|-----------|
|
|
26
|
-
| Mobile → `session.zip` | Mobile → `session.zip` |
|
|
27
|
-
| Web → `session/` (unzipped) | Web → `session.zip` (same bucket) |
|
|
28
|
-
| Duplicate storage & deploys | One source of truth |
|
|
29
|
-
|
|
30
|
-
```mermaid
|
|
31
|
-
flowchart LR
|
|
32
|
-
subgraph before [Before]
|
|
33
|
-
M1[Mobile] --> Z[session.zip]
|
|
34
|
-
W1[Web] --> U[session/]
|
|
35
|
-
end
|
|
36
|
-
subgraph after [After]
|
|
37
|
-
M2[Mobile] --> Z2[session.zip]
|
|
38
|
-
W2[Web] --> Z2
|
|
39
|
-
end
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## What we believed about browsers and ZIP files
|
|
45
|
-
|
|
46
|
-
When my manager said we should **remove the unzipped bucket** and have the **web app use the zipped bucket**, my first reaction was honest: *this is madness.*
|
|
47
|
-
|
|
48
|
-
The mental model was simple and brutal:
|
|
49
|
-
|
|
50
|
-
1. To use a file inside a ZIP in the browser, you’d have to **download the whole archive**.
|
|
51
|
-
2. Then **unzip it in JavaScript**—CPU, memory, time on the main thread or a worker.
|
|
52
|
-
3. For a large session package (slides, images, audio), that would **destroy first load**, **hurt low-end devices**, and **waste bandwidth** on assets the user never opens.
|
|
53
|
-
|
|
54
|
-
Mobile can download `session.zip`, stash it, and read entries locally. The web, we assumed, **cannot** download a ZIP and unzip it without a performance cliff—so we **had** to keep the unzipped bucket for the web app.
|
|
55
|
-
|
|
56
|
-
That belief wasn’t lazy. It matched how most teams ship: ZIP on mobile, folder tree on CDN for web. It’s the industry default.
|
|
57
|
-
|
|
58
|
-
So when the task landed on my desk—*make the web app work from the zip bucket, performantly*—it felt like being asked to disprove physics.
|
|
59
|
-
|
|
60
|
-
**Spoiler alert: it can be done wonderfully.** But not by downloading and unpacking the whole ZIP in the page. By changing *where* the work happens.
|
|
61
|
-
|
|
62
|
-
| ❌ Naive approach | ✓ zip-peek |
|
|
63
|
-
|-------------------|------------|
|
|
64
|
-
| Download entire 200 MB ZIP | Range: ~40 KB for one PNG |
|
|
65
|
-
| Unzip in JS — CPU + memory | One asset at a time |
|
|
66
|
-
| Browser chokes | Fast, normal responses |
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## The insight: browsers don’t need the whole ZIP—only the bytes for one file
|
|
71
|
-
|
|
72
|
-
ZIP isn’t a black box you must swallow whole. At the end of every archive is a **central directory**: an index of filenames, compressed sizes, and byte offsets. With **HTTP range requests**, you can:
|
|
73
|
-
|
|
74
|
-
1. Fetch a small slice from the **end** of the file to find that index.
|
|
75
|
-
2. Fetch only the **central directory** bytes.
|
|
76
|
-
3. For each asset request, fetch **only the byte range** for that one entry.
|
|
77
|
-
4. Inflate that chunk if it’s deflated, return a normal `Response`, and **cache** the result.
|
|
78
|
-
|
|
79
|
-
No 200 MB download for a 40 KB PNG. No app-wide unzip loop. No rewriting every screen to understand “archive semantics.”
|
|
80
|
-
|
|
81
|
-
The web doesn’t need a second bucket. It needs something in front of the zip bucket that speaks **URL paths** on the outside and **byte ranges** on the inside.
|
|
82
|
-
|
|
83
|
-
That something is what I built: **`zip-peek`**.
|
|
84
|
-
|
|
85
|
-
**What happens (in order):**
|
|
86
|
-
|
|
87
|
-
1. App requests `session.zip/slides/hero.png`
|
|
88
|
-
2. Service worker intercepts — not a normal loose file
|
|
89
|
-
3. Range: last 64 KB of ZIP → locate End of Central Directory
|
|
90
|
-
4. Range: fetch central directory only (the index)
|
|
91
|
-
5. Range: fetch bytes for `hero.png` only → inflate if needed
|
|
92
|
-
6. Cache extracted asset; return `200 image/png` to the app
|
|
93
|
-
|
|
94
|
-
```mermaid
|
|
95
|
-
sequenceDiagram
|
|
96
|
-
participant App
|
|
97
|
-
participant SW as zip-peek SW
|
|
98
|
-
participant CDN
|
|
99
|
-
App->>SW: GET session.zip/.../hero.png
|
|
100
|
-
SW->>CDN: Range tail 64KB
|
|
101
|
-
CDN-->>SW: 206
|
|
102
|
-
SW->>CDN: Range central directory
|
|
103
|
-
CDN-->>SW: 206
|
|
104
|
-
SW->>CDN: Range entry bytes only
|
|
105
|
-
CDN-->>SW: 206
|
|
106
|
-
SW-->>App: 200 image/png
|
|
7
|
+
```text
|
|
8
|
+
https://cdn.example.com/packages/session.zip/path/to/asset.png
|
|
107
9
|
```
|
|
108
10
|
|
|
109
|
-
|
|
11
|
+
The 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.
|
|
110
12
|
|
|
111
|
-
##
|
|
13
|
+
## When Is This Package Useful?
|
|
112
14
|
|
|
113
|
-
|
|
15
|
+
Use this package when your app receives a base path and later appends folders and file names to fetch assets from that path.
|
|
114
16
|
|
|
115
|
-
|
|
116
|
-
https://cdn.example.com/packages/session.zip/slides/slide-1/hero.png
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Our web app **keeps the same URL-building pattern** it used for the unzipped bucket—only the base path changes from a folder to a `.zip` URL:
|
|
17
|
+
For example, your app may normally treat the base path as a directory:
|
|
120
18
|
|
|
121
19
|
```ts
|
|
122
|
-
// Before (unzipped bucket)
|
|
123
20
|
const basePath = "https://cdn.example.com/packages/session";
|
|
124
|
-
const imageUrl = `${basePath}/slides/slide-1/
|
|
125
|
-
|
|
126
|
-
// After (zip bucket — same pattern)
|
|
127
|
-
const basePath = "https://cdn.example.com/packages/session.zip";
|
|
128
|
-
const imageUrl = `${basePath}/slides/slide-1/hero.png`;
|
|
21
|
+
const imageUrl = `${basePath}/slides/slide-1/image.png`;
|
|
129
22
|
```
|
|
130
23
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
- Parses the ZIP index from the remote archive (range requests on the tail, then the central directory).
|
|
134
|
-
- Resolves the inner path (`slides/slide-1/hero.png`).
|
|
135
|
-
- Fetches **only** that entry’s bytes.
|
|
136
|
-
- Decompresses when needed (stored or deflated entries).
|
|
137
|
-
- Serves a normal cached response to the browser.
|
|
138
|
-
|
|
139
|
-
From product code’s perspective, the zip bucket **behaves like a virtual folder**. The “impossible” migration was mostly: point `basePath` at `.zip`, initialize the worker, ensure the CDN supports `Accept-Ranges: bytes`.
|
|
140
|
-
|
|
141
|
-
**Simple flow:**
|
|
142
|
-
|
|
143
|
-
1. **Your app** — `fetch()` or `<img src>` — no ZIP code.
|
|
144
|
-
2. **zip-peek (service worker)** — sees a path into a `.zip` file, fetches only that asset from the archive.
|
|
145
|
-
3. **CDN** — sends a small byte range from `session.zip`, not the whole file.
|
|
146
|
-
4. **Back to your app** — a normal image response, as if the PNG lived in a folder.
|
|
147
|
-
|
|
148
|
-
*The URL looks like a folder path. The service worker makes it work.*
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
## The real win: shared engines, zero code changes
|
|
153
|
-
|
|
154
|
-
The performance story mattered—but the reason this package was **so** valuable on our stack is structural.
|
|
155
|
-
|
|
156
|
-
We run **engines** that power both the web app and the mobile app. They are the same code paths: load a package, resolve assets, render slides, play audio, and so on. The host passes in a **`packageBasePath`**, and the engine builds URLs the same way every time:
|
|
24
|
+
If the same assets are sometimes delivered as a ZIP file instead:
|
|
157
25
|
|
|
158
26
|
```ts
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
The engine does not branch on web vs mobile. It **`fetch`es** what looks like an ordinary absolute URL and expects the platform to return bytes.
|
|
163
|
-
|
|
164
|
-
**Mobile and web prepare that base differently**—but the engine never sees the difference in code:
|
|
165
|
-
|
|
166
|
-
| Host | How the host prepares assets | What the engine receives as `packageBasePath` |
|
|
167
|
-
|------|------------------------------|-----------------------------------------------|
|
|
168
|
-
| **Mobile** | Download `session.zip` → uncompress on device → **localhost** server over the extracted folder | `http://127.0.0.1:3847/` |
|
|
169
|
-
| **Web (before)** | Unzipped CDN bucket | `https://cdn.example.com/packages/session` |
|
|
170
|
-
| **Web (after zip-peek)** | Zip on CDN + `initZipPeek()` | `https://cdn.example.com/packages/session.zip` |
|
|
171
|
-
|
|
172
|
-
On mobile, the engine always talked to a **real folder**—just served locally:
|
|
173
|
-
|
|
174
|
-
```text
|
|
175
|
-
http://127.0.0.1:3847/slides/slide-1/image.png
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Mobile never passed the zip URL into the engine. The app shell downloaded the archive, unpacked it, stood up localhost, and handed the engine an unzipped base path.
|
|
179
|
-
|
|
180
|
-
**After `zip-peek`**, web could use the **same zip bucket** mobile downloads from. Web passes:
|
|
181
|
-
|
|
182
|
-
```text
|
|
183
|
-
https://cdn.example.com/packages/session.zip/slides/slide-1/image.png
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Only the **service worker** knows that URL points *into* a remote ZIP.
|
|
187
|
-
|
|
188
|
-
- **Mobile:** unchanged (download zip → unzip → localhost → directory `packageBasePath`).
|
|
189
|
-
- **Web:** inject the zip URL and wire up `initZipPeek()` once.
|
|
190
|
-
- **Engine:** zero changes.
|
|
191
|
-
|
|
192
|
-
```mermaid
|
|
193
|
-
flowchart TB
|
|
194
|
-
subgraph engine["Shared engine — unchanged"]
|
|
195
|
-
U["`${packageBasePath}/asset.png`"]
|
|
196
|
-
F[fetch]
|
|
197
|
-
U --> F
|
|
198
|
-
end
|
|
199
|
-
subgraph mobile["Mobile host"]
|
|
200
|
-
CDN1[(CDN session.zip)]
|
|
201
|
-
DL[Download and unzip]
|
|
202
|
-
LH[Localhost server]
|
|
203
|
-
PM["packageBasePath = http://127.0.0.1:…/"]
|
|
204
|
-
CDN1 --> DL --> LH --> PM --> engine
|
|
205
|
-
end
|
|
206
|
-
subgraph web["Web host"]
|
|
207
|
-
CDN2[(CDN session.zip)]
|
|
208
|
-
P["packageBasePath = …/session.zip"]
|
|
209
|
-
SW[zip-peek service worker]
|
|
210
|
-
CDN2 --> P --> engine
|
|
211
|
-
F -. intercepted .-> SW
|
|
212
|
-
SW --> CDN2
|
|
213
|
-
end
|
|
27
|
+
const basePath = "https://cdn.example.com/packages/session.zip";
|
|
28
|
+
const imageUrl = `${basePath}/slides/slide-1/image.png`;
|
|
214
29
|
```
|
|
215
30
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
## From “pure madness” to one source of truth
|
|
219
|
-
|
|
220
|
-
| Before | After |
|
|
221
|
-
|--------|--------|
|
|
222
|
-
| Two buckets, same content | **One zip bucket** for mobile *and* web |
|
|
223
|
-
| Web tied to unzipped CDN layout | Web uses **path-under-zip** URLs |
|
|
224
|
-
| Fear of full-archive download + JS unzip | **Range-based peek** per asset + Cache API |
|
|
225
|
-
| “Browsers can’t do ZIP performantly” | **Browsers can**, with the right network layer |
|
|
226
|
-
|
|
227
|
-
Mobile still downloads the zip, uncompresses it, and serves it on localhost for offline—that workflow unchanged. Web now reads from the **same zip on the CDN**, on demand via zip-peek, without downloading the full archive.
|
|
228
|
-
|
|
229
|
-
---
|
|
230
|
-
|
|
231
|
-
## What had to be true on the infrastructure side
|
|
232
|
-
|
|
233
|
-
- The zip origin must support **HTTP byte ranges** (`Accept-Ranges: bytes`, `206 Partial Content`).
|
|
234
|
-
- The service worker script must be served **same-origin** (with `Service-Worker-Allowed` when scope is wider than the worker path).
|
|
235
|
-
- Package URLs should end with **`.zip`**.
|
|
236
|
-
|
|
237
|
-
We also support **presigned URLs**, **allow-lists** for zip URLs, and **cache TTL** for extracted assets. See the [README](../README.md#documentation) for details.
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
## A note to the next person who hears “use the zip bucket on web”
|
|
242
|
-
|
|
243
|
-
If your gut says *browsers will choke*, you’re half right: **browsers will choke if you download and unzip the whole archive in the client.**
|
|
244
|
-
|
|
245
|
-
If your gut says *therefore we need a parallel unzipped bucket forever*, that’s the part we overturned.
|
|
246
|
-
|
|
247
|
-
The performant model is **peek, don’t gulp**: index the archive remotely, pull one entry at a time, cache what you’ve already extracted.
|
|
248
|
-
|
|
249
|
-
**It can be done wonderfully.**
|
|
250
|
-
|
|
251
|
-
---
|
|
252
|
-
|
|
253
|
-
## Documentation
|
|
31
|
+
`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.
|
|
254
32
|
|
|
255
33
|
## What It Does
|
|
256
34
|
|
|
@@ -319,9 +97,11 @@ type InitZipPeekOptions = {
|
|
|
319
97
|
reloadOnFirstInstall?: boolean;
|
|
320
98
|
cacheClearingStrategy?: ZipCacheClearingStrategy;
|
|
321
99
|
allowedZipUrls?: string[];
|
|
100
|
+
requireExactManifestPath?: boolean;
|
|
322
101
|
zipAssetCacheName?: string;
|
|
323
102
|
assetCacheTtlMs?: number;
|
|
324
103
|
logPrefix?: string;
|
|
104
|
+
onError?: (error: Error, info?: string) => void;
|
|
325
105
|
};
|
|
326
106
|
|
|
327
107
|
type ZipCacheClearingStrategy = "keep-all" | "keep-allowed-urls" | "clear-all";
|
|
@@ -395,6 +175,21 @@ allowedZipUrls: [
|
|
|
395
175
|
];
|
|
396
176
|
```
|
|
397
177
|
|
|
178
|
+
`requireExactManifestPath`
|
|
179
|
+
|
|
180
|
+
Defaults to `false`. Controls how the service worker resolves a requested inner path against the ZIP manifest.
|
|
181
|
+
|
|
182
|
+
- `false` (default): try an exact manifest key match first; if that fails, retry with `{zipBasename}/{requestedPath}` where `zipBasename` is the ZIP filename without the `.zip` extension (e.g. for `session.zip`, request `slides/image.png` falls back to manifest key `session/slides/image.png`).
|
|
183
|
+
- `true`: only exact manifest key matches are accepted.
|
|
184
|
+
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
`onError`
|
|
188
|
+
|
|
189
|
+
Optional error handler for zip-peek failures and informational reports from the service worker. Receives `(error, info?)` where `info` carries additional context when available.
|
|
190
|
+
|
|
191
|
+
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.
|
|
192
|
+
|
|
398
193
|
`zipAssetCacheName`
|
|
399
194
|
|
|
400
195
|
Overrides the browser Cache API bucket used by the service worker for ZIP manifests and extracted assets.
|
|
@@ -446,7 +241,73 @@ Content-Range: bytes start-end/total
|
|
|
446
241
|
|
|
447
242
|
If the ZIP server does not support range requests, zip-peek cannot stream individual files.
|
|
448
243
|
|
|
449
|
-
### 2.
|
|
244
|
+
### 2. Configure CORS on the ZIP origin (required for cross-origin apps)
|
|
245
|
+
|
|
246
|
+
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.`
|
|
247
|
+
|
|
248
|
+
A normal CDN asset load (simple `GET` without `Range`) may work even when zip-peek fails — zip-peek has stricter requirements.
|
|
249
|
+
|
|
250
|
+
#### S3 bucket CORS
|
|
251
|
+
|
|
252
|
+
In the S3 console, open the ZIP bucket → **Permissions** → **Cross-origin resource sharing (CORS)** and add a rule like:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
[
|
|
256
|
+
{
|
|
257
|
+
"AllowedHeaders": ["*"],
|
|
258
|
+
"AllowedMethods": ["GET", "HEAD"],
|
|
259
|
+
"AllowedOrigins": ["https://your-app.example.com", "http://localhost:3000"],
|
|
260
|
+
"ExposeHeaders": ["Content-Range", "Accept-Ranges"]
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Required for zip-peek:**
|
|
266
|
+
|
|
267
|
+
| Setting | Why |
|
|
268
|
+
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
269
|
+
| `AllowedHeaders: ["*"]` | Allows the `Range` request header (triggers preflight). |
|
|
270
|
+
| `AllowedMethods`: `GET`, `HEAD` | Manifest and asset reads. |
|
|
271
|
+
| `AllowedOrigins` | Must include your app origin(s). Use `*` only for quick local testing. |
|
|
272
|
+
| `ExposeHeaders`: **`Content-Range`**, **`Accept-Ranges`** | The service worker reads `Content-Range` when parsing the ZIP index. These two headers are **required** in `ExposeHeaders`. |
|
|
273
|
+
|
|
274
|
+
S3 answers `OPTIONS` preflight automatically when CORS is configured; you do not list `OPTIONS` in `AllowedMethods`.
|
|
275
|
+
|
|
276
|
+
#### CloudFront (when the ZIP is served through CloudFront)
|
|
277
|
+
|
|
278
|
+
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.
|
|
279
|
+
|
|
280
|
+
On the CloudFront **behavior** that serves your ZIP objects, configure:
|
|
281
|
+
|
|
282
|
+
1. **Allowed HTTP methods**: `GET`, `HEAD`, `OPTIONS`
|
|
283
|
+
`OPTIONS` is required so CORS preflight for `Range` requests succeeds.
|
|
284
|
+
|
|
285
|
+
2. **Origin request policy**: **CORS-S3Origin** (AWS managed)
|
|
286
|
+
Forwards the `Origin` header (and related CORS preflight headers) to S3 so the bucket can return the correct CORS response.
|
|
287
|
+
|
|
288
|
+
3. **Response headers policy**: **CORS** with **preflight** enabled
|
|
289
|
+
Ensures `Access-Control-Allow-Origin` and related headers are present on `206 Partial Content` responses. Match your app origin(s) in the policy.
|
|
290
|
+
|
|
291
|
+
After changing CORS or CloudFront settings, **invalidate the CloudFront cache** for the ZIP path so cached responses without CORS headers are not served.
|
|
292
|
+
|
|
293
|
+
**Verify from your app origin:**
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# Preflight — must not return 403
|
|
297
|
+
curl -sI -X OPTIONS \
|
|
298
|
+
-H "Origin: https://your-app.example.com" \
|
|
299
|
+
-H "Access-Control-Request-Method: GET" \
|
|
300
|
+
-H "Access-Control-Request-Headers: range" \
|
|
301
|
+
"https://cdn.example.com/packages/session.zip"
|
|
302
|
+
|
|
303
|
+
# Range GET — must return 206 with Access-Control-Allow-Origin
|
|
304
|
+
curl -sI -X GET \
|
|
305
|
+
-H "Origin: https://your-app.example.com" \
|
|
306
|
+
-H "Range: bytes=0-100" \
|
|
307
|
+
"https://cdn.example.com/packages/session.zip"
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 3. The service worker file must be same-origin
|
|
450
311
|
|
|
451
312
|
Browsers require service worker scripts to be served from the same origin as the page.
|
|
452
313
|
|
|
@@ -462,7 +323,7 @@ Invalid if the page is on another origin:
|
|
|
462
323
|
https://static-assets.example.com/zipServiceWorker.a1b2c3d4.js
|
|
463
324
|
```
|
|
464
325
|
|
|
465
|
-
###
|
|
326
|
+
### 4. Configure `Service-Worker-Allowed`
|
|
466
327
|
|
|
467
328
|
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`.
|
|
468
329
|
|
|
@@ -480,7 +341,7 @@ Service-Worker-Allowed: /
|
|
|
480
341
|
|
|
481
342
|
The header must be returned on the `zipServiceWorker.js` or `zipServiceWorker.<hash>.js` response itself.
|
|
482
343
|
|
|
483
|
-
###
|
|
344
|
+
### 5. Scope and worker location must match
|
|
484
345
|
|
|
485
346
|
The registered scope must be allowed by both:
|
|
486
347
|
|
|
@@ -548,10 +409,7 @@ For example, after resolving the emitted filename:
|
|
|
548
409
|
|
|
549
410
|
```ts
|
|
550
411
|
await initZipPeek({
|
|
551
|
-
workerUrl: new URL(
|
|
552
|
-
`/assets/${zipServiceWorkerFilename}`,
|
|
553
|
-
window.location.origin,
|
|
554
|
-
).href,
|
|
412
|
+
workerUrl: new URL(`/assets/${zipServiceWorkerFilename}`, window.location.origin).href,
|
|
555
413
|
scopeUrl: new URL("/", window.location.origin).href,
|
|
556
414
|
});
|
|
557
415
|
```
|
|
@@ -620,18 +478,11 @@ For a detailed explanation of the modular service worker (`src/zipServiceWorker.
|
|
|
620
478
|
|
|
621
479
|
- The ZIP URL must end with `.zip`.
|
|
622
480
|
- The ZIP server must support HTTP range requests.
|
|
481
|
+
- Cross-origin ZIP URLs require CORS: S3 `ExposeHeaders` must expose `Content-Range` and `Accept-Ranges`; CloudFront (if used) must allow `GET`/`HEAD`/`OPTIONS` and use CORS-S3Origin plus a CORS response headers policy with preflight.
|
|
623
482
|
- The service worker file must be same-origin.
|
|
624
483
|
- The service worker can only intercept requests inside its registered scope.
|
|
625
484
|
- Supported ZIP compression methods are stored (`0`) and deflated (`8`).
|
|
626
485
|
- ZIP64 is not currently supported.
|
|
627
|
-
-
|
|
486
|
+
- Manifest entry fallback prepends the ZIP basename only (e.g. `session/icon.png` for `session.zip`); generic `*/filename` suffix matching is not used.
|
|
628
487
|
- Multi-range client requests are not supported (416).
|
|
629
488
|
- The package is browser-only and depends on Service Worker, Cache API, and HTTP range request support.
|
|
630
|
-
|
|
631
|
-
---
|
|
632
|
-
|
|
633
|
-
## Further reading
|
|
634
|
-
|
|
635
|
-
- [Medium-style article (HTML)](./docs/MEDIUM_POST.html) — animated diagrams
|
|
636
|
-
- [Article (Markdown)](./docs/MEDIUM_POST.md) — same story as Markdown
|
|
637
|
-
- [Zip service worker flow](./docs/ZIP_SERVICE_WORKER_FLOW.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export type { InitZipPeekOptions, InitZipPeekResult, ZipCacheClearingStrategy, Z
|
|
|
11
11
|
* `cacheClearingStrategy` to keep existing cache entries, keep only allowed
|
|
12
12
|
* ZIP URLs, or clear all zip-peek cache entries before use.
|
|
13
13
|
*/
|
|
14
|
-
export declare function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall, cacheClearingStrategy, allowedZipUrls, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, }: InitZipPeekOptions): Promise<InitZipPeekResult>;
|
|
14
|
+
export declare function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall, cacheClearingStrategy, allowedZipUrls, requireExactManifestPath, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, }: InitZipPeekOptions): Promise<InitZipPeekResult>;
|
|
15
15
|
export { isZipPackage };
|
package/dist/index.js
CHANGED
|
@@ -195,7 +195,7 @@ async function postWorkerMessageOrThrow(message, info) {
|
|
|
195
195
|
* `cacheClearingStrategy` to keep existing cache entries, keep only allowed
|
|
196
196
|
* ZIP URLs, or clear all zip-peek cache entries before use.
|
|
197
197
|
*/
|
|
198
|
-
async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, cacheClearingStrategy = "keep-all", allowedZipUrls, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, }) {
|
|
198
|
+
async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, cacheClearingStrategy = "keep-all", allowedZipUrls, requireExactManifestPath = false, zipAssetCacheName, assetCacheTtlMs, logPrefix, onError, }) {
|
|
199
199
|
if (onError) {
|
|
200
200
|
currentOnError = onError;
|
|
201
201
|
}
|
|
@@ -230,6 +230,7 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
|
|
|
230
230
|
assetCacheTtlMs,
|
|
231
231
|
logPrefix,
|
|
232
232
|
allowedZipUrls: normalizedAllowedZipUrls ?? null,
|
|
233
|
+
requireExactManifestPath,
|
|
233
234
|
},
|
|
234
235
|
}, "Failed to configure service worker");
|
|
235
236
|
if (cacheClearingStrategy === "clear-all") {
|
|
@@ -243,6 +244,7 @@ async function initZipPeek({ workerUrl, scopeUrl, reloadOnFirstInstall = true, c
|
|
|
243
244
|
assetCacheTtlMs,
|
|
244
245
|
logPrefix,
|
|
245
246
|
allowedZipUrls: normalizedAllowedZipUrls ?? null,
|
|
247
|
+
requireExactManifestPath,
|
|
246
248
|
},
|
|
247
249
|
}, "Failed to reconfigure service worker after cache clear");
|
|
248
250
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type InitZipPeekOptions = {
|
|
|
11
11
|
reloadOnFirstInstall?: boolean;
|
|
12
12
|
cacheClearingStrategy?: ZipCacheClearingStrategy;
|
|
13
13
|
allowedZipUrls?: string[];
|
|
14
|
+
requireExactManifestPath?: boolean;
|
|
14
15
|
zipAssetCacheName?: string;
|
|
15
16
|
assetCacheTtlMs?: number;
|
|
16
17
|
logPrefix?: string;
|
package/dist/zip-utils.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type NormalizeZipUrlOptions = {
|
|
|
12
12
|
* are not re-encoded.
|
|
13
13
|
*/
|
|
14
14
|
export declare function normalizeZipUrl(urlString: string, options?: NormalizeZipUrlOptions): string;
|
|
15
|
+
/** Basename of the ZIP file from a package URL, without the `.zip` extension. */
|
|
16
|
+
export declare function zipBasenameWithoutExtension(zipUrl: string): string;
|
|
15
17
|
/** Align zip entry names with URL paths under `*.zip/…`. */
|
|
16
18
|
export declare function normalizeZipEntryPath(name: string): string;
|
|
17
19
|
export type ParsedZipAssetRequest = {
|
package/dist/zip-utils.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isZipPackage = isZipPackage;
|
|
4
4
|
exports.normalizeZipUrl = normalizeZipUrl;
|
|
5
|
+
exports.zipBasenameWithoutExtension = zipBasenameWithoutExtension;
|
|
5
6
|
exports.normalizeZipEntryPath = normalizeZipEntryPath;
|
|
6
7
|
exports.parseZipAssetRequest = parseZipAssetRequest;
|
|
7
8
|
exports.canonicalZipAssetRequestUrl = canonicalZipAssetRequestUrl;
|
|
@@ -44,6 +45,19 @@ function normalizeZipUrl(urlString, options = {}) {
|
|
|
44
45
|
return prefix + hashSection;
|
|
45
46
|
return prefix + "?" + filtered.join("&") + hashSection;
|
|
46
47
|
}
|
|
48
|
+
/** Basename of the ZIP file from a package URL, without the `.zip` extension. */
|
|
49
|
+
function zipBasenameWithoutExtension(zipUrl) {
|
|
50
|
+
try {
|
|
51
|
+
const pathname = new URL(zipUrl, "http://local").pathname;
|
|
52
|
+
const file = pathname.split("/").pop() ?? "";
|
|
53
|
+
return file.endsWith(".zip") ? file.slice(0, -4) : file;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
const path = zipUrl.split("?")[0].split("#")[0];
|
|
57
|
+
const file = path.split("/").pop() ?? "";
|
|
58
|
+
return file.endsWith(".zip") ? file.slice(0, -4) : file;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
47
61
|
/** Align zip entry names with URL paths under `*.zip/…`. */
|
|
48
62
|
function normalizeZipEntryPath(name) {
|
|
49
63
|
let s = name.replace(/\\/g, "/"); // Replace backslashes with forward slashes.
|
package/dist/zipServiceWorker.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(()=>{"use strict";var e={180(e,t){function n(e){const n=e.trim();return n.startsWith(t.ZIP_PEEK_ERROR_PREFIX)?n:`${t.ZIP_PEEK_ERROR_PREFIX} ${n}`}t.ZIP_PEEK_ERROR_PREFIX=void 0,t.formatZipPeekError=n,t.createZipPeekError=function(e){return new Error(n(e))},t.toZipPeekError=function(e){const r=e instanceof Error?e:new Error(String(e));return r.message.startsWith(t.ZIP_PEEK_ERROR_PREFIX)||(r.message=n(r.message)),r},t.errorMessage=function(e){return e instanceof Error?e.message:String(e)},t.ZIP_PEEK_ERROR_PREFIX="[zip-peek-error]"},451(e,t,n){t.ensureAllowedZipUrlsLoaded=async function(){if(s.allowedZipUrlsLoaded)return s.allowedZipUrlsLoaded;const e=(async()=>{try{const e=await caches.open(s.CONFIG_CACHE_NAME),t=await e.match(s.CONFIG_CACHE_KEY);if(!t)return;const n=await t.json();Array.isArray(n.allowedZipUrls)?(0,s.setAllowedZipUrls)(new Set(n.allowedZipUrls.filter(e=>"string"==typeof e).map(e=>(0,r.normalizeZipUrl)(e)))):(0,s.setAllowedZipUrls)(null)}catch(e){(0,i.warn)("failed to load runtime config",e)}})();return(0,s.setAllowedZipUrlsLoaded)(e),e},t.applyRuntimeConfig=async function(e){if(!e)return;let t={...s.runtimeConfig},n=!1;if("string"==typeof e.zipAssetCacheName&&e.zipAssetCacheName.trim()&&(t.zipAssetCacheName=e.zipAssetCacheName,n=!0),"number"==typeof e.assetCacheTtlMs&&Number.isFinite(e.assetCacheTtlMs)&&(t.assetCacheTtlMs=e.assetCacheTtlMs,n=!0),"string"==typeof e.logPrefix&&e.logPrefix.trim()&&(t.logPrefix=e.logPrefix,n=!0),n&&(0,s.setRuntimeConfig)(t),"allowedZipUrls"in e){const t=Array.isArray(e.allowedZipUrls)?e.allowedZipUrls.map(e=>(0,r.normalizeZipUrl)(e)):null;(0,s.setAllowedZipUrls)(t?new Set(t):null),(0,s.setAllowedZipUrlsLoaded)(Promise.resolve()),await async function(e){try{const t=await caches.open(s.CONFIG_CACHE_NAME);await t.put(new Request(s.CONFIG_CACHE_KEY),new Response(JSON.stringify({allowedZipUrls:e}),{headers:{"Content-Type":"application/json"}}))}catch(e){(0,i.warn)("failed to persist runtime config",e)}}(t)}},t.isZipUrlAllowed=function(e){return null===s.allowedZipUrls||s.allowedZipUrls.has(e)};const r=n(134),s=n(167),i=n(777)},919(e,t,n){t.canonicalAssetRequest=function(e,t){return new Request((0,s.canonicalZipAssetRequestUrl)(e,t),{method:"GET"})},t.clearAllZipAssets=async function(){const e=await caches.open(i.runtimeConfig.zipAssetCacheName),t=await e.keys();await Promise.all(t.map(t=>e.delete(t))),await caches.delete(i.CONFIG_CACHE_NAME),i.zipManifests.clear(),i.pendingManifestLoads.clear(),(0,i.setAllowedZipUrlsLoaded)(Promise.resolve())},t.clearZipAssetsExceptAllowed=async function(e){try{const t=new Set(e),n=await caches.open(i.runtimeConfig.zipAssetCacheName),r=await n.keys();for(const e of r){const r=e.url;if(r.includes(i.MANIFEST_CACHE_KEY)){const s=(0,o.zipUrlFromManifestCacheKey)(r);s&&!t.has(s)&&await n.delete(e);continue}const a=(0,s.parseZipAssetRequest)(r);a&&!t.has(a.zipUrl)&&await n.delete(e)}for(const e of i.zipManifests.keys())t.has(e)||i.zipManifests.delete(e)}catch(e){throw(0,r.createZipPeekError)(`Failed to clear zip assets except allowed URLs: ${(0,r.errorMessage)(e)}`)}},t.putCachedFullAssetIfAbsent=async function(e,t){try{const n=await caches.open(i.runtimeConfig.zipAssetCacheName),r=await n.match(e);if(r&&!l(r))return;const s=t.type||"application/octet-stream";await n.put(e,new Response(t,{status:200,headers:{"Content-Type":s,"Content-Length":String(t.size),"Access-Control-Allow-Origin":"*",[i.ASSET_CACHED_AT_HEADER]:String(Date.now())}}))}catch(e){(0,a.warn)("asset cache put failed",e)}},t.readCachedZipAssetIfFresh=async function(e){let t=null,n="network";try{const r=await caches.open(i.runtimeConfig.zipAssetCacheName),s=await r.match(e);if(s)if(l(s))try{await r.delete(e)}catch(e){(0,a.warn)("asset cache delete expired failed",e)}else t=await s.blob(),n="cache-api"}catch(e){(0,a.warn)("asset cache read failed",e)}return{blob:t,assetSource:n}};const r=n(180),s=n(134),i=n(167),a=n(777),o=n(483);function l(e){const t=e.headers.get(i.ASSET_CACHED_AT_HEADER);if(!t)return!0;const n=parseInt(t,10);return!Number.isFinite(n)||Date.now()-n>i.runtimeConfig.assetCacheTtlMs}},705(e,t,n){t.installFetchHandler=function(){self.addEventListener("fetch",e=>{const t=e.request.method;if("GET"!==t&&"HEAD"!==t)return;const n="HEAD"===t,d=(0,s.parseZipAssetRequest)(e.request.url);if(!d)return;const g=(0,s.normalizeZipUrl)(d.zipUrl),m=d.internalPath,w=(0,s.normalizeZipEntryPath)(m);e.respondWith((async()=>{try{if(await(0,i.ensureAllowedZipUrlsLoaded)(),!(0,i.isZipUrlAllowed)(g))return(0,l.warn)("zip URL not allowed",{requestedZipUrl:g}),(0,f.corsErrorResponse)(`ZIP URL not allowed (not in allowedZipUrls): ${g}`,403);let t=o.zipManifests.get(g);if(t||(t=await(0,c.ensureManifestAvailable)(g,e.clientId||void 0)??void 0),!t){const e=Array.from(o.zipManifests.keys());return(0,l.warn)("manifest missing for zipUrl",{requestedZipUrl:g,knownZipUrls:e}),(0,f.corsErrorResponse)(`ZIP manifest not loaded for ${g}. Ensure the ZIP is reachable and supports byte-range requests.`,404)}const r=(0,c.resolveManifestEntry)(t,w);if(!r){const e=Array.from(t.keys()).slice(0,50);return(0,l.warn)("file not in manifest or ambiguous suffix match",{zipUrl:g,internalPathRaw:m,internalPathNormalized:w,manifestSize:t.size,sampleKeys:e}),(0,f.corsErrorResponse)(`File "${m}" not found in ZIP manifest for ${g}`,404)}const{key:s,info:d}=r;s!==w&&(0,l.log)("resolved via suffix match",{requested:w,manifestKey:s});const h=(0,a.canonicalAssetRequest)(g,s);if(n)return function(e,t,n){const r=(0,f.getMimeType)(t),s=(0,c.entryUncompressedSize)(n),i=e.headers.get("range");if(null!==s&&i)return p(null,r,s,"manifest",i);const a={"Content-Type":r,...u("manifest")};return null!==s&&(a["Content-Length"]=s.toString()),new Response(null,{status:200,headers:a})}(e.request,s,d);let{blob:E,assetSource:y}=await(0,a.readCachedZipAssetIfFresh)(h);if(!E){const e=await(0,f.fetchUncompressedZipEntryFromNetwork)(g,s,d,h);if(!e.ok)return e.response;E=e.blob,y="network"}return p(E,E.type,E.size,y,e.request.headers.get("range"))}catch(e){return(0,l.error)("fetch handler failed",{zipUrl:g,internalPath:m,message:(0,r.errorMessage)(e)}),(0,f.corsErrorResponse)(`Failed to serve "${m}" from ${g}: ${(0,r.errorMessage)(e)}`,500)}})())})};const r=n(180),s=n(134),i=n(451),a=n(919),o=n(167),l=n(777),c=n(483),f=n(237);function u(e){return{"Access-Control-Allow-Origin":"*","Access-Control-Expose-Headers":o.ASSET_SOURCE_HEADER,[o.ASSET_SOURCE_HEADER]:e}}function p(e,t,n,r,s){if(s){const i=s.replace(/^bytes=/,"");if(i.includes(","))return new Response(null,{status:416,headers:{"Content-Range":`bytes */${n}`,...u(r)}});const[a,o]=i.split("-"),l=""!==a?parseInt(a,10):NaN,c=""!==o?parseInt(o,10):n-1;if(isNaN(l)||isNaN(c)||l<0||c<l||l>=n)return new Response(null,{status:416,headers:{"Content-Range":`bytes */${n}`,...u(r)}});const f=Math.min(c,n-1),p=f-l+1,d=null===e?null:e.slice(l,f+1);return new Response(d,{status:206,headers:{"Content-Type":t,"Content-Range":`bytes ${l}-${f}/${n}`,"Content-Length":p.toString(),"Accept-Ranges":"bytes",...u(r)}})}return new Response(e,{status:200,headers:{"Content-Type":t,"Content-Length":n.toString(),...u(r)}})}},167(e,t){t.CONFIG_CACHE_KEY=t.allowedZipUrlsLoaded=t.allowedZipUrls=t.runtimeConfig=t.pendingManifestLoads=t.zipManifests=t.MANIFEST_CACHE_KEY=t.LOCAL_FILE_HEADER_SIG=t.MIME_TYPES=t.ASSET_SOURCE_HEADER=t.ASSET_CACHED_AT_HEADER=t.CONFIG_CACHE_NAME=t.kF=t.o2=void 0,t.setRuntimeConfig=function(e){t.runtimeConfig=e},t.setAllowedZipUrls=function(e){t.allowedZipUrls=e},t.setAllowedZipUrlsLoaded=function(e){t.allowedZipUrlsLoaded=e},t.o2="zip-cache-v1",t.kF=72e5,t.CONFIG_CACHE_NAME="zip-peek-config-v1",t.o2,t.CONFIG_CACHE_NAME,t.ASSET_CACHED_AT_HEADER="X-ZipSW-Cached-At",t.ASSET_SOURCE_HEADER="X-ZipSW-Asset-Source",t.MIME_TYPES={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",json:"application/json",js:"application/javascript",css:"text/css; charset=utf-8",html:"text/html; charset=utf-8",txt:"text/plain; charset=utf-8",mp4:"video/mp4",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",oga:"audio/ogg",ogg:"audio/ogg",xml:"application/xml",wasm:"application/wasm"},t.LOCAL_FILE_HEADER_SIG=67324752,t.MANIFEST_CACHE_KEY="__zipsw_manifest__=1",t.zipManifests=new Map,t.pendingManifestLoads=new Map,t.runtimeConfig={zipAssetCacheName:t.o2,assetCacheTtlMs:t.kF,logPrefix:"[zipSW]"},t.allowedZipUrls=null,t.allowedZipUrlsLoaded=null,t.CONFIG_CACHE_KEY=new URL("__zip_peek_runtime_config__",self.registration.scope).href},777(e,t,n){t.log=function(...e){console.log(s.runtimeConfig.logPrefix,...e)},t.warn=function(...e){console.warn(s.runtimeConfig.logPrefix,...e)},t.error=function(...e){console.error(s.runtimeConfig.logPrefix,r.ZIP_PEEK_ERROR_PREFIX,...e)},t.installClientErrorReporting=function(){self.addEventListener("error",e=>{i(e.error??e.message,"Service worker uncaught error")}),self.addEventListener("unhandledrejection",e=>{i(e.reason,"Service worker unhandled promise rejection")})};const r=n(180),s=n(167);function i(e,t){const n=(0,r.toZipPeekError)(e);self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(e=>{const s={type:"ZIP_SW_ERROR",error:{message:n.message,stack:n.stack},info:t?(0,r.formatZipPeekError)(t):void 0};for(const t of e)t.postMessage(s)})}},483(e,t,n){t.manifestFilesToMap=a,t.sendManifestToClientId=l,t.ensureManifestAvailable=function(e,t){const n=s.zipManifests.get(e);if(n)return Promise.resolve(n);let c=s.pendingManifestLoads.get(e);return c||(c=(async()=>{const n=await async function(e,t){const n=await o(t);return n?function(e,t){return new Promise(n=>{const r=new MessageChannel,s=setTimeout(()=>{r.port1.close(),n(null)},500);r.port1.onmessage=e=>{clearTimeout(s),r.port1.close();const t=e.data;t?.ok&&Array.isArray(t.files)?n(a(t.files)):n(null)},r.port1.onmessageerror=()=>{clearTimeout(s),r.port1.close(),n(null)};try{e.postMessage({type:"ZIP_SW_MANIFEST_REQUEST",zipUrl:t},[r.port2])}catch(e){clearTimeout(s),r.port1.close(),(0,i.warn)("failed to request manifest from client",e),n(null)}})}(n,e):null}(e,t);if(n)return s.zipManifests.set(e,n),(0,i.log)("restored manifest from client memory",{zipUrl:e,entryCount:n.size}),n;const c=await async function(e){const t=await fetch(e,{headers:{Range:"bytes=-65536"}});if(206!==t.status)return(0,i.error)("range requests not supported for ZIP manifest",{zipUrl:e,status:t.status}),null;const n=await t.arrayBuffer(),s=new DataView(n);let a=-1;for(let e=n.byteLength-22;e>=0;e--)if(101010256===s.getUint32(e,!0)){a=e;break}if(-1===a)return(0,i.error)("EOCD not found in last 64KB of ZIP file",e),null;const o=s.getUint32(a+16,!0),l=s.getUint32(a+12,!0);let c=n,f=-1;const u=t.headers.get("Content-Range");let p=0;if(u){const e=u.match(/\/(\d+)$/);e&&(p=parseInt(e[1],10))}if(p<=0)return(0,i.error)("could not determine ZIP file size from Content-Range header",{zipUrl:e,contentRange:u}),null;const d=p-n.byteLength;if(o>=d)f=o-d;else{const t=await fetch(e,{headers:{Range:`bytes=${o}-${o+l-1}`}});if(206!==t.status)return(0,i.error)("failed to fetch ZIP central directory",{zipUrl:e,status:t.status,cdOffset:o,cdSize:l}),null;c=await t.arrayBuffer(),f=0}return function(e,t,n,s){const a=new DataView(e);let o=t;const l=[];for(;o+46<=e.byteLength&&33639248===a.getUint32(o,!0);){const t=a.getUint16(o+10,!0),n=a.getUint32(o+20,!0),s=a.getUint32(o+24,!0),i=a.getUint16(o+28,!0),c=a.getUint16(o+30,!0),f=a.getUint16(o+32,!0),u=a.getUint32(o+42,!0),p=new TextDecoder;if(o+46+i>e.byteLength)break;const d=new Uint8Array(e,o+46,i),g=p.decode(d),m=(0,r.normalizeZipEntryPath)(g);""!==m&&l.push({filename:m,offset:u,compressedSize:n,uncompressedSize:s,compression:t}),o+=46+i+c+f}if(0===l.length)return(0,i.error)("no files parsed from central directory",s),null;l.sort((e,t)=>e.offset-t.offset);const c=new Map;for(let e=0;e<l.length;e++){const t=l[e],s=(0,r.normalizeZipEntryPath)(t.filename);""!==s&&(c.has(s)&&(0,i.warn)("duplicate manifest key after normalize; overwriting",s),c.set(s,{filename:t.filename,offset:t.offset,compressedSize:t.compressedSize,uncompressedSize:t.uncompressedSize,compression:t.compression,nextOffset:e<l.length-1?l[e+1].offset:n}))}return c.size>0?c:null}(c,f,o,e)}(e);return c&&(s.zipManifests.set(e,c),await l(e,c,t),(0,i.log)("manifest fetched and stored in memory",{zipUrl:e,entryCount:c.size})),c})().finally(()=>{s.pendingManifestLoads.delete(e)}),s.pendingManifestLoads.set(e,c),c)},t.entryUncompressedSize=function(e){return void 0!==e.uncompressedSize?e.uncompressedSize:0===e.compression?e.compressedSize:null},t.resolveManifestEntry=function(e,t){if(e.has(t))return{key:t,info:e.get(t)};const n=[];for(const r of e.keys())r.endsWith("/"+t)&&n.push(r);if(1===n.length){const t=n[0];return{key:t,info:e.get(t)}}return n.length>1?((0,i.error)("ambiguous suffix match for ZIP entry",{requested:t,candidates:n.slice(0,8)}),null):null},t.zipUrlFromManifestCacheKey=function(e){const t=s.MANIFEST_CACHE_KEY;if(!e.endsWith(t))return null;let n=e.slice(0,-t.length);return(n.endsWith("?")||n.endsWith("&"))&&(n=n.slice(0,-1)),n};const r=n(134),s=n(167),i=n(777);function a(e){const t=new Map;for(const n of e){const e=(0,r.normalizeZipEntryPath)(n.filename);""!==e&&(t.has(e)&&(0,i.warn)("duplicate manifest key after normalize; overwriting",e),t.set(e,n))}return t.size>0?t:null}async function o(e){if(!e)return null;const t=await self.clients.get(e);return"window"===t?.type?t:null}async function l(e,t,n){const r=await o(n);if(!r)return;const s=Array.from(t.values());try{r.postMessage({type:"ZIP_SW_MANIFEST_CREATED",zipUrl:e,files:s})}catch(e){(0,i.warn)("failed to send manifest to client",e)}}},814(e,t,n){t.installMessageHandler=function(){self.addEventListener("message",e=>{if(!e.data)return;const{type:t,zipUrl:n,files:p,config:d,allowedZipUrls:g}=e.data,m=function(e){if(!e||!("id"in e))return;const t=e.id;return"string"==typeof t?t:void 0}(e.source);if("ZIP_SW_CONFIG"===t){const t=(0,i.applyRuntimeConfig)(d).then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));return void e.waitUntil(t)}if("ZIP_MANIFEST"===t){if(n&&p){const t=(0,s.normalizeZipUrl)(n),r=(0,c.manifestFilesToMap)(p),i=o.zipManifests.get(t);if(!r)return void(0,l.warn)("rejecting empty manifest; keeping existing",{zipUrl:t,existingSize:i?i.size:0});if(i&&i.size>r.size)return void(0,l.warn)("rejecting smaller manifest; keeping existing",{zipUrl:t,incomingSize:r.size,existingSize:i.size});o.zipManifests.set(t,r),e.waitUntil((0,c.sendManifestToClientId)(t,r,m));const a=Array.from(r.keys()).slice(0,40);(0,l.log)("ZIP_MANIFEST loaded",{zipUrl:t,entryCount:r.size,sampleKeys:a,replacedExistingSize:i?i.size:0})}}else if("CLEAR_ZIP_ASSETS_EXCEPT_ALLOWED"===t&&g){const t=(0,a.clearZipAssetsExceptAllowed)(g).then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));e.waitUntil(t)}else if("CLEAR_ALL_ZIP_ASSETS"===t){const t=(0,a.clearAllZipAssets)().then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));e.waitUntil(t)}else if("PRELOAD_ZIP_MANIFESTS"===t&&g){const t=async function(e,t){const n=await Promise.all(e.map(async e=>({zipUrl:e,manifest:await(0,c.ensureManifestAvailable)(e,t)}))),s=n.filter(e=>!e.manifest).map(e=>e.zipUrl);if(s.length>0)throw(0,r.createZipPeekError)(`Failed to preload ZIP manifest(s): ${s.join(", ")}`)}(g,m).then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));e.waitUntil(t)}})};const r=n(180),s=n(134),i=n(451),a=n(919),o=n(167),l=n(777),c=n(483);function f(e){const t=(0,r.toZipPeekError)(e);return{ok:!1,error:t.message,errorStack:t.stack}}function u(e,t){e.ports[0]?.postMessage(t)}},237(e,t,n){t.corsErrorResponse=l,t.getMimeType=c,t.fetchUncompressedZipEntryFromNetwork=async function(e,t,n,s){const f=n.offset,u=n.nextOffset-1;(0,a.log)("fetching zip chunk from",{zipUrl:e,rangeStart:f,rangeEnd:u});const p=await fetch(e,{headers:{Range:`bytes=${f}-${u}`}});if(206!==p.status)return{ok:!1,response:l(`ZIP entry fetch for "${t}" expected HTTP 206 Partial Content from ${e}, got ${p.status}`,p.status>=400?p.status:502)};const d=await p.arrayBuffer(),g=new DataView(d);if(g.getUint32(0,!0)!==i.LOCAL_FILE_HEADER_SIG)return{ok:!1,response:l(`Invalid local file header for "${t}" in ${e} (expected ZIP signature 0x04034b50)`,500)};const m=30+g.getUint16(26,!0)+g.getUint16(28,!0);if(m>d.byteLength||m+n.compressedSize>d.byteLength)return{ok:!1,response:l(`Corrupt ZIP entry "${t}" in ${e}: dataStart=${m}, compressedSize=${n.compressedSize}, bufferLength=${d.byteLength}`,500)};const w=new Uint8Array(d,m,n.compressedSize);let h;if(8===n.compression)try{h=(0,r.inflateSync)(w)}catch(n){return(0,a.warn)("inflateSync failed",{zipUrl:e,manifestKey:t,error:n}),{ok:!1,response:l(`Failed to inflate DEFLATE-compressed entry "${t}" in ${e}`,500)}}else{if(0!==n.compression)return{ok:!1,response:l(`Unsupported compression method ${n.compression} for "${t}" in ${e} (only stored/0 and DEFLATE/8 are supported)`,500)};h=w}const E=c(t),y=new Blob([h],{type:E});return await(0,o.putCachedFullAssetIfAbsent)(s,y),{ok:!0,blob:y}};const r=n(612),s=n(180),i=n(167),a=n(777),o=n(919);function l(e,t){return new Response((0,s.formatZipPeekError)(e),{status:t,headers:{"Access-Control-Allow-Origin":"*"}})}function c(e){const t=e.split(".").pop()?.toLowerCase();return t?i.MIME_TYPES[t]??"application/octet-stream":"application/octet-stream"}},134(e,t){function n(e){try{return decodeURIComponent(e)}catch{return e}}t.normalizeZipUrl=function(e,t={}){const{stripCacheBuster:n=!0,stripHash:r=!0}=t,s=e=>r?e.split("#")[0]:e;if(!n)return s(e);const i=e.indexOf("?");if(-1===i)return s(e);const a=e.indexOf("#",i),o=e.substring(0,i),l=-1===a?e.substring(i+1):e.substring(i+1,a),c=r||-1===a?"":e.substring(a),f=l.split("&").filter(e=>{const t=e.indexOf("=");return"t"!==(-1===t?e:e.substring(0,t))});return 0===f.length?o+c:o+"?"+f.join("&")+c},t.normalizeZipEntryPath=function(e){let t=e.replace(/\\/g,"/");for(;t.startsWith("/");)t=t.slice(1);return t},t.parseZipAssetRequest=function(e){const t=/\.zip([/?])/.exec(e);if(!t)return null;const r=t.index+4,s=t[1],i=e.substring(0,r);if("/"===s){const t=e.substring(r+1),s=t.indexOf("?"),a=-1===s?t:t.substring(0,s);return""===a?null:{zipUrl:i,internalPath:n(a)}}const a=e.substring(r),o=a.indexOf("/");if(-1===o)return null;const l=a.substring(0,o),c=a.substring(o+1),f=c.indexOf("?"),u=-1===f?c:c.substring(0,f);return""===u?null:{zipUrl:i+l,internalPath:n(u)}},t.canonicalZipAssetRequestUrl=function(e,t){return e+"/"+encodeURIComponent(t)}},612(e,t){t.inflateSync=M;var n=Uint8Array,r=Uint16Array,s=Int32Array,i=new n([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),a=new n([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),o=new n([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),l=function(e,t){for(var n=new r(31),i=0;i<31;++i)n[i]=t+=1<<e[i-1];var a=new s(n[30]);for(i=1;i<30;++i)for(var o=n[i];o<n[i+1];++o)a[o]=o-n[i]<<5|i;return{b:n,r:a}},c=l(i,2),f=c.b,u=c.r;f[28]=258,u[258]=28;for(var p=l(a,0),d=p.b,g=(p.r,new r(32768)),m=0;m<32768;++m){var w=(43690&m)>>1|(21845&m)<<1;w=(61680&(w=(52428&w)>>2|(13107&w)<<2))>>4|(3855&w)<<4,g[m]=((65280&w)>>8|(255&w)<<8)>>1}var h=function(e,t,n){for(var s=e.length,i=0,a=new r(t);i<s;++i)e[i]&&++a[e[i]-1];var o,l=new r(t);for(i=1;i<t;++i)l[i]=l[i-1]+a[i-1]<<1;if(n){o=new r(1<<t);var c=15-t;for(i=0;i<s;++i)if(e[i])for(var f=i<<4|e[i],u=t-e[i],p=l[e[i]-1]++<<u,d=p|(1<<u)-1;p<=d;++p)o[g[p]>>c]=f}else for(o=new r(s),i=0;i<s;++i)e[i]&&(o[i]=g[l[e[i]-1]++]>>15-e[i]);return o},E=new n(288);for(m=0;m<144;++m)E[m]=8;for(m=144;m<256;++m)E[m]=9;for(m=256;m<280;++m)E[m]=7;for(m=280;m<288;++m)E[m]=8;var y=new n(32);for(m=0;m<32;++m)y[m]=5;var A=h(E,9,1),C=h(y,5,1),_=function(e){for(var t=e[0],n=1;n<e.length;++n)e[n]>t&&(t=e[n]);return t},U=function(e,t,n){var r=t/8|0;return(e[r]|e[r+1]<<8)>>(7&t)&n},z=function(e,t){var n=t/8|0;return(e[n]|e[n+1]<<8|e[n+2]<<16)>>(7&t)},S=function(e){return(e+7)/8|0},Z=function(e,t,r){return(null==t||t<0)&&(t=0),(null==r||r>e.length)&&(r=e.length),new n(e.subarray(t,r))},R=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],v=function(e,t,n){var r=new Error(t||R[e]);if(r.code=e,Error.captureStackTrace&&Error.captureStackTrace(r,v),!n)throw r;return r},P=function(e,t,r,s){var l=e.length,c=s?s.length:0;if(!l||t.f&&!t.l)return r||new n(0);var u=!r,p=u||2!=t.i,g=t.i;u&&(r=new n(3*l));var m=function(e){var t=r.length;if(e>t){var s=new n(Math.max(2*t,e));s.set(r),r=s}},w=t.f||0,E=t.p||0,y=t.b||0,R=t.l,P=t.d,I=t.m,M=t.n,b=8*l;do{if(!R){w=U(e,E,1);var k=U(e,E+1,3);if(E+=3,!k){var T=e[(W=S(E)+4)-4]|e[W-3]<<8,L=W+T;if(L>l){g&&v(0);break}p&&m(y+T),r.set(e.subarray(W,L),y),t.b=y+=T,t.p=E=8*L,t.f=w;continue}if(1==k)R=A,P=C,I=9,M=5;else if(2==k){var F=U(e,E,31)+257,x=U(e,E+10,15)+4,N=F+U(e,E+5,31)+1;E+=14;for(var O=new n(N),H=new n(19),$=0;$<x;++$)H[o[$]]=U(e,E+3*$,7);E+=3*x;var D=_(H),q=(1<<D)-1,K=h(H,D,1);for($=0;$<N;){var W,j=K[U(e,E,q)];if(E+=15&j,(W=j>>4)<16)O[$++]=W;else{var G=0,Y=0;for(16==W?(Y=3+U(e,E,3),E+=2,G=O[$-1]):17==W?(Y=3+U(e,E,7),E+=3):18==W&&(Y=11+U(e,E,127),E+=7);Y--;)O[$++]=G}}var X=O.subarray(0,F),B=O.subarray(F);I=_(X),M=_(B),R=h(X,I,1),P=h(B,M,1)}else v(1);if(E>b){g&&v(0);break}}p&&m(y+131072);for(var V=(1<<I)-1,J=(1<<M)-1,Q=E;;Q=E){var ee=(G=R[z(e,E)&V])>>4;if((E+=15&G)>b){g&&v(0);break}if(G||v(2),ee<256)r[y++]=ee;else{if(256==ee){Q=E,R=null;break}var te=ee-254;if(ee>264){var ne=i[$=ee-257];te=U(e,E,(1<<ne)-1)+f[$],E+=ne}var re=P[z(e,E)&J],se=re>>4;if(re||v(3),E+=15&re,B=d[se],se>3&&(ne=a[se],B+=z(e,E)&(1<<ne)-1,E+=ne),E>b){g&&v(0);break}p&&m(y+131072);var ie=y+te;if(y<B){var ae=c-B,oe=Math.min(B,ie);for(ae+y<0&&v(3);y<oe;++y)r[y]=s[ae+y]}for(;y<ie;++y)r[y]=r[y-B]}}t.l=R,t.p=Q,t.b=y,t.f=w,R&&(w=1,t.m=I,t.d=P,t.n=M)}while(!w);return y!=r.length&&u?Z(r,0,y):r.subarray(0,y)},I=new n(0);function M(e,t){return P(e,{i:2},t&&t.out,t&&t.dictionary)}var b="undefined"!=typeof TextDecoder&&new TextDecoder;try{b.decode(I,{stream:!0})}catch(e){}"function"==typeof queueMicrotask?queueMicrotask:"function"==typeof setTimeout&&setTimeout}},t={};function n(r){var s=t[r];if(void 0!==s)return s.exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}(()=>{const e=n(777),t=n(705),r=n(814);self.addEventListener("install",()=>{self.skipWaiting()}),self.addEventListener("activate",e=>{e.waitUntil(self.clients.claim())}),(0,e.installClientErrorReporting)(),(0,r.installMessageHandler)(),(0,t.installFetchHandler)()})()})();
|
|
1
|
+
(()=>{"use strict";var e={180(e,t){function n(e){const n=e.trim();return n.startsWith(t.ZIP_PEEK_ERROR_PREFIX)?n:`${t.ZIP_PEEK_ERROR_PREFIX} ${n}`}t.ZIP_PEEK_ERROR_PREFIX=void 0,t.formatZipPeekError=n,t.createZipPeekError=function(e){return new Error(n(e))},t.toZipPeekError=function(e){const r=e instanceof Error?e:new Error(String(e));return r.message.startsWith(t.ZIP_PEEK_ERROR_PREFIX)||(r.message=n(r.message)),r},t.errorMessage=function(e){return e instanceof Error?e.message:String(e)},t.ZIP_PEEK_ERROR_PREFIX="[zip-peek-error]"},451(e,t,n){t.ensureAllowedZipUrlsLoaded=async function(){if(i.allowedZipUrlsLoaded)return i.allowedZipUrlsLoaded;const e=(async()=>{try{const e=await caches.open(i.CONFIG_CACHE_NAME),t=await e.match(i.CONFIG_CACHE_KEY);if(!t)return;const n=await t.json();Array.isArray(n.allowedZipUrls)?(0,i.setAllowedZipUrls)(new Set(n.allowedZipUrls.filter(e=>"string"==typeof e).map(e=>(0,r.normalizeZipUrl)(e)))):(0,i.setAllowedZipUrls)(null)}catch(e){(0,s.warn)("failed to load runtime config",e)}})();return(0,i.setAllowedZipUrlsLoaded)(e),e},t.applyRuntimeConfig=async function(e){if(!e)return;let t={...i.runtimeConfig},n=!1;if("string"==typeof e.zipAssetCacheName&&e.zipAssetCacheName.trim()&&(t.zipAssetCacheName=e.zipAssetCacheName,n=!0),"number"==typeof e.assetCacheTtlMs&&Number.isFinite(e.assetCacheTtlMs)&&(t.assetCacheTtlMs=e.assetCacheTtlMs,n=!0),"string"==typeof e.logPrefix&&e.logPrefix.trim()&&(t.logPrefix=e.logPrefix,n=!0),"boolean"==typeof e.requireExactManifestPath&&(t.requireExactManifestPath=e.requireExactManifestPath,n=!0),n&&(0,i.setRuntimeConfig)(t),"allowedZipUrls"in e){const t=Array.isArray(e.allowedZipUrls)?e.allowedZipUrls.map(e=>(0,r.normalizeZipUrl)(e)):null;(0,i.setAllowedZipUrls)(t?new Set(t):null),(0,i.setAllowedZipUrlsLoaded)(Promise.resolve()),await async function(e){try{const t=await caches.open(i.CONFIG_CACHE_NAME);await t.put(new Request(i.CONFIG_CACHE_KEY),new Response(JSON.stringify({allowedZipUrls:e}),{headers:{"Content-Type":"application/json"}}))}catch(e){(0,s.warn)("failed to persist runtime config",e)}}(t)}},t.isZipUrlAllowed=function(e){return null===i.allowedZipUrls||i.allowedZipUrls.has(e)};const r=n(134),i=n(167),s=n(777)},919(e,t,n){t.canonicalAssetRequest=function(e,t){return new Request((0,i.canonicalZipAssetRequestUrl)(e,t),{method:"GET"})},t.clearAllZipAssets=async function(){const e=await caches.open(s.runtimeConfig.zipAssetCacheName),t=await e.keys();await Promise.all(t.map(t=>e.delete(t))),await caches.delete(s.CONFIG_CACHE_NAME),s.zipManifests.clear(),s.pendingManifestLoads.clear(),(0,s.setAllowedZipUrlsLoaded)(Promise.resolve())},t.clearZipAssetsExceptAllowed=async function(e){try{const t=new Set(e),n=await caches.open(s.runtimeConfig.zipAssetCacheName),r=await n.keys();for(const e of r){const r=e.url;if(r.includes(s.MANIFEST_CACHE_KEY)){const i=(0,o.zipUrlFromManifestCacheKey)(r);i&&!t.has(i)&&await n.delete(e);continue}const a=(0,i.parseZipAssetRequest)(r);a&&!t.has(a.zipUrl)&&await n.delete(e)}for(const e of s.zipManifests.keys())t.has(e)||s.zipManifests.delete(e)}catch(e){throw(0,r.createZipPeekError)(`Failed to clear zip assets except allowed URLs: ${(0,r.errorMessage)(e)}`)}},t.putCachedFullAssetIfAbsent=async function(e,t){try{const n=await caches.open(s.runtimeConfig.zipAssetCacheName),r=await n.match(e);if(r&&!l(r))return;const i=t.type||"application/octet-stream";await n.put(e,new Response(t,{status:200,headers:{"Content-Type":i,"Content-Length":String(t.size),"Access-Control-Allow-Origin":"*",[s.ASSET_CACHED_AT_HEADER]:String(Date.now())}}))}catch(e){(0,a.warn)("asset cache put failed",e)}},t.readCachedZipAssetIfFresh=async function(e){let t=null,n="network";try{const r=await caches.open(s.runtimeConfig.zipAssetCacheName),i=await r.match(e);if(i)if(l(i))try{await r.delete(e)}catch(e){(0,a.warn)("asset cache delete expired failed",e)}else t=await i.blob(),n="cache-api"}catch(e){(0,a.warn)("asset cache read failed",e)}return{blob:t,assetSource:n}};const r=n(180),i=n(134),s=n(167),a=n(777),o=n(483);function l(e){const t=e.headers.get(s.ASSET_CACHED_AT_HEADER);if(!t)return!0;const n=parseInt(t,10);return!Number.isFinite(n)||Date.now()-n>s.runtimeConfig.assetCacheTtlMs}},705(e,t,n){t.installFetchHandler=function(){self.addEventListener("fetch",e=>{const t=e.request.method;if("GET"!==t&&"HEAD"!==t)return;const n="HEAD"===t,d=(0,i.parseZipAssetRequest)(e.request.url);if(!d)return;const m=(0,i.normalizeZipUrl)(d.zipUrl),g=d.internalPath,h=(0,i.normalizeZipEntryPath)(g);e.respondWith((async()=>{try{if(await(0,s.ensureAllowedZipUrlsLoaded)(),!(0,s.isZipUrlAllowed)(m))return(0,l.warn)("zip URL not allowed",{requestedZipUrl:m}),(0,f.corsErrorResponse)(`ZIP URL not allowed (not in allowedZipUrls): ${m}`,403);let t=o.zipManifests.get(m);if(t||(t=await(0,c.ensureManifestAvailable)(m,e.clientId||void 0)??void 0),!t){const e=Array.from(o.zipManifests.keys());return(0,l.warn)("manifest missing for zipUrl",{requestedZipUrl:m,knownZipUrls:e}),(0,f.corsErrorResponse)(`ZIP manifest not loaded for ${m}. Ensure the ZIP is reachable and supports byte-range requests.`,404)}const i=(0,c.resolveManifestEntry)(t,h,m,o.runtimeConfig.requireExactManifestPath);if(!i){const e=Array.from(t.keys()).slice(0,50);return(0,l.warn)("file not in manifest",{zipUrl:m,internalPathRaw:g,internalPathNormalized:h,requireExactManifestPath:o.runtimeConfig.requireExactManifestPath,manifestSize:t.size,sampleKeys:e}),(0,f.corsErrorResponse)(`File "${g}" not found in ZIP manifest for ${m}`,404)}const{key:d,info:E,matchKind:w}=i;"zipBasenameFallback"===w&&((0,l.log)("resolved via zip-basename folder fallback",{requested:h,manifestKey:d}),(0,l.reportClientError)(new Error((0,r.formatZipPeekError)("Exact manifest path not found; resolved via zip-basename folder fallback")),`Exact manifest key "${h}" not found in ${m}; resolved using "${d}"`));const y=(0,a.canonicalAssetRequest)(m,d);if(n)return function(e,t,n){const r=(0,f.getMimeType)(t),i=(0,c.entryUncompressedSize)(n),s=e.headers.get("range");if(null!==i&&s)return p(null,r,i,"manifest",s);const a={"Content-Type":r,...u("manifest")};return null!==i&&(a["Content-Length"]=i.toString()),new Response(null,{status:200,headers:a})}(e.request,d,E);let{blob:A,assetSource:C}=await(0,a.readCachedZipAssetIfFresh)(y);if(!A){const e=await(0,f.fetchUncompressedZipEntryFromNetwork)(m,d,E,y);if(!e.ok)return e.response;A=e.blob,C="network"}return p(A,A.type,A.size,C,e.request.headers.get("range"))}catch(e){return(0,l.error)("fetch handler failed",{zipUrl:m,internalPath:g,message:(0,r.errorMessage)(e)}),(0,f.corsErrorResponse)(`Failed to serve "${g}" from ${m}: ${(0,r.errorMessage)(e)}`,500)}})())})};const r=n(180),i=n(134),s=n(451),a=n(919),o=n(167),l=n(777),c=n(483),f=n(237);function u(e){return{"Access-Control-Allow-Origin":"*","Access-Control-Expose-Headers":o.ASSET_SOURCE_HEADER,[o.ASSET_SOURCE_HEADER]:e}}function p(e,t,n,r,i){if(i){const s=i.replace(/^bytes=/,"");if(s.includes(","))return new Response(null,{status:416,headers:{"Content-Range":`bytes */${n}`,...u(r)}});const[a,o]=s.split("-"),l=""!==a?parseInt(a,10):NaN,c=""!==o?parseInt(o,10):n-1;if(isNaN(l)||isNaN(c)||l<0||c<l||l>=n)return new Response(null,{status:416,headers:{"Content-Range":`bytes */${n}`,...u(r)}});const f=Math.min(c,n-1),p=f-l+1,d=null===e?null:e.slice(l,f+1);return new Response(d,{status:206,headers:{"Content-Type":t,"Content-Range":`bytes ${l}-${f}/${n}`,"Content-Length":p.toString(),"Accept-Ranges":"bytes",...u(r)}})}return new Response(e,{status:200,headers:{"Content-Type":t,"Content-Length":n.toString(),...u(r)}})}},167(e,t){t.CONFIG_CACHE_KEY=t.allowedZipUrlsLoaded=t.allowedZipUrls=t.runtimeConfig=t.pendingManifestLoads=t.zipManifests=t.MANIFEST_CACHE_KEY=t.LOCAL_FILE_HEADER_SIG=t.MIME_TYPES=t.ASSET_SOURCE_HEADER=t.ASSET_CACHED_AT_HEADER=t.CONFIG_CACHE_NAME=t.kF=t.o2=void 0,t.setRuntimeConfig=function(e){t.runtimeConfig=e},t.setAllowedZipUrls=function(e){t.allowedZipUrls=e},t.setAllowedZipUrlsLoaded=function(e){t.allowedZipUrlsLoaded=e},t.o2="zip-cache-v1",t.kF=72e5,t.CONFIG_CACHE_NAME="zip-peek-config-v1",t.o2,t.CONFIG_CACHE_NAME,t.ASSET_CACHED_AT_HEADER="X-ZipSW-Cached-At",t.ASSET_SOURCE_HEADER="X-ZipSW-Asset-Source",t.MIME_TYPES={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",json:"application/json",js:"application/javascript",css:"text/css; charset=utf-8",html:"text/html; charset=utf-8",txt:"text/plain; charset=utf-8",mp4:"video/mp4",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",oga:"audio/ogg",ogg:"audio/ogg",xml:"application/xml",wasm:"application/wasm"},t.LOCAL_FILE_HEADER_SIG=67324752,t.MANIFEST_CACHE_KEY="__zipsw_manifest__=1",t.zipManifests=new Map,t.pendingManifestLoads=new Map,t.runtimeConfig={zipAssetCacheName:t.o2,assetCacheTtlMs:t.kF,logPrefix:"[zipSW]",requireExactManifestPath:!1},t.allowedZipUrls=null,t.allowedZipUrlsLoaded=null,t.CONFIG_CACHE_KEY=new URL("__zip_peek_runtime_config__",self.registration.scope).href},777(e,t,n){t.log=function(...e){console.log(i.runtimeConfig.logPrefix,...e)},t.warn=function(...e){console.warn(i.runtimeConfig.logPrefix,...e)},t.error=function(...e){console.error(i.runtimeConfig.logPrefix,r.ZIP_PEEK_ERROR_PREFIX,...e)},t.reportClientError=s,t.installClientErrorReporting=function(){self.addEventListener("error",e=>{s(e.error??e.message,"Service worker uncaught error")}),self.addEventListener("unhandledrejection",e=>{s(e.reason,"Service worker unhandled promise rejection")})};const r=n(180),i=n(167);function s(e,t){const n=(0,r.toZipPeekError)(e);self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(e=>{const i={type:"ZIP_SW_ERROR",error:{message:n.message,stack:n.stack},info:t?(0,r.formatZipPeekError)(t):void 0};for(const t of e)t.postMessage(i)})}},483(e,t,n){t.manifestFilesToMap=a,t.sendManifestToClientId=l,t.ensureManifestAvailable=function(e,t){const n=i.zipManifests.get(e);if(n)return Promise.resolve(n);let c=i.pendingManifestLoads.get(e);return c||(c=(async()=>{const n=await async function(e,t){const n=await o(t);return n?function(e,t){return new Promise(n=>{const r=new MessageChannel,i=setTimeout(()=>{r.port1.close(),n(null)},500);r.port1.onmessage=e=>{clearTimeout(i),r.port1.close();const t=e.data;t?.ok&&Array.isArray(t.files)?n(a(t.files)):n(null)},r.port1.onmessageerror=()=>{clearTimeout(i),r.port1.close(),n(null)};try{e.postMessage({type:"ZIP_SW_MANIFEST_REQUEST",zipUrl:t},[r.port2])}catch(e){clearTimeout(i),r.port1.close(),(0,s.warn)("failed to request manifest from client",e),n(null)}})}(n,e):null}(e,t);if(n)return i.zipManifests.set(e,n),(0,s.log)("restored manifest from client memory",{zipUrl:e,entryCount:n.size}),n;const c=await async function(e){const t=await fetch(e,{headers:{Range:"bytes=-65536"}});if(206!==t.status)return(0,s.error)("range requests not supported for ZIP manifest",{zipUrl:e,status:t.status}),null;const n=await t.arrayBuffer(),i=new DataView(n);let a=-1;for(let e=n.byteLength-22;e>=0;e--)if(101010256===i.getUint32(e,!0)){a=e;break}if(-1===a)return(0,s.error)("EOCD not found in last 64KB of ZIP file",e),null;const o=i.getUint32(a+16,!0),l=i.getUint32(a+12,!0);let c=n,f=-1;const u=t.headers.get("Content-Range");let p=0;if(u){const e=u.match(/\/(\d+)$/);e&&(p=parseInt(e[1],10))}if(p<=0)return(0,s.error)("could not determine ZIP file size from Content-Range header",{zipUrl:e,contentRange:u}),null;const d=p-n.byteLength;if(o>=d)f=o-d;else{const t=await fetch(e,{headers:{Range:`bytes=${o}-${o+l-1}`}});if(206!==t.status)return(0,s.error)("failed to fetch ZIP central directory",{zipUrl:e,status:t.status,cdOffset:o,cdSize:l}),null;c=await t.arrayBuffer(),f=0}return function(e,t,n,i){const a=new DataView(e);let o=t;const l=[];for(;o+46<=e.byteLength&&33639248===a.getUint32(o,!0);){const t=a.getUint16(o+10,!0),n=a.getUint32(o+20,!0),i=a.getUint32(o+24,!0),s=a.getUint16(o+28,!0),c=a.getUint16(o+30,!0),f=a.getUint16(o+32,!0),u=a.getUint32(o+42,!0),p=new TextDecoder;if(o+46+s>e.byteLength)break;const d=new Uint8Array(e,o+46,s),m=p.decode(d),g=(0,r.normalizeZipEntryPath)(m);""!==g&&l.push({filename:g,offset:u,compressedSize:n,uncompressedSize:i,compression:t}),o+=46+s+c+f}if(0===l.length)return(0,s.error)("no files parsed from central directory",i),null;l.sort((e,t)=>e.offset-t.offset);const c=new Map;for(let e=0;e<l.length;e++){const t=l[e],i=(0,r.normalizeZipEntryPath)(t.filename);""!==i&&(c.has(i)&&(0,s.warn)("duplicate manifest key after normalize; overwriting",i),c.set(i,{filename:t.filename,offset:t.offset,compressedSize:t.compressedSize,uncompressedSize:t.uncompressedSize,compression:t.compression,nextOffset:e<l.length-1?l[e+1].offset:n}))}return c.size>0?c:null}(c,f,o,e)}(e);return c&&(i.zipManifests.set(e,c),await l(e,c,t),(0,s.log)("manifest fetched and stored in memory",{zipUrl:e,entryCount:c.size})),c})().finally(()=>{i.pendingManifestLoads.delete(e)}),i.pendingManifestLoads.set(e,c),c)},t.entryUncompressedSize=function(e){return void 0!==e.uncompressedSize?e.uncompressedSize:0===e.compression?e.compressedSize:null},t.resolveManifestEntry=function(e,t,n,i){if(e.has(t))return{key:t,info:e.get(t),matchKind:"exact"};if(i)return null;const s=(0,r.zipBasenameWithoutExtension)(n);if(!s)return null;const a=`${s}/${t}`;return e.has(a)?{key:a,info:e.get(a),matchKind:"zipBasenameFallback"}:null},t.zipUrlFromManifestCacheKey=function(e){const t=i.MANIFEST_CACHE_KEY;if(!e.endsWith(t))return null;let n=e.slice(0,-t.length);return(n.endsWith("?")||n.endsWith("&"))&&(n=n.slice(0,-1)),n};const r=n(134),i=n(167),s=n(777);function a(e){const t=new Map;for(const n of e){const e=(0,r.normalizeZipEntryPath)(n.filename);""!==e&&(t.has(e)&&(0,s.warn)("duplicate manifest key after normalize; overwriting",e),t.set(e,n))}return t.size>0?t:null}async function o(e){if(!e)return null;const t=await self.clients.get(e);return"window"===t?.type?t:null}async function l(e,t,n){const r=await o(n);if(!r)return;const i=Array.from(t.values());try{r.postMessage({type:"ZIP_SW_MANIFEST_CREATED",zipUrl:e,files:i})}catch(e){(0,s.warn)("failed to send manifest to client",e)}}},814(e,t,n){t.installMessageHandler=function(){self.addEventListener("message",e=>{if(!e.data)return;const{type:t,zipUrl:n,files:p,config:d,allowedZipUrls:m}=e.data,g=function(e){if(!e||!("id"in e))return;const t=e.id;return"string"==typeof t?t:void 0}(e.source);if("ZIP_SW_CONFIG"===t){const t=(0,s.applyRuntimeConfig)(d).then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));return void e.waitUntil(t)}if("ZIP_MANIFEST"===t){if(n&&p){const t=(0,i.normalizeZipUrl)(n),r=(0,c.manifestFilesToMap)(p),s=o.zipManifests.get(t);if(!r)return void(0,l.warn)("rejecting empty manifest; keeping existing",{zipUrl:t,existingSize:s?s.size:0});if(s&&s.size>r.size)return void(0,l.warn)("rejecting smaller manifest; keeping existing",{zipUrl:t,incomingSize:r.size,existingSize:s.size});o.zipManifests.set(t,r),e.waitUntil((0,c.sendManifestToClientId)(t,r,g));const a=Array.from(r.keys()).slice(0,40);(0,l.log)("ZIP_MANIFEST loaded",{zipUrl:t,entryCount:r.size,sampleKeys:a,replacedExistingSize:s?s.size:0})}}else if("CLEAR_ZIP_ASSETS_EXCEPT_ALLOWED"===t&&m){const t=(0,a.clearZipAssetsExceptAllowed)(m).then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));e.waitUntil(t)}else if("CLEAR_ALL_ZIP_ASSETS"===t){const t=(0,a.clearAllZipAssets)().then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));e.waitUntil(t)}else if("PRELOAD_ZIP_MANIFESTS"===t&&m){const t=async function(e,t){const n=await Promise.all(e.map(async e=>({zipUrl:e,manifest:await(0,c.ensureManifestAvailable)(e,t)}))),i=n.filter(e=>!e.manifest).map(e=>e.zipUrl);if(i.length>0)throw(0,r.createZipPeekError)(`Failed to preload ZIP manifest(s): ${i.join(", ")}`)}(m,g).then(()=>u(e,{ok:!0})).catch(t=>u(e,f(t)));e.waitUntil(t)}})};const r=n(180),i=n(134),s=n(451),a=n(919),o=n(167),l=n(777),c=n(483);function f(e){const t=(0,r.toZipPeekError)(e);return{ok:!1,error:t.message,errorStack:t.stack}}function u(e,t){e.ports[0]?.postMessage(t)}},237(e,t,n){t.corsErrorResponse=l,t.getMimeType=c,t.fetchUncompressedZipEntryFromNetwork=async function(e,t,n,i){const f=n.offset,u=n.nextOffset-1;(0,a.log)("fetching zip chunk from",{zipUrl:e,rangeStart:f,rangeEnd:u});const p=await fetch(e,{headers:{Range:`bytes=${f}-${u}`}});if(206!==p.status)return{ok:!1,response:l(`ZIP entry fetch for "${t}" expected HTTP 206 Partial Content from ${e}, got ${p.status}`,p.status>=400?p.status:502)};const d=await p.arrayBuffer(),m=new DataView(d);if(m.getUint32(0,!0)!==s.LOCAL_FILE_HEADER_SIG)return{ok:!1,response:l(`Invalid local file header for "${t}" in ${e} (expected ZIP signature 0x04034b50)`,500)};const g=30+m.getUint16(26,!0)+m.getUint16(28,!0);if(g>d.byteLength||g+n.compressedSize>d.byteLength)return{ok:!1,response:l(`Corrupt ZIP entry "${t}" in ${e}: dataStart=${g}, compressedSize=${n.compressedSize}, bufferLength=${d.byteLength}`,500)};const h=new Uint8Array(d,g,n.compressedSize);let E;if(8===n.compression)try{E=(0,r.inflateSync)(h)}catch(n){return(0,a.warn)("inflateSync failed",{zipUrl:e,manifestKey:t,error:n}),{ok:!1,response:l(`Failed to inflate DEFLATE-compressed entry "${t}" in ${e}`,500)}}else{if(0!==n.compression)return{ok:!1,response:l(`Unsupported compression method ${n.compression} for "${t}" in ${e} (only stored/0 and DEFLATE/8 are supported)`,500)};E=h}const w=c(t),y=new Blob([E],{type:w});return await(0,o.putCachedFullAssetIfAbsent)(i,y),{ok:!0,blob:y}};const r=n(612),i=n(180),s=n(167),a=n(777),o=n(919);function l(e,t){return new Response((0,i.formatZipPeekError)(e),{status:t,headers:{"Access-Control-Allow-Origin":"*"}})}function c(e){const t=e.split(".").pop()?.toLowerCase();return t?s.MIME_TYPES[t]??"application/octet-stream":"application/octet-stream"}},134(e,t){function n(e){try{return decodeURIComponent(e)}catch{return e}}t.normalizeZipUrl=function(e,t={}){const{stripCacheBuster:n=!0,stripHash:r=!0}=t,i=e=>r?e.split("#")[0]:e;if(!n)return i(e);const s=e.indexOf("?");if(-1===s)return i(e);const a=e.indexOf("#",s),o=e.substring(0,s),l=-1===a?e.substring(s+1):e.substring(s+1,a),c=r||-1===a?"":e.substring(a),f=l.split("&").filter(e=>{const t=e.indexOf("=");return"t"!==(-1===t?e:e.substring(0,t))});return 0===f.length?o+c:o+"?"+f.join("&")+c},t.zipBasenameWithoutExtension=function(e){try{const t=new URL(e,"http://local").pathname.split("/").pop()??"";return t.endsWith(".zip")?t.slice(0,-4):t}catch{const t=e.split("?")[0].split("#")[0].split("/").pop()??"";return t.endsWith(".zip")?t.slice(0,-4):t}},t.normalizeZipEntryPath=function(e){let t=e.replace(/\\/g,"/");for(;t.startsWith("/");)t=t.slice(1);return t},t.parseZipAssetRequest=function(e){const t=/\.zip([/?])/.exec(e);if(!t)return null;const r=t.index+4,i=t[1],s=e.substring(0,r);if("/"===i){const t=e.substring(r+1),i=t.indexOf("?"),a=-1===i?t:t.substring(0,i);return""===a?null:{zipUrl:s,internalPath:n(a)}}const a=e.substring(r),o=a.indexOf("/");if(-1===o)return null;const l=a.substring(0,o),c=a.substring(o+1),f=c.indexOf("?"),u=-1===f?c:c.substring(0,f);return""===u?null:{zipUrl:s+l,internalPath:n(u)}},t.canonicalZipAssetRequestUrl=function(e,t){return e+"/"+encodeURIComponent(t)}},612(e,t){t.inflateSync=b;var n=Uint8Array,r=Uint16Array,i=Int32Array,s=new n([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),a=new n([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),o=new n([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),l=function(e,t){for(var n=new r(31),s=0;s<31;++s)n[s]=t+=1<<e[s-1];var a=new i(n[30]);for(s=1;s<30;++s)for(var o=n[s];o<n[s+1];++o)a[o]=o-n[s]<<5|s;return{b:n,r:a}},c=l(s,2),f=c.b,u=c.r;f[28]=258,u[258]=28;for(var p=l(a,0),d=p.b,m=(p.r,new r(32768)),g=0;g<32768;++g){var h=(43690&g)>>1|(21845&g)<<1;h=(61680&(h=(52428&h)>>2|(13107&h)<<2))>>4|(3855&h)<<4,m[g]=((65280&h)>>8|(255&h)<<8)>>1}var E=function(e,t,n){for(var i=e.length,s=0,a=new r(t);s<i;++s)e[s]&&++a[e[s]-1];var o,l=new r(t);for(s=1;s<t;++s)l[s]=l[s-1]+a[s-1]<<1;if(n){o=new r(1<<t);var c=15-t;for(s=0;s<i;++s)if(e[s])for(var f=s<<4|e[s],u=t-e[s],p=l[e[s]-1]++<<u,d=p|(1<<u)-1;p<=d;++p)o[m[p]>>c]=f}else for(o=new r(i),s=0;s<i;++s)e[s]&&(o[s]=m[l[e[s]-1]++]>>15-e[s]);return o},w=new n(288);for(g=0;g<144;++g)w[g]=8;for(g=144;g<256;++g)w[g]=9;for(g=256;g<280;++g)w[g]=7;for(g=280;g<288;++g)w[g]=8;var y=new n(32);for(g=0;g<32;++g)y[g]=5;var A=E(w,9,1),C=E(y,5,1),z=function(e){for(var t=e[0],n=1;n<e.length;++n)e[n]>t&&(t=e[n]);return t},_=function(e,t,n){var r=t/8|0;return(e[r]|e[r+1]<<8)>>(7&t)&n},U=function(e,t){var n=t/8|0;return(e[n]|e[n+1]<<8|e[n+2]<<16)>>(7&t)},S=function(e){return(e+7)/8|0},Z=function(e,t,r){return(null==t||t<0)&&(t=0),(null==r||r>e.length)&&(r=e.length),new n(e.subarray(t,r))},v=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],R=function(e,t,n){var r=new Error(t||v[e]);if(r.code=e,Error.captureStackTrace&&Error.captureStackTrace(r,R),!n)throw r;return r},P=function(e,t,r,i){var l=e.length,c=i?i.length:0;if(!l||t.f&&!t.l)return r||new n(0);var u=!r,p=u||2!=t.i,m=t.i;u&&(r=new n(3*l));var g=function(e){var t=r.length;if(e>t){var i=new n(Math.max(2*t,e));i.set(r),r=i}},h=t.f||0,w=t.p||0,y=t.b||0,v=t.l,P=t.d,M=t.m,b=t.n,I=8*l;do{if(!v){h=_(e,w,1);var k=_(e,w+1,3);if(w+=3,!k){var T=e[(W=S(w)+4)-4]|e[W-3]<<8,x=W+T;if(x>l){m&&R(0);break}p&&g(y+T),r.set(e.subarray(W,x),y),t.b=y+=T,t.p=w=8*x,t.f=h;continue}if(1==k)v=A,P=C,M=9,b=5;else if(2==k){var L=_(e,w,31)+257,F=_(e,w+10,15)+4,N=L+_(e,w+5,31)+1;w+=14;for(var O=new n(N),$=new n(19),H=0;H<F;++H)$[o[H]]=_(e,w+3*H,7);w+=3*F;var D=z($),q=(1<<D)-1,K=E($,D,1);for(H=0;H<N;){var W,j=K[_(e,w,q)];if(w+=15&j,(W=j>>4)<16)O[H++]=W;else{var G=0,Y=0;for(16==W?(Y=3+_(e,w,3),w+=2,G=O[H-1]):17==W?(Y=3+_(e,w,7),w+=3):18==W&&(Y=11+_(e,w,127),w+=7);Y--;)O[H++]=G}}var B=O.subarray(0,L),X=O.subarray(L);M=z(B),b=z(X),v=E(B,M,1),P=E(X,b,1)}else R(1);if(w>I){m&&R(0);break}}p&&g(y+131072);for(var V=(1<<M)-1,J=(1<<b)-1,Q=w;;Q=w){var ee=(G=v[U(e,w)&V])>>4;if((w+=15&G)>I){m&&R(0);break}if(G||R(2),ee<256)r[y++]=ee;else{if(256==ee){Q=w,v=null;break}var te=ee-254;if(ee>264){var ne=s[H=ee-257];te=_(e,w,(1<<ne)-1)+f[H],w+=ne}var re=P[U(e,w)&J],ie=re>>4;if(re||R(3),w+=15&re,X=d[ie],ie>3&&(ne=a[ie],X+=U(e,w)&(1<<ne)-1,w+=ne),w>I){m&&R(0);break}p&&g(y+131072);var se=y+te;if(y<X){var ae=c-X,oe=Math.min(X,se);for(ae+y<0&&R(3);y<oe;++y)r[y]=i[ae+y]}for(;y<se;++y)r[y]=r[y-X]}}t.l=v,t.p=Q,t.b=y,t.f=h,v&&(h=1,t.m=M,t.d=P,t.n=b)}while(!h);return y!=r.length&&u?Z(r,0,y):r.subarray(0,y)},M=new n(0);function b(e,t){return P(e,{i:2},t&&t.out,t&&t.dictionary)}var I="undefined"!=typeof TextDecoder&&new TextDecoder;try{I.decode(M,{stream:!0})}catch(e){}"function"==typeof queueMicrotask?queueMicrotask:"function"==typeof setTimeout&&setTimeout}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}(()=>{const e=n(777),t=n(705),r=n(814);self.addEventListener("install",()=>{self.skipWaiting()}),self.addEventListener("activate",e=>{e.waitUntil(self.clients.claim())}),(0,e.installClientErrorReporting)(),(0,r.installMessageHandler)(),(0,t.installFetchHandler)()})()})();
|
|
2
2
|
//# sourceMappingURL=zipServiceWorker.js.map
|