surf-cli 2.18.0 → 2.20.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 +276 -8
- package/dist/content/index.js +4 -4
- package/dist/content/index.js.map +1 -1
- package/dist/options/options.html +1 -18
- package/dist/service-worker/index.js +44 -15
- package/dist/service-worker/index.js.map +1 -1
- package/native/cli.cjs +430 -14
- package/native/do-executor.cjs +35 -8
- package/native/doctor.cjs +211 -42
- package/native/endpoint.cjs +121 -36
- package/native/extract.cjs +362 -0
- package/native/host-helpers.cjs +133 -11
- package/native/host-sessions.cjs +10 -0
- package/native/host.cjs +53 -23
- package/native/mcp-server.cjs +25 -0
- package/native/native-host-launch-probe.cjs +69 -0
- package/native/private-state.cjs +25 -1
- package/native/script-options.cjs +33 -0
- package/native/semantic-cli.cjs +764 -0
- package/native/semantic-core.cjs +369 -0
- package/native/semantic-credentials.cjs +207 -0
- package/native/semantic-provider.cjs +61 -0
- package/native/semantic-workflow-executor.cjs +65 -0
- package/native/semantic-workflow-state.cjs +271 -0
- package/native/semantic-workflow.cjs +398 -0
- package/native/stdin-frames.cjs +33 -0
- package/native/tool-scope.cjs +3 -2
- package/native/workflow-definition.cjs +125 -1
- package/native/workflow-runtime.cjs +71 -5
- package/package.json +11 -7
- package/scripts/install-native-host.cjs +103 -60
- package/scripts/uninstall-native-host.cjs +40 -38
- package/scripts/windows-interop.cjs +89 -0
- package/skills/surf/SKILL.md +96 -1
package/native/host.cjs
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const LAUNCH_PROBE_ARGUMENT = "--surf-native-host-launch-probe";
|
|
3
|
+
const LAUNCH_PROBE_MARKER = "SURF_NATIVE_HOST_LAUNCH_PROBE_OK";
|
|
4
|
+
|
|
5
|
+
if (process.argv.length === 3 && process.argv[2] === LAUNCH_PROBE_ARGUMENT) {
|
|
6
|
+
process.stdout.write(`${LAUNCH_PROBE_MARKER}\n`);
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
if (process.argv.length === 3 && process.argv[2] === `${LAUNCH_PROBE_ARGUMENT}-distro`) {
|
|
10
|
+
process.stdout.write(`${LAUNCH_PROBE_MARKER}:${JSON.stringify(process.env.WSL_DISTRO_NAME || null)}\n`);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
2
14
|
const net = require("net");
|
|
3
15
|
const fs = require("fs");
|
|
4
16
|
const path = require("path");
|
|
@@ -15,11 +27,12 @@ const grokClient = require("./grok-client.cjs");
|
|
|
15
27
|
const kimiClient = require("./kimi-client.cjs");
|
|
16
28
|
const aistudioClient = require("./aistudio-client.cjs");
|
|
17
29
|
const aistudioBuild = require("./aistudio-build.cjs");
|
|
18
|
-
const { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage } = require("./host-helpers.cjs");
|
|
30
|
+
const { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage, applySemanticExpectedIdentity } = require("./host-helpers.cjs");
|
|
19
31
|
const { createOracleHost } = require("./oracle-host.cjs");
|
|
20
32
|
|
|
21
33
|
const IS_WIN = process.platform === "win32";
|
|
22
34
|
const { SOCKET_PATH, SURF_TMP } = require("./socket-path.cjs");
|
|
35
|
+
const { takeFrames } = require("./stdin-frames.cjs");
|
|
23
36
|
const { parseListenEndpoint } = require("./listener.cjs");
|
|
24
37
|
const { getStateDir } = require("./remote-auth.cjs");
|
|
25
38
|
const { createFrameParser, createServerAuthSession, createSocketWriter, isClientAuthorized, writeFrame, MAX_FRAME_BYTES } = require("./remote-transport.cjs");
|
|
@@ -843,7 +856,7 @@ function currentFrameContext(request) {
|
|
|
843
856
|
function applyFrameContextToMessage(request, extensionMessage) {
|
|
844
857
|
if (!extensionMessage || !FRAME_CONTEXT_MESSAGE_TYPES.has(extensionMessage.type)) return;
|
|
845
858
|
const context = currentFrameContext(request);
|
|
846
|
-
if (context) extensionMessage.frameId = context.frameId;
|
|
859
|
+
if (context && !Number.isInteger(extensionMessage.frameId)) extensionMessage.frameId = context.frameId;
|
|
847
860
|
}
|
|
848
861
|
|
|
849
862
|
function persistFrameContext(request, frameId, url) {
|
|
@@ -1496,6 +1509,7 @@ async function executeMappedHostTool(request, tool, args, tabId) {
|
|
|
1496
1509
|
if (!extensionMsg) throw new Error(`Unknown tool: ${tool}`);
|
|
1497
1510
|
if (request.target?.strict) extensionMsg.strictTarget = true;
|
|
1498
1511
|
applyFrameContextToMessage(request, extensionMsg);
|
|
1512
|
+
applySemanticExpectedIdentity(request, extensionMsg, args);
|
|
1499
1513
|
if (extensionMsg.type === "UNSUPPORTED_ACTION") throw new Error(extensionMsg.message);
|
|
1500
1514
|
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
1501
1515
|
await abortableDelay(extensionMsg.seconds * 1000, request.signal);
|
|
@@ -1719,6 +1733,9 @@ function sendToolResponse(socket, id, result, error) {
|
|
|
1719
1733
|
let output = result;
|
|
1720
1734
|
try {
|
|
1721
1735
|
if (!error) output = await sendRequestDownloads(context, request, result);
|
|
1736
|
+
if (!error && output?.semanticObservation?.identity && request?.target) {
|
|
1737
|
+
output.semanticObservation.identity.browserEpoch = request.target.browserEpoch;
|
|
1738
|
+
}
|
|
1722
1739
|
} catch (transferFailure) {
|
|
1723
1740
|
finalError = transferFailure.message;
|
|
1724
1741
|
}
|
|
@@ -1765,7 +1782,12 @@ function sendToolResponse(socket, id, result, error) {
|
|
|
1765
1782
|
}
|
|
1766
1783
|
if (request?.notice) response.notice = request.notice;
|
|
1767
1784
|
if (formattedError) response.error = formattedError;
|
|
1768
|
-
else
|
|
1785
|
+
else {
|
|
1786
|
+
response.result = { content: formatToolContent(output, log, { suppressImages: Boolean(context?.isRemote) }) };
|
|
1787
|
+
if (request?.tool === "tab.new" && Number.isInteger(output?.tabId) && output.tabId > 0) {
|
|
1788
|
+
response.result.tabId = output.tabId;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1769
1791
|
if (!context?.closed) await sendSocket(socket, response);
|
|
1770
1792
|
})().catch((sendError) => log(`Error sending tool_response: ${sendError.message}`));
|
|
1771
1793
|
}
|
|
@@ -1899,6 +1921,12 @@ function handleToolRequest(msg, socket, requestContext = requestStorage.getStore
|
|
|
1899
1921
|
}
|
|
1900
1922
|
if (requestContext.target?.strict) extensionMsg.strictTarget = true;
|
|
1901
1923
|
applyFrameContextToMessage(requestContext, extensionMsg);
|
|
1924
|
+
try {
|
|
1925
|
+
applySemanticExpectedIdentity(requestContext, extensionMsg, args);
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
sendToolResponse(socket, originalId, null, error);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1902
1930
|
|
|
1903
1931
|
if (extensionMsg.type === "UNSUPPORTED_ACTION") {
|
|
1904
1932
|
sendToolResponse(socket, originalId, null, extensionMsg.message);
|
|
@@ -2805,25 +2833,23 @@ function writeMessage(msg) {
|
|
|
2805
2833
|
let inputBuffer = Buffer.alloc(0);
|
|
2806
2834
|
|
|
2807
2835
|
function processInput() {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
inputBuffer = inputBuffer.slice(4 + msgLen);
|
|
2814
|
-
|
|
2836
|
+
// Take every complete frame out of the buffer before dispatching: one
|
|
2837
|
+
// chunk routinely carries a TARGET_EVENT and the reply to a tool request.
|
|
2838
|
+
const { frames, rest } = takeFrames(inputBuffer);
|
|
2839
|
+
inputBuffer = rest;
|
|
2840
|
+
for (const jsonStr of frames) {
|
|
2815
2841
|
try {
|
|
2816
2842
|
const msg = JSON.parse(jsonStr);
|
|
2817
2843
|
log(`Received from extension: ${msg.type || "unknown"}${msg.id !== undefined ? ` id=${msg.id}` : ""}`);
|
|
2818
2844
|
|
|
2819
2845
|
if (msg.type === "EXTENSION_HELLO") {
|
|
2820
2846
|
setBrowserIdentity(msg);
|
|
2821
|
-
|
|
2847
|
+
continue;
|
|
2822
2848
|
}
|
|
2823
2849
|
|
|
2824
2850
|
if (msg.type === "TARGET_EVENT") {
|
|
2825
2851
|
handleTargetEvent(msg);
|
|
2826
|
-
|
|
2852
|
+
continue;
|
|
2827
2853
|
}
|
|
2828
2854
|
|
|
2829
2855
|
if (msg.type === "GET_AUTH") {
|
|
@@ -2847,12 +2873,12 @@ function processInput() {
|
|
|
2847
2873
|
hint: "Failed to read auth credentials. Run 'pi --login anthropic' in terminal to authenticate."
|
|
2848
2874
|
});
|
|
2849
2875
|
}
|
|
2850
|
-
|
|
2876
|
+
continue;
|
|
2851
2877
|
}
|
|
2852
2878
|
|
|
2853
2879
|
if (msg.type === "API_REQUEST") {
|
|
2854
2880
|
handleApiRequest(msg, writeMessage);
|
|
2855
|
-
|
|
2881
|
+
continue;
|
|
2856
2882
|
}
|
|
2857
2883
|
|
|
2858
2884
|
if (msg.type === "PLAYBOOK_WATCH_EVENT") {
|
|
@@ -2865,14 +2891,14 @@ function processInput() {
|
|
|
2865
2891
|
tabId: msg.tabId,
|
|
2866
2892
|
timestamp: msg.timestamp || new Date().toISOString(),
|
|
2867
2893
|
});
|
|
2868
|
-
|
|
2894
|
+
continue;
|
|
2869
2895
|
}
|
|
2870
2896
|
|
|
2871
2897
|
if (msg.type === "VIDEO_FRAME") {
|
|
2872
2898
|
if (activeVideoRecorder && msg.recorderId === activeVideoRecorder.recorderId && msg.tabId === activeVideoRecorder.tabId) {
|
|
2873
2899
|
activeVideoRecorder.recorder.addFrame(msg.data, Number.isFinite(msg.receivedAt) ? msg.receivedAt : Date.now());
|
|
2874
2900
|
}
|
|
2875
|
-
|
|
2901
|
+
continue;
|
|
2876
2902
|
}
|
|
2877
2903
|
|
|
2878
2904
|
if (msg.type === "VIDEO_ERROR") {
|
|
@@ -2882,7 +2908,7 @@ function processInput() {
|
|
|
2882
2908
|
msg.error || "Video screencast failed",
|
|
2883
2909
|
));
|
|
2884
2910
|
}
|
|
2885
|
-
|
|
2911
|
+
continue;
|
|
2886
2912
|
}
|
|
2887
2913
|
|
|
2888
2914
|
if (msg.type === "STREAM_EVENT") {
|
|
@@ -2894,7 +2920,7 @@ function processInput() {
|
|
|
2894
2920
|
stream.socket.destroy(error);
|
|
2895
2921
|
});
|
|
2896
2922
|
}
|
|
2897
|
-
|
|
2923
|
+
continue;
|
|
2898
2924
|
}
|
|
2899
2925
|
|
|
2900
2926
|
if (msg.type === "STREAM_ERROR") {
|
|
@@ -2907,7 +2933,7 @@ function processInput() {
|
|
|
2907
2933
|
})
|
|
2908
2934
|
.finally(() => stopActiveStream(msg.streamId));
|
|
2909
2935
|
}
|
|
2910
|
-
|
|
2936
|
+
continue;
|
|
2911
2937
|
}
|
|
2912
2938
|
|
|
2913
2939
|
|
|
@@ -2920,13 +2946,13 @@ function processInput() {
|
|
|
2920
2946
|
if (topLevelResponse && request?.context) {
|
|
2921
2947
|
completeOwnedRequest(request.context, request.id, "cleanup-settled");
|
|
2922
2948
|
}
|
|
2923
|
-
|
|
2949
|
+
continue;
|
|
2924
2950
|
}
|
|
2925
2951
|
handleFrameContextFailure(pending.request, msg);
|
|
2926
2952
|
updateFrameContextFromResult(pending.request, pending.tool, msg);
|
|
2927
2953
|
if (pending.resolve || pending.onComplete) {
|
|
2928
2954
|
pendingToolRequests.resolve(msg.id, msg);
|
|
2929
|
-
|
|
2955
|
+
continue;
|
|
2930
2956
|
}
|
|
2931
2957
|
pendingToolRequests.delete(msg.id);
|
|
2932
2958
|
{
|
|
@@ -2935,7 +2961,11 @@ function processInput() {
|
|
|
2935
2961
|
const tabId = storedTabId || msg._resolvedTabId;
|
|
2936
2962
|
const failAutoScreenshot = (message) => pending.autoScreenshotOutput
|
|
2937
2963
|
? sendToolResponse(socket, originalId, null, `Auto-screenshot failed: ${message}`)
|
|
2938
|
-
: sendToolResponse(socket, originalId, {
|
|
2964
|
+
: sendToolResponse(socket, originalId, {
|
|
2965
|
+
...msg,
|
|
2966
|
+
screenshotError: message,
|
|
2967
|
+
autoScreenshotError: message,
|
|
2968
|
+
}, null);
|
|
2939
2969
|
|
|
2940
2970
|
if (pending.networkExport && Array.isArray(msg.entries)) {
|
|
2941
2971
|
try {
|
|
@@ -3026,7 +3056,7 @@ function processInput() {
|
|
|
3026
3056
|
}
|
|
3027
3057
|
})
|
|
3028
3058
|
.catch((error) => failAutoScreenshot(error.message));
|
|
3029
|
-
|
|
3059
|
+
continue;
|
|
3030
3060
|
} else if (autoScreenshot && pending.autoScreenshotOutput && !msg.error) {
|
|
3031
3061
|
failAutoScreenshot(tabId ? "screenshot response was invalid" : "no tab available");
|
|
3032
3062
|
} else if (msg.results && msg.savePath) {
|
package/native/mcp-server.cjs
CHANGED
|
@@ -159,6 +159,27 @@ const TOOL_SCHEMAS = {
|
|
|
159
159
|
timeout: z.number().optional().describe("Max wait time in ms")
|
|
160
160
|
}
|
|
161
161
|
},
|
|
162
|
+
"wait.ready": {
|
|
163
|
+
desc: "Wait until the page is ready, or fail fast with a typed state (login, challenge, not-found, error)",
|
|
164
|
+
schema: {
|
|
165
|
+
selector: z.string().optional().describe("Visible CSS selector that marks a ready page"),
|
|
166
|
+
text: z.string().optional().describe("Page text that marks a ready page"),
|
|
167
|
+
urlPrefix: z.string().optional().describe("Expected URL prefix; anything else is a bounce"),
|
|
168
|
+
emptyText: z.string().optional().describe("Text of an explicit no-results render (state 'empty')"),
|
|
169
|
+
accept: z.string().optional().describe("Negative states to return instead of fail, comma-separated"),
|
|
170
|
+
timeout: z.number().optional().describe("Max wait time in ms (default 20000, max 120000)"),
|
|
171
|
+
interval: z.number().optional().describe("Poll interval in ms (default 400)")
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"page.readiness": {
|
|
175
|
+
desc: "Classify the page once: ready, empty, loading, login, challenge, not-found, error",
|
|
176
|
+
schema: {
|
|
177
|
+
selector: z.string().optional().describe("Visible CSS selector that marks a ready page"),
|
|
178
|
+
text: z.string().optional().describe("Page text that marks a ready page"),
|
|
179
|
+
urlPrefix: z.string().optional().describe("Expected URL prefix"),
|
|
180
|
+
emptyText: z.string().optional().describe("Text of an explicit no-results render")
|
|
181
|
+
}
|
|
182
|
+
},
|
|
162
183
|
"wait.load": {
|
|
163
184
|
desc: "Wait for page to fully load",
|
|
164
185
|
schema: { timeout: z.number().optional().describe("Max wait time in ms") }
|
|
@@ -242,6 +263,10 @@ const TOOL_SCHEMAS = {
|
|
|
242
263
|
desc: "List all frames in page",
|
|
243
264
|
schema: {}
|
|
244
265
|
},
|
|
266
|
+
"frame.diagnose": {
|
|
267
|
+
desc: "Compare DOM iframes, extension frames (with content-script reachability) and the CDP frame tree, with warnings",
|
|
268
|
+
schema: {}
|
|
269
|
+
},
|
|
245
270
|
"frame.js": {
|
|
246
271
|
desc: "Execute JS in specific frame",
|
|
247
272
|
schema: {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const { execFileSync } = require("child_process");
|
|
2
|
+
const { runWindowsExecutable } = require("../scripts/windows-interop.cjs");
|
|
3
|
+
|
|
4
|
+
const LAUNCH_PROBE_ARGUMENT = "--surf-native-host-launch-probe";
|
|
5
|
+
const LAUNCH_PROBE_MARKER = "SURF_NATIVE_HOST_LAUNCH_PROBE_OK";
|
|
6
|
+
const WRAPPER_PROBE_CAPABILITY_MARKER = "rem SURF_NATIVE_HOST_LAUNCH_PROBE_V1";
|
|
7
|
+
const LAUNCH_PROBE_TIMEOUT_MS = 5000;
|
|
8
|
+
|
|
9
|
+
function renderWslWrapper(nodePath, hostPath, distro) {
|
|
10
|
+
const path = require("path");
|
|
11
|
+
for (const value of [nodePath, hostPath, distro].filter((value) => value !== undefined)) {
|
|
12
|
+
if (/["%!\r\n]/.test(value)) throw new Error("WSL wrapper path or distro contains unsupported batch characters");
|
|
13
|
+
}
|
|
14
|
+
const distroArg = distro ? ` -d "${distro}"` : "";
|
|
15
|
+
return `@echo off\r\n${WRAPPER_PROBE_CAPABILITY_MARKER}\r\nwsl.exe${distroArg} --cd "${path.dirname(hostPath)}" --exec "${nodePath}" "${hostPath}" %*\r\n`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function probeWindowsWrapper(wrapperPath, deps = {}) {
|
|
19
|
+
if (!/^[a-z]:\\[^"%\r\n]*\.cmd$/i.test(wrapperPath) || /[&|<>^!]/.test(wrapperPath)) {
|
|
20
|
+
throw new Error("Native host wrapper path cannot be safely passed to cmd.exe");
|
|
21
|
+
}
|
|
22
|
+
let output;
|
|
23
|
+
try {
|
|
24
|
+
output = runWindowsExecutable(
|
|
25
|
+
"cmd.exe",
|
|
26
|
+
["/d", "/s", "/c", `""${wrapperPath}" ${LAUNCH_PROBE_ARGUMENT}${deps.verifyDistro ? "-distro" : ""}"`],
|
|
27
|
+
{
|
|
28
|
+
execFileSync: deps.execFileSync || execFileSync,
|
|
29
|
+
allowWslFallback: true,
|
|
30
|
+
execOptions: {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
timeout: deps.timeoutMs ?? LAUNCH_PROBE_TIMEOUT_MS,
|
|
33
|
+
maxBuffer: 64 * 1024,
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(`Native host wrapper launch probe failed: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const response = String(output).trim();
|
|
43
|
+
if (deps.verifyDistro) {
|
|
44
|
+
if (!response.startsWith(`${LAUNCH_PROBE_MARKER}:`)) {
|
|
45
|
+
throw new Error("Native host wrapper launch probe failed: host returned unexpected output");
|
|
46
|
+
}
|
|
47
|
+
let distro;
|
|
48
|
+
try {
|
|
49
|
+
distro = JSON.parse(response.slice(LAUNCH_PROBE_MARKER.length + 1));
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error("Native host wrapper launch probe failed: invalid distro identity");
|
|
52
|
+
}
|
|
53
|
+
if (distro !== null && (typeof distro !== "string" || !distro)) {
|
|
54
|
+
throw new Error("Native host wrapper launch probe failed: invalid distro identity");
|
|
55
|
+
}
|
|
56
|
+
return distro;
|
|
57
|
+
}
|
|
58
|
+
if (response !== LAUNCH_PROBE_MARKER) {
|
|
59
|
+
throw new Error("Native host wrapper launch probe failed: host returned unexpected output");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
LAUNCH_PROBE_ARGUMENT,
|
|
65
|
+
LAUNCH_PROBE_MARKER,
|
|
66
|
+
WRAPPER_PROBE_CAPABILITY_MARKER,
|
|
67
|
+
renderWslWrapper,
|
|
68
|
+
probeWindowsWrapper,
|
|
69
|
+
};
|
package/native/private-state.cjs
CHANGED
|
@@ -30,6 +30,19 @@ function assertWithin(root, targetPath) {
|
|
|
30
30
|
return resolvedTarget;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function assertPrivatePath(targetPath, root, allowMissing) {
|
|
34
|
+
const resolvedRoot = path.resolve(root);
|
|
35
|
+
const resolvedTarget = assertWithin(resolvedRoot, targetPath);
|
|
36
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
37
|
+
let current = resolvedRoot;
|
|
38
|
+
for (const segment of ["", ...relative.split(path.sep).filter(Boolean)]) {
|
|
39
|
+
if (segment) current = path.join(current, segment);
|
|
40
|
+
const stat = assertNotSymlink(current, allowMissing);
|
|
41
|
+
if (!stat) return null;
|
|
42
|
+
}
|
|
43
|
+
return fs.lstatSync(resolvedTarget);
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
function ensurePrivateDir(dirPath, root = getPrivateStateRoot()) {
|
|
34
47
|
const resolvedRoot = path.resolve(root);
|
|
35
48
|
const resolvedDir = assertWithin(resolvedRoot, dirPath);
|
|
@@ -129,13 +142,23 @@ function readPrivateFile(filePath, options = {}) {
|
|
|
129
142
|
const resolved = path.resolve(filePath);
|
|
130
143
|
const root = options.root || getPrivateStateRoot();
|
|
131
144
|
assertWithin(root, resolved);
|
|
132
|
-
const stat =
|
|
145
|
+
const stat = assertPrivatePath(resolved, root, options.allowMissing === true);
|
|
133
146
|
if (!stat) return options.fallback;
|
|
134
147
|
if (!stat.isFile()) throw new Error(`private state path is not a file: ${resolved}`);
|
|
135
148
|
if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) throw new Error(`private state file permissions are too broad: ${resolved}`);
|
|
136
149
|
return fs.readFileSync(resolved, options.encoding || null);
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
function removePrivateFile(filePath, options = {}) {
|
|
153
|
+
const resolved = path.resolve(filePath);
|
|
154
|
+
const root = options.root || getPrivateStateRoot();
|
|
155
|
+
const stat = assertPrivatePath(resolved, root, true);
|
|
156
|
+
if (!stat) return false;
|
|
157
|
+
if (!stat.isFile()) throw new Error(`private state path is not a file: ${resolved}`);
|
|
158
|
+
fs.unlinkSync(resolved);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
function readPrivateJson(filePath, fallback = null, options = {}) {
|
|
140
163
|
const content = readPrivateFile(filePath, { ...options, allowMissing: true, fallback: null, encoding: "utf8" });
|
|
141
164
|
return content === null ? fallback : JSON.parse(content);
|
|
@@ -152,5 +175,6 @@ module.exports = {
|
|
|
152
175
|
privateStatePath,
|
|
153
176
|
readPrivateFile,
|
|
154
177
|
readPrivateJson,
|
|
178
|
+
removePrivateFile,
|
|
155
179
|
writePrivateFileExclusive,
|
|
156
180
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
3
|
+
const proto = Object.getPrototypeOf(value);
|
|
4
|
+
return proto === Object.prototype || proto === null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseScriptOptions(input) {
|
|
8
|
+
if (input === undefined || input === null || input === "") return {};
|
|
9
|
+
if (input === true) throw new Error("--options needs a JSON object value");
|
|
10
|
+
let value = input;
|
|
11
|
+
if (typeof input === "string") {
|
|
12
|
+
try {
|
|
13
|
+
value = JSON.parse(input);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
throw new Error(`--options is not valid JSON: ${error.message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (!isPlainObject(value)) {
|
|
19
|
+
throw new Error("--options must be a JSON object, e.g. '{\"limit\": 20}'");
|
|
20
|
+
}
|
|
21
|
+
// Round-trip so functions, undefined and prototypes cannot leak into the page.
|
|
22
|
+
return JSON.parse(JSON.stringify(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function applyOptionsPrelude(code, options) {
|
|
26
|
+
const normalized = parseScriptOptions(options);
|
|
27
|
+
const prelude = `const SURF_OPTIONS = Object.freeze(JSON.parse(${JSON.stringify(JSON.stringify(normalized))}));\n`;
|
|
28
|
+
const strict = code.match(/^\s*(["'])use strict\1\s*;/);
|
|
29
|
+
if (!strict) return `${prelude}${code}`;
|
|
30
|
+
return `${strict[0]}\n${prelude}${code.slice(strict[0].length)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { applyOptionsPrelude, parseScriptOptions };
|