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,114 @@
|
|
|
1
|
+
import { CircuitBreakerOpenError } from '../errors.js';
|
|
2
|
+
const FAILURE_THRESHOLD = 5; // consecutive failures to trip the circuit
|
|
3
|
+
const WINDOW_MS = 60_000; // failure tracking window (60 s)
|
|
4
|
+
const COOLDOWN_MS = 120_000; // open → half-open cooldown (120 s)
|
|
5
|
+
function emptyState() {
|
|
6
|
+
return {
|
|
7
|
+
failures: 0,
|
|
8
|
+
firstFailureAt: 0,
|
|
9
|
+
openedAt: null,
|
|
10
|
+
probeAllowed: false,
|
|
11
|
+
probeInFlight: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export class CircuitBreaker {
|
|
15
|
+
states = new Map();
|
|
16
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Record a successful call. Resets the failure counter and closes the circuit.
|
|
19
|
+
*/
|
|
20
|
+
recordSuccess(domain) {
|
|
21
|
+
const state = this.getState_(domain);
|
|
22
|
+
state.failures = 0;
|
|
23
|
+
state.firstFailureAt = 0;
|
|
24
|
+
state.openedAt = null;
|
|
25
|
+
state.probeAllowed = false;
|
|
26
|
+
state.probeInFlight = false;
|
|
27
|
+
this.states.set(domain, state);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Record a failed call. May open the circuit if the threshold is reached.
|
|
31
|
+
*/
|
|
32
|
+
recordFailure(domain) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const state = this.getState_(domain);
|
|
35
|
+
// A probe failure in half-open state immediately re-opens the circuit,
|
|
36
|
+
// bypassing the normal threshold, and resets the cooldown clock.
|
|
37
|
+
if (state.probeInFlight) {
|
|
38
|
+
state.openedAt = now;
|
|
39
|
+
state.probeAllowed = false;
|
|
40
|
+
state.probeInFlight = false;
|
|
41
|
+
// Keep failures at threshold so the circuit stays open on next check
|
|
42
|
+
state.failures = FAILURE_THRESHOLD;
|
|
43
|
+
state.firstFailureAt = now;
|
|
44
|
+
this.states.set(domain, state);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Reset failure count if previous run is outside the tracking window
|
|
48
|
+
if (state.failures > 0 && now - state.firstFailureAt > WINDOW_MS) {
|
|
49
|
+
state.failures = 0;
|
|
50
|
+
state.firstFailureAt = 0;
|
|
51
|
+
}
|
|
52
|
+
if (state.failures === 0) {
|
|
53
|
+
state.firstFailureAt = now;
|
|
54
|
+
}
|
|
55
|
+
state.failures += 1;
|
|
56
|
+
if (state.failures >= FAILURE_THRESHOLD) {
|
|
57
|
+
state.openedAt = now;
|
|
58
|
+
state.probeAllowed = false;
|
|
59
|
+
}
|
|
60
|
+
this.states.set(domain, state);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns true when the circuit is open (calls should be rejected).
|
|
64
|
+
* Half-open circuits return false — a probe is permitted.
|
|
65
|
+
*/
|
|
66
|
+
isOpen(domain) {
|
|
67
|
+
return this.getState(domain) === 'open';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compute the current circuit state for a domain.
|
|
71
|
+
*/
|
|
72
|
+
getState(domain) {
|
|
73
|
+
const state = this.getState_(domain);
|
|
74
|
+
if (state.openedAt === null) {
|
|
75
|
+
return 'closed';
|
|
76
|
+
}
|
|
77
|
+
const elapsed = Date.now() - state.openedAt;
|
|
78
|
+
if (elapsed >= COOLDOWN_MS) {
|
|
79
|
+
return 'half-open';
|
|
80
|
+
}
|
|
81
|
+
return 'open';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Assert the circuit is not open before executing a call.
|
|
85
|
+
* Call this at the entry point of any guarded operation.
|
|
86
|
+
* Throws CircuitBreakerOpenError when the circuit is open.
|
|
87
|
+
* In half-open state, marks the probe as issued and allows one call through.
|
|
88
|
+
*/
|
|
89
|
+
assertClosed(domain) {
|
|
90
|
+
const circuitState = this.getState(domain);
|
|
91
|
+
const state = this.getState_(domain);
|
|
92
|
+
if (circuitState === 'open') {
|
|
93
|
+
const remaining = COOLDOWN_MS - (Date.now() - (state.openedAt ?? 0));
|
|
94
|
+
throw new CircuitBreakerOpenError(domain, Math.ceil(remaining / 1000));
|
|
95
|
+
}
|
|
96
|
+
if (circuitState === 'half-open') {
|
|
97
|
+
if (state.probeAllowed) {
|
|
98
|
+
// Already issued a probe — reject subsequent calls until success/fail
|
|
99
|
+
throw new CircuitBreakerOpenError(domain, 0);
|
|
100
|
+
}
|
|
101
|
+
state.probeAllowed = true;
|
|
102
|
+
state.probeInFlight = true;
|
|
103
|
+
this.states.set(domain, state);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ── Internal ─────────────────────────────────────────────────────────────────
|
|
107
|
+
getState_(domain) {
|
|
108
|
+
if (!this.states.has(domain)) {
|
|
109
|
+
this.states.set(domain, emptyState());
|
|
110
|
+
}
|
|
111
|
+
return this.states.get(domain);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/security/circuit-breaker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type TrustLevel = 'trusted' | 'untrusted' | 'unknown';
|
|
2
|
+
interface PolicyRule {
|
|
3
|
+
trust: TrustLevel;
|
|
4
|
+
privateWindow: boolean;
|
|
5
|
+
extensionAllowed: boolean;
|
|
6
|
+
maxActionsPerMinute: number;
|
|
7
|
+
}
|
|
8
|
+
export interface EvaluateResult {
|
|
9
|
+
trust: TrustLevel;
|
|
10
|
+
privateWindow: boolean;
|
|
11
|
+
extensionAllowed: boolean;
|
|
12
|
+
maxActionsPerMinute: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class DomainPolicy {
|
|
15
|
+
private rules;
|
|
16
|
+
constructor();
|
|
17
|
+
addRule(domain: string, policy: Partial<PolicyRule>): void;
|
|
18
|
+
removeRule(domain: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Evaluate a URL against all stored rules.
|
|
21
|
+
* Rules are checked in insertion order; first match wins.
|
|
22
|
+
* Falls back to DEFAULT_POLICY if no rule matches.
|
|
23
|
+
*/
|
|
24
|
+
evaluate(url: string): EvaluateResult;
|
|
25
|
+
getRules(): Array<{
|
|
26
|
+
domain: string;
|
|
27
|
+
} & PolicyRule>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const DEFAULT_POLICY = {
|
|
2
|
+
trust: 'unknown',
|
|
3
|
+
privateWindow: false,
|
|
4
|
+
extensionAllowed: false,
|
|
5
|
+
maxActionsPerMinute: 60,
|
|
6
|
+
};
|
|
7
|
+
// Built-in sensitive domain patterns → untrusted, force private window
|
|
8
|
+
const SENSITIVE_PATTERNS = [
|
|
9
|
+
'*.bank.*',
|
|
10
|
+
'*.banking.*',
|
|
11
|
+
'paypal.com',
|
|
12
|
+
'*.paypal.com',
|
|
13
|
+
'stripe.com',
|
|
14
|
+
'*.stripe.com',
|
|
15
|
+
'venmo.com',
|
|
16
|
+
'*.venmo.com',
|
|
17
|
+
'chase.com',
|
|
18
|
+
'*.chase.com',
|
|
19
|
+
'wellsfargo.com',
|
|
20
|
+
'*.wellsfargo.com',
|
|
21
|
+
'bankofamerica.com',
|
|
22
|
+
'*.bankofamerica.com',
|
|
23
|
+
'citibank.com',
|
|
24
|
+
'*.citibank.com',
|
|
25
|
+
];
|
|
26
|
+
const SENSITIVE_POLICY = {
|
|
27
|
+
trust: 'untrusted',
|
|
28
|
+
privateWindow: true,
|
|
29
|
+
extensionAllowed: false,
|
|
30
|
+
maxActionsPerMinute: 30,
|
|
31
|
+
};
|
|
32
|
+
// ── Glob matching ─────────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Convert a glob pattern (only * wildcard supported) to a RegExp.
|
|
35
|
+
* *.example.com matches sub.example.com but NOT example.com itself.
|
|
36
|
+
*/
|
|
37
|
+
function globToRegex(pattern) {
|
|
38
|
+
const escaped = pattern
|
|
39
|
+
.split('*')
|
|
40
|
+
.map((part) => part.replace(/[.+^${}()|[\]\\]/g, '\\$&'))
|
|
41
|
+
.join('[^.]+');
|
|
42
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
43
|
+
}
|
|
44
|
+
function extractHostname(url) {
|
|
45
|
+
try {
|
|
46
|
+
return new URL(url).hostname.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// If it's already a hostname (no scheme), return as-is
|
|
50
|
+
return url.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function matchesDomain(pattern, hostname) {
|
|
54
|
+
if (pattern.includes('*')) {
|
|
55
|
+
return globToRegex(pattern).test(hostname);
|
|
56
|
+
}
|
|
57
|
+
return pattern.toLowerCase() === hostname;
|
|
58
|
+
}
|
|
59
|
+
// ─── DomainPolicy class ───────────────────────────────────────────────────────
|
|
60
|
+
export class DomainPolicy {
|
|
61
|
+
rules = new Map();
|
|
62
|
+
constructor() {
|
|
63
|
+
// Register built-in sensitive domain rules
|
|
64
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
65
|
+
this.rules.set(pattern, { ...SENSITIVE_POLICY });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── Rule management ─────────────────────────────────────────────────────────
|
|
69
|
+
addRule(domain, policy) {
|
|
70
|
+
const existing = this.rules.get(domain) ?? { ...DEFAULT_POLICY };
|
|
71
|
+
this.rules.set(domain, { ...existing, ...policy });
|
|
72
|
+
}
|
|
73
|
+
removeRule(domain) {
|
|
74
|
+
this.rules.delete(domain);
|
|
75
|
+
}
|
|
76
|
+
// ── Evaluation ──────────────────────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Evaluate a URL against all stored rules.
|
|
79
|
+
* Rules are checked in insertion order; first match wins.
|
|
80
|
+
* Falls back to DEFAULT_POLICY if no rule matches.
|
|
81
|
+
*/
|
|
82
|
+
evaluate(url) {
|
|
83
|
+
const hostname = extractHostname(url);
|
|
84
|
+
for (const [pattern, rule] of this.rules) {
|
|
85
|
+
if (matchesDomain(pattern, hostname)) {
|
|
86
|
+
return { ...rule };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { ...DEFAULT_POLICY };
|
|
90
|
+
}
|
|
91
|
+
// ── Introspection ────────────────────────────────────────────────────────────
|
|
92
|
+
getRules() {
|
|
93
|
+
return Array.from(this.rules.entries()).map(([domain, rule]) => ({ domain, ...rule }));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=domain-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-policy.js","sourceRoot":"","sources":["../../src/security/domain-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ApprovalResult {
|
|
2
|
+
required: boolean;
|
|
3
|
+
reason?: string;
|
|
4
|
+
category?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class HumanApproval {
|
|
7
|
+
/**
|
|
8
|
+
* Inspect an action/url/params combination and return whether human approval
|
|
9
|
+
* is required. Returns `{ required: false }` for benign actions, or
|
|
10
|
+
* `{ required: true, reason, category }` for sensitive ones.
|
|
11
|
+
*
|
|
12
|
+
* Does NOT throw — use `assertApproved` for a throwing guard.
|
|
13
|
+
*/
|
|
14
|
+
requiresApproval(action: string, url: string, params?: Record<string, unknown>): ApprovalResult;
|
|
15
|
+
/**
|
|
16
|
+
* Guard variant: throws `HumanApprovalRequiredError` when approval is needed.
|
|
17
|
+
* Use in tool pipelines where blocking is the correct behaviour.
|
|
18
|
+
*/
|
|
19
|
+
assertApproved(action: string, url: string, params?: Record<string, unknown>): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { HumanApprovalRequiredError } from '../errors.js';
|
|
2
|
+
// ─── Pattern Registry ─────────────────────────────────────────────────────────
|
|
3
|
+
/** OAuth / SSO provider URL patterns. */
|
|
4
|
+
const OAUTH_URL_PATTERNS = [
|
|
5
|
+
/accounts\.google\.com\/o\/oauth/i,
|
|
6
|
+
/login\.microsoftonline\.com/i,
|
|
7
|
+
/github\.com\/login\/oauth/i,
|
|
8
|
+
/auth0\.com/i,
|
|
9
|
+
/okta\.com/i,
|
|
10
|
+
/login\.live\.com/i,
|
|
11
|
+
/appleid\.apple\.com/i,
|
|
12
|
+
/facebook\.com\/dialog\/oauth/i,
|
|
13
|
+
];
|
|
14
|
+
/** Financial checkout / payment URL patterns. */
|
|
15
|
+
const FINANCIAL_URL_PATTERNS = [
|
|
16
|
+
/paypal\.com\/checkout/i,
|
|
17
|
+
/paypal\.com\/pay/i,
|
|
18
|
+
/stripe\.com\/pay/i,
|
|
19
|
+
/checkout\.stripe\.com/i,
|
|
20
|
+
/pay\.amazon\.com/i,
|
|
21
|
+
/venmo\.com\/pay/i,
|
|
22
|
+
];
|
|
23
|
+
/** Sensitive financial form field names. */
|
|
24
|
+
const FINANCIAL_FIELD_NAMES = [
|
|
25
|
+
/^card[_-]?number$/i,
|
|
26
|
+
/^cvv$/i,
|
|
27
|
+
/^cvc$/i,
|
|
28
|
+
/^account[_-]?number$/i,
|
|
29
|
+
/^routing[_-]?number$/i,
|
|
30
|
+
/^bank[_-]?account$/i,
|
|
31
|
+
/^credit[_-]?card$/i,
|
|
32
|
+
];
|
|
33
|
+
/** Downloadable file extensions that require approval. */
|
|
34
|
+
const DOWNLOAD_EXTENSIONS = /\.(exe|dmg|pkg|zip|tar|gz|rar|deb|rpm|msi|app|apk)(\?.*)?$/i;
|
|
35
|
+
/** Account-mutating / high-risk URL path patterns. */
|
|
36
|
+
const ACCOUNT_SETTINGS_PATTERNS = [
|
|
37
|
+
/\/settings\/security/i,
|
|
38
|
+
/\/account\/delete/i,
|
|
39
|
+
/\/password\/change/i,
|
|
40
|
+
/\/password\/reset/i,
|
|
41
|
+
/\/delete[_-]?account/i,
|
|
42
|
+
/\/security\/two-factor/i,
|
|
43
|
+
];
|
|
44
|
+
/** Sensitive field names in form submissions. */
|
|
45
|
+
const SENSITIVE_FORM_FIELDS = [
|
|
46
|
+
/^password$/i,
|
|
47
|
+
/^passwd$/i,
|
|
48
|
+
/^pass$/i,
|
|
49
|
+
/^ssn$/i,
|
|
50
|
+
/^social[_-]?security/i,
|
|
51
|
+
/^credit[_-]?card/i,
|
|
52
|
+
/^card[_-]?number/i,
|
|
53
|
+
/^cvv$/i,
|
|
54
|
+
/^cvc$/i,
|
|
55
|
+
];
|
|
56
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
57
|
+
function matchesAny(value, patterns) {
|
|
58
|
+
return patterns.some((p) => p.test(value));
|
|
59
|
+
}
|
|
60
|
+
function extractDomain(url) {
|
|
61
|
+
try {
|
|
62
|
+
return new URL(url).hostname;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return url;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ─── HumanApproval ───────────────────────────────────────────────────────────
|
|
69
|
+
export class HumanApproval {
|
|
70
|
+
/**
|
|
71
|
+
* Inspect an action/url/params combination and return whether human approval
|
|
72
|
+
* is required. Returns `{ required: false }` for benign actions, or
|
|
73
|
+
* `{ required: true, reason, category }` for sensitive ones.
|
|
74
|
+
*
|
|
75
|
+
* Does NOT throw — use `assertApproved` for a throwing guard.
|
|
76
|
+
*/
|
|
77
|
+
requiresApproval(action, url, params) {
|
|
78
|
+
// 1. OAuth / SSO flows
|
|
79
|
+
if (matchesAny(url, OAUTH_URL_PATTERNS)) {
|
|
80
|
+
return {
|
|
81
|
+
required: true,
|
|
82
|
+
category: 'oauth',
|
|
83
|
+
reason: 'URL matches an OAuth/SSO authentication flow',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// 2. Financial checkout / payment pages
|
|
87
|
+
if (matchesAny(url, FINANCIAL_URL_PATTERNS)) {
|
|
88
|
+
return {
|
|
89
|
+
required: true,
|
|
90
|
+
category: 'financial',
|
|
91
|
+
reason: 'URL matches a financial payment or checkout flow',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// 3. Financial form fields in params
|
|
95
|
+
if (params !== undefined) {
|
|
96
|
+
const fieldNames = Object.keys(params);
|
|
97
|
+
const sensitiveField = fieldNames.find((f) => matchesAny(f, FINANCIAL_FIELD_NAMES));
|
|
98
|
+
if (sensitiveField !== undefined) {
|
|
99
|
+
return {
|
|
100
|
+
required: true,
|
|
101
|
+
category: 'financial',
|
|
102
|
+
reason: `Request contains sensitive financial field: "${sensitiveField}"`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 4. Download actions or downloadable file extensions in URL
|
|
107
|
+
if (action === 'download' || DOWNLOAD_EXTENSIONS.test(url)) {
|
|
108
|
+
return {
|
|
109
|
+
required: true,
|
|
110
|
+
category: 'download',
|
|
111
|
+
reason: 'Action involves downloading a file',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// 5. Account settings / destructive account operations
|
|
115
|
+
if (matchesAny(url, ACCOUNT_SETTINGS_PATTERNS)) {
|
|
116
|
+
return {
|
|
117
|
+
required: true,
|
|
118
|
+
category: 'account_settings',
|
|
119
|
+
reason: 'URL matches a sensitive account settings or deletion path',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// 6. Form submissions (POST) with sensitive field names
|
|
123
|
+
if (action === 'submit' || action === 'post') {
|
|
124
|
+
if (params !== undefined) {
|
|
125
|
+
const fieldNames = Object.keys(params);
|
|
126
|
+
const sensitiveField = fieldNames.find((f) => matchesAny(f, SENSITIVE_FORM_FIELDS));
|
|
127
|
+
if (sensitiveField !== undefined) {
|
|
128
|
+
return {
|
|
129
|
+
required: true,
|
|
130
|
+
category: 'form_submission',
|
|
131
|
+
reason: `Form submission contains sensitive field: "${sensitiveField}"`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { required: false };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Guard variant: throws `HumanApprovalRequiredError` when approval is needed.
|
|
140
|
+
* Use in tool pipelines where blocking is the correct behaviour.
|
|
141
|
+
*/
|
|
142
|
+
assertApproved(action, url, params) {
|
|
143
|
+
const result = this.requiresApproval(action, url, params);
|
|
144
|
+
if (result.required) {
|
|
145
|
+
const domain = extractDomain(url);
|
|
146
|
+
throw new HumanApprovalRequiredError(action, domain);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=human-approval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"human-approval.js","sourceRoot":"","sources":["../../src/security/human-approval.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface IdpiThreat {
|
|
2
|
+
pattern: string;
|
|
3
|
+
confidence: number;
|
|
4
|
+
match: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ScanResult {
|
|
7
|
+
safe: boolean;
|
|
8
|
+
threats: IdpiThreat[];
|
|
9
|
+
}
|
|
10
|
+
export declare class IdpiScanner {
|
|
11
|
+
/**
|
|
12
|
+
* Scan a text string for indirect prompt injection patterns.
|
|
13
|
+
*
|
|
14
|
+
* Returns a result with:
|
|
15
|
+
* - `safe` — false when any threat confidence exceeds 0.5
|
|
16
|
+
* - `threats` — all matched threats with their pattern name, confidence, and
|
|
17
|
+
* the excerpt that triggered the match
|
|
18
|
+
*/
|
|
19
|
+
scan(text: string): ScanResult;
|
|
20
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ─── IdpiScanner ─────────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Indirect Prompt Injection (IDPI) defence layer. Scans arbitrary text content
|
|
4
|
+
// retrieved from web pages for patterns that attempt to hijack agent behaviour.
|
|
5
|
+
//
|
|
6
|
+
// Each detector is a named rule with a confidence weight (0.0–1.0). A result
|
|
7
|
+
// is marked unsafe when any matched threat has confidence > 0.5.
|
|
8
|
+
// ─── Pattern Registry ─────────────────────────────────────────────────────────
|
|
9
|
+
const PATTERN_RULES = [
|
|
10
|
+
// 1. Instruction override attempts
|
|
11
|
+
{
|
|
12
|
+
name: 'instruction_override',
|
|
13
|
+
regex: /ignore\s+(previous|prior|all\s+prior|above)\s+instructions?|disregard\s+(the\s+)?(above|previous|prior)/gi,
|
|
14
|
+
confidence: 0.95,
|
|
15
|
+
},
|
|
16
|
+
// 2. Role reassignment attempts
|
|
17
|
+
{
|
|
18
|
+
name: 'role_reassignment',
|
|
19
|
+
regex: /you\s+are\s+now\s+(a|an)\s+\w+|you\s+are\s+a\s+\w+/gi,
|
|
20
|
+
confidence: 0.80,
|
|
21
|
+
},
|
|
22
|
+
// 3. Fake system prompt injection
|
|
23
|
+
{
|
|
24
|
+
name: 'fake_system_prompt',
|
|
25
|
+
regex: /^system:|^SYSTEM:|###\s*System\b/gim,
|
|
26
|
+
confidence: 0.90,
|
|
27
|
+
},
|
|
28
|
+
// 4. Base64-encoded payloads (50+ contiguous base64 chars)
|
|
29
|
+
{
|
|
30
|
+
name: 'base64_payload',
|
|
31
|
+
regex: /[A-Za-z0-9+/]{50,}={0,2}/g,
|
|
32
|
+
confidence: 0.65,
|
|
33
|
+
},
|
|
34
|
+
// 5. Secrecy / concealment instructions
|
|
35
|
+
{
|
|
36
|
+
name: 'secrecy_instruction',
|
|
37
|
+
regex: /do\s+not\s+tell\s+(the\s+)?user|keep\s+this\s+secret|don['']?t\s+mention\s+this/gi,
|
|
38
|
+
confidence: 0.90,
|
|
39
|
+
},
|
|
40
|
+
// 6. HTML-encoded instruction sequences (&#x...; or &#...;)
|
|
41
|
+
{
|
|
42
|
+
name: 'html_encoded_instruction',
|
|
43
|
+
regex: /(?:&#x[0-9a-fA-F]{2,4};){4,}|(?:&#\d{2,5};){4,}/g,
|
|
44
|
+
confidence: 0.75,
|
|
45
|
+
},
|
|
46
|
+
// 7. CSS content property with non-trivial text
|
|
47
|
+
{
|
|
48
|
+
name: 'css_content_injection',
|
|
49
|
+
regex: /content\s*:\s*["'][^"']{10,}["']/gi,
|
|
50
|
+
confidence: 0.70,
|
|
51
|
+
},
|
|
52
|
+
// 8. Hidden text patterns via CSS
|
|
53
|
+
{
|
|
54
|
+
name: 'hidden_text',
|
|
55
|
+
regex: /display\s*:\s*none|visibility\s*:\s*hidden|font-size\s*:\s*0(px|pt|em|rem)?/gi,
|
|
56
|
+
confidence: 0.60,
|
|
57
|
+
},
|
|
58
|
+
// 9. Unicode homoglyph attacks — non-ASCII characters mixed into Latin words
|
|
59
|
+
{
|
|
60
|
+
name: 'unicode_homoglyph',
|
|
61
|
+
regex: /[^\x00-\x7F\s]{1}[a-zA-Z]{2,}|[a-zA-Z]{2,}[^\x00-\x7F\s]{1}/g,
|
|
62
|
+
confidence: 0.75,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
// ─── IdpiScanner ─────────────────────────────────────────────────────────────
|
|
66
|
+
export class IdpiScanner {
|
|
67
|
+
/**
|
|
68
|
+
* Scan a text string for indirect prompt injection patterns.
|
|
69
|
+
*
|
|
70
|
+
* Returns a result with:
|
|
71
|
+
* - `safe` — false when any threat confidence exceeds 0.5
|
|
72
|
+
* - `threats` — all matched threats with their pattern name, confidence, and
|
|
73
|
+
* the excerpt that triggered the match
|
|
74
|
+
*/
|
|
75
|
+
scan(text) {
|
|
76
|
+
const threats = [];
|
|
77
|
+
for (const rule of PATTERN_RULES) {
|
|
78
|
+
// Reset lastIndex so repeated calls work correctly on global regexes
|
|
79
|
+
rule.regex.lastIndex = 0;
|
|
80
|
+
let match;
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
while ((match = rule.regex.exec(text)) !== null) {
|
|
83
|
+
const excerpt = match[0].slice(0, 100); // cap excerpt length
|
|
84
|
+
if (seen.has(excerpt))
|
|
85
|
+
continue; // deduplicate identical matches
|
|
86
|
+
seen.add(excerpt);
|
|
87
|
+
threats.push({
|
|
88
|
+
pattern: rule.name,
|
|
89
|
+
confidence: rule.confidence,
|
|
90
|
+
match: excerpt,
|
|
91
|
+
});
|
|
92
|
+
// Prevent infinite loops on zero-length matches
|
|
93
|
+
if (match.index === rule.regex.lastIndex) {
|
|
94
|
+
rule.regex.lastIndex++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const safe = threats.every((t) => t.confidence <= 0.5);
|
|
99
|
+
return { safe, threats };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=idpi-scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idpi-scanner.js","sourceRoot":"","sources":["../../src/security/idpi-scanner.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,EAAE;AACF,6EAA6E;AAC7E,iEAAiE;AAmBjE,iFAAiF;AAEjF,MAAM,aAAa,GAAkB;IACnC,mCAAmC;IACnC;QACE,IAAI,EAAE,sBAAsB;QAC5B,KAAK,EAAE,2GAA2G;QAClH,UAAU,EAAE,IAAI;KACjB;IAED,gCAAgC;IAChC;QACE,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,sDAAsD;QAC7D,UAAU,EAAE,IAAI;KACjB;IAED,kCAAkC;IAClC;QACE,IAAI,EAAE,oBAAoB;QAC1B,KAAK,EAAE,qCAAqC;QAC5C,UAAU,EAAE,IAAI;KACjB;IAED,2DAA2D;IAC3D;QACE,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,2BAA2B;QAClC,UAAU,EAAE,IAAI;KACjB;IAED,wCAAwC;IACxC;QACE,IAAI,EAAE,qBAAqB;QAC3B,KAAK,EAAE,mFAAmF;QAC1F,UAAU,EAAE,IAAI;KACjB;IAED,4DAA4D;IAC5D;QACE,IAAI,EAAE,0BAA0B;QAChC,KAAK,EAAE,kDAAkD;QACzD,UAAU,EAAE,IAAI;KACjB;IAED,gDAAgD;IAChD;QACE,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,oCAAoC;QAC3C,UAAU,EAAE,IAAI;KACjB;IAED,kCAAkC;IAClC;QACE,IAAI,EAAE,aAAa;QACnB,KAAK,EAAE,+EAA+E;QACtF,UAAU,EAAE,IAAI;KACjB;IAED,6EAA6E;IAC7E;QACE,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,8DAA8D;QACrE,UAAU,EAAE,IAAI;KACjB;CACF,CAAC;AAEF,gFAAgF;AAEhF,MAAM,OAAO,WAAW;IACtB;;;;;;;OAOG;IACH,IAAI,CAAC,IAAY;QACf,MAAM,OAAO,GAAiB,EAAE,CAAC;QAEjC,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,qEAAqE;YACrE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;YAEzB,IAAI,KAA6B,CAAC;YAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;YAE/B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;gBAChD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,qBAAqB;gBAC7D,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;oBAAE,SAAS,CAAC,gCAAgC;gBACjE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAElB,OAAO,CAAC,IAAI,CAAC;oBACX,OAAO,EAAE,IAAI,CAAC,IAAI;oBAClB,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,KAAK,EAAE,OAAO;iBACf,CAAC,CAAC;gBAEH,gDAAgD;gBAChD,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;oBACzC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;QAEvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC3B,CAAC;CACF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AuditLog } from './audit-log.js';
|
|
2
|
+
export interface ActivationState {
|
|
3
|
+
active: boolean;
|
|
4
|
+
reason?: string;
|
|
5
|
+
activatedAt?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AutoActivationThreshold {
|
|
8
|
+
/** Number of errors that triggers auto-activation. */
|
|
9
|
+
maxErrors: number;
|
|
10
|
+
/** Rolling window (seconds) within which errors are counted. */
|
|
11
|
+
windowSeconds: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class KillSwitch {
|
|
14
|
+
private _active;
|
|
15
|
+
private _reason;
|
|
16
|
+
private _activatedAt;
|
|
17
|
+
private readonly auditLog;
|
|
18
|
+
private readonly threshold;
|
|
19
|
+
private readonly errorWindow;
|
|
20
|
+
constructor(options?: {
|
|
21
|
+
auditLog?: AuditLog;
|
|
22
|
+
autoActivation?: AutoActivationThreshold;
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Activate the kill switch. All subsequent `checkBeforeAction` calls will
|
|
26
|
+
* throw `KillSwitchActiveError` until `deactivate` is called.
|
|
27
|
+
*/
|
|
28
|
+
activate(reason: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Deactivate the kill switch, re-enabling automation.
|
|
31
|
+
*/
|
|
32
|
+
deactivate(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Returns true if the kill switch is currently active.
|
|
35
|
+
*/
|
|
36
|
+
isActive(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Returns the current activation state including reason and timestamp.
|
|
39
|
+
*/
|
|
40
|
+
getActivation(): ActivationState;
|
|
41
|
+
/**
|
|
42
|
+
* Call before every automation action. Throws `KillSwitchActiveError` if the
|
|
43
|
+
* kill switch is active, otherwise returns normally.
|
|
44
|
+
*/
|
|
45
|
+
checkBeforeAction(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Record an error event. If the configured threshold is exceeded within the
|
|
48
|
+
* rolling window, the kill switch auto-activates.
|
|
49
|
+
*/
|
|
50
|
+
recordError(): void;
|
|
51
|
+
}
|