@predicatelabs/sdk 0.99.9
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/LICENSE +24 -0
- package/README.md +252 -0
- package/dist/actions.d.ts +185 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +1120 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent-runtime.d.ts +352 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +1170 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent.d.ts +164 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +408 -0
- package/dist/agent.js.map +1 -0
- package/dist/asserts/expect.d.ts +159 -0
- package/dist/asserts/expect.d.ts.map +1 -0
- package/dist/asserts/expect.js +547 -0
- package/dist/asserts/expect.js.map +1 -0
- package/dist/asserts/index.d.ts +58 -0
- package/dist/asserts/index.d.ts.map +1 -0
- package/dist/asserts/index.js +70 -0
- package/dist/asserts/index.js.map +1 -0
- package/dist/asserts/query.d.ts +199 -0
- package/dist/asserts/query.d.ts.map +1 -0
- package/dist/asserts/query.js +288 -0
- package/dist/asserts/query.js.map +1 -0
- package/dist/backends/actions.d.ts +119 -0
- package/dist/backends/actions.d.ts.map +1 -0
- package/dist/backends/actions.js +291 -0
- package/dist/backends/actions.js.map +1 -0
- package/dist/backends/browser-use-adapter.d.ts +131 -0
- package/dist/backends/browser-use-adapter.d.ts.map +1 -0
- package/dist/backends/browser-use-adapter.js +219 -0
- package/dist/backends/browser-use-adapter.js.map +1 -0
- package/dist/backends/cdp-backend.d.ts +66 -0
- package/dist/backends/cdp-backend.d.ts.map +1 -0
- package/dist/backends/cdp-backend.js +273 -0
- package/dist/backends/cdp-backend.js.map +1 -0
- package/dist/backends/index.d.ts +80 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +101 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/protocol.d.ts +156 -0
- package/dist/backends/protocol.d.ts.map +1 -0
- package/dist/backends/protocol.js +16 -0
- package/dist/backends/protocol.js.map +1 -0
- package/dist/backends/sentience-context.d.ts +143 -0
- package/dist/backends/sentience-context.d.ts.map +1 -0
- package/dist/backends/sentience-context.js +359 -0
- package/dist/backends/sentience-context.js.map +1 -0
- package/dist/backends/snapshot.d.ts +188 -0
- package/dist/backends/snapshot.d.ts.map +1 -0
- package/dist/backends/snapshot.js +360 -0
- package/dist/backends/snapshot.js.map +1 -0
- package/dist/browser.d.ts +154 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +920 -0
- package/dist/browser.js.map +1 -0
- package/dist/canonicalization.d.ts +126 -0
- package/dist/canonicalization.d.ts.map +1 -0
- package/dist/canonicalization.js +161 -0
- package/dist/canonicalization.js.map +1 -0
- package/dist/captcha/strategies.d.ts +12 -0
- package/dist/captcha/strategies.d.ts.map +1 -0
- package/dist/captcha/strategies.js +43 -0
- package/dist/captcha/strategies.js.map +1 -0
- package/dist/captcha/types.d.ts +45 -0
- package/dist/captcha/types.d.ts.map +1 -0
- package/dist/captcha/types.js +12 -0
- package/dist/captcha/types.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +422 -0
- package/dist/cli.js.map +1 -0
- package/dist/conversational-agent.d.ts +123 -0
- package/dist/conversational-agent.d.ts.map +1 -0
- package/dist/conversational-agent.js +341 -0
- package/dist/conversational-agent.js.map +1 -0
- package/dist/cursor-policy.d.ts +41 -0
- package/dist/cursor-policy.d.ts.map +1 -0
- package/dist/cursor-policy.js +81 -0
- package/dist/cursor-policy.js.map +1 -0
- package/dist/debugger.d.ts +28 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +107 -0
- package/dist/debugger.js.map +1 -0
- package/dist/expect.d.ts +16 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +67 -0
- package/dist/expect.js.map +1 -0
- package/dist/failure-artifacts.d.ts +95 -0
- package/dist/failure-artifacts.d.ts.map +1 -0
- package/dist/failure-artifacts.js +805 -0
- package/dist/failure-artifacts.js.map +1 -0
- package/dist/generator.d.ts +16 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +205 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector.d.ts +13 -0
- package/dist/inspector.d.ts.map +1 -0
- package/dist/inspector.js +153 -0
- package/dist/inspector.js.map +1 -0
- package/dist/llm-provider.d.ts +144 -0
- package/dist/llm-provider.d.ts.map +1 -0
- package/dist/llm-provider.js +460 -0
- package/dist/llm-provider.js.map +1 -0
- package/dist/ordinal.d.ts +90 -0
- package/dist/ordinal.d.ts.map +1 -0
- package/dist/ordinal.js +249 -0
- package/dist/ordinal.js.map +1 -0
- package/dist/overlay.d.ts +63 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +102 -0
- package/dist/overlay.js.map +1 -0
- package/dist/protocols/browser-protocol.d.ts +79 -0
- package/dist/protocols/browser-protocol.d.ts.map +1 -0
- package/dist/protocols/browser-protocol.js +9 -0
- package/dist/protocols/browser-protocol.js.map +1 -0
- package/dist/query.d.ts +66 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +482 -0
- package/dist/query.js.map +1 -0
- package/dist/read.d.ts +47 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +128 -0
- package/dist/read.js.map +1 -0
- package/dist/recorder.d.ts +44 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +262 -0
- package/dist/recorder.js.map +1 -0
- package/dist/runtime-agent.d.ts +72 -0
- package/dist/runtime-agent.d.ts.map +1 -0
- package/dist/runtime-agent.js +357 -0
- package/dist/runtime-agent.js.map +1 -0
- package/dist/screenshot.d.ts +17 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +40 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/snapshot-diff.d.ts +23 -0
- package/dist/snapshot-diff.d.ts.map +1 -0
- package/dist/snapshot-diff.js +119 -0
- package/dist/snapshot-diff.js.map +1 -0
- package/dist/snapshot.d.ts +47 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +358 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/textSearch.d.ts +64 -0
- package/dist/textSearch.d.ts.map +1 -0
- package/dist/textSearch.js +113 -0
- package/dist/textSearch.js.map +1 -0
- package/dist/tools/context.d.ts +18 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +40 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/defaults.d.ts +5 -0
- package/dist/tools/defaults.d.ts.map +1 -0
- package/dist/tools/defaults.js +368 -0
- package/dist/tools/defaults.js.map +1 -0
- package/dist/tools/filesystem.d.ts +12 -0
- package/dist/tools/filesystem.d.ts.map +1 -0
- package/dist/tools/filesystem.js +137 -0
- package/dist/tools/filesystem.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +38 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +100 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tracing/cloud-sink.d.ts +189 -0
- package/dist/tracing/cloud-sink.d.ts.map +1 -0
- package/dist/tracing/cloud-sink.js +1067 -0
- package/dist/tracing/cloud-sink.js.map +1 -0
- package/dist/tracing/index-schema.d.ts +231 -0
- package/dist/tracing/index-schema.d.ts.map +1 -0
- package/dist/tracing/index-schema.js +235 -0
- package/dist/tracing/index-schema.js.map +1 -0
- package/dist/tracing/index.d.ts +12 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +28 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/tracing/indexer.d.ts +20 -0
- package/dist/tracing/indexer.d.ts.map +1 -0
- package/dist/tracing/indexer.js +347 -0
- package/dist/tracing/indexer.js.map +1 -0
- package/dist/tracing/jsonl-sink.d.ts +51 -0
- package/dist/tracing/jsonl-sink.d.ts.map +1 -0
- package/dist/tracing/jsonl-sink.js +329 -0
- package/dist/tracing/jsonl-sink.js.map +1 -0
- package/dist/tracing/sink.d.ts +25 -0
- package/dist/tracing/sink.d.ts.map +1 -0
- package/dist/tracing/sink.js +15 -0
- package/dist/tracing/sink.js.map +1 -0
- package/dist/tracing/tracer-factory.d.ts +102 -0
- package/dist/tracing/tracer-factory.d.ts.map +1 -0
- package/dist/tracing/tracer-factory.js +375 -0
- package/dist/tracing/tracer-factory.js.map +1 -0
- package/dist/tracing/tracer.d.ts +140 -0
- package/dist/tracing/tracer.d.ts.map +1 -0
- package/dist/tracing/tracer.js +336 -0
- package/dist/tracing/tracer.js.map +1 -0
- package/dist/tracing/types.d.ts +203 -0
- package/dist/tracing/types.d.ts.map +1 -0
- package/dist/tracing/types.js +8 -0
- package/dist/tracing/types.js.map +1 -0
- package/dist/types.d.ts +422 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/action-executor.d.ts +25 -0
- package/dist/utils/action-executor.d.ts.map +1 -0
- package/dist/utils/action-executor.js +121 -0
- package/dist/utils/action-executor.js.map +1 -0
- package/dist/utils/browser-evaluator.d.ts +76 -0
- package/dist/utils/browser-evaluator.d.ts.map +1 -0
- package/dist/utils/browser-evaluator.js +130 -0
- package/dist/utils/browser-evaluator.js.map +1 -0
- package/dist/utils/browser.d.ts +30 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +75 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/element-filter.d.ts +76 -0
- package/dist/utils/element-filter.d.ts.map +1 -0
- package/dist/utils/element-filter.js +195 -0
- package/dist/utils/element-filter.js.map +1 -0
- package/dist/utils/grid-utils.d.ts +37 -0
- package/dist/utils/grid-utils.d.ts.map +1 -0
- package/dist/utils/grid-utils.js +283 -0
- package/dist/utils/grid-utils.js.map +1 -0
- package/dist/utils/llm-interaction-handler.d.ts +41 -0
- package/dist/utils/llm-interaction-handler.d.ts.map +1 -0
- package/dist/utils/llm-interaction-handler.js +171 -0
- package/dist/utils/llm-interaction-handler.js.map +1 -0
- package/dist/utils/llm-response-builder.d.ts +56 -0
- package/dist/utils/llm-response-builder.d.ts.map +1 -0
- package/dist/utils/llm-response-builder.js +130 -0
- package/dist/utils/llm-response-builder.js.map +1 -0
- package/dist/utils/selector-utils.d.ts +12 -0
- package/dist/utils/selector-utils.d.ts.map +1 -0
- package/dist/utils/selector-utils.js +32 -0
- package/dist/utils/selector-utils.js.map +1 -0
- package/dist/utils/snapshot-event-builder.d.ts +28 -0
- package/dist/utils/snapshot-event-builder.d.ts.map +1 -0
- package/dist/utils/snapshot-event-builder.js +88 -0
- package/dist/utils/snapshot-event-builder.js.map +1 -0
- package/dist/utils/snapshot-processor.d.ts +27 -0
- package/dist/utils/snapshot-processor.d.ts.map +1 -0
- package/dist/utils/snapshot-processor.js +47 -0
- package/dist/utils/snapshot-processor.js.map +1 -0
- package/dist/utils/trace-event-builder.d.ts +122 -0
- package/dist/utils/trace-event-builder.d.ts.map +1 -0
- package/dist/utils/trace-event-builder.js +365 -0
- package/dist/utils/trace-event-builder.js.map +1 -0
- package/dist/utils/trace-file-manager.d.ts +70 -0
- package/dist/utils/trace-file-manager.d.ts.map +1 -0
- package/dist/utils/trace-file-manager.js +194 -0
- package/dist/utils/trace-file-manager.js.map +1 -0
- package/dist/utils/zod.d.ts +5 -0
- package/dist/utils/zod.d.ts.map +1 -0
- package/dist/utils/zod.js +80 -0
- package/dist/utils/zod.js.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/dist/verification.d.ts +194 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +530 -0
- package/dist/verification.js.map +1 -0
- package/dist/vision-executor.d.ts +18 -0
- package/dist/vision-executor.d.ts.map +1 -0
- package/dist/vision-executor.js +60 -0
- package/dist/vision-executor.js.map +1 -0
- package/dist/visual-agent.d.ts +120 -0
- package/dist/visual-agent.d.ts.map +1 -0
- package/dist/visual-agent.js +796 -0
- package/dist/visual-agent.js.map +1 -0
- package/dist/wait.d.ts +35 -0
- package/dist/wait.d.ts.map +1 -0
- package/dist/wait.js +76 -0
- package/dist/wait.js.map +1 -0
- package/package.json +94 -0
- package/spec/README.md +72 -0
- package/spec/SNAPSHOT_V1.md +208 -0
- package/spec/sdk-types.md +259 -0
- package/spec/snapshot.schema.json +148 -0
- package/src/extension/background.js +104 -0
- package/src/extension/content.js +162 -0
- package/src/extension/injected_api.js +1399 -0
- package/src/extension/manifest.json +36 -0
- package/src/extension/pkg/README.md +1340 -0
- package/src/extension/pkg/package.json +15 -0
- package/src/extension/pkg/sentience_core.d.ts +51 -0
- package/src/extension/pkg/sentience_core.js +371 -0
- package/src/extension/pkg/sentience_core_bg.wasm +0 -0
- package/src/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
- package/src/extension/release.json +116 -0
package/dist/browser.js
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Playwright browser harness with extension loading
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.PredicateBrowser = exports.SentienceBrowser = void 0;
|
|
40
|
+
exports.normalizeDomain = normalizeDomain;
|
|
41
|
+
exports.domainMatches = domainMatches;
|
|
42
|
+
exports.extractHost = extractHost;
|
|
43
|
+
exports.isDomainAllowed = isDomainAllowed;
|
|
44
|
+
const playwright_1 = require("playwright");
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const url_1 = require("url");
|
|
49
|
+
const snapshot_1 = require("./snapshot");
|
|
50
|
+
function normalizeDomain(domain) {
|
|
51
|
+
const raw = domain.trim();
|
|
52
|
+
let host = raw;
|
|
53
|
+
if (raw.includes('://')) {
|
|
54
|
+
try {
|
|
55
|
+
host = new url_1.URL(raw).hostname || '';
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
host = raw;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
host = raw.split('/', 1)[0];
|
|
63
|
+
}
|
|
64
|
+
host = host.split(':', 1)[0];
|
|
65
|
+
return host.trim().toLowerCase().replace(/^\./, '');
|
|
66
|
+
}
|
|
67
|
+
function domainMatches(host, pattern) {
|
|
68
|
+
const hostNorm = normalizeDomain(host);
|
|
69
|
+
let pat = normalizeDomain(pattern);
|
|
70
|
+
if (pat.startsWith('*.')) {
|
|
71
|
+
pat = pat.slice(2);
|
|
72
|
+
}
|
|
73
|
+
return hostNorm === pat || hostNorm.endsWith(`.${pat}`);
|
|
74
|
+
}
|
|
75
|
+
function extractHost(url) {
|
|
76
|
+
let raw = url.trim();
|
|
77
|
+
if (!raw.includes('://')) {
|
|
78
|
+
raw = `https://${raw}`;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const parsed = new url_1.URL(raw);
|
|
82
|
+
return parsed.hostname || null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isDomainAllowed(host, allowed, prohibited) {
|
|
89
|
+
if (!host)
|
|
90
|
+
return false;
|
|
91
|
+
if (prohibited && prohibited.length > 0) {
|
|
92
|
+
for (const pattern of prohibited) {
|
|
93
|
+
if (domainMatches(host, pattern)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (allowed && allowed.length > 0) {
|
|
99
|
+
return allowed.some(pattern => domainMatches(host, pattern));
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
class SentienceBrowser {
|
|
104
|
+
/**
|
|
105
|
+
* Create a new SentienceBrowser instance
|
|
106
|
+
*
|
|
107
|
+
* @param apiKey - Optional API key for server-side processing (Pro/Enterprise tiers)
|
|
108
|
+
* @param apiUrl - Optional API URL (defaults to https://api.sentienceapi.com if apiKey provided)
|
|
109
|
+
* @param headless - Whether to run in headless mode (defaults to true in CI, false locally)
|
|
110
|
+
* @param proxy - Optional proxy server URL (e.g., 'http://user:pass@proxy.example.com:8080')
|
|
111
|
+
* @param userDataDir - Optional path to user data directory for persistent sessions
|
|
112
|
+
* @param storageState - Optional storage state to inject (cookies + localStorage)
|
|
113
|
+
* @param recordVideoDir - Optional directory path to save video recordings
|
|
114
|
+
* @param recordVideoSize - Optional video resolution as object with 'width' and 'height' keys
|
|
115
|
+
* @param viewport - Optional viewport size as object with 'width' and 'height' keys
|
|
116
|
+
* @param deviceScaleFactor - Optional device scale factor to emulate high-DPI (Retina) screens.
|
|
117
|
+
* Examples: 1.0 (default, standard DPI), 2.0 (Retina/high-DPI, like MacBook Pro), 3.0 (very high DPI)
|
|
118
|
+
* If undefined, defaults to 1.0 (standard DPI).
|
|
119
|
+
* @param allowedDomains - Optional list of allowed domains for navigation.
|
|
120
|
+
* @param prohibitedDomains - Optional list of prohibited domains for navigation.
|
|
121
|
+
* @param keepAlive - Keep browser alive after close() (no teardown).
|
|
122
|
+
*/
|
|
123
|
+
constructor(apiKey, apiUrl, headless, proxy, userDataDir, storageState, recordVideoDir, recordVideoSize, viewport, deviceScaleFactor, allowedDomains, prohibitedDomains, keepAlive = false, permissionPolicy) {
|
|
124
|
+
this.context = null;
|
|
125
|
+
this.browser = null;
|
|
126
|
+
this.page = null;
|
|
127
|
+
this.extensionPath = null;
|
|
128
|
+
this.userDataDir = null;
|
|
129
|
+
this._apiKey = apiKey;
|
|
130
|
+
// Determine headless mode
|
|
131
|
+
if (headless === undefined) {
|
|
132
|
+
// Default to true in CI, false locally
|
|
133
|
+
const ci = process.env.CI?.toLowerCase();
|
|
134
|
+
this.headless = ci === 'true' || ci === '1' || ci === 'yes';
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this.headless = headless;
|
|
138
|
+
}
|
|
139
|
+
// Configure API URL
|
|
140
|
+
if (apiKey) {
|
|
141
|
+
this._apiUrl = apiUrl || 'https://api.sentienceapi.com';
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this._apiUrl = undefined;
|
|
145
|
+
}
|
|
146
|
+
// Support proxy from parameter or environment variable
|
|
147
|
+
// Only use env var if it's a valid non-empty string
|
|
148
|
+
const envProxy = process.env.SENTIENCE_PROXY;
|
|
149
|
+
this._proxy = proxy || (envProxy && envProxy.trim() ? envProxy : undefined);
|
|
150
|
+
// Auth injection support
|
|
151
|
+
this._userDataDir = userDataDir;
|
|
152
|
+
this._storageState = storageState;
|
|
153
|
+
// Video recording support
|
|
154
|
+
this._recordVideoDir = recordVideoDir;
|
|
155
|
+
this._recordVideoSize = recordVideoSize || { width: 1280, height: 800 };
|
|
156
|
+
// Viewport configuration
|
|
157
|
+
this._viewport = viewport || { width: 1280, height: 800 };
|
|
158
|
+
// Device scale factor for high-DPI emulation
|
|
159
|
+
this._deviceScaleFactor = deviceScaleFactor;
|
|
160
|
+
this._allowedDomains = allowedDomains;
|
|
161
|
+
this._prohibitedDomains = prohibitedDomains;
|
|
162
|
+
this._keepAlive = keepAlive;
|
|
163
|
+
this._permissionPolicy = permissionPolicy;
|
|
164
|
+
}
|
|
165
|
+
async applyPermissionPolicy(context, policy) {
|
|
166
|
+
const defaultPolicy = policy.default ?? 'clear';
|
|
167
|
+
if (defaultPolicy === 'clear' || defaultPolicy === 'deny') {
|
|
168
|
+
await context.clearPermissions();
|
|
169
|
+
}
|
|
170
|
+
if (policy.geolocation) {
|
|
171
|
+
await context.setGeolocation(policy.geolocation);
|
|
172
|
+
}
|
|
173
|
+
if (policy.autoGrant && policy.autoGrant.length > 0) {
|
|
174
|
+
const options = policy.origin ? { origin: policy.origin } : undefined;
|
|
175
|
+
await context.grantPermissions(policy.autoGrant, options);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async start() {
|
|
179
|
+
// 1. Resolve Extension Path
|
|
180
|
+
// Handle: src/extension (local dev), dist/extension (prod), or ../sentience-chrome (monorepo)
|
|
181
|
+
let extensionSource = '';
|
|
182
|
+
const candidates = [
|
|
183
|
+
// Production / Installed Package
|
|
184
|
+
path.resolve(__dirname, '../extension'),
|
|
185
|
+
path.resolve(__dirname, 'extension'),
|
|
186
|
+
// Local Monorepo Dev
|
|
187
|
+
path.resolve(__dirname, '../../sentience-chrome'),
|
|
188
|
+
path.resolve(__dirname, '../../../sentience-chrome'),
|
|
189
|
+
// CI Artifact
|
|
190
|
+
path.resolve(process.cwd(), 'extension'),
|
|
191
|
+
];
|
|
192
|
+
for (const loc of candidates) {
|
|
193
|
+
if (fs.existsSync(path.join(loc, 'manifest.json'))) {
|
|
194
|
+
extensionSource = loc;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!extensionSource) {
|
|
199
|
+
throw new Error(`Sentience extension not found. Checked:\n${candidates.map(c => `- ${c}`).join('\n')}\n` +
|
|
200
|
+
'Ensure the extension is built/downloaded.');
|
|
201
|
+
}
|
|
202
|
+
// 2. Setup User Data Directory
|
|
203
|
+
if (this._userDataDir) {
|
|
204
|
+
// Use provided directory for persistent sessions
|
|
205
|
+
this.userDataDir = this._userDataDir;
|
|
206
|
+
if (!fs.existsSync(this.userDataDir)) {
|
|
207
|
+
fs.mkdirSync(this.userDataDir, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// Create temp directory (ephemeral, existing behavior)
|
|
212
|
+
this.userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentience-ts-'));
|
|
213
|
+
}
|
|
214
|
+
this.extensionPath = path.join(this.userDataDir, 'extension');
|
|
215
|
+
// Copy extension to temp dir
|
|
216
|
+
this._copyRecursive(extensionSource, this.extensionPath);
|
|
217
|
+
// 3. Build Args
|
|
218
|
+
const args = [
|
|
219
|
+
`--disable-extensions-except=${this.extensionPath}`,
|
|
220
|
+
`--load-extension=${this.extensionPath}`,
|
|
221
|
+
'--disable-blink-features=AutomationControlled',
|
|
222
|
+
'--no-sandbox',
|
|
223
|
+
'--disable-infobars',
|
|
224
|
+
];
|
|
225
|
+
// CRITICAL: Headless Extensions Support
|
|
226
|
+
// headless: true -> NO extensions.
|
|
227
|
+
// headless: false + args: '--headless=new' -> YES extensions.
|
|
228
|
+
if (this.headless) {
|
|
229
|
+
args.push('--headless=new');
|
|
230
|
+
}
|
|
231
|
+
// CRITICAL: WebRTC leak protection for datacenter usage with proxies
|
|
232
|
+
// Prevents WebRTC from leaking the real IP address even when using proxies
|
|
233
|
+
if (this._proxy) {
|
|
234
|
+
args.push('--disable-features=WebRtcHideLocalIpsWithMdns');
|
|
235
|
+
args.push('--force-webrtc-ip-handling-policy=disable_non_proxied_udp');
|
|
236
|
+
}
|
|
237
|
+
// 4. Parse proxy configuration
|
|
238
|
+
const proxyConfig = this.parseProxy(this._proxy);
|
|
239
|
+
// 5. Setup video recording directory if requested
|
|
240
|
+
if (this._recordVideoDir) {
|
|
241
|
+
if (!fs.existsSync(this._recordVideoDir)) {
|
|
242
|
+
fs.mkdirSync(this._recordVideoDir, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
console.log(`🎥 [Sentience] Recording video to: ${this._recordVideoDir}`);
|
|
245
|
+
console.log(` Resolution: ${this._recordVideoSize.width}x${this._recordVideoSize.height}`);
|
|
246
|
+
}
|
|
247
|
+
// 6. Launch Browser
|
|
248
|
+
const launchOptions = {
|
|
249
|
+
headless: false, // Must be false here, handled via args above
|
|
250
|
+
args: args,
|
|
251
|
+
viewport: this._viewport,
|
|
252
|
+
// Clean User-Agent to avoid "HeadlessChrome" detection
|
|
253
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
254
|
+
proxy: proxyConfig, // Pass proxy configuration
|
|
255
|
+
// CRITICAL: Ignore HTTPS errors when using proxy (proxies often use self-signed certs)
|
|
256
|
+
ignoreHTTPSErrors: proxyConfig !== undefined,
|
|
257
|
+
};
|
|
258
|
+
// Add device scale factor if configured
|
|
259
|
+
if (this._deviceScaleFactor !== undefined) {
|
|
260
|
+
launchOptions.deviceScaleFactor = this._deviceScaleFactor;
|
|
261
|
+
}
|
|
262
|
+
// Add video recording if configured
|
|
263
|
+
if (this._recordVideoDir) {
|
|
264
|
+
launchOptions.recordVideo = {
|
|
265
|
+
dir: this._recordVideoDir,
|
|
266
|
+
size: this._recordVideoSize,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
this.context = await playwright_1.chromium.launchPersistentContext(this.userDataDir, launchOptions);
|
|
270
|
+
if (this._permissionPolicy) {
|
|
271
|
+
await this.applyPermissionPolicy(this.context, this._permissionPolicy);
|
|
272
|
+
}
|
|
273
|
+
this.page = this.context.pages()[0] || (await this.context.newPage());
|
|
274
|
+
// Inject storage state if provided (must be after context creation)
|
|
275
|
+
if (this._storageState) {
|
|
276
|
+
await this.injectStorageState(this._storageState);
|
|
277
|
+
}
|
|
278
|
+
// Apply context-level stealth patches (runs on every new page)
|
|
279
|
+
await this.context.addInitScript(() => {
|
|
280
|
+
// Early webdriver hiding - runs before any page script
|
|
281
|
+
// Use multiple strategies to completely hide webdriver
|
|
282
|
+
// Strategy 1: Try to delete it first
|
|
283
|
+
try {
|
|
284
|
+
delete navigator.webdriver;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Property might not be deletable
|
|
288
|
+
}
|
|
289
|
+
// Strategy 2: Redefine to return undefined and hide from enumeration
|
|
290
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
291
|
+
get: () => undefined,
|
|
292
|
+
configurable: true,
|
|
293
|
+
enumerable: false,
|
|
294
|
+
writable: false,
|
|
295
|
+
});
|
|
296
|
+
// Strategy 3: Override 'in' operator check
|
|
297
|
+
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
|
|
298
|
+
Object.prototype.hasOwnProperty = function (prop) {
|
|
299
|
+
if (this === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return originalHasOwnProperty.call(this, prop);
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
// 5. Apply Comprehensive Stealth Patches
|
|
306
|
+
// Use both CDP (earlier) and addInitScript (backup) for maximum coverage
|
|
307
|
+
// Strategy A: Use CDP to inject at the earliest possible moment
|
|
308
|
+
const client = await this.page.context().newCDPSession(this.page);
|
|
309
|
+
await client.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
310
|
+
source: `
|
|
311
|
+
// Aggressive webdriver hiding - must run before ANY page script
|
|
312
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
313
|
+
get: () => undefined,
|
|
314
|
+
configurable: true,
|
|
315
|
+
enumerable: false
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Override Object.getOwnPropertyDescriptor
|
|
319
|
+
const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
320
|
+
Object.getOwnPropertyDescriptor = function(obj, prop) {
|
|
321
|
+
if (obj === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
return originalGetOwnPropertyDescriptor(obj, prop);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Override Object.keys
|
|
328
|
+
const originalKeys = Object.keys;
|
|
329
|
+
Object.keys = function(obj) {
|
|
330
|
+
const keys = originalKeys(obj);
|
|
331
|
+
if (obj === navigator) {
|
|
332
|
+
return keys.filter(k => k !== 'webdriver' && k !== 'Webdriver');
|
|
333
|
+
}
|
|
334
|
+
return keys;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Override Object.getOwnPropertyNames
|
|
338
|
+
const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
|
|
339
|
+
Object.getOwnPropertyNames = function(obj) {
|
|
340
|
+
const names = originalGetOwnPropertyNames(obj);
|
|
341
|
+
if (obj === navigator) {
|
|
342
|
+
return names.filter(n => n !== 'webdriver' && n !== 'Webdriver');
|
|
343
|
+
}
|
|
344
|
+
return names;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Override 'in' operator check
|
|
348
|
+
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
|
|
349
|
+
Object.prototype.hasOwnProperty = function(prop) {
|
|
350
|
+
if (this === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return originalHasOwnProperty.call(this, prop);
|
|
354
|
+
};
|
|
355
|
+
`,
|
|
356
|
+
});
|
|
357
|
+
// Strategy B: Also use addInitScript as backup (runs after CDP but before page scripts)
|
|
358
|
+
await this.page.addInitScript(() => {
|
|
359
|
+
// 1. Hide navigator.webdriver (comprehensive approach for advanced detection)
|
|
360
|
+
// Advanced detection checks for property descriptor, so we need multiple strategies
|
|
361
|
+
try {
|
|
362
|
+
// Strategy 1: Try to delete the property
|
|
363
|
+
delete navigator.webdriver;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Property might not be deletable, continue with redefine
|
|
367
|
+
}
|
|
368
|
+
// Strategy 2: Redefine to return undefined (better than false)
|
|
369
|
+
// Also set enumerable: false to hide from Object.keys() checks
|
|
370
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
371
|
+
get: () => undefined,
|
|
372
|
+
configurable: true,
|
|
373
|
+
enumerable: false,
|
|
374
|
+
});
|
|
375
|
+
// Strategy 3: Override Object.getOwnPropertyDescriptor only for navigator.webdriver
|
|
376
|
+
// This prevents advanced detection that checks the property descriptor
|
|
377
|
+
const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
378
|
+
Object.getOwnPropertyDescriptor = function (obj, prop) {
|
|
379
|
+
if (obj === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
return originalGetOwnPropertyDescriptor(obj, prop);
|
|
383
|
+
};
|
|
384
|
+
// Strategy 4: Hide from Object.keys() and Object.getOwnPropertyNames()
|
|
385
|
+
const originalKeys = Object.keys;
|
|
386
|
+
Object.keys = function (obj) {
|
|
387
|
+
const keys = originalKeys(obj);
|
|
388
|
+
if (obj === navigator) {
|
|
389
|
+
return keys.filter(k => k !== 'webdriver' && k !== 'Webdriver');
|
|
390
|
+
}
|
|
391
|
+
return keys;
|
|
392
|
+
};
|
|
393
|
+
// Strategy 5: Hide from Object.getOwnPropertyNames()
|
|
394
|
+
const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
|
|
395
|
+
Object.getOwnPropertyNames = function (obj) {
|
|
396
|
+
const names = originalGetOwnPropertyNames(obj);
|
|
397
|
+
if (obj === navigator) {
|
|
398
|
+
return names.filter(n => n !== 'webdriver' && n !== 'Webdriver');
|
|
399
|
+
}
|
|
400
|
+
return names;
|
|
401
|
+
};
|
|
402
|
+
// Strategy 6: Override hasOwnProperty to hide from 'in' operator checks
|
|
403
|
+
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
|
|
404
|
+
Object.prototype.hasOwnProperty = function (prop) {
|
|
405
|
+
if (this === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
return originalHasOwnProperty.call(this, prop);
|
|
409
|
+
};
|
|
410
|
+
// 2. Inject window.chrome object (required for Chrome detection)
|
|
411
|
+
if (typeof window.chrome === 'undefined') {
|
|
412
|
+
window.chrome = {
|
|
413
|
+
runtime: {},
|
|
414
|
+
loadTimes: function () { },
|
|
415
|
+
csi: function () { },
|
|
416
|
+
app: {},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
// 3. Patch navigator.plugins (should have length > 0)
|
|
420
|
+
// Only patch if plugins array is empty (headless mode issue)
|
|
421
|
+
const originalPlugins = navigator.plugins;
|
|
422
|
+
if (originalPlugins.length === 0) {
|
|
423
|
+
// Create a PluginArray-like object with minimal plugins
|
|
424
|
+
const fakePlugins = [
|
|
425
|
+
{
|
|
426
|
+
name: 'Chrome PDF Plugin',
|
|
427
|
+
filename: 'internal-pdf-viewer',
|
|
428
|
+
description: 'Portable Document Format',
|
|
429
|
+
length: 1,
|
|
430
|
+
item: function () {
|
|
431
|
+
return null;
|
|
432
|
+
},
|
|
433
|
+
namedItem: function () {
|
|
434
|
+
return null;
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'Chrome PDF Viewer',
|
|
439
|
+
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
|
|
440
|
+
description: '',
|
|
441
|
+
length: 0,
|
|
442
|
+
item: function () {
|
|
443
|
+
return null;
|
|
444
|
+
},
|
|
445
|
+
namedItem: function () {
|
|
446
|
+
return null;
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: 'Native Client',
|
|
451
|
+
filename: 'internal-nacl-plugin',
|
|
452
|
+
description: '',
|
|
453
|
+
length: 0,
|
|
454
|
+
item: function () {
|
|
455
|
+
return null;
|
|
456
|
+
},
|
|
457
|
+
namedItem: function () {
|
|
458
|
+
return null;
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
];
|
|
462
|
+
// Create PluginArray-like object (array-like but not a real array)
|
|
463
|
+
// This needs to behave like the real PluginArray for detection to pass
|
|
464
|
+
const pluginArray = {};
|
|
465
|
+
fakePlugins.forEach((plugin, index) => {
|
|
466
|
+
Object.defineProperty(pluginArray, index.toString(), {
|
|
467
|
+
value: plugin,
|
|
468
|
+
enumerable: true,
|
|
469
|
+
configurable: true,
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
Object.defineProperty(pluginArray, 'length', {
|
|
473
|
+
value: fakePlugins.length,
|
|
474
|
+
enumerable: false,
|
|
475
|
+
configurable: false,
|
|
476
|
+
});
|
|
477
|
+
pluginArray.item = function (index) {
|
|
478
|
+
return this[index] || null;
|
|
479
|
+
};
|
|
480
|
+
pluginArray.namedItem = function (name) {
|
|
481
|
+
for (let i = 0; i < this.length; i++) {
|
|
482
|
+
if (this[i] && this[i].name === name)
|
|
483
|
+
return this[i];
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
};
|
|
487
|
+
// Make it iterable (for for...of loops)
|
|
488
|
+
pluginArray[Symbol.iterator] = function* () {
|
|
489
|
+
for (let i = 0; i < this.length; i++) {
|
|
490
|
+
yield this[i];
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
// Make it array-like for Array.from() and spread
|
|
494
|
+
Object.setPrototypeOf(pluginArray, Object.create(null));
|
|
495
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
496
|
+
get: () => pluginArray,
|
|
497
|
+
configurable: true,
|
|
498
|
+
enumerable: true,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
// 4. Ensure navigator.languages exists and has values
|
|
502
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
503
|
+
Object.defineProperty(navigator, 'languages', {
|
|
504
|
+
get: () => ['en-US', 'en'],
|
|
505
|
+
configurable: true,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// 5. Patch permissions API (should exist)
|
|
509
|
+
if (!navigator.permissions) {
|
|
510
|
+
navigator.permissions = {
|
|
511
|
+
query: (_parameters) => {
|
|
512
|
+
return { state: 'granted', onchange: null };
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
// Inject API Key if present
|
|
518
|
+
if (this._apiKey) {
|
|
519
|
+
await this.page.addInitScript(key => {
|
|
520
|
+
window.__SENTIENCE_API_KEY__ = key;
|
|
521
|
+
}, this._apiKey);
|
|
522
|
+
}
|
|
523
|
+
// Wait for extension background pages to spin up
|
|
524
|
+
await new Promise(r => setTimeout(r, 500));
|
|
525
|
+
}
|
|
526
|
+
async goto(url) {
|
|
527
|
+
const page = this.getPage();
|
|
528
|
+
if (!page) {
|
|
529
|
+
throw new Error('Browser not started. Call start() first.');
|
|
530
|
+
}
|
|
531
|
+
const host = extractHost(url);
|
|
532
|
+
if (!isDomainAllowed(host, this._allowedDomains, this._prohibitedDomains)) {
|
|
533
|
+
throw new Error(`domain not allowed: ${host}`);
|
|
534
|
+
}
|
|
535
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
536
|
+
if (!(await this.waitForExtension(page, 15000))) {
|
|
537
|
+
// Gather Debug Info
|
|
538
|
+
const diag = await page
|
|
539
|
+
.evaluate(() => ({
|
|
540
|
+
sentience_global: typeof window.sentience !== 'undefined',
|
|
541
|
+
wasm_ready: window.sentience && window.sentience._wasmModule !== null,
|
|
542
|
+
ext_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
|
|
543
|
+
url: window.location.href,
|
|
544
|
+
}))
|
|
545
|
+
.catch(e => ({ error: String(e) }));
|
|
546
|
+
throw new Error('Extension failed to load after navigation.\n' +
|
|
547
|
+
`Path: ${this.extensionPath}\n` +
|
|
548
|
+
`Diagnostics: ${JSON.stringify(diag, null, 2)}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async waitForExtension(page, timeoutMs) {
|
|
552
|
+
const start = Date.now();
|
|
553
|
+
while (Date.now() - start < timeoutMs) {
|
|
554
|
+
try {
|
|
555
|
+
const ready = await page.evaluate(() => {
|
|
556
|
+
// Check for API AND Wasm Module (set by injected_api.js)
|
|
557
|
+
const s = window.sentience;
|
|
558
|
+
return s && s._wasmModule !== null; // Strict check for null (it's initialized as null)
|
|
559
|
+
});
|
|
560
|
+
if (ready)
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Context invalid errors expected during navigation
|
|
565
|
+
}
|
|
566
|
+
await new Promise(r => setTimeout(r, 100));
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
getPage() {
|
|
571
|
+
return this.page;
|
|
572
|
+
}
|
|
573
|
+
// Helper for recursive copy (fs.cp is Node 16.7+)
|
|
574
|
+
_copyRecursive(src, dest) {
|
|
575
|
+
if (fs.statSync(src).isDirectory()) {
|
|
576
|
+
if (!fs.existsSync(dest))
|
|
577
|
+
fs.mkdirSync(dest);
|
|
578
|
+
fs.readdirSync(src).forEach(child => {
|
|
579
|
+
this._copyRecursive(path.join(src, child), path.join(dest, child));
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
fs.copyFileSync(src, dest);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Expose API configuration (read-only)
|
|
587
|
+
getApiKey() {
|
|
588
|
+
return this._apiKey;
|
|
589
|
+
}
|
|
590
|
+
getApiUrl() {
|
|
591
|
+
return this._apiUrl;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Take a snapshot of the current page
|
|
595
|
+
* Implements IBrowser interface
|
|
596
|
+
*/
|
|
597
|
+
async snapshot(options) {
|
|
598
|
+
return (0, snapshot_1.snapshot)(this, options);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Parse proxy connection string into Playwright format.
|
|
602
|
+
*
|
|
603
|
+
* @param proxyString - Standard format "http://username:password@host:port"
|
|
604
|
+
* or "socks5://user:pass@host:port"
|
|
605
|
+
* @returns Playwright proxy object or undefined if invalid
|
|
606
|
+
*/
|
|
607
|
+
parseProxy(proxyString) {
|
|
608
|
+
if (!proxyString || !proxyString.trim()) {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const parsed = new url_1.URL(proxyString);
|
|
613
|
+
// Validate scheme
|
|
614
|
+
const validSchemes = ['http:', 'https:', 'socks5:'];
|
|
615
|
+
if (!validSchemes.includes(parsed.protocol)) {
|
|
616
|
+
throw new Error(`Unsupported proxy scheme: ${parsed.protocol}`);
|
|
617
|
+
}
|
|
618
|
+
// Validate host and port
|
|
619
|
+
if (!parsed.hostname || !parsed.port) {
|
|
620
|
+
throw new Error('Proxy URL must include hostname and port');
|
|
621
|
+
}
|
|
622
|
+
// Build Playwright proxy object
|
|
623
|
+
const proxyConfig = {
|
|
624
|
+
server: `${parsed.protocol}//${parsed.hostname}:${parsed.port}`,
|
|
625
|
+
};
|
|
626
|
+
// Add credentials if present
|
|
627
|
+
if (parsed.username && parsed.password) {
|
|
628
|
+
proxyConfig.username = parsed.username;
|
|
629
|
+
proxyConfig.password = parsed.password;
|
|
630
|
+
}
|
|
631
|
+
return proxyConfig;
|
|
632
|
+
}
|
|
633
|
+
catch (e) {
|
|
634
|
+
console.warn(`⚠️ [Sentience] Invalid proxy configuration: ${e.message}`);
|
|
635
|
+
console.warn(' Expected format: http://username:password@host:port');
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Inject storage state (cookies + localStorage) into browser context.
|
|
641
|
+
*
|
|
642
|
+
* @param storageState - Path to JSON file, StorageState object, or plain object
|
|
643
|
+
*/
|
|
644
|
+
async injectStorageState(storageState) {
|
|
645
|
+
// Load storage state
|
|
646
|
+
let state;
|
|
647
|
+
if (typeof storageState === 'string') {
|
|
648
|
+
// Load from file
|
|
649
|
+
const content = fs.readFileSync(storageState, 'utf-8');
|
|
650
|
+
state = JSON.parse(content);
|
|
651
|
+
}
|
|
652
|
+
else if (typeof storageState === 'object' && storageState !== null) {
|
|
653
|
+
// Already an object (StorageState or plain object)
|
|
654
|
+
state = storageState;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
throw new Error(`Invalid storageState type: ${typeof storageState}. ` +
|
|
658
|
+
'Expected string (file path), StorageState, or object.');
|
|
659
|
+
}
|
|
660
|
+
// Inject cookies (works globally)
|
|
661
|
+
if (state.cookies && Array.isArray(state.cookies) && state.cookies.length > 0) {
|
|
662
|
+
// Convert to Playwright cookie format
|
|
663
|
+
const playwrightCookies = state.cookies.map(cookie => {
|
|
664
|
+
const playwrightCookie = {
|
|
665
|
+
name: cookie.name,
|
|
666
|
+
value: cookie.value,
|
|
667
|
+
domain: cookie.domain,
|
|
668
|
+
path: cookie.path || '/',
|
|
669
|
+
};
|
|
670
|
+
if (cookie.expires !== undefined) {
|
|
671
|
+
playwrightCookie.expires = cookie.expires;
|
|
672
|
+
}
|
|
673
|
+
if (cookie.httpOnly !== undefined) {
|
|
674
|
+
playwrightCookie.httpOnly = cookie.httpOnly;
|
|
675
|
+
}
|
|
676
|
+
if (cookie.secure !== undefined) {
|
|
677
|
+
playwrightCookie.secure = cookie.secure;
|
|
678
|
+
}
|
|
679
|
+
if (cookie.sameSite !== undefined) {
|
|
680
|
+
playwrightCookie.sameSite = cookie.sameSite;
|
|
681
|
+
}
|
|
682
|
+
return playwrightCookie;
|
|
683
|
+
});
|
|
684
|
+
await this.context.addCookies(playwrightCookies);
|
|
685
|
+
console.log(`✅ [Sentience] Injected ${state.cookies.length} cookie(s)`);
|
|
686
|
+
}
|
|
687
|
+
// Inject LocalStorage (requires navigation to each domain)
|
|
688
|
+
if (state.origins && Array.isArray(state.origins)) {
|
|
689
|
+
for (const originData of state.origins) {
|
|
690
|
+
const origin = originData.origin;
|
|
691
|
+
if (!origin) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
// Navigate to origin
|
|
696
|
+
await this.page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
697
|
+
// Inject localStorage
|
|
698
|
+
if (originData.localStorage && Array.isArray(originData.localStorage)) {
|
|
699
|
+
// Convert to dict format for JavaScript
|
|
700
|
+
const localStorageDict = {};
|
|
701
|
+
for (const item of originData.localStorage) {
|
|
702
|
+
localStorageDict[item.name] = item.value;
|
|
703
|
+
}
|
|
704
|
+
await this.page.evaluate((localStorageData) => {
|
|
705
|
+
for (const [key, value] of Object.entries(localStorageData)) {
|
|
706
|
+
localStorage.setItem(key, value);
|
|
707
|
+
}
|
|
708
|
+
}, localStorageDict);
|
|
709
|
+
console.log(`✅ [Sentience] Injected ${originData.localStorage.length} localStorage item(s) for ${origin}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
console.warn(`⚠️ [Sentience] Failed to inject localStorage for ${origin}: ${error.message}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Get the browser context (for utilities like saveStorageState)
|
|
720
|
+
*/
|
|
721
|
+
getContext() {
|
|
722
|
+
return this.context;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Create SentienceBrowser from an existing Playwright BrowserContext.
|
|
726
|
+
*
|
|
727
|
+
* This allows you to use Sentience SDK with a browser context you've already created,
|
|
728
|
+
* giving you more control over browser initialization.
|
|
729
|
+
*
|
|
730
|
+
* @param context - Existing Playwright BrowserContext
|
|
731
|
+
* @param apiKey - Optional API key for server-side processing
|
|
732
|
+
* @param apiUrl - Optional API URL (defaults to https://api.sentienceapi.com if apiKey provided)
|
|
733
|
+
* @returns SentienceBrowser instance configured to use the existing context
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* ```typescript
|
|
737
|
+
* import { chromium } from 'playwright';
|
|
738
|
+
* import { SentienceBrowser } from '@sentience/sdk';
|
|
739
|
+
*
|
|
740
|
+
* const context = await chromium.launchPersistentContext(...);
|
|
741
|
+
* const browser = SentienceBrowser.fromExisting(context);
|
|
742
|
+
* await browser.getPage().goto('https://example.com');
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
static async fromExisting(context, apiKey, apiUrl) {
|
|
746
|
+
const instance = new SentienceBrowser(apiKey, apiUrl);
|
|
747
|
+
instance.context = context;
|
|
748
|
+
const pages = context.pages();
|
|
749
|
+
instance.page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
750
|
+
// Wait for extension to be ready (if extension is loaded)
|
|
751
|
+
// Note: In TypeScript, we can't easily apply stealth here without the page
|
|
752
|
+
// The user should ensure stealth is applied to their context if needed
|
|
753
|
+
return instance;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Create SentienceBrowser from an existing Playwright Page.
|
|
757
|
+
*
|
|
758
|
+
* This allows you to use Sentience SDK with a page you've already created,
|
|
759
|
+
* giving you more control over browser initialization.
|
|
760
|
+
*
|
|
761
|
+
* @param page - Existing Playwright Page
|
|
762
|
+
* @param apiKey - Optional API key for server-side processing
|
|
763
|
+
* @param apiUrl - Optional API URL (defaults to https://api.sentienceapi.com if apiKey provided)
|
|
764
|
+
* @returns SentienceBrowser instance configured to use the existing page
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```typescript
|
|
768
|
+
* import { chromium } from 'playwright';
|
|
769
|
+
* import { SentienceBrowser } from '@sentience/sdk';
|
|
770
|
+
*
|
|
771
|
+
* const browserInstance = await chromium.launch();
|
|
772
|
+
* const context = await browserInstance.newContext();
|
|
773
|
+
* const page = await context.newPage();
|
|
774
|
+
* await page.goto('https://example.com');
|
|
775
|
+
*
|
|
776
|
+
* const browser = SentienceBrowser.fromPage(page);
|
|
777
|
+
* ```
|
|
778
|
+
*/
|
|
779
|
+
static fromPage(page, apiKey, apiUrl) {
|
|
780
|
+
const instance = new SentienceBrowser(apiKey, apiUrl);
|
|
781
|
+
instance.page = page;
|
|
782
|
+
instance.context = page.context();
|
|
783
|
+
// Wait for extension to be ready (if extension is loaded)
|
|
784
|
+
// Note: In TypeScript, we can't easily apply stealth here without the page
|
|
785
|
+
// The user should ensure stealth is applied to their context if needed
|
|
786
|
+
return instance;
|
|
787
|
+
}
|
|
788
|
+
async close(outputPath) {
|
|
789
|
+
if (this._keepAlive) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
let tempVideoPath = null;
|
|
793
|
+
// Get video path before closing (if recording was enabled)
|
|
794
|
+
// Note: Playwright saves videos when pages/context close, but we can get the
|
|
795
|
+
// expected path before closing. The actual file will be available after close.
|
|
796
|
+
if (this._recordVideoDir) {
|
|
797
|
+
try {
|
|
798
|
+
// Try to get video path from the first page
|
|
799
|
+
if (this.page) {
|
|
800
|
+
const video = this.page.video();
|
|
801
|
+
if (video) {
|
|
802
|
+
tempVideoPath = await video.path();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// If that fails, check all pages in the context (before closing)
|
|
806
|
+
if (!tempVideoPath && this.context) {
|
|
807
|
+
const pages = this.context.pages();
|
|
808
|
+
for (const page of pages) {
|
|
809
|
+
try {
|
|
810
|
+
const video = page.video();
|
|
811
|
+
if (video) {
|
|
812
|
+
tempVideoPath = await video.path();
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
// Continue to next page
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
// Video path might not be available until after close
|
|
824
|
+
// We'll use fallback mechanism below
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const cleanup = [];
|
|
828
|
+
// Close context first (this also closes the browser for persistent contexts)
|
|
829
|
+
// This triggers video file finalization
|
|
830
|
+
if (this.context) {
|
|
831
|
+
cleanup.push(this.context.close().catch(() => {
|
|
832
|
+
// Ignore errors during cleanup
|
|
833
|
+
}));
|
|
834
|
+
this.context = null;
|
|
835
|
+
}
|
|
836
|
+
// Close browser if it exists (for non-persistent contexts)
|
|
837
|
+
if (this.browser) {
|
|
838
|
+
cleanup.push(this.browser.close().catch(() => {
|
|
839
|
+
// Ignore errors during cleanup
|
|
840
|
+
}));
|
|
841
|
+
this.browser = null;
|
|
842
|
+
}
|
|
843
|
+
// Wait for all cleanup to complete
|
|
844
|
+
await Promise.all(cleanup);
|
|
845
|
+
// Clean up extension directory
|
|
846
|
+
if (this.extensionPath && fs.existsSync(this.extensionPath)) {
|
|
847
|
+
try {
|
|
848
|
+
fs.rmSync(this.extensionPath, { recursive: true, force: true });
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
// Ignore cleanup errors
|
|
852
|
+
}
|
|
853
|
+
this.extensionPath = null;
|
|
854
|
+
}
|
|
855
|
+
// After context closes, verify video file exists if we have a path
|
|
856
|
+
let finalPath = tempVideoPath;
|
|
857
|
+
if (tempVideoPath && fs.existsSync(tempVideoPath)) {
|
|
858
|
+
// Video file exists, proceed with rename if needed
|
|
859
|
+
}
|
|
860
|
+
else if (this._recordVideoDir && fs.existsSync(this._recordVideoDir)) {
|
|
861
|
+
// Fallback: If we couldn't get the path but recording was enabled,
|
|
862
|
+
// check the directory for video files
|
|
863
|
+
try {
|
|
864
|
+
const videoFiles = fs
|
|
865
|
+
.readdirSync(this._recordVideoDir)
|
|
866
|
+
.filter(f => f.endsWith('.webm'))
|
|
867
|
+
.map(f => ({
|
|
868
|
+
path: path.join(this._recordVideoDir, f),
|
|
869
|
+
mtime: fs.statSync(path.join(this._recordVideoDir, f)).mtime.getTime(),
|
|
870
|
+
}))
|
|
871
|
+
.sort((a, b) => b.mtime - a.mtime); // Most recent first
|
|
872
|
+
if (videoFiles.length > 0) {
|
|
873
|
+
finalPath = videoFiles[0].path;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
// Ignore errors when scanning directory
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
// Rename/move video if output_path is specified
|
|
881
|
+
if (finalPath && outputPath && fs.existsSync(finalPath)) {
|
|
882
|
+
try {
|
|
883
|
+
// Ensure parent directory exists
|
|
884
|
+
const outputDir = path.dirname(outputPath);
|
|
885
|
+
if (!fs.existsSync(outputDir)) {
|
|
886
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
887
|
+
}
|
|
888
|
+
fs.renameSync(finalPath, outputPath);
|
|
889
|
+
finalPath = outputPath;
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
console.warn(`Failed to rename video file: ${error.message}`);
|
|
893
|
+
// Return original path if rename fails
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// Clean up user data directory (only if it's a temp directory)
|
|
897
|
+
// If user provided a custom userDataDir, we don't delete it (persistent sessions)
|
|
898
|
+
if (this.userDataDir && fs.existsSync(this.userDataDir)) {
|
|
899
|
+
// Only delete if it's a temp directory (starts with os.tmpdir())
|
|
900
|
+
const isTempDir = this.userDataDir.startsWith(os.tmpdir());
|
|
901
|
+
if (isTempDir) {
|
|
902
|
+
try {
|
|
903
|
+
fs.rmSync(this.userDataDir, { recursive: true, force: true });
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
// Ignore cleanup errors
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
this.userDataDir = null;
|
|
910
|
+
}
|
|
911
|
+
return finalPath;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
exports.SentienceBrowser = SentienceBrowser;
|
|
915
|
+
/**
|
|
916
|
+
* Predicate rebrand alias for SentienceBrowser.
|
|
917
|
+
* Kept as a runtime alias to avoid breaking existing integrations.
|
|
918
|
+
*/
|
|
919
|
+
exports.PredicateBrowser = SentienceBrowser;
|
|
920
|
+
//# sourceMappingURL=browser.js.map
|