@pagepocket/lib 0.8.6 → 0.9.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/dist/cheerio/types.d.ts +5 -0
- package/dist/cheerio/types.js +1 -0
- package/dist/core/completion.js +2 -1
- package/dist/core/content-store.js +2 -2
- package/dist/css-rewrite.d.ts +1 -1
- package/dist/css-rewrite.js +2 -2
- package/dist/hackers/replay-dom-rewrite/script-part-1.d.ts +1 -0
- package/dist/hackers/replay-dom-rewrite/script-part-1.js +202 -0
- package/dist/hackers/replay-dom-rewrite/script-part-2.d.ts +1 -0
- package/dist/hackers/replay-dom-rewrite/script-part-2.js +173 -0
- package/dist/hackers/replay-dom-rewrite.js +3 -373
- package/dist/hackers/replay-svg-image.js +32 -8
- package/dist/hackers/replay-xhr.js +3 -3
- package/dist/path-resolver.js +2 -2
- package/dist/replace-elements/actions.d.ts +3 -3
- package/dist/replace-elements/actions.js +36 -16
- package/dist/replace-elements/match.d.ts +2 -2
- package/dist/replace-elements/match.js +22 -11
- package/dist/replace-elements/normalize.d.ts +1 -1
- package/dist/replace-elements/normalize.js +4 -2
- package/dist/replay/match-api.js +29 -17
- package/dist/replay/templates/replay-script-template.js +16 -332
- package/dist/replay/templates/replay-script-template.part-1.d.ts +5 -0
- package/dist/replay/templates/replay-script-template.part-1.js +101 -0
- package/dist/replay/templates/replay-script-template.part-2.d.ts +3 -0
- package/dist/replay/templates/replay-script-template.part-2.js +222 -0
- package/dist/replay/templates/replay-script-template.part-3.d.ts +3 -0
- package/dist/replay/templates/replay-script-template.part-3.js +9 -0
- package/dist/resource-proxy/pathname-variants.js +8 -5
- package/dist/resource-proxy.js +10 -10
- package/dist/resources.d.ts +3 -2
- package/dist/resources.js +6 -3
- package/dist/rewrite-links/js-imports.d.ts +1 -1
- package/dist/rewrite-links/link-rel.d.ts +2 -2
- package/dist/rewrite-links/meta-refresh.d.ts +1 -1
- package/dist/rewrite-links/meta-refresh.js +6 -3
- package/dist/rewrite-links/srcset.d.ts +1 -1
- package/dist/rewrite-links/srcset.js +4 -2
- package/dist/rewrite-links/url-resolve.d.ts +2 -2
- package/dist/rewrite-links/url-resolve.js +2 -2
- package/dist/rewrite-links.d.ts +1 -1
- package/dist/rewrite-links.js +12 -6
- package/dist/snapshot-builder/build-snapshot.js +2 -3
- package/dist/snapshot-builder/capture-index/index-capture.js +2 -1
- package/dist/snapshot-builder/emit-document.d.ts +1 -1
- package/dist/snapshot-builder/emit-document.js +1 -1
- package/dist/snapshot-builder/grouping.js +2 -2
- package/dist/snapshot-builder/path-map.d.ts +1 -1
- package/dist/snapshot-builder/path-map.js +1 -1
- package/dist/snapshot-builder/resources-path.js +8 -4
- package/dist/snapshot-builder/rewrite-resource.d.ts +2 -2
- package/dist/snapshot-builder/rewrite-resource.js +2 -2
- package/dist/types.d.ts +3 -3
- package/dist/units/internal/async-queue.d.ts +9 -0
- package/dist/units/internal/async-queue.js +57 -0
- package/dist/units/internal/deferred-tracker.d.ts +5 -0
- package/dist/units/internal/deferred-tracker.js +13 -0
- package/dist/units/internal/runtime.d.ts +37 -0
- package/dist/units/internal/runtime.js +113 -0
- package/dist/units/runner.js +3 -184
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +6 -6
- package/package.json +5 -4
- package/README.md +0 -357
package/dist/replay/match-api.js
CHANGED
|
@@ -2,10 +2,12 @@ import { urlEquivalent } from "../utils.js";
|
|
|
2
2
|
export function matchAPI(options) {
|
|
3
3
|
const { records, byKey, baseUrl, method, url, body } = options;
|
|
4
4
|
const normalizeBody = (value) => {
|
|
5
|
-
if (value === undefined || value === null)
|
|
5
|
+
if (value === undefined || value === null) {
|
|
6
6
|
return "";
|
|
7
|
-
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === "string") {
|
|
8
9
|
return value;
|
|
10
|
+
}
|
|
9
11
|
try {
|
|
10
12
|
return String(value);
|
|
11
13
|
}
|
|
@@ -36,13 +38,14 @@ export function matchAPI(options) {
|
|
|
36
38
|
return new URL(input, baseUrl);
|
|
37
39
|
}
|
|
38
40
|
catch {
|
|
39
|
-
return
|
|
41
|
+
return undefined;
|
|
40
42
|
}
|
|
41
43
|
};
|
|
42
44
|
const toPathSearch = (input) => {
|
|
43
45
|
const parsed = safeUrl(input);
|
|
44
|
-
if (!parsed)
|
|
46
|
+
if (!parsed) {
|
|
45
47
|
return input;
|
|
48
|
+
}
|
|
46
49
|
return parsed.pathname + parsed.search;
|
|
47
50
|
};
|
|
48
51
|
const toPathname = (input) => {
|
|
@@ -52,8 +55,9 @@ export function matchAPI(options) {
|
|
|
52
55
|
const buildUrlVariants = (input) => {
|
|
53
56
|
const variants = new Set();
|
|
54
57
|
const push = (value) => {
|
|
55
|
-
if (!value)
|
|
58
|
+
if (!value) {
|
|
56
59
|
return;
|
|
60
|
+
}
|
|
57
61
|
variants.add(value);
|
|
58
62
|
};
|
|
59
63
|
const raw = String(input ?? "");
|
|
@@ -79,12 +83,14 @@ export function matchAPI(options) {
|
|
|
79
83
|
const bodyValue = normalizeBody(body);
|
|
80
84
|
const methodValue = (method || "GET").toUpperCase();
|
|
81
85
|
const tryLookup = (keyMethod, keyBody) => {
|
|
82
|
-
if (!byKey)
|
|
86
|
+
if (!byKey) {
|
|
83
87
|
return undefined;
|
|
88
|
+
}
|
|
84
89
|
for (const urlVariant of urlVariants) {
|
|
85
90
|
const record = byKey.get(makeKey(keyMethod, urlVariant, keyBody));
|
|
86
|
-
if (record)
|
|
91
|
+
if (record) {
|
|
87
92
|
return record;
|
|
93
|
+
}
|
|
88
94
|
}
|
|
89
95
|
return undefined;
|
|
90
96
|
};
|
|
@@ -96,15 +102,16 @@ export function matchAPI(options) {
|
|
|
96
102
|
];
|
|
97
103
|
for (const [keyMethod, keyBody] of matchOrder) {
|
|
98
104
|
const record = tryLookup(keyMethod, keyBody);
|
|
99
|
-
if (record)
|
|
105
|
+
if (record) {
|
|
100
106
|
return record;
|
|
107
|
+
}
|
|
101
108
|
}
|
|
102
|
-
const
|
|
109
|
+
const toUrlOrUndefined = (value) => {
|
|
103
110
|
try {
|
|
104
111
|
return new URL(value, baseUrl);
|
|
105
112
|
}
|
|
106
113
|
catch {
|
|
107
|
-
return
|
|
114
|
+
return undefined;
|
|
108
115
|
}
|
|
109
116
|
};
|
|
110
117
|
const normalizePath = (value) => {
|
|
@@ -124,8 +131,8 @@ export function matchAPI(options) {
|
|
|
124
131
|
return true;
|
|
125
132
|
}
|
|
126
133
|
// Relaxed fallback: allow origin mismatch but keep path/search stable.
|
|
127
|
-
const left =
|
|
128
|
-
const right =
|
|
134
|
+
const left = toUrlOrUndefined(inputUrl);
|
|
135
|
+
const right = toUrlOrUndefined(recordUrl);
|
|
129
136
|
if (!left || !right) {
|
|
130
137
|
return false;
|
|
131
138
|
}
|
|
@@ -140,23 +147,28 @@ export function matchAPI(options) {
|
|
|
140
147
|
};
|
|
141
148
|
const scanRecords = (keyMethod, keyBody) => {
|
|
142
149
|
for (const record of records || []) {
|
|
143
|
-
if (!record || !record.url || !record.method)
|
|
150
|
+
if (!record || !record.url || !record.method) {
|
|
144
151
|
continue;
|
|
145
|
-
|
|
152
|
+
}
|
|
153
|
+
if (record.method.toUpperCase() !== keyMethod) {
|
|
146
154
|
continue;
|
|
147
|
-
|
|
155
|
+
}
|
|
156
|
+
if (!urlMatches(url, record.url)) {
|
|
148
157
|
continue;
|
|
158
|
+
}
|
|
149
159
|
const recordBody = record.requestBody || record.requestBodyBase64 || "";
|
|
150
|
-
if (keyBody && recordBody !== keyBody)
|
|
160
|
+
if (keyBody && recordBody !== keyBody) {
|
|
151
161
|
continue;
|
|
162
|
+
}
|
|
152
163
|
return record;
|
|
153
164
|
}
|
|
154
165
|
return undefined;
|
|
155
166
|
};
|
|
156
167
|
for (const [keyMethod, keyBody] of matchOrder) {
|
|
157
168
|
const record = scanRecords(keyMethod, keyBody);
|
|
158
|
-
if (record)
|
|
169
|
+
if (record) {
|
|
159
170
|
return record;
|
|
171
|
+
}
|
|
160
172
|
}
|
|
161
173
|
return undefined;
|
|
162
174
|
}
|
|
@@ -1,337 +1,21 @@
|
|
|
1
1
|
import { matchAPISource } from "./match-api-source.js";
|
|
2
|
+
import { buildReplayInjectedScriptPart1 } from "./replay-script-template.part-1.js";
|
|
3
|
+
import { buildReplayInjectedScriptPart2 } from "./replay-script-template.part-2.js";
|
|
4
|
+
import { buildReplayInjectedScriptPart3 } from "./replay-script-template.part-3.js";
|
|
2
5
|
import { resourceProxyScript } from "./resource-proxy-script.js";
|
|
3
6
|
export const buildReplayInjectedScript = (options) => {
|
|
4
7
|
const { basePayload, apiPayload, hackerScripts } = options;
|
|
5
|
-
return
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (injected && injected.version === "1.0" && Array.isArray(injected.items)) {
|
|
19
|
-
return injected;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!__pagepocketOriginalFetch) {
|
|
23
|
-
throw new Error("Fetch is unavailable");
|
|
24
|
-
}
|
|
25
|
-
const response = await __pagepocketOriginalFetch(resourcesPathUrl);
|
|
26
|
-
if (!response.ok) {
|
|
27
|
-
throw new Error("Failed to load resources_path.json");
|
|
28
|
-
}
|
|
29
|
-
const json = await response.json();
|
|
30
|
-
if (!json || json.version !== "1.0" || !Array.isArray(json.items)) {
|
|
31
|
-
throw new Error("Invalid resources_path.json");
|
|
32
|
-
}
|
|
33
|
-
return json;
|
|
34
|
-
} catch {
|
|
35
|
-
return { version: "1.0", createdAt: 0, items: [] };
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const loadApiSnapshot = async () => {
|
|
40
|
-
try {
|
|
41
|
-
const injected = window && window.__pagepocketApiSnapshot;
|
|
42
|
-
if (injected && injected.version === "1.0" && Array.isArray(injected.records)) {
|
|
43
|
-
return injected;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!__pagepocketOriginalFetch) {
|
|
47
|
-
throw new Error("Fetch is unavailable");
|
|
48
|
-
}
|
|
49
|
-
const response = await __pagepocketOriginalFetch(apiUrl);
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
throw new Error("Failed to load api.json");
|
|
52
|
-
}
|
|
53
|
-
return await response.json();
|
|
54
|
-
} catch {
|
|
55
|
-
return { version: "1.0", url: baseUrl, createdAt: 0, records: [] };
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const originalResponseJson = Response && Response.prototype && Response.prototype.json;
|
|
60
|
-
if (originalResponseJson) {
|
|
61
|
-
Response.prototype.json = function(...args) {
|
|
62
|
-
try {
|
|
63
|
-
return originalResponseJson.apply(this, args).catch(() => null);
|
|
64
|
-
} catch {
|
|
65
|
-
return Promise.resolve(null);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const ensureReplayPatches = () => {
|
|
71
|
-
try {
|
|
72
|
-
if (!window.fetch.__pagepocketOriginal && typeof __pagepocketOriginalFetch === "function") {
|
|
73
|
-
window.fetch.__pagepocketOriginal = __pagepocketOriginalFetch;
|
|
74
|
-
}
|
|
75
|
-
} catch {}
|
|
76
|
-
try {
|
|
77
|
-
if (!XMLHttpRequest.prototype.send.__pagepocketOriginal) {
|
|
78
|
-
XMLHttpRequest.prototype.send.__pagepocketOriginal = XMLHttpRequest.prototype.send;
|
|
79
|
-
}
|
|
80
|
-
} catch {}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
let records = [];
|
|
84
|
-
const byKey = new Map();
|
|
85
|
-
|
|
86
|
-
let resourceIndex = null;
|
|
87
|
-
let embeddedByPath = new Map();
|
|
88
|
-
const blobUrlByPath = new Map();
|
|
89
|
-
|
|
90
|
-
const normalizeUrl = (input) => {
|
|
91
|
-
try { return new URL(input, baseUrl).toString(); } catch { return input; }
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const normalizeBody = (body) => {
|
|
95
|
-
if (body === undefined || body === null) return "";
|
|
96
|
-
if (typeof body === "string") return body;
|
|
97
|
-
try { return String(body); } catch { return ""; }
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const makeKey = (method, url, body) => method.toUpperCase() + " " + normalizeUrl(url) + " " + normalizeBody(body);
|
|
101
|
-
const makeVariantKeys = (method, url, body) => [makeKey(method, url, body)];
|
|
102
|
-
|
|
103
|
-
const matchAPI = ${matchAPISource};
|
|
104
|
-
|
|
105
|
-
const primeLookups = (snapshot) => {
|
|
106
|
-
records = snapshot.records || [];
|
|
107
|
-
byKey.clear();
|
|
108
|
-
for (const record of records) {
|
|
109
|
-
if (!record || !record.url || !record.method) continue;
|
|
110
|
-
const keys = makeVariantKeys(record.method, record.url, record.requestBody || record.requestBodyBase64 || "");
|
|
111
|
-
for (const key of keys) {
|
|
112
|
-
if (!byKey.has(key)) {
|
|
113
|
-
byKey.set(key, record);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const ready = (async () => {
|
|
120
|
-
const [apiSnapshot, resourcesPath] = await Promise.all([loadApiSnapshot(), loadResourcesPath()]);
|
|
121
|
-
resourceIndex = __pagepocketResourceProxy.buildIndex(resourcesPath || {});
|
|
122
|
-
|
|
123
|
-
embeddedByPath = new Map();
|
|
124
|
-
try {
|
|
125
|
-
const items = (resourcesPath && resourcesPath.items) || [];
|
|
126
|
-
for (const item of items) {
|
|
127
|
-
if (!item || !item.path || !item.data || item.dataEncoding !== "base64") {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
embeddedByPath.set(item.path, item);
|
|
131
|
-
}
|
|
132
|
-
} catch {}
|
|
133
|
-
|
|
134
|
-
const snapshot = apiSnapshot || {};
|
|
135
|
-
primeLookups(snapshot);
|
|
136
|
-
return snapshot;
|
|
137
|
-
})();
|
|
138
|
-
|
|
139
|
-
const isLocalResource = (value) => {
|
|
140
|
-
if (!value) return false;
|
|
141
|
-
if (value.startsWith("data:") || value.startsWith("blob:")) return true;
|
|
142
|
-
// Protocol-relative URLs ("//host/path") are NOT local paths.
|
|
143
|
-
// Treat them as absolute URLs so they can be mapped via resources_path.json.
|
|
144
|
-
if (value.startsWith("//")) return false;
|
|
145
|
-
if (value.startsWith("/")) return true;
|
|
146
|
-
return false;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const findRecord = (method, url, body) => {
|
|
150
|
-
return matchAPI({ records, byKey, baseUrl, method, url, body });
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const urlLookupCache = new Map();
|
|
154
|
-
const findByUrl = (url) => {
|
|
155
|
-
if (isLocalResource(url)) return null;
|
|
156
|
-
if (urlLookupCache.has(url)) {
|
|
157
|
-
return urlLookupCache.get(url);
|
|
158
|
-
}
|
|
159
|
-
const record = matchAPI({ records, byKey, baseUrl, method: "GET", url, body: "" });
|
|
160
|
-
urlLookupCache.set(url, record || null);
|
|
161
|
-
return record;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const findLocalPath = (url) => {
|
|
165
|
-
try {
|
|
166
|
-
if (!url || isLocalResource(String(url))) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
if (!resourceIndex) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const resolvedPath = __pagepocketResourceProxy.resolveToLocalPath(String(url), baseUrl, resourceIndex);
|
|
174
|
-
if (!resolvedPath) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const embedded = embeddedByPath && embeddedByPath.get(resolvedPath);
|
|
179
|
-
if (embedded && embedded.dataEncoding === "base64" && embedded.data) {
|
|
180
|
-
if (blobUrlByPath.has(resolvedPath)) {
|
|
181
|
-
return blobUrlByPath.get(resolvedPath);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const binary = atob(embedded.data);
|
|
185
|
-
const bytes = new Uint8Array(binary.length);
|
|
186
|
-
for (let i = 0; i < binary.length; i += 1) {
|
|
187
|
-
bytes[i] = binary.charCodeAt(i);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const mime = embedded.mimeType || "application/octet-stream";
|
|
191
|
-
const blob = new Blob([bytes], { type: mime });
|
|
192
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
193
|
-
blobUrlByPath.set(resolvedPath, blobUrl);
|
|
194
|
-
return blobUrl;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return resolvedPath;
|
|
198
|
-
} catch {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Rewrite a resource URL to a snapshot-local URL.
|
|
205
|
-
*
|
|
206
|
-
* Strategy (in order):
|
|
207
|
-
* 1) If already local (data:/blob:/absolute path), keep as-is.
|
|
208
|
-
* 2) If resources_path.json can map it to a local file, return that path.
|
|
209
|
-
* 3) Otherwise, if we have a recorded body for the URL, return a data: URL
|
|
210
|
-
* (used as a placeholder for some resource types).
|
|
211
|
-
* 4) Otherwise, keep the original URL.
|
|
212
|
-
*/
|
|
213
|
-
const rewriteResourceUrl = (value, ctx) => {
|
|
214
|
-
try {
|
|
215
|
-
const raw = String(value || "");
|
|
216
|
-
if (!raw) {
|
|
217
|
-
return raw;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (isLocalResource(raw)) {
|
|
221
|
-
return raw;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const localPath = findLocalPath(raw);
|
|
225
|
-
if (localPath) {
|
|
226
|
-
return localPath;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const fallbackType = ctx && ctx.fallbackType ? String(ctx.fallbackType) : undefined;
|
|
230
|
-
const record = findByUrl(raw);
|
|
231
|
-
if (record) {
|
|
232
|
-
const dataUrl = toDataUrl(record, fallbackType);
|
|
233
|
-
if (dataUrl) {
|
|
234
|
-
return dataUrl;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return raw;
|
|
239
|
-
} catch {
|
|
240
|
-
try {
|
|
241
|
-
return String(value || "");
|
|
242
|
-
} catch {
|
|
243
|
-
return value;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const defineProp = (obj, key, value) => {
|
|
249
|
-
try {
|
|
250
|
-
Object.defineProperty(obj, key, { value, configurable: true });
|
|
251
|
-
} catch {}
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const decodeBase64 = (input) => {
|
|
255
|
-
try {
|
|
256
|
-
const binary = atob(input || "");
|
|
257
|
-
const bytes = new Uint8Array(binary.length);
|
|
258
|
-
Array.from(binary).forEach((char, index) => {
|
|
259
|
-
bytes[index] = char.charCodeAt(0);
|
|
260
|
-
});
|
|
261
|
-
return bytes;
|
|
262
|
-
} catch {
|
|
263
|
-
return new Uint8Array();
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const bytesToBase64 = (bytes) => {
|
|
268
|
-
const binary = Array.from(bytes, (value) => String.fromCharCode(value)).join("");
|
|
269
|
-
return btoa(binary);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const textToBase64 = (text) => {
|
|
273
|
-
try {
|
|
274
|
-
const bytes = new TextEncoder().encode(text || "");
|
|
275
|
-
return bytesToBase64(bytes);
|
|
276
|
-
} catch {
|
|
277
|
-
return btoa(text || "");
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const getContentType = (record) => {
|
|
282
|
-
const headers = record.responseHeaders || {};
|
|
283
|
-
for (const key in headers) {
|
|
284
|
-
if (key.toLowerCase() === "content-type") {
|
|
285
|
-
return headers[key] || "application/octet-stream";
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return "application/octet-stream";
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const dataUrlCache = new Map();
|
|
292
|
-
const toDataUrl = (record, fallbackType) => {
|
|
293
|
-
if (!record) return "";
|
|
294
|
-
const contentType = getContentType(record) || fallbackType || "application/octet-stream";
|
|
295
|
-
const cacheKey = (record.url || "") + "|" + contentType + "|" + (record.responseEncoding || "") + "|" +
|
|
296
|
-
(record.responseBodyBase64 ? "b64:" + record.responseBodyBase64.length : "txt:" + (record.responseBody ? record.responseBody.length : 0));
|
|
297
|
-
if (dataUrlCache.has(cacheKey)) {
|
|
298
|
-
return dataUrlCache.get(cacheKey);
|
|
299
|
-
}
|
|
300
|
-
if (record.responseEncoding === "base64" && record.responseBodyBase64) {
|
|
301
|
-
const dataUrl = "data:" + contentType + ";base64," + record.responseBodyBase64;
|
|
302
|
-
dataUrlCache.set(cacheKey, dataUrl);
|
|
303
|
-
return dataUrl;
|
|
304
|
-
}
|
|
305
|
-
if (record.responseBody) {
|
|
306
|
-
const dataUrl = "data:" + contentType + ";base64," + textToBase64(record.responseBody);
|
|
307
|
-
dataUrlCache.set(cacheKey, dataUrl);
|
|
308
|
-
return dataUrl;
|
|
309
|
-
}
|
|
310
|
-
const dataUrl = "data:" + (fallbackType || "application/octet-stream") + ",";
|
|
311
|
-
dataUrlCache.set(cacheKey, dataUrl);
|
|
312
|
-
return dataUrl;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const responseFromRecord = (record) => {
|
|
316
|
-
const headers = new Headers(record.responseHeaders || {});
|
|
317
|
-
if (record.responseEncoding === "base64" && record.responseBodyBase64) {
|
|
318
|
-
const bytes = decodeBase64(record.responseBodyBase64);
|
|
319
|
-
return new Response(bytes, {
|
|
320
|
-
status: record.status || 200,
|
|
321
|
-
statusText: record.statusText || "OK",
|
|
322
|
-
headers
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
const bodyText = record.responseBody || "";
|
|
326
|
-
return new Response(bodyText, {
|
|
327
|
-
status: record.status || 200,
|
|
328
|
-
statusText: record.statusText || "OK",
|
|
329
|
-
headers
|
|
330
|
-
});
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
${hackerScripts}
|
|
334
|
-
})();
|
|
335
|
-
</script>
|
|
336
|
-
`;
|
|
8
|
+
return [
|
|
9
|
+
buildReplayInjectedScriptPart1({
|
|
10
|
+
basePayload,
|
|
11
|
+
apiPayload,
|
|
12
|
+
resourceProxyScript
|
|
13
|
+
}),
|
|
14
|
+
buildReplayInjectedScriptPart2({
|
|
15
|
+
matchAPISource
|
|
16
|
+
}),
|
|
17
|
+
buildReplayInjectedScriptPart3({
|
|
18
|
+
hackerScripts
|
|
19
|
+
})
|
|
20
|
+
].join("");
|
|
337
21
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const buildReplayInjectedScriptPart1 = (options) => {
|
|
2
|
+
const { basePayload, apiPayload, resourceProxyScript } = options;
|
|
3
|
+
return `
|
|
4
|
+
<script>
|
|
5
|
+
(function(){
|
|
6
|
+
const baseUrl = ${basePayload};
|
|
7
|
+
const apiUrl = ${apiPayload};
|
|
8
|
+
const resourcesPathUrl = "/resources_path.json";
|
|
9
|
+
const __pagepocketOriginalFetch = window.fetch ? window.fetch.bind(window) : null;
|
|
10
|
+
|
|
11
|
+
${resourceProxyScript}
|
|
12
|
+
|
|
13
|
+
const loadResourcesPath = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const injected = window && window.__pagepocketResourcesPath;
|
|
16
|
+
if (injected && injected.version === "1.0" && Array.isArray(injected.items)) {
|
|
17
|
+
return injected;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!__pagepocketOriginalFetch) {
|
|
21
|
+
throw new Error("Fetch is unavailable");
|
|
22
|
+
}
|
|
23
|
+
const response = await __pagepocketOriginalFetch(resourcesPathUrl);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error("Failed to load resources_path.json");
|
|
26
|
+
}
|
|
27
|
+
const json = await response.json();
|
|
28
|
+
if (!json || json.version !== "1.0" || !Array.isArray(json.items)) {
|
|
29
|
+
throw new Error("Invalid resources_path.json");
|
|
30
|
+
}
|
|
31
|
+
return json;
|
|
32
|
+
} catch {
|
|
33
|
+
return { version: "1.0", createdAt: 0, items: [] };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const loadApiSnapshot = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const injected = window && window.__pagepocketApiSnapshot;
|
|
40
|
+
if (injected && injected.version === "1.0" && Array.isArray(injected.records)) {
|
|
41
|
+
return injected;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!__pagepocketOriginalFetch) {
|
|
45
|
+
throw new Error("Fetch is unavailable");
|
|
46
|
+
}
|
|
47
|
+
const response = await __pagepocketOriginalFetch(apiUrl);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error("Failed to load api.json");
|
|
50
|
+
}
|
|
51
|
+
return await response.json();
|
|
52
|
+
} catch {
|
|
53
|
+
return { version: "1.0", url: baseUrl, createdAt: 0, records: [] };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const originalResponseJson = Response && Response.prototype && Response.prototype.json;
|
|
58
|
+
if (originalResponseJson) {
|
|
59
|
+
Response.prototype.json = function(...args) {
|
|
60
|
+
try {
|
|
61
|
+
return originalResponseJson.apply(this, args).catch(() => null);
|
|
62
|
+
} catch {
|
|
63
|
+
return Promise.resolve(null);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ensureReplayPatches = () => {
|
|
69
|
+
try {
|
|
70
|
+
if (!window.fetch.__pagepocketOriginal && typeof __pagepocketOriginalFetch === "function") {
|
|
71
|
+
window.fetch.__pagepocketOriginal = __pagepocketOriginalFetch;
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
try {
|
|
75
|
+
if (!XMLHttpRequest.prototype.send.__pagepocketOriginal) {
|
|
76
|
+
XMLHttpRequest.prototype.send.__pagepocketOriginal = XMLHttpRequest.prototype.send;
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let records = [];
|
|
82
|
+
const byKey = new Map();
|
|
83
|
+
|
|
84
|
+
let resourceIndex = null;
|
|
85
|
+
let embeddedByPath = new Map();
|
|
86
|
+
const blobUrlByPath = new Map();
|
|
87
|
+
|
|
88
|
+
const normalizeUrl = (input) => {
|
|
89
|
+
try { return new URL(input, baseUrl).toString(); } catch { return input; }
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const normalizeBody = (body) => {
|
|
93
|
+
if (body === undefined || body === null) return "";
|
|
94
|
+
if (typeof body === "string") return body;
|
|
95
|
+
try { return String(body); } catch { return ""; }
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const makeKey = (method, url, body) => method.toUpperCase() + " " + normalizeUrl(url) + " " + normalizeBody(body);
|
|
99
|
+
const makeVariantKeys = (method, url, body) => [makeKey(method, url, body)];
|
|
100
|
+
`;
|
|
101
|
+
};
|