safari-pilot 0.1.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/.claude-plugin/plugin.json +35 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +324 -0
- package/bin/.gitkeep +0 -0
- package/bin/Safari Pilot.app/Contents/CodeResources +0 -0
- package/bin/Safari Pilot.app/Contents/Info.plist +58 -0
- package/bin/Safari Pilot.app/Contents/MacOS/Safari Pilot +0 -0
- package/bin/Safari Pilot.app/Contents/PkgInfo +1 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Info.plist +55 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/MacOS/Safari Pilot Extension +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/background.js +294 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-isolated.js +80 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-main.js +310 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-128.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-48.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-96.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/manifest.json +39 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/_CodeSignature/CodeResources +194 -0
- package/bin/Safari Pilot.app/Contents/Resources/AppIcon.icns +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Assets.car +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.html +19 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Icon.png +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Script.js +22 -0
- package/bin/Safari Pilot.app/Contents/Resources/Style.css +45 -0
- package/bin/Safari Pilot.app/Contents/_CodeSignature/CodeResources +236 -0
- package/bin/Safari Pilot.zip +0 -0
- package/bin/SafariPilotd +0 -0
- package/dist/engine-selector.d.ts +10 -0
- package/dist/engine-selector.js +55 -0
- package/dist/engine-selector.js.map +1 -0
- package/dist/engines/applescript.d.ts +53 -0
- package/dist/engines/applescript.js +290 -0
- package/dist/engines/applescript.js.map +1 -0
- package/dist/engines/daemon.d.ts +19 -0
- package/dist/engines/daemon.js +187 -0
- package/dist/engines/daemon.js.map +1 -0
- package/dist/engines/engine.d.ts +15 -0
- package/dist/engines/engine.js +42 -0
- package/dist/engines/engine.js.map +1 -0
- package/dist/engines/extension.d.ts +34 -0
- package/dist/engines/extension.js +66 -0
- package/dist/engines/extension.js.map +1 -0
- package/dist/errors.d.ts +128 -0
- package/dist/errors.js +250 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/security/audit-log.d.ts +23 -0
- package/dist/security/audit-log.js +68 -0
- package/dist/security/audit-log.js.map +1 -0
- package/dist/security/circuit-breaker.d.ts +29 -0
- package/dist/security/circuit-breaker.js +114 -0
- package/dist/security/circuit-breaker.js.map +1 -0
- package/dist/security/domain-policy.d.ts +29 -0
- package/dist/security/domain-policy.js +96 -0
- package/dist/security/domain-policy.js.map +1 -0
- package/dist/security/human-approval.d.ts +20 -0
- package/dist/security/human-approval.js +150 -0
- package/dist/security/human-approval.js.map +1 -0
- package/dist/security/idpi-scanner.d.ts +20 -0
- package/dist/security/idpi-scanner.js +102 -0
- package/dist/security/idpi-scanner.js.map +1 -0
- package/dist/security/kill-switch.d.ts +51 -0
- package/dist/security/kill-switch.js +103 -0
- package/dist/security/kill-switch.js.map +1 -0
- package/dist/security/rate-limiter.d.ts +30 -0
- package/dist/security/rate-limiter.js +70 -0
- package/dist/security/rate-limiter.js.map +1 -0
- package/dist/security/screenshot-redaction.d.ts +42 -0
- package/dist/security/screenshot-redaction.js +134 -0
- package/dist/security/screenshot-redaction.js.map +1 -0
- package/dist/security/tab-ownership.d.ts +46 -0
- package/dist/security/tab-ownership.js +85 -0
- package/dist/security/tab-ownership.js.map +1 -0
- package/dist/server.d.ts +53 -0
- package/dist/server.js +347 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/clipboard.d.ts +15 -0
- package/dist/tools/clipboard.js +128 -0
- package/dist/tools/clipboard.js.map +1 -0
- package/dist/tools/compound.d.ts +68 -0
- package/dist/tools/compound.js +491 -0
- package/dist/tools/compound.js.map +1 -0
- package/dist/tools/extraction.d.ts +26 -0
- package/dist/tools/extraction.js +414 -0
- package/dist/tools/extraction.js.map +1 -0
- package/dist/tools/frames.d.ts +22 -0
- package/dist/tools/frames.js +165 -0
- package/dist/tools/frames.js.map +1 -0
- package/dist/tools/interaction.d.ts +30 -0
- package/dist/tools/interaction.js +651 -0
- package/dist/tools/interaction.js.map +1 -0
- package/dist/tools/navigation.d.ts +41 -0
- package/dist/tools/navigation.js +316 -0
- package/dist/tools/navigation.js.map +1 -0
- package/dist/tools/network.d.ts +27 -0
- package/dist/tools/network.js +721 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/performance.d.ts +16 -0
- package/dist/tools/performance.js +240 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/permissions.d.ts +25 -0
- package/dist/tools/permissions.js +308 -0
- package/dist/tools/permissions.js.map +1 -0
- package/dist/tools/service-workers.d.ts +15 -0
- package/dist/tools/service-workers.js +136 -0
- package/dist/tools/service-workers.js.map +1 -0
- package/dist/tools/shadow.d.ts +21 -0
- package/dist/tools/shadow.js +126 -0
- package/dist/tools/shadow.js.map +1 -0
- package/dist/tools/storage.d.ts +30 -0
- package/dist/tools/storage.js +679 -0
- package/dist/tools/storage.js.map +1 -0
- package/dist/tools/structured-extraction.d.ts +22 -0
- package/dist/tools/structured-extraction.js +433 -0
- package/dist/tools/structured-extraction.js.map +1 -0
- package/dist/tools/wait.d.ts +18 -0
- package/dist/tools/wait.js +182 -0
- package/dist/tools/wait.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/extension/background.js +294 -0
- package/extension/content-isolated.js +80 -0
- package/extension/content-main.js +310 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/icon-96.png +0 -0
- package/extension/manifest.json +39 -0
- package/hooks/session-end.sh +67 -0
- package/hooks/session-start.sh +66 -0
- package/package.json +46 -0
- package/scripts/build-extension.sh +135 -0
- package/scripts/postinstall.sh +91 -0
- package/scripts/preuninstall.sh +25 -0
- package/scripts/update-daemon.sh +62 -0
- package/skills/safari-pilot/SKILL.md +157 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
2
|
+
const DEFAULT_POLL_INTERVAL_MS = 250;
|
|
3
|
+
const NETWORK_IDLE_QUIET_MS = 500;
|
|
4
|
+
/**
|
|
5
|
+
* Build the JS snippet that checks a condition and returns true/false.
|
|
6
|
+
*/
|
|
7
|
+
function buildConditionJs(condition, value) {
|
|
8
|
+
switch (condition) {
|
|
9
|
+
case 'selector':
|
|
10
|
+
return `return document.querySelector(${JSON.stringify(value)}) !== null`;
|
|
11
|
+
case 'selectorHidden':
|
|
12
|
+
return `return document.querySelector(${JSON.stringify(value)}) === null`;
|
|
13
|
+
case 'text':
|
|
14
|
+
return `return document.body && document.body.textContent.includes(${JSON.stringify(value)})`;
|
|
15
|
+
case 'textGone':
|
|
16
|
+
return `return !document.body || !document.body.textContent.includes(${JSON.stringify(value)})`;
|
|
17
|
+
case 'urlMatch':
|
|
18
|
+
return `return location.href.includes(${JSON.stringify(value)})`;
|
|
19
|
+
case 'networkidle': {
|
|
20
|
+
// Track in-flight XHR/fetch count and resolve when quiet for NETWORK_IDLE_QUIET_MS.
|
|
21
|
+
// The JS returns true once there has been no XHR/fetch activity for the threshold.
|
|
22
|
+
return `
|
|
23
|
+
(function() {
|
|
24
|
+
if (!window.__safariPilotNetworkIdleSetup) {
|
|
25
|
+
window.__safariPilotNetworkIdleSetup = true;
|
|
26
|
+
window.__safariPilotInflight = 0;
|
|
27
|
+
window.__safariPilotLastActivity = Date.now();
|
|
28
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
29
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
30
|
+
XMLHttpRequest.prototype.send = function() {
|
|
31
|
+
window.__safariPilotInflight++;
|
|
32
|
+
window.__safariPilotLastActivity = Date.now();
|
|
33
|
+
this.addEventListener('loadend', function() {
|
|
34
|
+
window.__safariPilotInflight = Math.max(0, window.__safariPilotInflight - 1);
|
|
35
|
+
window.__safariPilotLastActivity = Date.now();
|
|
36
|
+
});
|
|
37
|
+
return origSend.apply(this, arguments);
|
|
38
|
+
};
|
|
39
|
+
const origFetch = window.fetch;
|
|
40
|
+
window.fetch = function() {
|
|
41
|
+
window.__safariPilotInflight++;
|
|
42
|
+
window.__safariPilotLastActivity = Date.now();
|
|
43
|
+
return origFetch.apply(this, arguments).finally(function() {
|
|
44
|
+
window.__safariPilotInflight = Math.max(0, window.__safariPilotInflight - 1);
|
|
45
|
+
window.__safariPilotLastActivity = Date.now();
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return window.__safariPilotInflight === 0 && (Date.now() - window.__safariPilotLastActivity) >= ${NETWORK_IDLE_QUIET_MS};
|
|
50
|
+
})()
|
|
51
|
+
`.trim();
|
|
52
|
+
}
|
|
53
|
+
case 'function':
|
|
54
|
+
// value is a JS function body — evaluate and return its truthy result
|
|
55
|
+
return `return (function() { ${value} })()`;
|
|
56
|
+
default:
|
|
57
|
+
return 'return false';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class WaitTools {
|
|
61
|
+
engine;
|
|
62
|
+
constructor(engine) {
|
|
63
|
+
this.engine = engine;
|
|
64
|
+
}
|
|
65
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
66
|
+
getDefinitions() {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
name: 'safari_wait_for',
|
|
70
|
+
description: 'Wait for a condition to be met in the specified tab before proceeding. ' +
|
|
71
|
+
'Polls the page at configurable intervals until the condition is satisfied or the timeout expires. ' +
|
|
72
|
+
'Supports waiting for DOM selectors, text content, URL patterns, network idle, and custom JS functions.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab to poll' },
|
|
77
|
+
condition: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
enum: ['selector', 'selectorHidden', 'text', 'textGone', 'urlMatch', 'networkidle', 'function'],
|
|
80
|
+
description: 'The condition to wait for: ' +
|
|
81
|
+
'"selector" — element matching CSS selector exists; ' +
|
|
82
|
+
'"selectorHidden" — element has disappeared; ' +
|
|
83
|
+
'"text" — text appears in page body; ' +
|
|
84
|
+
'"textGone" — text is absent from page body; ' +
|
|
85
|
+
'"urlMatch" — current URL contains the pattern; ' +
|
|
86
|
+
'"networkidle" — no XHR/fetch activity for 500 ms; ' +
|
|
87
|
+
'"function" — custom JS function body returns truthy',
|
|
88
|
+
},
|
|
89
|
+
value: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'The CSS selector, text, URL pattern, or JS function body depending on condition',
|
|
92
|
+
},
|
|
93
|
+
timeout: {
|
|
94
|
+
type: 'number',
|
|
95
|
+
description: 'Maximum time to wait in milliseconds',
|
|
96
|
+
default: DEFAULT_TIMEOUT_MS,
|
|
97
|
+
},
|
|
98
|
+
pollInterval: {
|
|
99
|
+
type: 'number',
|
|
100
|
+
description: 'How often to check the condition in milliseconds',
|
|
101
|
+
default: DEFAULT_POLL_INTERVAL_MS,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ['tabUrl', 'condition'],
|
|
105
|
+
},
|
|
106
|
+
requirements: {},
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
getHandler(name) {
|
|
111
|
+
switch (name) {
|
|
112
|
+
case 'safari_wait_for':
|
|
113
|
+
return (p) => this.handleWaitFor(p);
|
|
114
|
+
default:
|
|
115
|
+
throw new Error(`WaitTools: unknown tool "${name}"`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ── Handlers ────────────────────────────────────────────────────────────────
|
|
119
|
+
async handleWaitFor(params) {
|
|
120
|
+
const wallStart = Date.now();
|
|
121
|
+
const tabUrl = params['tabUrl'];
|
|
122
|
+
const condition = params['condition'];
|
|
123
|
+
const value = typeof params['value'] === 'string' ? params['value'] : '';
|
|
124
|
+
const timeout = typeof params['timeout'] === 'number' ? params['timeout'] : DEFAULT_TIMEOUT_MS;
|
|
125
|
+
const pollInterval = typeof params['pollInterval'] === 'number' ? params['pollInterval'] : DEFAULT_POLL_INTERVAL_MS;
|
|
126
|
+
const conditionJs = buildConditionJs(condition, value);
|
|
127
|
+
let met = false;
|
|
128
|
+
let timedOut = false;
|
|
129
|
+
while (true) {
|
|
130
|
+
const elapsed = Date.now() - wallStart;
|
|
131
|
+
if (elapsed >= timeout) {
|
|
132
|
+
timedOut = true;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const result = await this.executeJsInTab(tabUrl, conditionJs);
|
|
136
|
+
if (result === true) {
|
|
137
|
+
met = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
// Sleep for the poll interval — but cap it so we don't overshoot the timeout.
|
|
141
|
+
const remaining = timeout - (Date.now() - wallStart);
|
|
142
|
+
if (remaining <= 0) {
|
|
143
|
+
timedOut = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
await sleep(Math.min(pollInterval, remaining));
|
|
147
|
+
}
|
|
148
|
+
const elapsed = Date.now() - wallStart;
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: 'text', text: JSON.stringify({ met, elapsed, timedOut }) }],
|
|
151
|
+
metadata: { engine: 'applescript', degraded: false, latencyMs: elapsed },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
155
|
+
/**
|
|
156
|
+
* Execute JS in a tab and return the raw boolean/truthy result.
|
|
157
|
+
* Returns false on engine failure or unparseable output.
|
|
158
|
+
*/
|
|
159
|
+
async executeJsInTab(tabUrl, jsCode) {
|
|
160
|
+
const script = this.engine.buildTabScript(tabUrl, jsCode);
|
|
161
|
+
const result = await this.engine.execute(script);
|
|
162
|
+
if (!result.ok || !result.value)
|
|
163
|
+
return false;
|
|
164
|
+
const raw = result.value.trim();
|
|
165
|
+
// AppleScript returns 'true'/'false' as strings; also handle JSON booleans
|
|
166
|
+
if (raw === 'true')
|
|
167
|
+
return true;
|
|
168
|
+
if (raw === 'false')
|
|
169
|
+
return false;
|
|
170
|
+
try {
|
|
171
|
+
return Boolean(JSON.parse(raw));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return Boolean(raw);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ── Module-level helpers ─────────────────────────────────────────────────────
|
|
179
|
+
function sleep(ms) {
|
|
180
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=wait.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait.js","sourceRoot":"","sources":["../../src/tools/wait.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type Engine = 'extension' | 'daemon' | 'applescript';
|
|
2
|
+
export interface EngineCapabilities {
|
|
3
|
+
shadowDom: boolean;
|
|
4
|
+
cspBypass: boolean;
|
|
5
|
+
dialogIntercept: boolean;
|
|
6
|
+
networkIntercept: boolean;
|
|
7
|
+
cookieHttpOnly: boolean;
|
|
8
|
+
framesCrossOrigin: boolean;
|
|
9
|
+
latencyMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolRequirements {
|
|
12
|
+
requiresShadowDom?: boolean;
|
|
13
|
+
requiresCspBypass?: boolean;
|
|
14
|
+
requiresDialogIntercept?: boolean;
|
|
15
|
+
requiresNetworkIntercept?: boolean;
|
|
16
|
+
requiresCookieHttpOnly?: boolean;
|
|
17
|
+
requiresFramesCrossOrigin?: boolean;
|
|
18
|
+
prefersFastLatency?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface EngineResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
value?: string;
|
|
23
|
+
error?: EngineError;
|
|
24
|
+
elapsed_ms: number;
|
|
25
|
+
}
|
|
26
|
+
export interface EngineError {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
retryable: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface ToolError {
|
|
32
|
+
code: string;
|
|
33
|
+
message: string;
|
|
34
|
+
retryable: boolean;
|
|
35
|
+
hints: string[];
|
|
36
|
+
context: {
|
|
37
|
+
engine: Engine;
|
|
38
|
+
url: string;
|
|
39
|
+
selector?: string;
|
|
40
|
+
elapsed_ms: number;
|
|
41
|
+
};
|
|
42
|
+
cause_chain?: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface ToolResponse {
|
|
45
|
+
content: Array<{
|
|
46
|
+
type: 'text' | 'image';
|
|
47
|
+
text?: string;
|
|
48
|
+
data?: string;
|
|
49
|
+
mimeType?: string;
|
|
50
|
+
}>;
|
|
51
|
+
metadata: {
|
|
52
|
+
engine: Engine;
|
|
53
|
+
degraded: boolean;
|
|
54
|
+
degradedReason?: string;
|
|
55
|
+
latencyMs: number;
|
|
56
|
+
tabUrl?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export type TabId = number;
|
|
60
|
+
export interface TabInfo {
|
|
61
|
+
tabId: TabId;
|
|
62
|
+
url: string;
|
|
63
|
+
title: string;
|
|
64
|
+
windowId: number;
|
|
65
|
+
isPrivate: boolean;
|
|
66
|
+
ownedByAgent: boolean;
|
|
67
|
+
isActive: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface DomainPolicy {
|
|
70
|
+
domain: string;
|
|
71
|
+
trust: 'trusted' | 'untrusted' | 'unknown';
|
|
72
|
+
privateWindow: boolean;
|
|
73
|
+
extensionAllowed: boolean;
|
|
74
|
+
maxActionsPerMinute: number;
|
|
75
|
+
}
|
|
76
|
+
export interface AuditEntry {
|
|
77
|
+
timestamp: string;
|
|
78
|
+
tool: string;
|
|
79
|
+
tabUrl: string;
|
|
80
|
+
engine: Engine;
|
|
81
|
+
params: Record<string, unknown>;
|
|
82
|
+
result: 'ok' | 'error';
|
|
83
|
+
elapsed_ms: number;
|
|
84
|
+
session: string;
|
|
85
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// background.js — Extension Service Worker
|
|
2
|
+
// Handles native messaging to the Safari app extension handler via
|
|
3
|
+
// browser.runtime.sendNativeMessage, routes commands from content scripts,
|
|
4
|
+
// manages cookie/DNR APIs, and tracks active tabs.
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
// Bundle ID of the containing macOS app — Safari routes
|
|
12
|
+
// sendNativeMessage calls to the app's web extension handler.
|
|
13
|
+
const APP_BUNDLE_ID = 'com.safari-pilot.app';
|
|
14
|
+
const POLL_INTERVAL_MS = 200;
|
|
15
|
+
|
|
16
|
+
// ─── State ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
let isConnected = false;
|
|
19
|
+
const activeTabs = new Map(); // tabId → { url, status }
|
|
20
|
+
let pollTimerId = null;
|
|
21
|
+
|
|
22
|
+
// ─── Native Messaging (sendNativeMessage-based) ───────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send a message to the native extension handler and return the response.
|
|
26
|
+
* Uses browser.runtime.sendNativeMessage (request/response per call).
|
|
27
|
+
*/
|
|
28
|
+
function sendNativeRequest(message) {
|
|
29
|
+
return browser.runtime.sendNativeMessage(APP_BUNDLE_ID, message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Poll the native handler for pending commands from the daemon.
|
|
34
|
+
* If a command is returned, route it to the content script,
|
|
35
|
+
* collect the result, and send it back via native messaging.
|
|
36
|
+
*/
|
|
37
|
+
async function pollForCommands() {
|
|
38
|
+
try {
|
|
39
|
+
const response = await sendNativeRequest({ type: 'poll' });
|
|
40
|
+
|
|
41
|
+
if (response && response.command && response.command !== null) {
|
|
42
|
+
const cmd = response.command;
|
|
43
|
+
await executeAndReturnResult(cmd);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn('[SafariPilot] Poll error:', e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Execute a command received from the daemon (via poll) and send the
|
|
52
|
+
* result back through the native handler.
|
|
53
|
+
*/
|
|
54
|
+
async function executeAndReturnResult(cmd) {
|
|
55
|
+
const commandId = cmd.id;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
let result;
|
|
59
|
+
|
|
60
|
+
if (cmd.script) {
|
|
61
|
+
// Route script execution to the active tab's content script
|
|
62
|
+
const tabs = await browser.tabs.query({
|
|
63
|
+
active: true,
|
|
64
|
+
currentWindow: true,
|
|
65
|
+
});
|
|
66
|
+
const tabId = tabs[0]?.id;
|
|
67
|
+
|
|
68
|
+
if (tabId != null) {
|
|
69
|
+
const responses = await browser.tabs.sendMessage(tabId, {
|
|
70
|
+
type: 'SAFARI_PILOT_COMMAND',
|
|
71
|
+
method: 'execute_script',
|
|
72
|
+
params: { script: cmd.script },
|
|
73
|
+
});
|
|
74
|
+
result = responses;
|
|
75
|
+
} else {
|
|
76
|
+
result = { ok: false, error: { message: 'No active tab' } };
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
result = { ok: true, value: null };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await sendNativeRequest({
|
|
83
|
+
type: 'result',
|
|
84
|
+
id: commandId,
|
|
85
|
+
result: result,
|
|
86
|
+
});
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// Send error result back so the daemon doesn't hang
|
|
89
|
+
try {
|
|
90
|
+
await sendNativeRequest({
|
|
91
|
+
type: 'result',
|
|
92
|
+
id: commandId,
|
|
93
|
+
result: { ok: false, error: { message: err.message } },
|
|
94
|
+
});
|
|
95
|
+
} catch (sendErr) {
|
|
96
|
+
console.error('[SafariPilot] Failed to send error result:', sendErr);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Poll Loop ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function startPolling() {
|
|
104
|
+
if (pollTimerId != null) return;
|
|
105
|
+
pollTimerId = setInterval(pollForCommands, POLL_INTERVAL_MS);
|
|
106
|
+
console.log('[SafariPilot] Polling started');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stopPolling() {
|
|
110
|
+
if (pollTimerId != null) {
|
|
111
|
+
clearInterval(pollTimerId);
|
|
112
|
+
pollTimerId = null;
|
|
113
|
+
console.log('[SafariPilot] Polling stopped');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Cookie Operations ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
async function handleCookieGet(params) {
|
|
120
|
+
const result = await browser.cookies.get({
|
|
121
|
+
url: params.url,
|
|
122
|
+
name: params.name,
|
|
123
|
+
storeId: params.storeId,
|
|
124
|
+
});
|
|
125
|
+
return { ok: true, value: result };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function handleCookieSet(params) {
|
|
129
|
+
const result = await browser.cookies.set({
|
|
130
|
+
url: params.url,
|
|
131
|
+
name: params.name,
|
|
132
|
+
value: params.value,
|
|
133
|
+
domain: params.domain,
|
|
134
|
+
path: params.path ?? '/',
|
|
135
|
+
secure: params.secure ?? false,
|
|
136
|
+
httpOnly: params.httpOnly ?? false,
|
|
137
|
+
sameSite: params.sameSite,
|
|
138
|
+
expirationDate: params.expirationDate,
|
|
139
|
+
storeId: params.storeId,
|
|
140
|
+
});
|
|
141
|
+
return { ok: true, value: result };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function handleCookieRemove(params) {
|
|
145
|
+
const result = await browser.cookies.remove({
|
|
146
|
+
url: params.url,
|
|
147
|
+
name: params.name,
|
|
148
|
+
storeId: params.storeId,
|
|
149
|
+
});
|
|
150
|
+
return { ok: true, value: result };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handleCookieGetAll(params) {
|
|
154
|
+
const result = await browser.cookies.getAll({
|
|
155
|
+
url: params.url,
|
|
156
|
+
domain: params.domain,
|
|
157
|
+
name: params.name,
|
|
158
|
+
path: params.path,
|
|
159
|
+
secure: params.secure,
|
|
160
|
+
storeId: params.storeId,
|
|
161
|
+
});
|
|
162
|
+
return { ok: true, value: result };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Declarative Net Request Operations ───────────────────────────────────
|
|
166
|
+
|
|
167
|
+
async function handleDnrAddRule(params) {
|
|
168
|
+
await browser.declarativeNetRequest.updateDynamicRules({
|
|
169
|
+
addRules: [params.rule],
|
|
170
|
+
removeRuleIds: [],
|
|
171
|
+
});
|
|
172
|
+
return { ok: true, value: { added: true, ruleId: params.rule?.id } };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function handleDnrRemoveRule(params) {
|
|
176
|
+
await browser.declarativeNetRequest.updateDynamicRules({
|
|
177
|
+
addRules: [],
|
|
178
|
+
removeRuleIds: [params.ruleId],
|
|
179
|
+
});
|
|
180
|
+
return { ok: true, value: { removed: true, ruleId: params.ruleId } };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── execute_in_main (forward to content script) ──────────────────────────
|
|
184
|
+
|
|
185
|
+
async function handleExecuteInMain(message, sender) {
|
|
186
|
+
const tabId = sender?.tab?.id;
|
|
187
|
+
if (tabId == null) {
|
|
188
|
+
return { ok: false, error: { message: 'No tab context available' } };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const [result] = await browser.tabs.sendMessage(tabId, {
|
|
193
|
+
type: 'SAFARI_PILOT_COMMAND',
|
|
194
|
+
method: message.method,
|
|
195
|
+
params: message.params ?? {},
|
|
196
|
+
});
|
|
197
|
+
return result ?? { ok: true, value: null };
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return { ok: false, error: { message: err.message } };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Command Router ────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
async function handleCommand(message, sender) {
|
|
206
|
+
const { command, params } = message;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
switch (command) {
|
|
210
|
+
case 'execute_in_main':
|
|
211
|
+
return await handleExecuteInMain(message, sender);
|
|
212
|
+
|
|
213
|
+
case 'cookie_get':
|
|
214
|
+
return await handleCookieGet(params ?? {});
|
|
215
|
+
|
|
216
|
+
case 'cookie_set':
|
|
217
|
+
return await handleCookieSet(params ?? {});
|
|
218
|
+
|
|
219
|
+
case 'cookie_remove':
|
|
220
|
+
return await handleCookieRemove(params ?? {});
|
|
221
|
+
|
|
222
|
+
case 'cookie_get_all':
|
|
223
|
+
return await handleCookieGetAll(params ?? {});
|
|
224
|
+
|
|
225
|
+
case 'dnr_add_rule':
|
|
226
|
+
return await handleDnrAddRule(params ?? {});
|
|
227
|
+
|
|
228
|
+
case 'dnr_remove_rule':
|
|
229
|
+
return await handleDnrRemoveRule(params ?? {});
|
|
230
|
+
|
|
231
|
+
default:
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
error: { message: `Unknown command: ${command}` },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: { message: err.message, name: err.name },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Runtime Message Listener ─────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
248
|
+
// Health check
|
|
249
|
+
if (message && message.type === 'ping') {
|
|
250
|
+
sendResponse({ ok: true, type: 'pong', extensionVersion: '0.1.0' });
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Command dispatch from content-isolated.js
|
|
255
|
+
if (message && message.type === 'SAFARI_PILOT_COMMAND') {
|
|
256
|
+
handleCommand(message, sender).then(sendResponse);
|
|
257
|
+
return true; // Keep message channel open for async response
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return false;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ─── Tab Tracking ─────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
266
|
+
if (changeInfo.status) {
|
|
267
|
+
activeTabs.set(tabId, {
|
|
268
|
+
url: tab.url ?? activeTabs.get(tabId)?.url,
|
|
269
|
+
status: changeInfo.status,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
browser.tabs.onRemoved.addListener((tabId) => {
|
|
275
|
+
activeTabs.delete(tabId);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ─── Initialization ────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
// Send status check on startup to register as connected
|
|
281
|
+
sendNativeRequest({ type: 'status' })
|
|
282
|
+
.then((response) => {
|
|
283
|
+
if (response && response.connected) {
|
|
284
|
+
isConnected = true;
|
|
285
|
+
console.log('[SafariPilot] Native handler connected, version:', response.version);
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
.catch((err) => {
|
|
289
|
+
console.warn('[SafariPilot] Initial status check failed:', err);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Start polling for daemon commands
|
|
293
|
+
startPolling();
|
|
294
|
+
})();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// content-isolated.js — ISOLATED world
|
|
2
|
+
// This script CANNOT be modified by page JS. It serves as a trusted relay.
|
|
3
|
+
//
|
|
4
|
+
// Role: Secure bridge between the background service worker and the MAIN world
|
|
5
|
+
// content script. Page JavaScript operates in a separate context and cannot
|
|
6
|
+
// read or tamper with this script's state or the browser extension API.
|
|
7
|
+
//
|
|
8
|
+
// Message flow:
|
|
9
|
+
// Background (runtime.sendMessage)
|
|
10
|
+
// → ISOLATED world (browser.runtime.onMessage)
|
|
11
|
+
// → MAIN world (window.postMessage with type SAFARI_PILOT_CMD)
|
|
12
|
+
// → ISOLATED world (window.addEventListener 'message' SAFARI_PILOT_RESPONSE)
|
|
13
|
+
// → Background (sendResponse callback)
|
|
14
|
+
|
|
15
|
+
(() => {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
let nextRequestId = 0;
|
|
19
|
+
const pendingRequests = new Map();
|
|
20
|
+
|
|
21
|
+
// ─── MAIN World → ISOLATED World ──────────────────────────────────────────
|
|
22
|
+
// Receive responses from the MAIN world content script.
|
|
23
|
+
// Only process messages from the same window (blocks cross-frame injection).
|
|
24
|
+
|
|
25
|
+
window.addEventListener('message', (event) => {
|
|
26
|
+
if (event.source !== window) return;
|
|
27
|
+
if (event.data?.type !== 'SAFARI_PILOT_RESPONSE') return;
|
|
28
|
+
|
|
29
|
+
const { requestId, ok, value, error } = event.data;
|
|
30
|
+
const pending = pendingRequests.get(requestId);
|
|
31
|
+
if (!pending) return;
|
|
32
|
+
|
|
33
|
+
pendingRequests.delete(requestId);
|
|
34
|
+
if (ok) {
|
|
35
|
+
pending.resolve(value);
|
|
36
|
+
} else {
|
|
37
|
+
pending.reject(error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ─── Background → ISOLATED World ──────────────────────────────────────────
|
|
42
|
+
// Receive commands from the background service worker via runtime messaging.
|
|
43
|
+
// Returns true to indicate async sendResponse (keeps message channel open).
|
|
44
|
+
|
|
45
|
+
browser.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
46
|
+
if (message.type !== 'SAFARI_PILOT_COMMAND') return false;
|
|
47
|
+
|
|
48
|
+
const requestId = `sp_${++nextRequestId}_${Date.now()}`;
|
|
49
|
+
|
|
50
|
+
const promise = new Promise((resolve, reject) => {
|
|
51
|
+
pendingRequests.set(requestId, { resolve, reject });
|
|
52
|
+
|
|
53
|
+
// Forward to MAIN world. Use window.location.origin (never '*') per spec.
|
|
54
|
+
window.postMessage(
|
|
55
|
+
{
|
|
56
|
+
type: 'SAFARI_PILOT_CMD',
|
|
57
|
+
requestId,
|
|
58
|
+
method: message.method,
|
|
59
|
+
params: message.params ?? {},
|
|
60
|
+
},
|
|
61
|
+
window.location.origin
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Timeout: MAIN world has 10 s to respond before we fail the request
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
if (pendingRequests.has(requestId)) {
|
|
67
|
+
pendingRequests.delete(requestId);
|
|
68
|
+
reject({ message: 'MAIN world timeout', code: 'TIMEOUT' });
|
|
69
|
+
}
|
|
70
|
+
}, 10_000);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
promise.then(
|
|
74
|
+
value => sendResponse({ ok: true, value }),
|
|
75
|
+
error => sendResponse({ ok: false, error })
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return true; // Keep the message channel open for async sendResponse
|
|
79
|
+
});
|
|
80
|
+
})();
|