@blamejs/blamejs-shop 0.0.72 → 0.0.75
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/CHANGELOG.md +6 -0
- package/lib/announcement-bar.js +753 -0
- package/lib/banner-ab-tests.js +806 -0
- package/lib/bin-locations.js +791 -0
- package/lib/blog-articles.js +1173 -0
- package/lib/carrier-accounts.js +805 -0
- package/lib/cart-recovery.js +1133 -0
- package/lib/category-navigation.js +934 -0
- package/lib/consent-ledger.js +539 -0
- package/lib/customer-impersonation.js +743 -0
- package/lib/customer-merge.js +879 -0
- package/lib/demand-forecast.js +1121 -0
- package/lib/dispute-resolution.js +886 -0
- package/lib/email-ab-tests.js +918 -0
- package/lib/email-engagement-score.js +649 -0
- package/lib/event-log.js +713 -0
- package/lib/fulfillment-sla.js +791 -0
- package/lib/index.js +41 -0
- package/lib/inventory-audits.js +852 -0
- package/lib/line-gift-wrap.js +430 -0
- package/lib/marketing-budget.js +792 -0
- package/lib/operator-activity-feed.js +977 -0
- package/lib/operator-approvals.js +942 -0
- package/lib/operator-help-center.js +1020 -0
- package/lib/operator-inbox.js +889 -0
- package/lib/operator-sessions.js +701 -0
- package/lib/order-exchanges.js +602 -0
- package/lib/product-compare.js +804 -0
- package/lib/pwa-manifest.js +1005 -0
- package/lib/referral-leaderboard.js +612 -0
- package/lib/sales-tax-filings.js +807 -0
- package/lib/search-ranking.js +859 -0
- package/lib/shipping-insurance.js +757 -0
- package/lib/shrinkage-report.js +1182 -0
- package/lib/sidebar-widgets.js +952 -0
- package/lib/smart-restocking.js +1048 -0
- package/lib/stock-receipts.js +834 -0
- package/lib/subscription-analytics.js +1032 -0
- package/lib/suggestion-box.js +921 -0
- package/lib/tax-remittance.js +625 -0
- package/lib/vendor-invoices.js +1021 -0
- package/lib/winback-campaigns.js +1350 -0
- package/lib/wishlist-digest.js +1133 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.pwaManifest
|
|
4
|
+
* @title PWA manifest — operator-configurable webmanifest + service worker
|
|
5
|
+
*
|
|
6
|
+
* @intro
|
|
7
|
+
* Owns the bytes the storefront serves at
|
|
8
|
+
* `/manifest.webmanifest` and `/sw.js` — the two artifacts a
|
|
9
|
+
* browser fetches to treat the site as a Progressive Web App
|
|
10
|
+
* (install prompt, standalone display, offline shell). Operators
|
|
11
|
+
* author both surfaces through this primitive; the worker
|
|
12
|
+
* renders the active versions on demand.
|
|
13
|
+
*
|
|
14
|
+
* Manifest surface:
|
|
15
|
+
* - `defineManifest({ name, short_name, description, start_url,
|
|
16
|
+
* scope, display, orientation, theme_color,
|
|
17
|
+
* background_color, lang?, dir?, icons })`
|
|
18
|
+
* — append a new version row. `display` and `orientation` are
|
|
19
|
+
* closed enums (display: standalone / fullscreen / minimal-ui
|
|
20
|
+
* / browser; orientation: any / natural / portrait /
|
|
21
|
+
* landscape). Colors must be lowercase `#rrggbb` 6-digit hex —
|
|
22
|
+
* any other shape (3-digit hex, named colors, `rgb(...)`) is
|
|
23
|
+
* refused so the byte-level output is predictable across
|
|
24
|
+
* browsers. `start_url` + `scope` are validated through
|
|
25
|
+
* `b.safeUrl.parse` (https-only) when absolute or pass the
|
|
26
|
+
* `/`-rooted absolute-path allow-list (the common case — the
|
|
27
|
+
* manifest references the site's own routes). `icons` is an
|
|
28
|
+
* array of `{ src, sizes, type, purpose? }` descriptors that
|
|
29
|
+
* passes through `validateIcons` before storage.
|
|
30
|
+
*
|
|
31
|
+
* - `getActive()` — the currently-active manifest row (or null
|
|
32
|
+
* when no version has been activated yet).
|
|
33
|
+
*
|
|
34
|
+
* - `setActive(versionNumber)` — flip the active flag to the
|
|
35
|
+
* named version inside a single sweep. The prior active row
|
|
36
|
+
* drops to `is_active=0`. Archived versions can not become
|
|
37
|
+
* active.
|
|
38
|
+
*
|
|
39
|
+
* - `listVersions({ cursor?, limit? })` — paginate the version
|
|
40
|
+
* history newest-first. Cursor is HMAC-tagged via
|
|
41
|
+
* `b.pagination.encodeCursor` so a tampered cursor refuses.
|
|
42
|
+
*
|
|
43
|
+
* - `archiveVersion(versionNumber)` — soft-delete a version.
|
|
44
|
+
* Archiving the active version clears the active flag (no
|
|
45
|
+
* archived version can stay active).
|
|
46
|
+
*
|
|
47
|
+
* - `renderManifestJson()` — emit the JSON bytes for
|
|
48
|
+
* `/manifest.webmanifest`. The output is a deterministic
|
|
49
|
+
* JSON.stringify (sorted keys, two-space indent) so a byte-
|
|
50
|
+
* diff between releases reflects an operator edit rather
|
|
51
|
+
* than serializer noise.
|
|
52
|
+
*
|
|
53
|
+
* - `validateIcons(icons)` — exposed icon validation entry
|
|
54
|
+
* point. Operators previewing icon arrays before
|
|
55
|
+
* defineManifest call this directly.
|
|
56
|
+
*
|
|
57
|
+
* Service-worker surface:
|
|
58
|
+
* - `defineServiceWorkerConfig({ cache_name, precache_urls,
|
|
59
|
+
* runtime_rules, offline_fallback?,
|
|
60
|
+
* navigation_fallback? })` —
|
|
61
|
+
* append a new SW config version. `cache_name` namespaces
|
|
62
|
+
* the caches so two parallel SW versions don't collide
|
|
63
|
+
* during a rollout. `precache_urls` is the URL list cached
|
|
64
|
+
* on `install`. `runtime_rules` is an array of
|
|
65
|
+
* `{ url_pattern, strategy }` entries (strategy: cache-first
|
|
66
|
+
* / network-first / stale-while-revalidate / network-only).
|
|
67
|
+
*
|
|
68
|
+
* - Companion `getActive('sw')` / `setActive('sw', version)` /
|
|
69
|
+
* `listVersions('sw', ...)` / `archiveVersion('sw', version)`
|
|
70
|
+
* overloads route the same lifecycle to the SW table. The
|
|
71
|
+
* default scope when omitted is `'manifest'`.
|
|
72
|
+
*
|
|
73
|
+
* - `renderServiceWorkerJs()` — emit the JS bytes for `/sw.js`.
|
|
74
|
+
* The body is a self-contained `addEventListener('install', …)`
|
|
75
|
+
* / `addEventListener('fetch', …)` script that reads the
|
|
76
|
+
* active SW config row. Operator-sourced URL fragments are
|
|
77
|
+
* JSON.stringify'd into the body so a hostile URL can't
|
|
78
|
+
* break out of the string literal.
|
|
79
|
+
*
|
|
80
|
+
* Composes ONLY blamejs:
|
|
81
|
+
* - `b.framework.safeUrl.parse` — start_url / scope / icon src
|
|
82
|
+
* validation (https-only absolute).
|
|
83
|
+
* - `b.framework.uuid.v7` — row ids.
|
|
84
|
+
* - `b.framework.pagination` — listVersions cursor (HMAC-tagged).
|
|
85
|
+
*
|
|
86
|
+
* Monotonic per-process clock — two writes in the same millisecond
|
|
87
|
+
* would tie on `updated_at` and make a sort-by-timestamp read
|
|
88
|
+
* ambiguous. `_now` bumps to `prior + 1` on collision so the
|
|
89
|
+
* timeline stays strictly increasing.
|
|
90
|
+
*
|
|
91
|
+
* Storage:
|
|
92
|
+
* - `pwa_manifests` — versioned manifest rows.
|
|
93
|
+
* - `pwa_sw_configs` — versioned SW config rows.
|
|
94
|
+
* (migration `0168_pwa_manifest.sql`)
|
|
95
|
+
*
|
|
96
|
+
* @primitive pwaManifest
|
|
97
|
+
* @related b.safeUrl.parse, b.uuid.v7, b.pagination
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
// ---- enums -------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
var ALLOWED_DISPLAYS = Object.freeze([
|
|
103
|
+
"standalone",
|
|
104
|
+
"fullscreen",
|
|
105
|
+
"minimal-ui",
|
|
106
|
+
"browser",
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
var ALLOWED_ORIENTATIONS = Object.freeze([
|
|
110
|
+
"any",
|
|
111
|
+
"natural",
|
|
112
|
+
"portrait",
|
|
113
|
+
"landscape",
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
var ALLOWED_DIRS = Object.freeze(["ltr", "rtl", "auto"]);
|
|
117
|
+
|
|
118
|
+
var ALLOWED_STRATEGIES = Object.freeze([
|
|
119
|
+
"cache-first",
|
|
120
|
+
"network-first",
|
|
121
|
+
"stale-while-revalidate",
|
|
122
|
+
"network-only",
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
var ALLOWED_ICON_PURPOSES = Object.freeze(["any", "maskable", "monochrome"]);
|
|
126
|
+
|
|
127
|
+
var ALLOWED_SCOPES = Object.freeze(["manifest", "sw"]);
|
|
128
|
+
|
|
129
|
+
// ---- bounds ------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
var MAX_NAME_LEN = 200;
|
|
132
|
+
var MAX_SHORT_NAME_LEN = 60;
|
|
133
|
+
var MAX_DESCRIPTION_LEN = 1024;
|
|
134
|
+
var MAX_URL_LEN = 2048;
|
|
135
|
+
var MAX_LANG_LEN = 35;
|
|
136
|
+
var MAX_CACHE_NAME_LEN = 120;
|
|
137
|
+
var MAX_ICON_COUNT = 32;
|
|
138
|
+
var MAX_ICON_SIZES_LEN = 200;
|
|
139
|
+
var MAX_ICON_TYPE_LEN = 80;
|
|
140
|
+
var MAX_PRECACHE_URLS = 256;
|
|
141
|
+
var MAX_RUNTIME_RULES = 64;
|
|
142
|
+
var MAX_URL_PATTERN_LEN = 256;
|
|
143
|
+
var MAX_LIST_LIMIT = 100;
|
|
144
|
+
var DEFAULT_LIST_LIMIT = 25;
|
|
145
|
+
|
|
146
|
+
// ---- regexes -----------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
// Lowercase 6-digit hex only. 3-digit shorthand, named colors,
|
|
149
|
+
// rgb()/hsl()/hwb(), and uppercase are refused so the rendered
|
|
150
|
+
// bytes are predictable across user agents (some Android Chrome
|
|
151
|
+
// flavors render `#fff` differently from `#FFFFFF`).
|
|
152
|
+
var HEX_COLOR_RE = /^#[0-9a-f]{6}$/;
|
|
153
|
+
|
|
154
|
+
// BCP-47 tag — same shape as knowledgeBase / emailTemplates.
|
|
155
|
+
var LANG_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/;
|
|
156
|
+
|
|
157
|
+
// Cache name — alnum + dot + hyphen + underscore. Reaches the
|
|
158
|
+
// rendered SW body inside a JSON.stringify, but a narrow cache
|
|
159
|
+
// name keeps the operator-visible value grep-friendly.
|
|
160
|
+
var CACHE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,118}[a-z0-9]$|^[a-z0-9]$/;
|
|
161
|
+
|
|
162
|
+
// Icon sizes — one or more space-separated `WxH` or `any` tokens.
|
|
163
|
+
// e.g. `192x192`, `512x512`, `192x192 512x512`, `any`.
|
|
164
|
+
var ICON_SIZES_RE = /^(?:any|\d+x\d+)(?:\s+(?:any|\d+x\d+))*$/;
|
|
165
|
+
|
|
166
|
+
// MIME type — narrow shape covering the icon types the manifest
|
|
167
|
+
// spec recommends (image/png / image/svg+xml / image/webp).
|
|
168
|
+
var ICON_TYPE_RE = /^[a-z]+\/[a-z0-9.+-]+$/;
|
|
169
|
+
|
|
170
|
+
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
171
|
+
var ZERO_WIDTH_RE = new RegExp(
|
|
172
|
+
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
var bShop;
|
|
176
|
+
function _b() {
|
|
177
|
+
if (!bShop) bShop = require("./index");
|
|
178
|
+
return bShop.framework;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- monotonic clock ---------------------------------------------------
|
|
182
|
+
//
|
|
183
|
+
// Two writes in the same millisecond would tie on `updated_at`.
|
|
184
|
+
// Bump by 1 on collision so the listVersions sort is strictly
|
|
185
|
+
// increasing.
|
|
186
|
+
|
|
187
|
+
var _lastTs = 0;
|
|
188
|
+
function _now() {
|
|
189
|
+
var t = Date.now();
|
|
190
|
+
if (t <= _lastTs) t = _lastTs + 1;
|
|
191
|
+
_lastTs = t;
|
|
192
|
+
return t;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---- validators --------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function _string(s, label, max) {
|
|
198
|
+
if (typeof s !== "string") {
|
|
199
|
+
throw new TypeError("pwaManifest: " + label + " must be a string");
|
|
200
|
+
}
|
|
201
|
+
var trimmed = s.trim();
|
|
202
|
+
if (!trimmed.length) {
|
|
203
|
+
throw new TypeError("pwaManifest: " + label + " must be non-empty after trim");
|
|
204
|
+
}
|
|
205
|
+
if (s.length > max) {
|
|
206
|
+
throw new TypeError("pwaManifest: " + label + " must be <= " + max + " characters");
|
|
207
|
+
}
|
|
208
|
+
if (CONTROL_BYTE_RE.test(s)) {
|
|
209
|
+
throw new TypeError("pwaManifest: " + label + " contains control bytes");
|
|
210
|
+
}
|
|
211
|
+
if (ZERO_WIDTH_RE.test(s)) {
|
|
212
|
+
throw new TypeError("pwaManifest: " + label + " contains zero-width / direction-override characters");
|
|
213
|
+
}
|
|
214
|
+
return s;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _hexColor(s, label) {
|
|
218
|
+
if (typeof s !== "string") {
|
|
219
|
+
throw new TypeError("pwaManifest: " + label + " must be a string");
|
|
220
|
+
}
|
|
221
|
+
if (!HEX_COLOR_RE.test(s)) {
|
|
222
|
+
throw new TypeError(
|
|
223
|
+
"pwaManifest: " + label + " must be a lowercase 6-digit hex color like '#1a2b3c' " +
|
|
224
|
+
"(3-digit shorthand, uppercase, rgb()/hsl(), and named colors are refused)"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return s;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _enum(s, allowed, label) {
|
|
231
|
+
if (typeof s !== "string" || allowed.indexOf(s) === -1) {
|
|
232
|
+
throw new TypeError("pwaManifest: " + label + " must be one of " + JSON.stringify(allowed));
|
|
233
|
+
}
|
|
234
|
+
return s;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function _lang(s) {
|
|
238
|
+
if (typeof s !== "string" || !s.length) {
|
|
239
|
+
throw new TypeError("pwaManifest: lang must be a non-empty string");
|
|
240
|
+
}
|
|
241
|
+
if (s.length > MAX_LANG_LEN) {
|
|
242
|
+
throw new TypeError("pwaManifest: lang must be <= " + MAX_LANG_LEN + " characters");
|
|
243
|
+
}
|
|
244
|
+
var lower = s.toLowerCase();
|
|
245
|
+
if (!LANG_RE.test(lower)) {
|
|
246
|
+
throw new TypeError("pwaManifest: lang must be a BCP-47 tag (e.g. 'en', 'fr-ca')");
|
|
247
|
+
}
|
|
248
|
+
return lower;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _cacheName(s) {
|
|
252
|
+
if (typeof s !== "string" || !s.length) {
|
|
253
|
+
throw new TypeError("pwaManifest: cache_name must be a non-empty string");
|
|
254
|
+
}
|
|
255
|
+
if (s.length > MAX_CACHE_NAME_LEN) {
|
|
256
|
+
throw new TypeError("pwaManifest: cache_name must be <= " + MAX_CACHE_NAME_LEN + " characters");
|
|
257
|
+
}
|
|
258
|
+
if (!CACHE_NAME_RE.test(s)) {
|
|
259
|
+
throw new TypeError("pwaManifest: cache_name must match /^[a-z0-9][a-z0-9._-]*[a-z0-9]$/");
|
|
260
|
+
}
|
|
261
|
+
return s;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// URL validation. A manifest's start_url / scope / icon src is
|
|
265
|
+
// either an absolute https:// URL (validated through b.safeUrl) OR
|
|
266
|
+
// a `/`-rooted absolute path on the same origin. Protocol-relative
|
|
267
|
+
// `//host` and `..`-bearing paths are refused so a hostile value
|
|
268
|
+
// can't smuggle a navigation off-origin.
|
|
269
|
+
function _url(s, label) {
|
|
270
|
+
if (typeof s !== "string" || !s.length) {
|
|
271
|
+
throw new TypeError("pwaManifest: " + label + " must be a non-empty string");
|
|
272
|
+
}
|
|
273
|
+
if (s.length > MAX_URL_LEN) {
|
|
274
|
+
throw new TypeError("pwaManifest: " + label + " must be <= " + MAX_URL_LEN + " characters");
|
|
275
|
+
}
|
|
276
|
+
if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
|
|
277
|
+
throw new TypeError("pwaManifest: " + label + " contains control / zero-width bytes");
|
|
278
|
+
}
|
|
279
|
+
if (s.charCodeAt(0) === 47 /* "/" */) {
|
|
280
|
+
if (s.length > 1 && s.charCodeAt(1) === 47) {
|
|
281
|
+
throw new TypeError("pwaManifest: " + label + " must not start with '//' (protocol-relative)");
|
|
282
|
+
}
|
|
283
|
+
if (s.indexOf("..") !== -1) {
|
|
284
|
+
throw new TypeError("pwaManifest: " + label + " must not contain '..'");
|
|
285
|
+
}
|
|
286
|
+
return s;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
_b().safeUrl.parse(s, { allowedProtocols: ["https:"] });
|
|
290
|
+
} catch (e) {
|
|
291
|
+
throw new TypeError(
|
|
292
|
+
"pwaManifest: " + label + " — " + (e && e.message || "must be a /-rooted path or https:// URL")
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return s;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function _scopeKind(s) {
|
|
299
|
+
if (s == null) return "manifest";
|
|
300
|
+
if (typeof s !== "string" || ALLOWED_SCOPES.indexOf(s) === -1) {
|
|
301
|
+
throw new TypeError("pwaManifest: scope kind must be one of " + JSON.stringify(ALLOWED_SCOPES));
|
|
302
|
+
}
|
|
303
|
+
return s;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function _versionNumber(n, label) {
|
|
307
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
308
|
+
throw new TypeError("pwaManifest: " + label + " must be a positive integer");
|
|
309
|
+
}
|
|
310
|
+
return n;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function _limit(n) {
|
|
314
|
+
if (n == null) return DEFAULT_LIST_LIMIT;
|
|
315
|
+
if (!Number.isInteger(n) || n <= 0 || n > MAX_LIST_LIMIT) {
|
|
316
|
+
throw new TypeError("pwaManifest: limit must be an integer 1..." + MAX_LIST_LIMIT);
|
|
317
|
+
}
|
|
318
|
+
return n;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- icon validation ---------------------------------------------------
|
|
322
|
+
//
|
|
323
|
+
// Each icon descriptor: { src, sizes, type, purpose? }. The src is
|
|
324
|
+
// gated by `_url`; sizes match the manifest spec's `WxH` shape or
|
|
325
|
+
// `any`; type is a narrow MIME shape; purpose (when present) is an
|
|
326
|
+
// enum. Duplicates are refused so an operator can't ship two
|
|
327
|
+
// conflicting descriptors for the same (sizes, purpose) tuple.
|
|
328
|
+
|
|
329
|
+
function _validateIcons(icons) {
|
|
330
|
+
if (!Array.isArray(icons)) {
|
|
331
|
+
throw new TypeError("pwaManifest: icons must be an array of { src, sizes, type, purpose? } descriptors");
|
|
332
|
+
}
|
|
333
|
+
if (!icons.length) {
|
|
334
|
+
throw new TypeError("pwaManifest: icons must contain at least one descriptor");
|
|
335
|
+
}
|
|
336
|
+
if (icons.length > MAX_ICON_COUNT) {
|
|
337
|
+
throw new TypeError("pwaManifest: icons must contain <= " + MAX_ICON_COUNT + " entries");
|
|
338
|
+
}
|
|
339
|
+
var seen = {};
|
|
340
|
+
var out = [];
|
|
341
|
+
for (var i = 0; i < icons.length; i += 1) {
|
|
342
|
+
var ic = icons[i];
|
|
343
|
+
if (!ic || typeof ic !== "object") {
|
|
344
|
+
throw new TypeError("pwaManifest: icons[" + i + "] must be an object");
|
|
345
|
+
}
|
|
346
|
+
var src = _url(ic.src, "icons[" + i + "].src");
|
|
347
|
+
if (typeof ic.sizes !== "string" || !ic.sizes.length) {
|
|
348
|
+
throw new TypeError("pwaManifest: icons[" + i + "].sizes must be a non-empty string");
|
|
349
|
+
}
|
|
350
|
+
if (ic.sizes.length > MAX_ICON_SIZES_LEN) {
|
|
351
|
+
throw new TypeError("pwaManifest: icons[" + i + "].sizes must be <= " + MAX_ICON_SIZES_LEN + " characters");
|
|
352
|
+
}
|
|
353
|
+
if (!ICON_SIZES_RE.test(ic.sizes)) {
|
|
354
|
+
throw new TypeError(
|
|
355
|
+
"pwaManifest: icons[" + i + "].sizes must be space-separated 'WxH' tokens or 'any' " +
|
|
356
|
+
"(e.g. '192x192 512x512')"
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (typeof ic.type !== "string" || !ic.type.length) {
|
|
360
|
+
throw new TypeError("pwaManifest: icons[" + i + "].type must be a non-empty string");
|
|
361
|
+
}
|
|
362
|
+
if (ic.type.length > MAX_ICON_TYPE_LEN) {
|
|
363
|
+
throw new TypeError("pwaManifest: icons[" + i + "].type must be <= " + MAX_ICON_TYPE_LEN + " characters");
|
|
364
|
+
}
|
|
365
|
+
if (!ICON_TYPE_RE.test(ic.type)) {
|
|
366
|
+
throw new TypeError("pwaManifest: icons[" + i + "].type must be a MIME type (e.g. 'image/png')");
|
|
367
|
+
}
|
|
368
|
+
var purpose;
|
|
369
|
+
if (ic.purpose == null) {
|
|
370
|
+
purpose = "any";
|
|
371
|
+
} else if (typeof ic.purpose !== "string" || ALLOWED_ICON_PURPOSES.indexOf(ic.purpose) === -1) {
|
|
372
|
+
throw new TypeError(
|
|
373
|
+
"pwaManifest: icons[" + i + "].purpose must be one of " + JSON.stringify(ALLOWED_ICON_PURPOSES)
|
|
374
|
+
);
|
|
375
|
+
} else {
|
|
376
|
+
purpose = ic.purpose;
|
|
377
|
+
}
|
|
378
|
+
var key = ic.sizes + "|" + purpose;
|
|
379
|
+
if (seen[key]) {
|
|
380
|
+
throw new TypeError(
|
|
381
|
+
"pwaManifest: icons[" + i + "] duplicates an earlier (sizes='" + ic.sizes +
|
|
382
|
+
"', purpose='" + purpose + "') descriptor"
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
seen[key] = 1;
|
|
386
|
+
out.push({ src: src, sizes: ic.sizes, type: ic.type, purpose: purpose });
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---- runtime-rule validation ------------------------------------------
|
|
392
|
+
//
|
|
393
|
+
// Each runtime rule: { url_pattern, strategy }. `url_pattern` is a
|
|
394
|
+
// narrow string (no control bytes / no zero-width). `strategy` is
|
|
395
|
+
// a closed enum so the rendered SW body has a known finite branch
|
|
396
|
+
// surface. Duplicate url_patterns are refused so the operator's
|
|
397
|
+
// rule table is internally consistent.
|
|
398
|
+
|
|
399
|
+
function _validateRuntimeRules(rules) {
|
|
400
|
+
if (!Array.isArray(rules)) {
|
|
401
|
+
throw new TypeError("pwaManifest: runtime_rules must be an array");
|
|
402
|
+
}
|
|
403
|
+
if (rules.length > MAX_RUNTIME_RULES) {
|
|
404
|
+
throw new TypeError("pwaManifest: runtime_rules must contain <= " + MAX_RUNTIME_RULES + " entries");
|
|
405
|
+
}
|
|
406
|
+
var seen = {};
|
|
407
|
+
var out = [];
|
|
408
|
+
for (var i = 0; i < rules.length; i += 1) {
|
|
409
|
+
var r = rules[i];
|
|
410
|
+
if (!r || typeof r !== "object") {
|
|
411
|
+
throw new TypeError("pwaManifest: runtime_rules[" + i + "] must be an object");
|
|
412
|
+
}
|
|
413
|
+
if (typeof r.url_pattern !== "string" || !r.url_pattern.length) {
|
|
414
|
+
throw new TypeError("pwaManifest: runtime_rules[" + i + "].url_pattern must be a non-empty string");
|
|
415
|
+
}
|
|
416
|
+
if (r.url_pattern.length > MAX_URL_PATTERN_LEN) {
|
|
417
|
+
throw new TypeError(
|
|
418
|
+
"pwaManifest: runtime_rules[" + i + "].url_pattern must be <= " +
|
|
419
|
+
MAX_URL_PATTERN_LEN + " characters"
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (CONTROL_BYTE_RE.test(r.url_pattern) || ZERO_WIDTH_RE.test(r.url_pattern)) {
|
|
423
|
+
throw new TypeError(
|
|
424
|
+
"pwaManifest: runtime_rules[" + i + "].url_pattern contains control / zero-width bytes"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
_enum(r.strategy, ALLOWED_STRATEGIES, "runtime_rules[" + i + "].strategy");
|
|
428
|
+
if (seen[r.url_pattern]) {
|
|
429
|
+
throw new TypeError(
|
|
430
|
+
"pwaManifest: runtime_rules[" + i + "] duplicates url_pattern " + JSON.stringify(r.url_pattern)
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
seen[r.url_pattern] = 1;
|
|
434
|
+
out.push({ url_pattern: r.url_pattern, strategy: r.strategy });
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function _validatePrecacheUrls(urls) {
|
|
440
|
+
if (!Array.isArray(urls)) {
|
|
441
|
+
throw new TypeError("pwaManifest: precache_urls must be an array");
|
|
442
|
+
}
|
|
443
|
+
if (urls.length > MAX_PRECACHE_URLS) {
|
|
444
|
+
throw new TypeError("pwaManifest: precache_urls must contain <= " + MAX_PRECACHE_URLS + " entries");
|
|
445
|
+
}
|
|
446
|
+
var seen = {};
|
|
447
|
+
var out = [];
|
|
448
|
+
for (var i = 0; i < urls.length; i += 1) {
|
|
449
|
+
var u = _url(urls[i], "precache_urls[" + i + "]");
|
|
450
|
+
if (seen[u]) {
|
|
451
|
+
throw new TypeError("pwaManifest: precache_urls[" + i + "] duplicates an earlier entry");
|
|
452
|
+
}
|
|
453
|
+
seen[u] = 1;
|
|
454
|
+
out.push(u);
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- hydration ---------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
function _hydrateManifest(row) {
|
|
462
|
+
if (!row) return null;
|
|
463
|
+
var icons;
|
|
464
|
+
try { icons = JSON.parse(row.icons_json || "[]"); }
|
|
465
|
+
catch (_e) { icons = []; }
|
|
466
|
+
return {
|
|
467
|
+
id: row.id,
|
|
468
|
+
version_number: Number(row.version_number),
|
|
469
|
+
name: row.name,
|
|
470
|
+
short_name: row.short_name,
|
|
471
|
+
description: row.description,
|
|
472
|
+
start_url: row.start_url,
|
|
473
|
+
scope: row.scope,
|
|
474
|
+
display: row.display,
|
|
475
|
+
orientation: row.orientation,
|
|
476
|
+
theme_color: row.theme_color,
|
|
477
|
+
background_color: row.background_color,
|
|
478
|
+
lang: row.lang,
|
|
479
|
+
dir: row.dir,
|
|
480
|
+
icons: icons,
|
|
481
|
+
is_active: row.is_active === 1 || row.is_active === true,
|
|
482
|
+
archived_at: row.archived_at == null ? null : Number(row.archived_at),
|
|
483
|
+
created_at: Number(row.created_at),
|
|
484
|
+
updated_at: Number(row.updated_at),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function _hydrateSwConfig(row) {
|
|
489
|
+
if (!row) return null;
|
|
490
|
+
var precache;
|
|
491
|
+
try { precache = JSON.parse(row.precache_urls_json || "[]"); }
|
|
492
|
+
catch (_e) { precache = []; }
|
|
493
|
+
var runtime;
|
|
494
|
+
try { runtime = JSON.parse(row.runtime_rules_json || "[]"); }
|
|
495
|
+
catch (_e) { runtime = []; }
|
|
496
|
+
return {
|
|
497
|
+
id: row.id,
|
|
498
|
+
version_number: Number(row.version_number),
|
|
499
|
+
cache_name: row.cache_name,
|
|
500
|
+
precache_urls: precache,
|
|
501
|
+
runtime_rules: runtime,
|
|
502
|
+
offline_fallback: row.offline_fallback == null ? null : row.offline_fallback,
|
|
503
|
+
navigation_fallback: row.navigation_fallback == null ? null : row.navigation_fallback,
|
|
504
|
+
is_active: row.is_active === 1 || row.is_active === true,
|
|
505
|
+
archived_at: row.archived_at == null ? null : Number(row.archived_at),
|
|
506
|
+
created_at: Number(row.created_at),
|
|
507
|
+
updated_at: Number(row.updated_at),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---- deterministic JSON serialization ---------------------------------
|
|
512
|
+
//
|
|
513
|
+
// `JSON.stringify(obj, replacer, 2)` orders keys by insertion. A
|
|
514
|
+
// byte-stable manifest output needs a sorted-key serializer so a
|
|
515
|
+
// `git diff` of two releases reflects an operator edit rather than
|
|
516
|
+
// JavaScript's object-property iteration order.
|
|
517
|
+
|
|
518
|
+
function _stableStringify(value, indent) {
|
|
519
|
+
function _walk(v) {
|
|
520
|
+
if (v === null || typeof v !== "object") return v;
|
|
521
|
+
if (Array.isArray(v)) {
|
|
522
|
+
var arr = [];
|
|
523
|
+
for (var i = 0; i < v.length; i += 1) arr.push(_walk(v[i]));
|
|
524
|
+
return arr;
|
|
525
|
+
}
|
|
526
|
+
var keys = Object.keys(v).sort();
|
|
527
|
+
var obj = {};
|
|
528
|
+
for (var k = 0; k < keys.length; k += 1) {
|
|
529
|
+
obj[keys[k]] = _walk(v[keys[k]]);
|
|
530
|
+
}
|
|
531
|
+
return obj;
|
|
532
|
+
}
|
|
533
|
+
return JSON.stringify(_walk(value), null, indent);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ---- factory -----------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
function create(opts) {
|
|
539
|
+
opts = opts || {};
|
|
540
|
+
var query = opts.query;
|
|
541
|
+
if (!query) {
|
|
542
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (typeof opts.cursorSecret !== "string" || !opts.cursorSecret.length) {
|
|
546
|
+
if (process.env.NODE_ENV === "production") {
|
|
547
|
+
throw new Error("pwaManifest.create: opts.cursorSecret is required in production");
|
|
548
|
+
}
|
|
549
|
+
opts.cursorSecret = "pwa-manifest-cursor-secret-dev-only";
|
|
550
|
+
}
|
|
551
|
+
var cursorSecret = opts.cursorSecret;
|
|
552
|
+
|
|
553
|
+
var MANIFEST_ORDER_KEY = ["version_number:desc"];
|
|
554
|
+
var SW_ORDER_KEY = ["version_number:desc"];
|
|
555
|
+
|
|
556
|
+
function _decodeCursor(cursor, label, orderKey) {
|
|
557
|
+
if (cursor == null) return null;
|
|
558
|
+
if (typeof cursor !== "string") {
|
|
559
|
+
throw new TypeError("pwaManifest." + label + ": cursor must be an opaque string or null");
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
var state = _b().pagination.decodeCursor(cursor, cursorSecret);
|
|
563
|
+
if (JSON.stringify(state.orderKey) !== JSON.stringify(orderKey)) {
|
|
564
|
+
throw new TypeError("pwaManifest." + label + ": cursor orderKey mismatch");
|
|
565
|
+
}
|
|
566
|
+
return state.vals;
|
|
567
|
+
} catch (e) {
|
|
568
|
+
if (e instanceof TypeError) throw e;
|
|
569
|
+
throw new TypeError("pwaManifest." + label + ": cursor — " + (e && e.message || "malformed"));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function _encodeNext(rows, limit, orderKey) {
|
|
574
|
+
var last = rows[rows.length - 1];
|
|
575
|
+
if (!last || rows.length < limit) return null;
|
|
576
|
+
return _b().pagination.encodeCursor({
|
|
577
|
+
orderKey: orderKey,
|
|
578
|
+
vals: [Number(last.version_number)],
|
|
579
|
+
forward: true,
|
|
580
|
+
}, cursorSecret);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Compute next version_number for the given table. Reading the
|
|
584
|
+
// MAX is fine — the table's UNIQUE(version_number) catches a
|
|
585
|
+
// hypothetical concurrent insert and the lib layer is single-
|
|
586
|
+
// writer in the worker by construction.
|
|
587
|
+
async function _nextVersion(table) {
|
|
588
|
+
var r = await query(
|
|
589
|
+
"SELECT MAX(version_number) AS mx FROM " + table,
|
|
590
|
+
[],
|
|
591
|
+
);
|
|
592
|
+
var mx = r.rows[0] && r.rows[0].mx;
|
|
593
|
+
return (mx == null ? 0 : Number(mx)) + 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---- defineManifest ----------------------------------------------------
|
|
597
|
+
|
|
598
|
+
async function defineManifest(input) {
|
|
599
|
+
if (!input || typeof input !== "object") {
|
|
600
|
+
throw new TypeError("pwaManifest.defineManifest: input object required");
|
|
601
|
+
}
|
|
602
|
+
var name = _string(input.name, "name", MAX_NAME_LEN);
|
|
603
|
+
var shortName = _string(input.short_name, "short_name", MAX_SHORT_NAME_LEN);
|
|
604
|
+
var description = _string(input.description, "description", MAX_DESCRIPTION_LEN);
|
|
605
|
+
var startUrl = _url(input.start_url, "start_url");
|
|
606
|
+
var scopeUrl = _url(input.scope, "scope");
|
|
607
|
+
var display = _enum(input.display, ALLOWED_DISPLAYS, "display");
|
|
608
|
+
var orientation = _enum(input.orientation, ALLOWED_ORIENTATIONS, "orientation");
|
|
609
|
+
var themeColor = _hexColor(input.theme_color, "theme_color");
|
|
610
|
+
var backgroundColor = _hexColor(input.background_color, "background_color");
|
|
611
|
+
var lang = input.lang == null ? "en" : _lang(input.lang);
|
|
612
|
+
var dir = input.dir == null ? "auto" : _enum(input.dir, ALLOWED_DIRS, "dir");
|
|
613
|
+
var icons = _validateIcons(input.icons);
|
|
614
|
+
|
|
615
|
+
var versionNumber = await _nextVersion("pwa_manifests");
|
|
616
|
+
var ts = _now();
|
|
617
|
+
var id = _b().uuid.v7();
|
|
618
|
+
|
|
619
|
+
await query(
|
|
620
|
+
"INSERT INTO pwa_manifests " +
|
|
621
|
+
"(id, version_number, name, short_name, description, start_url, scope, " +
|
|
622
|
+
" display, orientation, theme_color, background_color, lang, dir, icons_json, " +
|
|
623
|
+
" is_active, archived_at, created_at, updated_at) " +
|
|
624
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, 0, NULL, ?15, ?15)",
|
|
625
|
+
[
|
|
626
|
+
id, versionNumber, name, shortName, description, startUrl, scopeUrl,
|
|
627
|
+
display, orientation, themeColor, backgroundColor, lang, dir,
|
|
628
|
+
JSON.stringify(icons), ts,
|
|
629
|
+
],
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
var r = await query(
|
|
633
|
+
"SELECT * FROM pwa_manifests WHERE version_number = ?1",
|
|
634
|
+
[versionNumber],
|
|
635
|
+
);
|
|
636
|
+
return _hydrateManifest(r.rows[0]);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ---- defineServiceWorkerConfig ----------------------------------------
|
|
640
|
+
|
|
641
|
+
async function defineServiceWorkerConfig(input) {
|
|
642
|
+
if (!input || typeof input !== "object") {
|
|
643
|
+
throw new TypeError("pwaManifest.defineServiceWorkerConfig: input object required");
|
|
644
|
+
}
|
|
645
|
+
var cacheName = _cacheName(input.cache_name);
|
|
646
|
+
var precacheUrls = _validatePrecacheUrls(input.precache_urls == null ? [] : input.precache_urls);
|
|
647
|
+
var runtimeRules = _validateRuntimeRules(input.runtime_rules == null ? [] : input.runtime_rules);
|
|
648
|
+
var offlineFallback = null;
|
|
649
|
+
var navigationFallback = null;
|
|
650
|
+
if (input.offline_fallback != null) {
|
|
651
|
+
offlineFallback = _url(input.offline_fallback, "offline_fallback");
|
|
652
|
+
}
|
|
653
|
+
if (input.navigation_fallback != null) {
|
|
654
|
+
navigationFallback = _url(input.navigation_fallback, "navigation_fallback");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
var versionNumber = await _nextVersion("pwa_sw_configs");
|
|
658
|
+
var ts = _now();
|
|
659
|
+
var id = _b().uuid.v7();
|
|
660
|
+
|
|
661
|
+
await query(
|
|
662
|
+
"INSERT INTO pwa_sw_configs " +
|
|
663
|
+
"(id, version_number, cache_name, precache_urls_json, runtime_rules_json, " +
|
|
664
|
+
" offline_fallback, navigation_fallback, is_active, archived_at, created_at, updated_at) " +
|
|
665
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 0, NULL, ?8, ?8)",
|
|
666
|
+
[
|
|
667
|
+
id, versionNumber, cacheName, JSON.stringify(precacheUrls), JSON.stringify(runtimeRules),
|
|
668
|
+
offlineFallback, navigationFallback, ts,
|
|
669
|
+
],
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
var r = await query(
|
|
673
|
+
"SELECT * FROM pwa_sw_configs WHERE version_number = ?1",
|
|
674
|
+
[versionNumber],
|
|
675
|
+
);
|
|
676
|
+
return _hydrateSwConfig(r.rows[0]);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ---- getActive ---------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
async function getActive(kind) {
|
|
682
|
+
var k = _scopeKind(kind);
|
|
683
|
+
if (k === "manifest") {
|
|
684
|
+
var r = await query(
|
|
685
|
+
"SELECT * FROM pwa_manifests WHERE is_active = 1 AND archived_at IS NULL LIMIT 1",
|
|
686
|
+
[],
|
|
687
|
+
);
|
|
688
|
+
return _hydrateManifest(r.rows[0] || null);
|
|
689
|
+
}
|
|
690
|
+
var rs = await query(
|
|
691
|
+
"SELECT * FROM pwa_sw_configs WHERE is_active = 1 AND archived_at IS NULL LIMIT 1",
|
|
692
|
+
[],
|
|
693
|
+
);
|
|
694
|
+
return _hydrateSwConfig(rs.rows[0] || null);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ---- setActive ---------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
async function setActive(arg1, arg2) {
|
|
700
|
+
// Two call shapes:
|
|
701
|
+
// setActive(versionNumber) → manifest scope
|
|
702
|
+
// setActive(kind, versionNumber) → explicit scope
|
|
703
|
+
var kind, versionNumber;
|
|
704
|
+
if (arg2 === undefined) {
|
|
705
|
+
kind = "manifest";
|
|
706
|
+
versionNumber = arg1;
|
|
707
|
+
} else {
|
|
708
|
+
kind = _scopeKind(arg1);
|
|
709
|
+
versionNumber = arg2;
|
|
710
|
+
}
|
|
711
|
+
versionNumber = _versionNumber(versionNumber, "version_number");
|
|
712
|
+
|
|
713
|
+
var table = kind === "manifest" ? "pwa_manifests" : "pwa_sw_configs";
|
|
714
|
+
|
|
715
|
+
var r = await query(
|
|
716
|
+
"SELECT * FROM " + table + " WHERE version_number = ?1",
|
|
717
|
+
[versionNumber],
|
|
718
|
+
);
|
|
719
|
+
var row = r.rows[0];
|
|
720
|
+
if (!row) {
|
|
721
|
+
var err = new Error(
|
|
722
|
+
"pwaManifest.setActive: " + kind + " version " + versionNumber + " not found"
|
|
723
|
+
);
|
|
724
|
+
err.code = "PWA_VERSION_NOT_FOUND";
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
727
|
+
if (row.archived_at != null) {
|
|
728
|
+
var aErr = new Error(
|
|
729
|
+
"pwaManifest.setActive: " + kind + " version " + versionNumber + " is archived"
|
|
730
|
+
);
|
|
731
|
+
aErr.code = "PWA_VERSION_ARCHIVED";
|
|
732
|
+
throw aErr;
|
|
733
|
+
}
|
|
734
|
+
var ts = _now();
|
|
735
|
+
await query(
|
|
736
|
+
"UPDATE " + table + " SET is_active = 0, updated_at = ?1 " +
|
|
737
|
+
"WHERE is_active = 1 AND version_number != ?2",
|
|
738
|
+
[ts, versionNumber],
|
|
739
|
+
);
|
|
740
|
+
await query(
|
|
741
|
+
"UPDATE " + table + " SET is_active = 1, updated_at = ?1 " +
|
|
742
|
+
"WHERE version_number = ?2",
|
|
743
|
+
[ts, versionNumber],
|
|
744
|
+
);
|
|
745
|
+
var fresh = await query(
|
|
746
|
+
"SELECT * FROM " + table + " WHERE version_number = ?1",
|
|
747
|
+
[versionNumber],
|
|
748
|
+
);
|
|
749
|
+
return kind === "manifest"
|
|
750
|
+
? _hydrateManifest(fresh.rows[0])
|
|
751
|
+
: _hydrateSwConfig(fresh.rows[0]);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ---- listVersions ------------------------------------------------------
|
|
755
|
+
|
|
756
|
+
async function listVersions(arg1, arg2) {
|
|
757
|
+
// Two call shapes:
|
|
758
|
+
// listVersions(opts) → manifest scope
|
|
759
|
+
// listVersions(kind, opts) → explicit scope
|
|
760
|
+
var kind, listOpts;
|
|
761
|
+
if (arg2 === undefined) {
|
|
762
|
+
if (typeof arg1 === "string") {
|
|
763
|
+
kind = _scopeKind(arg1);
|
|
764
|
+
listOpts = {};
|
|
765
|
+
} else {
|
|
766
|
+
kind = "manifest";
|
|
767
|
+
listOpts = arg1 || {};
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
kind = _scopeKind(arg1);
|
|
771
|
+
listOpts = arg2 || {};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
var table = kind === "manifest" ? "pwa_manifests" : "pwa_sw_configs";
|
|
775
|
+
var orderKey = kind === "manifest" ? MANIFEST_ORDER_KEY : SW_ORDER_KEY;
|
|
776
|
+
var limit = _limit(listOpts.limit);
|
|
777
|
+
var cursorVals = _decodeCursor(listOpts.cursor, "listVersions", orderKey);
|
|
778
|
+
|
|
779
|
+
var where = ["1 = 1"];
|
|
780
|
+
var params = [];
|
|
781
|
+
var idx = 1;
|
|
782
|
+
if (cursorVals) {
|
|
783
|
+
where.push("version_number < ?" + idx);
|
|
784
|
+
params.push(cursorVals[0]);
|
|
785
|
+
idx += 1;
|
|
786
|
+
}
|
|
787
|
+
params.push(limit);
|
|
788
|
+
var sql = "SELECT * FROM " + table + " WHERE " + where.join(" AND ") +
|
|
789
|
+
" ORDER BY version_number DESC LIMIT ?" + idx;
|
|
790
|
+
var r = await query(sql, params);
|
|
791
|
+
var rows = r.rows.map(kind === "manifest" ? _hydrateManifest : _hydrateSwConfig);
|
|
792
|
+
return { rows: rows, next_cursor: _encodeNext(r.rows, limit, orderKey) };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ---- archiveVersion ----------------------------------------------------
|
|
796
|
+
|
|
797
|
+
async function archiveVersion(arg1, arg2) {
|
|
798
|
+
var kind, versionNumber;
|
|
799
|
+
if (arg2 === undefined) {
|
|
800
|
+
kind = "manifest";
|
|
801
|
+
versionNumber = arg1;
|
|
802
|
+
} else {
|
|
803
|
+
kind = _scopeKind(arg1);
|
|
804
|
+
versionNumber = arg2;
|
|
805
|
+
}
|
|
806
|
+
versionNumber = _versionNumber(versionNumber, "version_number");
|
|
807
|
+
|
|
808
|
+
var table = kind === "manifest" ? "pwa_manifests" : "pwa_sw_configs";
|
|
809
|
+
|
|
810
|
+
var r = await query(
|
|
811
|
+
"SELECT * FROM " + table + " WHERE version_number = ?1",
|
|
812
|
+
[versionNumber],
|
|
813
|
+
);
|
|
814
|
+
var row = r.rows[0];
|
|
815
|
+
if (!row) {
|
|
816
|
+
var err = new Error(
|
|
817
|
+
"pwaManifest.archiveVersion: " + kind + " version " + versionNumber + " not found"
|
|
818
|
+
);
|
|
819
|
+
err.code = "PWA_VERSION_NOT_FOUND";
|
|
820
|
+
throw err;
|
|
821
|
+
}
|
|
822
|
+
if (row.archived_at != null) {
|
|
823
|
+
return kind === "manifest" ? _hydrateManifest(row) : _hydrateSwConfig(row);
|
|
824
|
+
}
|
|
825
|
+
var ts = _now();
|
|
826
|
+
await query(
|
|
827
|
+
"UPDATE " + table + " SET archived_at = ?1, is_active = 0, updated_at = ?1 " +
|
|
828
|
+
"WHERE version_number = ?2",
|
|
829
|
+
[ts, versionNumber],
|
|
830
|
+
);
|
|
831
|
+
var fresh = await query(
|
|
832
|
+
"SELECT * FROM " + table + " WHERE version_number = ?1",
|
|
833
|
+
[versionNumber],
|
|
834
|
+
);
|
|
835
|
+
return kind === "manifest"
|
|
836
|
+
? _hydrateManifest(fresh.rows[0])
|
|
837
|
+
: _hydrateSwConfig(fresh.rows[0]);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ---- renderManifestJson ------------------------------------------------
|
|
841
|
+
|
|
842
|
+
async function renderManifestJson() {
|
|
843
|
+
var active = await getActive("manifest");
|
|
844
|
+
if (!active) {
|
|
845
|
+
var err = new Error("pwaManifest.renderManifestJson: no active manifest");
|
|
846
|
+
err.code = "PWA_NO_ACTIVE_MANIFEST";
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
var manifestObj = {
|
|
850
|
+
name: active.name,
|
|
851
|
+
short_name: active.short_name,
|
|
852
|
+
description: active.description,
|
|
853
|
+
start_url: active.start_url,
|
|
854
|
+
scope: active.scope,
|
|
855
|
+
display: active.display,
|
|
856
|
+
orientation: active.orientation,
|
|
857
|
+
theme_color: active.theme_color,
|
|
858
|
+
background_color: active.background_color,
|
|
859
|
+
lang: active.lang,
|
|
860
|
+
dir: active.dir,
|
|
861
|
+
icons: active.icons,
|
|
862
|
+
};
|
|
863
|
+
return _stableStringify(manifestObj, 2);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// ---- renderServiceWorkerJs ---------------------------------------------
|
|
867
|
+
//
|
|
868
|
+
// Emit the JS bytes for `/sw.js`. The body is a self-contained
|
|
869
|
+
// script — install handler precaches the operator's URL list,
|
|
870
|
+
// fetch handler dispatches per-rule. Operator-sourced URLs and
|
|
871
|
+
// patterns are JSON.stringify'd into the body so a hostile URL
|
|
872
|
+
// can't break out of the string literal.
|
|
873
|
+
|
|
874
|
+
async function renderServiceWorkerJs() {
|
|
875
|
+
var active = await getActive("sw");
|
|
876
|
+
if (!active) {
|
|
877
|
+
var err = new Error("pwaManifest.renderServiceWorkerJs: no active service-worker config");
|
|
878
|
+
err.code = "PWA_NO_ACTIVE_SW";
|
|
879
|
+
throw err;
|
|
880
|
+
}
|
|
881
|
+
var lines = [];
|
|
882
|
+
lines.push("// service worker generated by shop.pwaManifest");
|
|
883
|
+
lines.push("// cache_name=" + JSON.stringify(active.cache_name) +
|
|
884
|
+
" version=" + active.version_number);
|
|
885
|
+
lines.push('"use strict";');
|
|
886
|
+
lines.push("var CACHE_NAME = " + JSON.stringify(active.cache_name) + ";");
|
|
887
|
+
lines.push("var PRECACHE_URLS = " + JSON.stringify(active.precache_urls) + ";");
|
|
888
|
+
lines.push("var RUNTIME_RULES = " + JSON.stringify(active.runtime_rules) + ";");
|
|
889
|
+
lines.push("var OFFLINE_FALLBACK = " +
|
|
890
|
+
(active.offline_fallback == null ? "null" : JSON.stringify(active.offline_fallback)) + ";");
|
|
891
|
+
lines.push("var NAVIGATION_FALLBACK = " +
|
|
892
|
+
(active.navigation_fallback == null ? "null" : JSON.stringify(active.navigation_fallback)) + ";");
|
|
893
|
+
lines.push("self.addEventListener('install', function (e) {");
|
|
894
|
+
lines.push(" e.waitUntil(caches.open(CACHE_NAME).then(function (c) {");
|
|
895
|
+
lines.push(" return c.addAll(PRECACHE_URLS);");
|
|
896
|
+
lines.push(" }).then(function () { return self.skipWaiting(); }));");
|
|
897
|
+
lines.push("});");
|
|
898
|
+
lines.push("self.addEventListener('activate', function (e) {");
|
|
899
|
+
lines.push(" e.waitUntil(caches.keys().then(function (keys) {");
|
|
900
|
+
lines.push(" return Promise.all(keys.map(function (k) {");
|
|
901
|
+
lines.push(" if (k !== CACHE_NAME) return caches.delete(k);");
|
|
902
|
+
lines.push(" }));");
|
|
903
|
+
lines.push(" }).then(function () { return self.clients.claim(); }));");
|
|
904
|
+
lines.push("});");
|
|
905
|
+
lines.push("function _matchRule(url) {");
|
|
906
|
+
lines.push(" for (var i = 0; i < RUNTIME_RULES.length; i += 1) {");
|
|
907
|
+
lines.push(" if (url.indexOf(RUNTIME_RULES[i].url_pattern) !== -1) return RUNTIME_RULES[i];");
|
|
908
|
+
lines.push(" }");
|
|
909
|
+
lines.push(" return null;");
|
|
910
|
+
lines.push("}");
|
|
911
|
+
lines.push("function _cacheFirst(req) {");
|
|
912
|
+
lines.push(" return caches.match(req).then(function (hit) {");
|
|
913
|
+
lines.push(" return hit || fetch(req).then(function (res) {");
|
|
914
|
+
lines.push(" var copy = res.clone();");
|
|
915
|
+
lines.push(" caches.open(CACHE_NAME).then(function (c) { c.put(req, copy); });");
|
|
916
|
+
lines.push(" return res;");
|
|
917
|
+
lines.push(" });");
|
|
918
|
+
lines.push(" });");
|
|
919
|
+
lines.push("}");
|
|
920
|
+
lines.push("function _networkFirst(req) {");
|
|
921
|
+
lines.push(" return fetch(req).then(function (res) {");
|
|
922
|
+
lines.push(" var copy = res.clone();");
|
|
923
|
+
lines.push(" caches.open(CACHE_NAME).then(function (c) { c.put(req, copy); });");
|
|
924
|
+
lines.push(" return res;");
|
|
925
|
+
lines.push(" }).catch(function () { return caches.match(req); });");
|
|
926
|
+
lines.push("}");
|
|
927
|
+
lines.push("function _staleWhileRevalidate(req) {");
|
|
928
|
+
lines.push(" var hit = caches.match(req);");
|
|
929
|
+
lines.push(" var net = fetch(req).then(function (res) {");
|
|
930
|
+
lines.push(" var copy = res.clone();");
|
|
931
|
+
lines.push(" caches.open(CACHE_NAME).then(function (c) { c.put(req, copy); });");
|
|
932
|
+
lines.push(" return res;");
|
|
933
|
+
lines.push(" }).catch(function () { return null; });");
|
|
934
|
+
lines.push(" return hit.then(function (h) { return h || net; });");
|
|
935
|
+
lines.push("}");
|
|
936
|
+
lines.push("self.addEventListener('fetch', function (e) {");
|
|
937
|
+
lines.push(" if (e.request.method !== 'GET') return;");
|
|
938
|
+
lines.push(" var url = e.request.url;");
|
|
939
|
+
lines.push(" var rule = _matchRule(url);");
|
|
940
|
+
lines.push(" if (rule) {");
|
|
941
|
+
lines.push(" if (rule.strategy === 'cache-first') { e.respondWith(_cacheFirst(e.request)); return; }");
|
|
942
|
+
lines.push(" if (rule.strategy === 'network-first') { e.respondWith(_networkFirst(e.request)); return; }");
|
|
943
|
+
lines.push(" if (rule.strategy === 'stale-while-revalidate') { e.respondWith(_staleWhileRevalidate(e.request)); return; }");
|
|
944
|
+
lines.push(" /* network-only: fall through to default fetch */");
|
|
945
|
+
lines.push(" }");
|
|
946
|
+
lines.push(" if (e.request.mode === 'navigate' && NAVIGATION_FALLBACK) {");
|
|
947
|
+
lines.push(" e.respondWith(fetch(e.request).catch(function () {");
|
|
948
|
+
lines.push(" return caches.match(NAVIGATION_FALLBACK);");
|
|
949
|
+
lines.push(" }));");
|
|
950
|
+
lines.push(" return;");
|
|
951
|
+
lines.push(" }");
|
|
952
|
+
lines.push(" if (OFFLINE_FALLBACK) {");
|
|
953
|
+
lines.push(" e.respondWith(fetch(e.request).catch(function () {");
|
|
954
|
+
lines.push(" return caches.match(OFFLINE_FALLBACK);");
|
|
955
|
+
lines.push(" }));");
|
|
956
|
+
lines.push(" }");
|
|
957
|
+
lines.push("});");
|
|
958
|
+
return lines.join("\n") + "\n";
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
ALLOWED_DISPLAYS: ALLOWED_DISPLAYS,
|
|
963
|
+
ALLOWED_ORIENTATIONS: ALLOWED_ORIENTATIONS,
|
|
964
|
+
ALLOWED_DIRS: ALLOWED_DIRS,
|
|
965
|
+
ALLOWED_STRATEGIES: ALLOWED_STRATEGIES,
|
|
966
|
+
ALLOWED_ICON_PURPOSES: ALLOWED_ICON_PURPOSES,
|
|
967
|
+
ALLOWED_SCOPES: ALLOWED_SCOPES,
|
|
968
|
+
MAX_NAME_LEN: MAX_NAME_LEN,
|
|
969
|
+
MAX_SHORT_NAME_LEN: MAX_SHORT_NAME_LEN,
|
|
970
|
+
MAX_DESCRIPTION_LEN: MAX_DESCRIPTION_LEN,
|
|
971
|
+
MAX_URL_LEN: MAX_URL_LEN,
|
|
972
|
+
MAX_ICON_COUNT: MAX_ICON_COUNT,
|
|
973
|
+
MAX_PRECACHE_URLS: MAX_PRECACHE_URLS,
|
|
974
|
+
MAX_RUNTIME_RULES: MAX_RUNTIME_RULES,
|
|
975
|
+
MAX_LIST_LIMIT: MAX_LIST_LIMIT,
|
|
976
|
+
|
|
977
|
+
defineManifest: defineManifest,
|
|
978
|
+
defineServiceWorkerConfig: defineServiceWorkerConfig,
|
|
979
|
+
getActive: getActive,
|
|
980
|
+
setActive: setActive,
|
|
981
|
+
renderManifestJson: renderManifestJson,
|
|
982
|
+
renderServiceWorkerJs: renderServiceWorkerJs,
|
|
983
|
+
listVersions: listVersions,
|
|
984
|
+
archiveVersion: archiveVersion,
|
|
985
|
+
validateIcons: function (icons) { return _validateIcons(icons); },
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
module.exports = {
|
|
990
|
+
create: create,
|
|
991
|
+
ALLOWED_DISPLAYS: ALLOWED_DISPLAYS,
|
|
992
|
+
ALLOWED_ORIENTATIONS: ALLOWED_ORIENTATIONS,
|
|
993
|
+
ALLOWED_DIRS: ALLOWED_DIRS,
|
|
994
|
+
ALLOWED_STRATEGIES: ALLOWED_STRATEGIES,
|
|
995
|
+
ALLOWED_ICON_PURPOSES: ALLOWED_ICON_PURPOSES,
|
|
996
|
+
ALLOWED_SCOPES: ALLOWED_SCOPES,
|
|
997
|
+
MAX_NAME_LEN: MAX_NAME_LEN,
|
|
998
|
+
MAX_SHORT_NAME_LEN: MAX_SHORT_NAME_LEN,
|
|
999
|
+
MAX_DESCRIPTION_LEN: MAX_DESCRIPTION_LEN,
|
|
1000
|
+
MAX_URL_LEN: MAX_URL_LEN,
|
|
1001
|
+
MAX_ICON_COUNT: MAX_ICON_COUNT,
|
|
1002
|
+
MAX_PRECACHE_URLS: MAX_PRECACHE_URLS,
|
|
1003
|
+
MAX_RUNTIME_RULES: MAX_RUNTIME_RULES,
|
|
1004
|
+
MAX_LIST_LIMIT: MAX_LIST_LIMIT,
|
|
1005
|
+
};
|