@pagepocket/lib 0.7.1 → 0.8.0
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 +7 -6
- package/dist/build-snapshot-from-bundle.d.ts +23 -0
- package/dist/build-snapshot-from-bundle.js +68 -0
- package/dist/builtin-blacklist.js +3 -6
- package/dist/bundle/from-network-store.d.ts +10 -0
- package/dist/bundle/from-network-store.js +26 -0
- package/dist/bundle/types.d.ts +32 -0
- package/dist/bundle/types.js +2 -0
- package/dist/capture/index.d.ts +14 -0
- package/dist/capture/index.js +86 -0
- package/dist/capture/memory-content-store.d.ts +4 -0
- package/dist/capture/memory-content-store.js +42 -0
- package/dist/capture/types.d.ts +61 -0
- package/dist/capture/types.js +2 -0
- package/dist/content-store.js +3 -8
- package/dist/content-type.d.ts +1 -1
- package/dist/content-type.js +2 -28
- package/dist/core/_impl/completion.d.ts +4 -0
- package/dist/core/_impl/completion.js +29 -0
- package/dist/core/_impl/content-store.d.ts +21 -0
- package/dist/core/_impl/content-store.js +91 -0
- package/dist/core/_impl/debug.d.ts +1 -0
- package/dist/core/_impl/debug.js +16 -0
- package/dist/core/_impl/inflight-tracker.d.ts +19 -0
- package/dist/core/_impl/inflight-tracker.js +48 -0
- package/dist/core/_impl/pagepocket.d.ts +27 -0
- package/dist/core/_impl/pagepocket.js +155 -0
- package/dist/core/capture/_impl/memory-content-store.d.ts +4 -0
- package/dist/core/capture/_impl/memory-content-store.js +42 -0
- package/dist/core/capture/_impl/types.d.ts +61 -0
- package/dist/core/capture/_impl/types.js +2 -0
- package/dist/core/capture/internal/memory-content-store.d.ts +4 -0
- package/dist/core/capture/internal/memory-content-store.js +42 -0
- package/dist/core/capture/internal/types.d.ts +61 -0
- package/dist/core/capture/internal/types.js +2 -0
- package/dist/core/capture/memory-content-store.d.ts +4 -0
- package/dist/core/capture/memory-content-store.js +38 -0
- package/dist/core/capture/types.d.ts +61 -0
- package/dist/core/capture/types.js +1 -0
- package/dist/core/completion.d.ts +4 -0
- package/dist/core/completion.js +23 -0
- package/dist/core/content-store.d.ts +21 -0
- package/dist/core/content-store.js +54 -0
- package/dist/core/debug.d.ts +1 -0
- package/dist/core/debug.js +12 -0
- package/dist/core/file-tree-merge.d.ts +2 -0
- package/dist/core/file-tree-merge.js +27 -0
- package/dist/core/file-tree.d.ts +36 -0
- package/dist/core/file-tree.js +1 -0
- package/dist/core/inflight-tracker.d.ts +19 -0
- package/dist/core/inflight-tracker.js +44 -0
- package/dist/core/internal/completion.d.ts +4 -0
- package/dist/core/internal/completion.js +29 -0
- package/dist/core/internal/content-store.d.ts +21 -0
- package/dist/core/internal/content-store.js +91 -0
- package/dist/core/internal/debug.d.ts +1 -0
- package/dist/core/internal/debug.js +16 -0
- package/dist/core/internal/inflight-tracker.d.ts +19 -0
- package/dist/core/internal/inflight-tracker.js +48 -0
- package/dist/core/internal/pagepocket.d.ts +27 -0
- package/dist/core/internal/pagepocket.js +155 -0
- package/dist/core/pagepocket.d.ts +38 -0
- package/dist/core/pagepocket.js +57 -0
- package/dist/core/plugin/_impl/context.d.ts +47 -0
- package/dist/core/plugin/_impl/context.js +142 -0
- package/dist/core/plugin/_impl/runner.d.ts +12 -0
- package/dist/core/plugin/_impl/runner.js +232 -0
- package/dist/core/plugin/_impl/types.d.ts +108 -0
- package/dist/core/plugin/_impl/types.js +2 -0
- package/dist/core/plugin/context.d.ts +47 -0
- package/dist/core/plugin/context.js +205 -0
- package/dist/core/plugin/internal/context.d.ts +47 -0
- package/dist/core/plugin/internal/context.js +142 -0
- package/dist/core/plugin/internal/runner.d.ts +12 -0
- package/dist/core/plugin/internal/runner.js +232 -0
- package/dist/core/plugin/internal/types.d.ts +108 -0
- package/dist/core/plugin/internal/types.js +2 -0
- package/dist/core/plugin/runner-utils.d.ts +9 -0
- package/dist/core/plugin/runner-utils.js +29 -0
- package/dist/core/plugin/runner.d.ts +12 -0
- package/dist/core/plugin/runner.js +118 -0
- package/dist/core/plugin/types.d.ts +117 -0
- package/dist/core/plugin/types.js +1 -0
- package/dist/core/runtime/types.d.ts +14 -0
- package/dist/core/runtime/types.js +2 -0
- package/dist/css-rewrite.js +1 -5
- package/dist/debug.d.ts +0 -1
- package/dist/debug.js +3 -5
- package/dist/files/types.d.ts +41 -0
- package/dist/files/types.js +2 -0
- package/dist/hack-html.js +20 -13
- package/dist/hackers/index.d.ts +1 -1
- package/dist/hackers/index.js +24 -27
- package/dist/hackers/preload-fetch.d.ts +1 -1
- package/dist/hackers/preload-fetch.js +1 -4
- package/dist/hackers/preload-xhr.d.ts +1 -1
- package/dist/hackers/preload-xhr.js +1 -4
- package/dist/hackers/replay-beacon.d.ts +1 -1
- package/dist/hackers/replay-beacon.js +1 -4
- package/dist/hackers/replay-block-text-fragment.d.ts +1 -1
- package/dist/hackers/replay-block-text-fragment.js +1 -4
- package/dist/hackers/replay-css-proxy.d.ts +1 -1
- package/dist/hackers/replay-css-proxy.js +9 -12
- package/dist/hackers/replay-dom-rewrite.d.ts +1 -1
- package/dist/hackers/replay-dom-rewrite.js +165 -154
- package/dist/hackers/replay-eventsource.d.ts +1 -1
- package/dist/hackers/replay-eventsource.js +1 -4
- package/dist/hackers/replay-fetch.d.ts +1 -1
- package/dist/hackers/replay-fetch.js +1 -4
- package/dist/hackers/replay-history-path.d.ts +1 -1
- package/dist/hackers/replay-history-path.js +1 -4
- package/dist/hackers/replay-svg-image.d.ts +1 -1
- package/dist/hackers/replay-svg-image.js +1 -4
- package/dist/hackers/replay-websocket.d.ts +1 -1
- package/dist/hackers/replay-websocket.js +1 -4
- package/dist/hackers/replay-xhr.d.ts +1 -1
- package/dist/hackers/replay-xhr.js +1 -4
- package/dist/hackers/types.js +1 -2
- package/dist/index.d.ts +29 -13
- package/dist/index.js +23 -44
- package/dist/kind-map.d.ts +68 -0
- package/dist/kind-map.js +58 -0
- package/dist/network-store.js +12 -1
- package/dist/pagepocket.d.ts +19 -4
- package/dist/pagepocket.js +36 -102
- package/dist/path-resolver.d.ts +1 -2
- package/dist/path-resolver.js +9 -16
- package/dist/plugin/builtins/build-snapshot-plugin.d.ts +5 -0
- package/dist/plugin/builtins/build-snapshot-plugin.js +84 -0
- package/dist/plugin/builtins/replace-elements-plugin.d.ts +8 -0
- package/dist/plugin/builtins/replace-elements-plugin.js +13 -0
- package/dist/plugin/builtins/to-directory-plugin.d.ts +7 -0
- package/dist/plugin/builtins/to-directory-plugin.js +20 -0
- package/dist/plugin/builtins/to-zip-plugin.d.ts +5 -0
- package/dist/plugin/builtins/to-zip-plugin.js +19 -0
- package/dist/plugin/context.d.ts +47 -0
- package/dist/plugin/context.js +142 -0
- package/dist/plugin/runner.d.ts +12 -0
- package/dist/plugin/runner.js +232 -0
- package/dist/plugin/types.d.ts +108 -0
- package/dist/plugin/types.js +2 -0
- package/dist/plugins/build-files-from-capture.d.ts +5 -0
- package/dist/plugins/build-files-from-capture.js +85 -0
- package/dist/plugins/build-warc.d.ts +5 -0
- package/dist/plugins/build-warc.js +225 -0
- package/dist/plugins/builtins/manifest.d.ts +2 -0
- package/dist/plugins/builtins/manifest.js +42 -0
- package/dist/plugins/builtins/snapshot-directory.d.ts +2 -0
- package/dist/plugins/builtins/snapshot-directory.js +24 -0
- package/dist/plugins/builtins/snapshot-zip.d.ts +2 -0
- package/dist/plugins/builtins/snapshot-zip.js +25 -0
- package/dist/plugins/capture-http-lighterceptor.d.ts +5 -0
- package/dist/plugins/capture-http-lighterceptor.js +85 -0
- package/dist/plugins/capture-http-puppeteer.d.ts +5 -0
- package/dist/plugins/capture-http-puppeteer.js +85 -0
- package/dist/plugins/host.d.ts +37 -0
- package/dist/plugins/host.js +105 -0
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.js +11 -0
- package/dist/plugins/ordering.d.ts +2 -0
- package/dist/plugins/ordering.js +19 -0
- package/dist/plugins/types.d.ts +51 -0
- package/dist/plugins/types.js +2 -0
- package/dist/preload.js +3 -7
- package/dist/replace-elements/actions.d.ts +5 -0
- package/dist/replace-elements/actions.js +86 -0
- package/dist/replace-elements/match.d.ts +5 -0
- package/dist/replace-elements/match.js +46 -0
- package/dist/replace-elements/normalize.d.ts +21 -0
- package/dist/replace-elements/normalize.js +50 -0
- package/dist/replace-elements.d.ts +1 -1
- package/dist/replace-elements.js +5 -185
- package/dist/replay/match-api.d.ts +10 -0
- package/dist/replay/match-api.js +162 -0
- package/dist/replay/templates/match-api-source.d.ts +1 -0
- package/dist/replay/templates/match-api-source.js +137 -0
- package/dist/replay/templates/replay-script-template.d.ts +5 -0
- package/dist/replay/templates/replay-script-template.js +337 -0
- package/dist/replay/templates/resource-proxy-script.d.ts +1 -0
- package/dist/replay/templates/resource-proxy-script.js +274 -0
- package/dist/replay-script.d.ts +3 -10
- package/dist/replay-script.js +11 -625
- package/dist/resource-filter.d.ts +1 -1
- package/dist/resource-filter.js +1 -5
- package/dist/resource-proxy/escape-percent.d.ts +1 -0
- package/dist/resource-proxy/escape-percent.js +12 -0
- package/dist/resource-proxy/multimap.d.ts +3 -0
- package/dist/resource-proxy/multimap.js +18 -0
- package/dist/resource-proxy/pathname-variants.d.ts +3 -0
- package/dist/resource-proxy/pathname-variants.js +54 -0
- package/dist/resource-proxy.d.ts +4 -2
- package/dist/resource-proxy.js +48 -117
- package/dist/resources.js +4 -42
- package/dist/rewrite-links/js-imports.d.ts +3 -0
- package/dist/rewrite-links/js-imports.js +56 -0
- package/dist/rewrite-links/link-rel.d.ts +2 -0
- package/dist/rewrite-links/link-rel.js +10 -0
- package/dist/rewrite-links/meta-refresh.d.ts +3 -0
- package/dist/rewrite-links/meta-refresh.js +22 -0
- package/dist/rewrite-links/skip.d.ts +1 -0
- package/dist/rewrite-links/skip.js +10 -0
- package/dist/rewrite-links/srcset.d.ts +3 -0
- package/dist/rewrite-links/srcset.js +63 -0
- package/dist/rewrite-links/url-resolve.d.ts +3 -0
- package/dist/rewrite-links/url-resolve.js +13 -0
- package/dist/rewrite-links.d.ts +3 -3
- package/dist/rewrite-links.js +31 -240
- package/dist/snapshot-builder/api.d.ts +3 -0
- package/dist/snapshot-builder/api.js +6 -0
- package/dist/snapshot-builder/build-snapshot.d.ts +3 -0
- package/dist/snapshot-builder/build-snapshot.js +138 -0
- package/dist/snapshot-builder/capture-index/index-capture.d.ts +13 -0
- package/dist/snapshot-builder/capture-index/index-capture.js +168 -0
- package/dist/snapshot-builder/capture-index/index.d.ts +2 -0
- package/dist/snapshot-builder/capture-index/index.js +1 -0
- package/dist/snapshot-builder/capture-index/types.d.ts +12 -0
- package/dist/snapshot-builder/capture-index/types.js +1 -0
- package/dist/snapshot-builder/capture-index.d.ts +12 -0
- package/dist/snapshot-builder/capture-index.js +173 -0
- package/dist/snapshot-builder/emit-document.d.ts +24 -0
- package/dist/snapshot-builder/emit-document.js +50 -0
- package/dist/snapshot-builder/grouping.d.ts +8 -0
- package/dist/snapshot-builder/grouping.js +87 -0
- package/dist/snapshot-builder/http.d.ts +6 -0
- package/dist/snapshot-builder/http.js +28 -0
- package/dist/snapshot-builder/index.d.ts +4 -0
- package/dist/snapshot-builder/index.js +2 -0
- package/dist/snapshot-builder/path-map.d.ts +3 -0
- package/dist/snapshot-builder/path-map.js +35 -0
- package/dist/snapshot-builder/resources-path.d.ts +23 -0
- package/dist/snapshot-builder/resources-path.js +47 -0
- package/dist/snapshot-builder/rewrite-resource.d.ts +18 -0
- package/dist/snapshot-builder/rewrite-resource.js +52 -0
- package/dist/snapshot-builder/types.d.ts +37 -0
- package/dist/snapshot-builder/types.js +2 -0
- package/dist/snapshot-builder.d.ts +12 -8
- package/dist/snapshot-builder.js +252 -27
- package/dist/types.d.ts +122 -78
- package/dist/types.js +4 -2
- package/dist/units/contracts-bridge.d.ts +76 -0
- package/dist/units/contracts-bridge.js +6 -0
- package/dist/units/index.d.ts +4 -0
- package/dist/units/index.js +2 -0
- package/dist/units/runner.d.ts +11 -0
- package/dist/units/runner.js +270 -0
- package/dist/units/types.d.ts +39 -0
- package/dist/units/types.js +1 -0
- package/dist/utils/streams.d.ts +2 -0
- package/dist/utils/streams.js +29 -0
- package/dist/utils.d.ts +35 -1
- package/dist/utils.js +107 -29
- package/dist/v3/contracts-bridge.d.ts +69 -0
- package/dist/v3/contracts-bridge.js +5 -0
- package/dist/v3/index.d.ts +4 -0
- package/dist/v3/index.js +2 -0
- package/dist/v3/runner.d.ts +20 -0
- package/dist/v3/runner.js +245 -0
- package/dist/v3/types.d.ts +39 -0
- package/dist/v3/types.js +1 -0
- package/dist/writers.js +3 -1
- package/package.json +11 -3
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { urlEquivalent } from "../utils.js";
|
|
2
|
+
export function matchAPI(options) {
|
|
3
|
+
const { records, byKey, baseUrl, method, url, body } = options;
|
|
4
|
+
const normalizeBody = (value) => {
|
|
5
|
+
if (value === undefined || value === null)
|
|
6
|
+
return "";
|
|
7
|
+
if (typeof value === "string")
|
|
8
|
+
return value;
|
|
9
|
+
try {
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const normalizeUrl = (input) => {
|
|
17
|
+
try {
|
|
18
|
+
return new URL(input, baseUrl).toString();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return input;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const stripHash = (value) => {
|
|
25
|
+
const index = value.indexOf("#");
|
|
26
|
+
return index === -1 ? value : value.slice(0, index);
|
|
27
|
+
};
|
|
28
|
+
const stripTrailingSlash = (value) => {
|
|
29
|
+
if (value.length > 1 && value.endsWith("/")) {
|
|
30
|
+
return value.slice(0, -1);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
const safeUrl = (input) => {
|
|
35
|
+
try {
|
|
36
|
+
return new URL(input, baseUrl);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const toPathSearch = (input) => {
|
|
43
|
+
const parsed = safeUrl(input);
|
|
44
|
+
if (!parsed)
|
|
45
|
+
return input;
|
|
46
|
+
return parsed.pathname + parsed.search;
|
|
47
|
+
};
|
|
48
|
+
const toPathname = (input) => {
|
|
49
|
+
const parsed = safeUrl(input);
|
|
50
|
+
return parsed ? parsed.pathname : input;
|
|
51
|
+
};
|
|
52
|
+
const buildUrlVariants = (input) => {
|
|
53
|
+
const variants = new Set();
|
|
54
|
+
const push = (value) => {
|
|
55
|
+
if (!value)
|
|
56
|
+
return;
|
|
57
|
+
variants.add(value);
|
|
58
|
+
};
|
|
59
|
+
const raw = String(input ?? "");
|
|
60
|
+
push(raw);
|
|
61
|
+
push(stripHash(raw));
|
|
62
|
+
push(stripTrailingSlash(raw));
|
|
63
|
+
push(stripTrailingSlash(stripHash(raw)));
|
|
64
|
+
const absolute = normalizeUrl(raw);
|
|
65
|
+
push(absolute);
|
|
66
|
+
const absoluteNoHash = stripHash(absolute);
|
|
67
|
+
push(absoluteNoHash);
|
|
68
|
+
push(stripTrailingSlash(absoluteNoHash));
|
|
69
|
+
const pathSearch = toPathSearch(raw);
|
|
70
|
+
push(pathSearch);
|
|
71
|
+
push(stripTrailingSlash(pathSearch));
|
|
72
|
+
const pathname = toPathname(raw);
|
|
73
|
+
push(pathname);
|
|
74
|
+
push(stripTrailingSlash(pathname));
|
|
75
|
+
return Array.from(variants);
|
|
76
|
+
};
|
|
77
|
+
const makeKey = (keyMethod, keyUrl, keyBody) => keyMethod.toUpperCase() + " " + normalizeUrl(keyUrl) + " " + normalizeBody(keyBody);
|
|
78
|
+
const urlVariants = buildUrlVariants(url);
|
|
79
|
+
const bodyValue = normalizeBody(body);
|
|
80
|
+
const methodValue = (method || "GET").toUpperCase();
|
|
81
|
+
const tryLookup = (keyMethod, keyBody) => {
|
|
82
|
+
if (!byKey)
|
|
83
|
+
return undefined;
|
|
84
|
+
for (const urlVariant of urlVariants) {
|
|
85
|
+
const record = byKey.get(makeKey(keyMethod, urlVariant, keyBody));
|
|
86
|
+
if (record)
|
|
87
|
+
return record;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
};
|
|
91
|
+
const matchOrder = [
|
|
92
|
+
[methodValue, bodyValue],
|
|
93
|
+
[methodValue, ""],
|
|
94
|
+
["GET", ""],
|
|
95
|
+
["GET", bodyValue]
|
|
96
|
+
];
|
|
97
|
+
for (const [keyMethod, keyBody] of matchOrder) {
|
|
98
|
+
const record = tryLookup(keyMethod, keyBody);
|
|
99
|
+
if (record)
|
|
100
|
+
return record;
|
|
101
|
+
}
|
|
102
|
+
const toUrlOrNull = (value) => {
|
|
103
|
+
try {
|
|
104
|
+
return new URL(value, baseUrl);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const normalizePath = (value) => {
|
|
111
|
+
if (value.length > 1 && value.endsWith("/")) {
|
|
112
|
+
return value.slice(0, -1);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
};
|
|
116
|
+
const urlMatches = (inputUrl, recordUrl) => {
|
|
117
|
+
// Strong match first.
|
|
118
|
+
if (urlEquivalent(inputUrl, recordUrl, {
|
|
119
|
+
baseUrl,
|
|
120
|
+
ignoreHash: true,
|
|
121
|
+
ignoreDefaultPort: true,
|
|
122
|
+
normalizeTrailingSlash: true
|
|
123
|
+
})) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// Relaxed fallback: allow origin mismatch but keep path/search stable.
|
|
127
|
+
const left = toUrlOrNull(inputUrl);
|
|
128
|
+
const right = toUrlOrNull(recordUrl);
|
|
129
|
+
if (!left || !right) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const leftPathSearch = normalizePath(left.pathname) + left.search;
|
|
133
|
+
const rightPathSearch = normalizePath(right.pathname) + right.search;
|
|
134
|
+
if (leftPathSearch === rightPathSearch) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const leftPath = normalizePath(left.pathname);
|
|
138
|
+
const rightPath = normalizePath(right.pathname);
|
|
139
|
+
return leftPath === rightPath;
|
|
140
|
+
};
|
|
141
|
+
const scanRecords = (keyMethod, keyBody) => {
|
|
142
|
+
for (const record of records || []) {
|
|
143
|
+
if (!record || !record.url || !record.method)
|
|
144
|
+
continue;
|
|
145
|
+
if (record.method.toUpperCase() !== keyMethod)
|
|
146
|
+
continue;
|
|
147
|
+
if (!urlMatches(url, record.url))
|
|
148
|
+
continue;
|
|
149
|
+
const recordBody = record.requestBody || record.requestBodyBase64 || "";
|
|
150
|
+
if (keyBody && recordBody !== keyBody)
|
|
151
|
+
continue;
|
|
152
|
+
return record;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
};
|
|
156
|
+
for (const [keyMethod, keyBody] of matchOrder) {
|
|
157
|
+
const record = scanRecords(keyMethod, keyBody);
|
|
158
|
+
if (record)
|
|
159
|
+
return record;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const matchAPISource = "function matchAPI(options) {\n const { records, byKey, baseUrl, method, url, body } = options;\n const normalizeBody = (value) => {\n if (value === undefined || value === null)\n return \"\";\n if (typeof value === \"string\")\n return value;\n try {\n return String(value);\n }\n catch {\n return \"\";\n }\n };\n const normalizeUrl = (input) => {\n try {\n return new URL(input, baseUrl).toString();\n }\n catch {\n return input;\n }\n };\n const stripHash = (value) => {\n const index = value.indexOf(\"#\");\n return index === -1 ? value : value.slice(0, index);\n };\n const stripTrailingSlash = (value) => {\n if (value.length > 1 && value.endsWith(\"/\")) {\n return value.slice(0, -1);\n }\n return value;\n };\n const safeUrl = (input) => {\n try {\n return new URL(input, baseUrl);\n }\n catch {\n return null;\n }\n };\n const toPathSearch = (input) => {\n const parsed = safeUrl(input);\n if (!parsed)\n return input;\n return parsed.pathname + parsed.search;\n };\n const toPathname = (input) => {\n const parsed = safeUrl(input);\n return parsed ? parsed.pathname : input;\n };\n const buildUrlVariants = (input) => {\n const variants = new Set();\n const push = (value) => {\n if (!value)\n return;\n variants.add(value);\n };\n const raw = String(input ?? \"\");\n push(raw);\n push(stripHash(raw));\n push(stripTrailingSlash(raw));\n push(stripTrailingSlash(stripHash(raw)));\n const absolute = normalizeUrl(raw);\n push(absolute);\n const absoluteNoHash = stripHash(absolute);\n push(absoluteNoHash);\n push(stripTrailingSlash(absoluteNoHash));\n const pathSearch = toPathSearch(raw);\n push(pathSearch);\n push(stripTrailingSlash(pathSearch));\n const pathname = toPathname(raw);\n push(pathname);\n push(stripTrailingSlash(pathname));\n return Array.from(variants);\n };\n const makeKey = (keyMethod, keyUrl, keyBody) => keyMethod.toUpperCase() + \" \" + normalizeUrl(keyUrl) + \" \" + normalizeBody(keyBody);\n const urlVariants = buildUrlVariants(url);\n const bodyValue = normalizeBody(body);\n const methodValue = (method || \"GET\").toUpperCase();\n const tryLookup = (keyMethod, keyBody) => {\n if (!byKey)\n return undefined;\n for (const urlVariant of urlVariants) {\n const record = byKey.get(makeKey(keyMethod, urlVariant, keyBody));\n if (record)\n return record;\n }\n return undefined;\n };\n const matchOrder = [\n [methodValue, bodyValue],\n [methodValue, \"\"],\n [\"GET\", \"\"],\n [\"GET\", bodyValue]\n ];\n for (const [keyMethod, keyBody] of matchOrder) {\n const record = tryLookup(keyMethod, keyBody);\n if (record)\n return record;\n }\n const urlMatches = (inputUrl, recordUrl) => {\n const inputAbs = stripHash(normalizeUrl(inputUrl));\n const recordAbs = stripHash(normalizeUrl(recordUrl));\n if (inputAbs === recordAbs)\n return true;\n const inputPathSearch = stripTrailingSlash(toPathSearch(inputUrl));\n const recordPathSearch = stripTrailingSlash(toPathSearch(recordUrl));\n if (inputPathSearch === recordPathSearch)\n return true;\n const inputPath = stripTrailingSlash(toPathname(inputUrl));\n const recordPath = stripTrailingSlash(toPathname(recordUrl));\n if (inputPath === recordPath)\n return true;\n return false;\n };\n const scanRecords = (keyMethod, keyBody) => {\n for (const record of records || []) {\n if (!record || !record.url || !record.method)\n continue;\n if (record.method.toUpperCase() !== keyMethod)\n continue;\n if (!urlMatches(url, record.url))\n continue;\n const recordBody = record.requestBody || record.requestBodyBase64 || \"\";\n if (keyBody && recordBody !== keyBody)\n continue;\n return record;\n }\n return undefined;\n };\n for (const [keyMethod, keyBody] of matchOrder) {\n const record = scanRecords(keyMethod, keyBody);\n if (record)\n return record;\n }\n return undefined;\n}";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export const matchAPISource = `function matchAPI(options) {
|
|
2
|
+
const { records, byKey, baseUrl, method, url, body } = options;
|
|
3
|
+
const normalizeBody = (value) => {
|
|
4
|
+
if (value === undefined || value === null)
|
|
5
|
+
return "";
|
|
6
|
+
if (typeof value === "string")
|
|
7
|
+
return value;
|
|
8
|
+
try {
|
|
9
|
+
return String(value);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const normalizeUrl = (input) => {
|
|
16
|
+
try {
|
|
17
|
+
return new URL(input, baseUrl).toString();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return input;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const stripHash = (value) => {
|
|
24
|
+
const index = value.indexOf("#");
|
|
25
|
+
return index === -1 ? value : value.slice(0, index);
|
|
26
|
+
};
|
|
27
|
+
const stripTrailingSlash = (value) => {
|
|
28
|
+
if (value.length > 1 && value.endsWith("/")) {
|
|
29
|
+
return value.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
};
|
|
33
|
+
const safeUrl = (input) => {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(input, baseUrl);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const toPathSearch = (input) => {
|
|
42
|
+
const parsed = safeUrl(input);
|
|
43
|
+
if (!parsed)
|
|
44
|
+
return input;
|
|
45
|
+
return parsed.pathname + parsed.search;
|
|
46
|
+
};
|
|
47
|
+
const toPathname = (input) => {
|
|
48
|
+
const parsed = safeUrl(input);
|
|
49
|
+
return parsed ? parsed.pathname : input;
|
|
50
|
+
};
|
|
51
|
+
const buildUrlVariants = (input) => {
|
|
52
|
+
const variants = new Set();
|
|
53
|
+
const push = (value) => {
|
|
54
|
+
if (!value)
|
|
55
|
+
return;
|
|
56
|
+
variants.add(value);
|
|
57
|
+
};
|
|
58
|
+
const raw = String(input ?? "");
|
|
59
|
+
push(raw);
|
|
60
|
+
push(stripHash(raw));
|
|
61
|
+
push(stripTrailingSlash(raw));
|
|
62
|
+
push(stripTrailingSlash(stripHash(raw)));
|
|
63
|
+
const absolute = normalizeUrl(raw);
|
|
64
|
+
push(absolute);
|
|
65
|
+
const absoluteNoHash = stripHash(absolute);
|
|
66
|
+
push(absoluteNoHash);
|
|
67
|
+
push(stripTrailingSlash(absoluteNoHash));
|
|
68
|
+
const pathSearch = toPathSearch(raw);
|
|
69
|
+
push(pathSearch);
|
|
70
|
+
push(stripTrailingSlash(pathSearch));
|
|
71
|
+
const pathname = toPathname(raw);
|
|
72
|
+
push(pathname);
|
|
73
|
+
push(stripTrailingSlash(pathname));
|
|
74
|
+
return Array.from(variants);
|
|
75
|
+
};
|
|
76
|
+
const makeKey = (keyMethod, keyUrl, keyBody) => keyMethod.toUpperCase() + " " + normalizeUrl(keyUrl) + " " + normalizeBody(keyBody);
|
|
77
|
+
const urlVariants = buildUrlVariants(url);
|
|
78
|
+
const bodyValue = normalizeBody(body);
|
|
79
|
+
const methodValue = (method || "GET").toUpperCase();
|
|
80
|
+
const tryLookup = (keyMethod, keyBody) => {
|
|
81
|
+
if (!byKey)
|
|
82
|
+
return undefined;
|
|
83
|
+
for (const urlVariant of urlVariants) {
|
|
84
|
+
const record = byKey.get(makeKey(keyMethod, urlVariant, keyBody));
|
|
85
|
+
if (record)
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
const matchOrder = [
|
|
91
|
+
[methodValue, bodyValue],
|
|
92
|
+
[methodValue, ""],
|
|
93
|
+
["GET", ""],
|
|
94
|
+
["GET", bodyValue]
|
|
95
|
+
];
|
|
96
|
+
for (const [keyMethod, keyBody] of matchOrder) {
|
|
97
|
+
const record = tryLookup(keyMethod, keyBody);
|
|
98
|
+
if (record)
|
|
99
|
+
return record;
|
|
100
|
+
}
|
|
101
|
+
const urlMatches = (inputUrl, recordUrl) => {
|
|
102
|
+
const inputAbs = stripHash(normalizeUrl(inputUrl));
|
|
103
|
+
const recordAbs = stripHash(normalizeUrl(recordUrl));
|
|
104
|
+
if (inputAbs === recordAbs)
|
|
105
|
+
return true;
|
|
106
|
+
const inputPathSearch = stripTrailingSlash(toPathSearch(inputUrl));
|
|
107
|
+
const recordPathSearch = stripTrailingSlash(toPathSearch(recordUrl));
|
|
108
|
+
if (inputPathSearch === recordPathSearch)
|
|
109
|
+
return true;
|
|
110
|
+
const inputPath = stripTrailingSlash(toPathname(inputUrl));
|
|
111
|
+
const recordPath = stripTrailingSlash(toPathname(recordUrl));
|
|
112
|
+
if (inputPath === recordPath)
|
|
113
|
+
return true;
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
const scanRecords = (keyMethod, keyBody) => {
|
|
117
|
+
for (const record of records || []) {
|
|
118
|
+
if (!record || !record.url || !record.method)
|
|
119
|
+
continue;
|
|
120
|
+
if (record.method.toUpperCase() !== keyMethod)
|
|
121
|
+
continue;
|
|
122
|
+
if (!urlMatches(url, record.url))
|
|
123
|
+
continue;
|
|
124
|
+
const recordBody = record.requestBody || record.requestBodyBase64 || "";
|
|
125
|
+
if (keyBody && recordBody !== keyBody)
|
|
126
|
+
continue;
|
|
127
|
+
return record;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
};
|
|
131
|
+
for (const [keyMethod, keyBody] of matchOrder) {
|
|
132
|
+
const record = scanRecords(keyMethod, keyBody);
|
|
133
|
+
if (record)
|
|
134
|
+
return record;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}`;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { matchAPISource } from "./match-api-source.js";
|
|
2
|
+
import { resourceProxyScript } from "./resource-proxy-script.js";
|
|
3
|
+
export const buildReplayInjectedScript = (options) => {
|
|
4
|
+
const { basePayload, apiPayload, hackerScripts } = options;
|
|
5
|
+
return `
|
|
6
|
+
<script>
|
|
7
|
+
(function(){
|
|
8
|
+
const baseUrl = ${basePayload};
|
|
9
|
+
const apiUrl = ${apiPayload};
|
|
10
|
+
const resourcesPathUrl = "/resources_path.json";
|
|
11
|
+
const __pagepocketOriginalFetch = window.fetch ? window.fetch.bind(window) : null;
|
|
12
|
+
|
|
13
|
+
${resourceProxyScript}
|
|
14
|
+
|
|
15
|
+
const loadResourcesPath = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const injected = window && window.__pagepocketResourcesPath;
|
|
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
|
+
`;
|
|
337
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const resourceProxyScript = "\n const __pagepocketResourceProxy = (() => {\n const stripHash = (value) => {\n const input = String(value || \"\");\n const index = input.indexOf(\"#\");\n return index === -1 ? input : input.slice(0, index);\n };\n\n const stripTrailingSlash = (value) => {\n const input = String(value || \"\");\n if (!input || input === \"/\") return input;\n return input.endsWith(\"/\") ? input.slice(0, -1) : input;\n };\n\n const looksAlreadyEscapedForStaticServers = (value) => {\n return /%25[0-9a-fA-F]{2}/.test(String(value || \"\"));\n };\n\n const escapePercentForStaticServersOnce = (value) => {\n const input = String(value || \"\");\n if (!input) return input;\n if (looksAlreadyEscapedForStaticServers(input)) return input;\n return input.split(\"%\").join(\"%25\");\n };\n\n const isLikelyHostname = (value) => {\n const input = String(value || \"\");\n if (!input) return false;\n if (input === \"localhost\") return true;\n return input.includes(\".\");\n };\n\n const encodeEmbeddedUrlTailIfPresent = (pathname) => {\n const raw = String(pathname || \"\");\n if (!raw.includes(\"/http\")) return null;\n const parts = raw.split(\"/\");\n for (let i = 0; i < parts.length; i += 1) {\n const scheme = parts[i];\n if (scheme !== \"http:\" && scheme !== \"https:\") continue;\n const hasDoubleSlash = parts[i + 1] === \"\";\n const host = parts[i + 2] || \"\";\n if (!hasDoubleSlash || !isLikelyHostname(host)) continue;\n const embedded = scheme + \"//\" + parts.slice(i + 2).join(\"/\");\n const encoded = encodeURIComponent(embedded);\n const nextParts = parts.slice(0, i).concat(encoded);\n const rebuilt = nextParts.join(\"/\") || \"/\";\n return rebuilt.startsWith(\"/\") ? rebuilt : \"/\" + rebuilt;\n }\n return null;\n };\n\n const makePathnameVariants = (pathname) => {\n const variants = new Set();\n const push = (value) => {\n if (!value) return;\n variants.add(value);\n };\n\n const raw = String(pathname || \"\");\n push(raw);\n push(stripTrailingSlash(raw));\n\n const encodedTail = encodeEmbeddedUrlTailIfPresent(raw);\n if (encodedTail && encodedTail !== raw) {\n push(encodedTail);\n push(stripTrailingSlash(encodedTail));\n }\n\n return Array.from(variants);\n };\n\n const toUrlOrNull = (value, base) => {\n try {\n return new URL(String(value || \"\"), String(base || \"\"));\n } catch {\n return null;\n }\n };\n\n const getBasename = (pathname) => {\n const clean = String(pathname || \"\").split(\"?\")[0] || \"\";\n const parts = clean.split(\"/\").filter(Boolean);\n return parts[parts.length - 1] || \"\";\n };\n\n const makeSuffixes = (pathname) => {\n const parts = String(pathname || \"\").split(\"/\").filter(Boolean);\n const out = [];\n for (let i = 0; i < parts.length; i += 1) {\n out.push({ key: \"/\" + parts.slice(i).join(\"/\"), depth: parts.length - i });\n }\n return out;\n };\n\n const toArray = (value) => {\n if (!value) return [];\n return Array.isArray(value) ? value : [value];\n };\n\n const addMulti = (map, key, value) => {\n const existing = map.get(key);\n if (!existing) {\n map.set(key, value);\n return;\n }\n if (Array.isArray(existing)) {\n existing.push(value);\n return;\n }\n map.set(key, [existing, value]);\n };\n\n const uniqByPath = (items) => {\n const seen = new Set();\n const out = [];\n for (const item of items || []) {\n const p = item && item.path;\n if (!p || seen.has(p)) continue;\n seen.add(p);\n out.push(item);\n }\n return out;\n };\n\n const chooseUnique = (items, baseUrl, depth) => {\n const unique = uniqByPath(items);\n if (unique.length === 0) return null;\n if (unique.length === 1) return unique[0];\n\n try {\n const base = new URL(String(baseUrl || \"\"));\n const sameOrigin = unique.filter((i) => i && i.origin === base.origin);\n if (sameOrigin.length === 1) {\n return sameOrigin[0];\n }\n if (sameOrigin.length > 1) {\n items = sameOrigin;\n }\n } catch {}\n\n if (depth < 2) {\n return null;\n }\n\n return null;\n };\n\n const buildIndex = (snapshot) => {\n const byExactUrl = new Map();\n const byCanonicalUrl = new Map();\n const byPathnameWithSearch = new Map();\n const byPathname = new Map();\n const byBasename = new Map();\n\n const canonicalizeHttpUrlForIndex = (url) => {\n try {\n if (!url || !url.protocol) return String(url || \"\");\n if (url.protocol === \"http:\" || url.protocol === \"https:\") {\n return \"//\" + url.host + (url.pathname || \"\") + (url.search || \"\");\n }\n return url.toString();\n } catch {\n return String(url || \"\");\n }\n };\n\n for (const item of (snapshot && snapshot.items) || []) {\n if (!item || !item.url || !item.path) continue;\n let parsed = null;\n try {\n parsed = new URL(item.url);\n } catch {\n parsed = null;\n }\n if (!parsed) continue;\n\n const pathname = parsed.pathname || \"/\";\n const pathnameWithSearch = pathname + (parsed.search || \"\");\n const basename = getBasename(pathname);\n\n const indexed = {\n url: parsed.toString(),\n canonicalUrl: canonicalizeHttpUrlForIndex(parsed),\n origin: parsed.origin,\n pathname,\n pathnameWithSearch,\n basename,\n path: item.path,\n resourceType: item.resourceType,\n mimeType: item.mimeType,\n size: item.size\n };\n\n if (!byExactUrl.has(indexed.url)) {\n byExactUrl.set(indexed.url, indexed);\n }\n\n if (!byCanonicalUrl.has(indexed.canonicalUrl)) {\n byCanonicalUrl.set(indexed.canonicalUrl, indexed);\n }\n addMulti(byPathnameWithSearch, pathnameWithSearch, indexed);\n addMulti(byPathname, pathname, indexed);\n if (basename) {\n addMulti(byBasename, basename, indexed);\n }\n }\n\n return { byExactUrl, byCanonicalUrl, byPathnameWithSearch, byPathname, byBasename };\n };\n\n const resolveToLocalPath = (requestUrl, baseUrl, index) => {\n if (!requestUrl) return null;\n const abs = toUrlOrNull(String(requestUrl), baseUrl);\n if (!abs) return null;\n\n const absString = abs.toString();\n const exact = index && index.byExactUrl && index.byExactUrl.get(absString);\n if (exact) return escapePercentForStaticServersOnce(exact.path);\n\n const canonicalAbs = \"//\" + abs.host + (abs.pathname || \"\") + (abs.search || \"\");\n const canonicalExact = index && index.byCanonicalUrl && index.byCanonicalUrl.get(canonicalAbs);\n if (canonicalExact) return escapePercentForStaticServersOnce(canonicalExact.path);\n\n const withoutHash = stripHash(absString);\n if (withoutHash !== absString) {\n const found = index.byExactUrl.get(withoutHash);\n if (found) return escapePercentForStaticServersOnce(found.path);\n\n try {\n const w = new URL(withoutHash);\n const canonicalWithoutHash = \"//\" + w.host + (w.pathname || \"\") + (w.search || \"\");\n const canonicalFound = index && index.byCanonicalUrl && index.byCanonicalUrl.get(canonicalWithoutHash);\n if (canonicalFound) return escapePercentForStaticServersOnce(canonicalFound.path);\n } catch {}\n }\n\n const pathname = abs.pathname || \"/\";\n const pathnameVariants = makePathnameVariants(pathname);\n const search = abs.search || \"\";\n const pathnameWithSearchVariants = pathnameVariants.map((p) => String(p) + search);\n\n for (const key of pathnameWithSearchVariants) {\n const items = toArray(index.byPathnameWithSearch.get(key));\n const chosen = chooseUnique(items, baseUrl, 99);\n if (chosen) return escapePercentForStaticServersOnce(chosen.path);\n }\n\n for (const key of pathnameVariants) {\n const items = toArray(index.byPathname.get(key));\n const chosen = chooseUnique(items, baseUrl, 99);\n if (chosen) return escapePercentForStaticServersOnce(chosen.path);\n }\n\n for (const variant of pathnameVariants) {\n for (const suffix of makeSuffixes(variant)) {\n const items = toArray(index.byPathname.get(suffix.key));\n const chosen = chooseUnique(items, baseUrl, suffix.depth);\n if (chosen) return escapePercentForStaticServersOnce(chosen.path);\n }\n }\n\n const basename = getBasename(pathname);\n if (basename) {\n const items = toArray(index.byBasename.get(basename));\n const chosen = chooseUnique(items, baseUrl, 1);\n if (chosen) return escapePercentForStaticServersOnce(chosen.path);\n }\n\n return null;\n };\n\n return { buildIndex, resolveToLocalPath };\n })();\n";
|