opensteer 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -3
- package/dist/chunk-2TIVULZY.js +4103 -0
- package/dist/chunk-2TIVULZY.js.map +1 -0
- package/dist/chunk-BMPUL66S.js +1170 -0
- package/dist/chunk-BMPUL66S.js.map +1 -0
- package/dist/chunk-FIMNKEG5.js +1800 -0
- package/dist/chunk-FIMNKEG5.js.map +1 -0
- package/dist/{chunk-656MQUSM.js → chunk-HD6KVZ42.js} +6080 -12739
- package/dist/chunk-HD6KVZ42.js.map +1 -0
- package/dist/{chunk-OIKLSFXA.js → chunk-KPYLS2KQ.js} +5 -35
- package/dist/chunk-KPYLS2KQ.js.map +1 -0
- package/dist/cli/bin.cjs +7436 -6861
- package/dist/cli/bin.cjs.map +1 -1
- package/dist/cli/bin.js +124 -7
- package/dist/cli/bin.js.map +1 -1
- package/dist/index.cjs +1048 -2584
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -757
- package/dist/index.d.ts +60 -757
- package/dist/index.js +4 -2
- package/dist/local-view/public/assets/app.css +770 -0
- package/dist/local-view/public/assets/app.js +2053 -0
- package/dist/local-view/public/index.html +235 -0
- package/dist/local-view/serve-entry.cjs +7203 -0
- package/dist/local-view/serve-entry.cjs.map +1 -0
- package/dist/local-view/serve-entry.d.cts +1 -0
- package/dist/local-view/serve-entry.d.ts +1 -0
- package/dist/local-view/serve-entry.js +23 -0
- package/dist/local-view/serve-entry.js.map +1 -0
- package/dist/opensteer-MIQ43CY4.js +6 -0
- package/dist/{opensteer-LKX3233A.js.map → opensteer-MIQ43CY4.js.map} +1 -1
- package/dist/session-control-IFE3IPS3.js +39 -0
- package/dist/session-control-IFE3IPS3.js.map +1 -0
- package/package.json +8 -8
- package/skills/README.md +3 -0
- package/skills/opensteer/SKILL.md +230 -49
- package/dist/chunk-656MQUSM.js.map +0 -1
- package/dist/chunk-OIKLSFXA.js.map +0 -1
- package/dist/opensteer-LKX3233A.js +0 -4
|
@@ -0,0 +1,4103 @@
|
|
|
1
|
+
import { filePathToUri, resolveStoragePath, encodePathSegment, ensureDirectory, pathExists, readJsonFile, writeJsonFileAtomic, normalizeTimestamp, withFilesystemLock, resolveLocalViewPreferencesPath, resolveLocalViewServiceLockDir, readLocalViewServiceState, isLocalViewServiceStateLive, clearLocalViewServiceState, getLocalViewServiceStateLiveness, clearPersistedSessionRecord, writePersistedSessionRecord, isProcessRunning, readPersistedLocalBrowserSessionRecord, buildLocalViewSessionId, selectAttachBrowserCandidate, resolveChromeExecutablePath, readDevToolsActivePort, inspectCdpEndpoint, normalizeNonEmptyString, toCanonicalJsonValue, canonicalJsonString, sha256Hex, joinStoragePath, writeBufferIfMissing, writeJsonFileExclusive, isAlreadyExistsError, readBinaryFile, listJsonFiles, expandHome, createLocalViewSessionManifest, writeLocalViewSessionManifest, deleteLocalViewSessionManifest, CURRENT_PROCESS_OWNER, getProcessLiveness, processOwnersEqual, parseProcessOwner } from './chunk-BMPUL66S.js';
|
|
2
|
+
import path7, { join, resolve, dirname, relative } from 'path';
|
|
3
|
+
import { randomBytes, randomUUID } from 'crypto';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { rm, mkdtemp, mkdir, cp, readdir, rename, stat, copyFile, readFile, writeFile } from 'fs/promises';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { connectPlaywrightChromiumBrowser, createPlaywrightBrowserCoreEngine, disconnectPlaywrightChromiumBrowser } from '@opensteer/engine-playwright';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
function normalizeScope(scope) {
|
|
13
|
+
if (scope === void 0) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
...scope.sessionRef === void 0 ? {} : { sessionRef: scope.sessionRef },
|
|
18
|
+
...scope.pageRef === void 0 ? {} : { pageRef: scope.pageRef },
|
|
19
|
+
...scope.frameRef === void 0 ? {} : { frameRef: scope.frameRef },
|
|
20
|
+
...scope.documentRef === void 0 ? {} : { documentRef: scope.documentRef },
|
|
21
|
+
...scope.documentEpoch === void 0 ? {} : { documentEpoch: scope.documentEpoch }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeProvenance(provenance) {
|
|
25
|
+
if (provenance === void 0) {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...provenance.sourceArtifactId === void 0 ? {} : {
|
|
30
|
+
sourceArtifactId: normalizeNonEmptyString(
|
|
31
|
+
"provenance.sourceArtifactId",
|
|
32
|
+
provenance.sourceArtifactId
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
...provenance.transform === void 0 ? {} : { transform: normalizeNonEmptyString("provenance.transform", provenance.transform) }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function readStructuredPayload(objectPath) {
|
|
39
|
+
return JSON.parse(Buffer.from(await readBinaryFile(objectPath)).toString("utf8"));
|
|
40
|
+
}
|
|
41
|
+
var FilesystemArtifactStore = class {
|
|
42
|
+
constructor(rootPath) {
|
|
43
|
+
this.rootPath = rootPath;
|
|
44
|
+
this.manifestsDirectory = path7.join(this.rootPath, "artifacts", "manifests");
|
|
45
|
+
this.objectsDirectory = path7.join(this.rootPath, "artifacts", "objects", "sha256");
|
|
46
|
+
}
|
|
47
|
+
manifestsDirectory;
|
|
48
|
+
objectsDirectory;
|
|
49
|
+
async initialize() {
|
|
50
|
+
await ensureDirectory(this.manifestsDirectory);
|
|
51
|
+
await ensureDirectory(this.objectsDirectory);
|
|
52
|
+
}
|
|
53
|
+
async writeStructured(input) {
|
|
54
|
+
const artifactId = normalizeNonEmptyString(
|
|
55
|
+
"artifactId",
|
|
56
|
+
input.artifactId ?? `artifact:${randomUUID()}`
|
|
57
|
+
);
|
|
58
|
+
const manifestPath = this.manifestPath(artifactId);
|
|
59
|
+
const jsonData = toCanonicalJsonValue(input.data);
|
|
60
|
+
const objectBytes = Buffer.from(canonicalJsonString(jsonData), "utf8");
|
|
61
|
+
const sha256 = sha256Hex(objectBytes);
|
|
62
|
+
const objectRelativePath = joinStoragePath("artifacts", "objects", "sha256", sha256);
|
|
63
|
+
const objectPath = resolveStoragePath(this.rootPath, objectRelativePath);
|
|
64
|
+
await writeBufferIfMissing(objectPath, objectBytes);
|
|
65
|
+
const provenance = normalizeProvenance(input.provenance);
|
|
66
|
+
const manifest = {
|
|
67
|
+
artifactId,
|
|
68
|
+
kind: input.kind,
|
|
69
|
+
createdAt: normalizeTimestamp("createdAt", input.createdAt ?? Date.now()),
|
|
70
|
+
...provenance === void 0 ? {} : { provenance },
|
|
71
|
+
scope: normalizeScope(input.scope),
|
|
72
|
+
mediaType: input.mediaType ?? "application/json",
|
|
73
|
+
payloadType: "structured",
|
|
74
|
+
byteLength: objectBytes.byteLength,
|
|
75
|
+
sha256,
|
|
76
|
+
objectRelativePath
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
await writeJsonFileExclusive(manifestPath, manifest);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (isAlreadyExistsError(error)) {
|
|
82
|
+
throw new Error(`artifact ${artifactId} already exists`);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return manifest;
|
|
87
|
+
}
|
|
88
|
+
async writeBinary(input) {
|
|
89
|
+
const artifactId = normalizeNonEmptyString(
|
|
90
|
+
"artifactId",
|
|
91
|
+
input.artifactId ?? `artifact:${randomUUID()}`
|
|
92
|
+
);
|
|
93
|
+
const manifestPath = this.manifestPath(artifactId);
|
|
94
|
+
const mediaType = normalizeNonEmptyString("mediaType", input.mediaType);
|
|
95
|
+
const data = new Uint8Array(input.data);
|
|
96
|
+
const sha256 = sha256Hex(data);
|
|
97
|
+
const extension = mediaTypeExtension(mediaType);
|
|
98
|
+
const objectRelativePath = joinStoragePath(
|
|
99
|
+
"artifacts",
|
|
100
|
+
"objects",
|
|
101
|
+
"sha256",
|
|
102
|
+
`${sha256}${extension}`
|
|
103
|
+
);
|
|
104
|
+
const objectPath = resolveStoragePath(this.rootPath, objectRelativePath);
|
|
105
|
+
await writeBufferIfMissing(objectPath, data);
|
|
106
|
+
const provenance = normalizeProvenance(input.provenance);
|
|
107
|
+
const manifest = {
|
|
108
|
+
artifactId,
|
|
109
|
+
kind: input.kind,
|
|
110
|
+
createdAt: normalizeTimestamp("createdAt", input.createdAt ?? Date.now()),
|
|
111
|
+
...provenance === void 0 ? {} : { provenance },
|
|
112
|
+
scope: normalizeScope(input.scope),
|
|
113
|
+
mediaType,
|
|
114
|
+
payloadType: "binary",
|
|
115
|
+
byteLength: data.byteLength,
|
|
116
|
+
sha256,
|
|
117
|
+
objectRelativePath
|
|
118
|
+
};
|
|
119
|
+
try {
|
|
120
|
+
await writeJsonFileExclusive(manifestPath, manifest);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (isAlreadyExistsError(error)) {
|
|
123
|
+
throw new Error(`artifact ${artifactId} already exists`);
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return manifest;
|
|
128
|
+
}
|
|
129
|
+
async getManifest(artifactId) {
|
|
130
|
+
const manifestPath = this.manifestPath(artifactId);
|
|
131
|
+
if (!await pathExists(manifestPath)) {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
return readJsonFile(manifestPath);
|
|
135
|
+
}
|
|
136
|
+
async read(artifactId) {
|
|
137
|
+
const manifest = await this.getManifest(artifactId);
|
|
138
|
+
if (manifest === void 0) {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
const objectPath = resolveStoragePath(this.rootPath, manifest.objectRelativePath);
|
|
142
|
+
if (manifest.kind === "screenshot") {
|
|
143
|
+
if (manifest.payloadType !== "binary") {
|
|
144
|
+
throw new Error(`artifact ${artifactId} has an invalid screenshot payload type`);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
manifest,
|
|
148
|
+
payload: {
|
|
149
|
+
kind: "screenshot",
|
|
150
|
+
payloadType: "binary",
|
|
151
|
+
data: await readBinaryFile(objectPath)
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (manifest.payloadType !== "structured") {
|
|
156
|
+
throw new Error(`artifact ${artifactId} has an invalid structured payload type`);
|
|
157
|
+
}
|
|
158
|
+
switch (manifest.kind) {
|
|
159
|
+
case "html-snapshot":
|
|
160
|
+
return {
|
|
161
|
+
manifest,
|
|
162
|
+
payload: {
|
|
163
|
+
kind: "html-snapshot",
|
|
164
|
+
payloadType: "structured",
|
|
165
|
+
data: await readStructuredPayload(objectPath)
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
case "dom-snapshot":
|
|
169
|
+
return {
|
|
170
|
+
manifest,
|
|
171
|
+
payload: {
|
|
172
|
+
kind: "dom-snapshot",
|
|
173
|
+
payloadType: "structured",
|
|
174
|
+
data: await readStructuredPayload(objectPath)
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
case "cookies":
|
|
178
|
+
return {
|
|
179
|
+
manifest,
|
|
180
|
+
payload: {
|
|
181
|
+
kind: "cookies",
|
|
182
|
+
payloadType: "structured",
|
|
183
|
+
data: await readStructuredPayload(objectPath)
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
case "storage-snapshot":
|
|
187
|
+
return {
|
|
188
|
+
manifest,
|
|
189
|
+
payload: {
|
|
190
|
+
kind: "storage-snapshot",
|
|
191
|
+
payloadType: "structured",
|
|
192
|
+
data: await readStructuredPayload(objectPath)
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
case "script-source":
|
|
196
|
+
return {
|
|
197
|
+
manifest,
|
|
198
|
+
payload: {
|
|
199
|
+
kind: "script-source",
|
|
200
|
+
payloadType: "structured",
|
|
201
|
+
data: await readStructuredPayload(objectPath)
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async toProtocolArtifactReference(artifactId, relation) {
|
|
207
|
+
const manifest = await this.getManifest(artifactId);
|
|
208
|
+
if (manifest === void 0) {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
artifactId: manifest.artifactId,
|
|
213
|
+
kind: manifest.kind,
|
|
214
|
+
relation
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async toProtocolArtifact(artifactId, options = {}) {
|
|
218
|
+
const record = await this.read(artifactId);
|
|
219
|
+
if (record === void 0) {
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
const delivery = options.delivery ?? "external";
|
|
223
|
+
const externalPayload = manifestToExternalBinaryLocation(this.rootPath, record.manifest);
|
|
224
|
+
const artifactBase = {
|
|
225
|
+
artifactId: record.manifest.artifactId,
|
|
226
|
+
createdAt: record.manifest.createdAt,
|
|
227
|
+
...record.manifest.provenance === void 0 ? {} : { provenance: record.manifest.provenance },
|
|
228
|
+
...record.manifest.scope.sessionRef === void 0 ? {} : { sessionRef: record.manifest.scope.sessionRef },
|
|
229
|
+
...record.manifest.scope.pageRef === void 0 ? {} : { pageRef: record.manifest.scope.pageRef },
|
|
230
|
+
...record.manifest.scope.frameRef === void 0 ? {} : { frameRef: record.manifest.scope.frameRef },
|
|
231
|
+
...record.manifest.scope.documentRef === void 0 ? {} : { documentRef: record.manifest.scope.documentRef },
|
|
232
|
+
...record.manifest.scope.documentEpoch === void 0 ? {} : { documentEpoch: record.manifest.scope.documentEpoch }
|
|
233
|
+
};
|
|
234
|
+
switch (record.payload.kind) {
|
|
235
|
+
case "screenshot":
|
|
236
|
+
return { ...artifactBase, kind: "screenshot", payload: externalPayload };
|
|
237
|
+
case "html-snapshot":
|
|
238
|
+
return {
|
|
239
|
+
...artifactBase,
|
|
240
|
+
kind: "html-snapshot",
|
|
241
|
+
payload: delivery === "inline-if-structured" ? { delivery: "inline", data: record.payload.data } : externalPayload
|
|
242
|
+
};
|
|
243
|
+
case "dom-snapshot":
|
|
244
|
+
return {
|
|
245
|
+
...artifactBase,
|
|
246
|
+
kind: "dom-snapshot",
|
|
247
|
+
payload: delivery === "inline-if-structured" ? { delivery: "inline", data: record.payload.data } : externalPayload
|
|
248
|
+
};
|
|
249
|
+
case "cookies":
|
|
250
|
+
return {
|
|
251
|
+
...artifactBase,
|
|
252
|
+
kind: "cookies",
|
|
253
|
+
payload: delivery === "inline-if-structured" ? { delivery: "inline", data: record.payload.data } : externalPayload
|
|
254
|
+
};
|
|
255
|
+
case "storage-snapshot":
|
|
256
|
+
return {
|
|
257
|
+
...artifactBase,
|
|
258
|
+
kind: "storage-snapshot",
|
|
259
|
+
payload: delivery === "inline-if-structured" ? { delivery: "inline", data: record.payload.data } : externalPayload
|
|
260
|
+
};
|
|
261
|
+
case "script-source":
|
|
262
|
+
return {
|
|
263
|
+
...artifactBase,
|
|
264
|
+
kind: "script-source",
|
|
265
|
+
payload: delivery === "inline-if-structured" ? { delivery: "inline", data: record.payload.data } : externalPayload
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
manifestPath(artifactId) {
|
|
270
|
+
return path7.join(this.manifestsDirectory, `${encodePathSegment(artifactId)}.json`);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
function createArtifactStore(rootPath) {
|
|
274
|
+
return new FilesystemArtifactStore(rootPath);
|
|
275
|
+
}
|
|
276
|
+
function manifestToExternalBinaryLocation(rootPath, manifest) {
|
|
277
|
+
return {
|
|
278
|
+
delivery: "external",
|
|
279
|
+
uri: filePathToUri(resolveStoragePath(rootPath, manifest.objectRelativePath)),
|
|
280
|
+
mimeType: manifest.mediaType,
|
|
281
|
+
byteLength: manifest.byteLength,
|
|
282
|
+
sha256: manifest.sha256
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function mediaTypeExtension(mediaType) {
|
|
286
|
+
switch (mediaType) {
|
|
287
|
+
case "image/png":
|
|
288
|
+
return ".png";
|
|
289
|
+
case "image/jpeg":
|
|
290
|
+
return ".jpeg";
|
|
291
|
+
case "image/webp":
|
|
292
|
+
return ".webp";
|
|
293
|
+
default:
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ../runtime-core/src/observation-utils.ts
|
|
299
|
+
var REDACTED = "[REDACTED]";
|
|
300
|
+
var SENSITIVE_KEY_PATTERN = /(authorization|proxy[_-]?authorization|cookie|set-cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|token|secret|password|passwd|private[_-]?key|database[_-]?url|db[_-]?url|session(?:id)?|csrf(?:token)?)/i;
|
|
301
|
+
var SENSITIVE_VALUE_PATTERNS = [
|
|
302
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
303
|
+
/\bAIza[0-9A-Za-z_-]{20,}\b/g,
|
|
304
|
+
/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g,
|
|
305
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
306
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/gi
|
|
307
|
+
];
|
|
308
|
+
function normalizeObservationContext(context) {
|
|
309
|
+
if (context === void 0) {
|
|
310
|
+
return void 0;
|
|
311
|
+
}
|
|
312
|
+
const normalized = {
|
|
313
|
+
...context.sessionRef === void 0 ? {} : { sessionRef: context.sessionRef },
|
|
314
|
+
...context.pageRef === void 0 ? {} : { pageRef: context.pageRef },
|
|
315
|
+
...context.frameRef === void 0 ? {} : { frameRef: context.frameRef },
|
|
316
|
+
...context.documentRef === void 0 ? {} : { documentRef: context.documentRef },
|
|
317
|
+
...context.documentEpoch === void 0 ? {} : { documentEpoch: context.documentEpoch }
|
|
318
|
+
};
|
|
319
|
+
return Object.keys(normalized).length === 0 ? void 0 : normalized;
|
|
320
|
+
}
|
|
321
|
+
function createObservationRedactor(config) {
|
|
322
|
+
const state = createRedactionState(config);
|
|
323
|
+
return {
|
|
324
|
+
redactText(value) {
|
|
325
|
+
return redactString(value, state);
|
|
326
|
+
},
|
|
327
|
+
redactJson(value) {
|
|
328
|
+
return value === void 0 ? void 0 : redactUnknown(value, state, /* @__PURE__ */ new WeakSet());
|
|
329
|
+
},
|
|
330
|
+
redactError(error) {
|
|
331
|
+
if (error === void 0) {
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
...error.code === void 0 ? {} : { code: redactString(error.code, state) },
|
|
336
|
+
message: redactString(error.message, state),
|
|
337
|
+
...error.retriable === void 0 ? {} : { retriable: error.retriable },
|
|
338
|
+
...error.details === void 0 ? {} : { details: toCanonicalJsonValue(redactUnknown(error.details, state, /* @__PURE__ */ new WeakSet())) }
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
redactLabels(labels) {
|
|
342
|
+
if (labels === void 0) {
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
const next = Object.entries(labels).reduce(
|
|
346
|
+
(accumulator, [key, value]) => {
|
|
347
|
+
accumulator[key] = isSensitiveKey(key, state) ? REDACTED : redactString(value, state);
|
|
348
|
+
return accumulator;
|
|
349
|
+
},
|
|
350
|
+
{}
|
|
351
|
+
);
|
|
352
|
+
return Object.keys(next).length === 0 ? void 0 : next;
|
|
353
|
+
},
|
|
354
|
+
redactTraceContext(traceContext) {
|
|
355
|
+
if (traceContext === void 0) {
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
const next = {
|
|
359
|
+
...traceContext.traceparent === void 0 ? {} : { traceparent: redactString(traceContext.traceparent, state) },
|
|
360
|
+
...traceContext.baggage === void 0 ? {} : { baggage: redactString(traceContext.baggage, state) }
|
|
361
|
+
};
|
|
362
|
+
return Object.keys(next).length === 0 ? void 0 : next;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function createRedactionState(config) {
|
|
367
|
+
return {
|
|
368
|
+
sensitiveKeys: new Set(
|
|
369
|
+
(config?.redaction?.sensitiveKeys ?? []).map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)
|
|
370
|
+
),
|
|
371
|
+
sensitiveValues: [
|
|
372
|
+
...new Set(
|
|
373
|
+
(config?.redaction?.sensitiveValues ?? []).map((value) => value.trim()).filter((value) => value.length > 0)
|
|
374
|
+
)
|
|
375
|
+
]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function redactUnknown(value, state, seen) {
|
|
379
|
+
if (value === null || value === void 0) {
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
if (typeof value === "string") {
|
|
383
|
+
return redactString(value, state);
|
|
384
|
+
}
|
|
385
|
+
if (typeof value !== "object") {
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
if (seen.has(value)) {
|
|
389
|
+
return REDACTED;
|
|
390
|
+
}
|
|
391
|
+
seen.add(value);
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
return value.map((entry) => redactUnknown(entry, state, seen));
|
|
394
|
+
}
|
|
395
|
+
const next = {};
|
|
396
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
397
|
+
next[key] = isSensitiveKey(key, state) ? REDACTED : redactUnknown(nestedValue, state, seen);
|
|
398
|
+
}
|
|
399
|
+
return next;
|
|
400
|
+
}
|
|
401
|
+
function redactString(value, state) {
|
|
402
|
+
let next = value;
|
|
403
|
+
for (const secret of state.sensitiveValues) {
|
|
404
|
+
next = next.split(secret).join(REDACTED);
|
|
405
|
+
}
|
|
406
|
+
for (const pattern of SENSITIVE_VALUE_PATTERNS) {
|
|
407
|
+
next = next.replace(pattern, REDACTED);
|
|
408
|
+
}
|
|
409
|
+
return redactUrlString(next, state);
|
|
410
|
+
}
|
|
411
|
+
function redactUrlString(value, state) {
|
|
412
|
+
let parsed;
|
|
413
|
+
try {
|
|
414
|
+
parsed = new URL(value);
|
|
415
|
+
} catch {
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
let changed = false;
|
|
419
|
+
if (parsed.username) {
|
|
420
|
+
parsed.username = REDACTED;
|
|
421
|
+
changed = true;
|
|
422
|
+
}
|
|
423
|
+
if (parsed.password) {
|
|
424
|
+
parsed.password = REDACTED;
|
|
425
|
+
changed = true;
|
|
426
|
+
}
|
|
427
|
+
for (const [key] of parsed.searchParams) {
|
|
428
|
+
if (!isSensitiveKey(key, state)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
parsed.searchParams.set(key, REDACTED);
|
|
432
|
+
changed = true;
|
|
433
|
+
}
|
|
434
|
+
return changed ? parsed.toString() : value;
|
|
435
|
+
}
|
|
436
|
+
function isSensitiveKey(key, state) {
|
|
437
|
+
return state.sensitiveKeys.has(key.trim().toLowerCase()) || SENSITIVE_KEY_PATTERN.test(key);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ../runtime-core/src/observations.ts
|
|
441
|
+
function normalizeObservabilityConfig(input) {
|
|
442
|
+
const profile = input?.profile ?? "diagnostic";
|
|
443
|
+
const labels = input?.labels === void 0 ? void 0 : Object.entries(input.labels).reduce((accumulator, [key, value]) => {
|
|
444
|
+
const normalizedKey = key.trim();
|
|
445
|
+
const normalizedValue = value.trim();
|
|
446
|
+
if (normalizedKey.length === 0 || normalizedValue.length === 0) {
|
|
447
|
+
return accumulator;
|
|
448
|
+
}
|
|
449
|
+
if (Object.keys(accumulator).length >= 20) {
|
|
450
|
+
return accumulator;
|
|
451
|
+
}
|
|
452
|
+
accumulator[normalizedKey] = normalizedValue;
|
|
453
|
+
return accumulator;
|
|
454
|
+
}, {});
|
|
455
|
+
const redaction = input?.redaction === void 0 ? void 0 : {
|
|
456
|
+
...input.redaction.sensitiveKeys === void 0 ? {} : {
|
|
457
|
+
sensitiveKeys: input.redaction.sensitiveKeys.map((value) => value.trim()).filter((value) => value.length > 0)
|
|
458
|
+
},
|
|
459
|
+
...input.redaction.sensitiveValues === void 0 ? {} : {
|
|
460
|
+
sensitiveValues: input.redaction.sensitiveValues.map((value) => value.trim()).filter((value) => value.length > 0)
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
return {
|
|
464
|
+
profile,
|
|
465
|
+
...labels === void 0 || Object.keys(labels).length === 0 ? {} : { labels },
|
|
466
|
+
...input?.traceContext === void 0 ? {} : {
|
|
467
|
+
traceContext: {
|
|
468
|
+
...input.traceContext.traceparent === void 0 ? {} : { traceparent: input.traceContext.traceparent.trim() },
|
|
469
|
+
...input.traceContext.baggage === void 0 ? {} : { baggage: input.traceContext.baggage.trim() }
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
...redaction === void 0 ? {} : { redaction }
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function eventFileName(sequence) {
|
|
476
|
+
return `${String(sequence).padStart(12, "0")}.json`;
|
|
477
|
+
}
|
|
478
|
+
var FilesystemSessionSink = class {
|
|
479
|
+
constructor(store, sessionId) {
|
|
480
|
+
this.store = store;
|
|
481
|
+
this.sessionId = sessionId;
|
|
482
|
+
}
|
|
483
|
+
append(input) {
|
|
484
|
+
return this.store.appendEvent(this.sessionId, input);
|
|
485
|
+
}
|
|
486
|
+
appendBatch(input) {
|
|
487
|
+
return this.store.appendEvents(this.sessionId, input);
|
|
488
|
+
}
|
|
489
|
+
writeArtifact(input) {
|
|
490
|
+
return this.store.writeArtifact(this.sessionId, input);
|
|
491
|
+
}
|
|
492
|
+
async flush() {
|
|
493
|
+
}
|
|
494
|
+
close(reason) {
|
|
495
|
+
return this.store.closeSession(this.sessionId, reason);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
var FilesystemObservationStoreImpl = class {
|
|
499
|
+
constructor(rootPath, artifacts) {
|
|
500
|
+
this.rootPath = rootPath;
|
|
501
|
+
this.artifacts = artifacts;
|
|
502
|
+
this.sessionsDirectory = path7.join(this.rootPath, "observations", "sessions");
|
|
503
|
+
}
|
|
504
|
+
sessionsDirectory;
|
|
505
|
+
redactors = /* @__PURE__ */ new Map();
|
|
506
|
+
async initialize() {
|
|
507
|
+
await ensureDirectory(this.sessionsDirectory);
|
|
508
|
+
}
|
|
509
|
+
async openSession(input) {
|
|
510
|
+
const sessionId = normalizeNonEmptyString("sessionId", input.sessionId);
|
|
511
|
+
const openedAt = normalizeTimestamp("openedAt", input.openedAt ?? Date.now());
|
|
512
|
+
const config = normalizeObservabilityConfig(input.config);
|
|
513
|
+
const redactor = createObservationRedactor(config);
|
|
514
|
+
this.redactors.set(sessionId, redactor);
|
|
515
|
+
const redactedLabels = redactor.redactLabels(config.labels);
|
|
516
|
+
const redactedTraceContext = redactor.redactTraceContext(config.traceContext);
|
|
517
|
+
await withFilesystemLock(this.sessionLockPath(sessionId), async () => {
|
|
518
|
+
const existing = await this.reconcileSessionManifest(sessionId);
|
|
519
|
+
if (existing === void 0) {
|
|
520
|
+
await ensureDirectory(this.sessionEventsDirectory(sessionId));
|
|
521
|
+
await ensureDirectory(this.sessionArtifactsDirectory(sessionId));
|
|
522
|
+
const session = {
|
|
523
|
+
sessionId,
|
|
524
|
+
profile: config.profile,
|
|
525
|
+
...redactedLabels === void 0 ? {} : { labels: redactedLabels },
|
|
526
|
+
...redactedTraceContext === void 0 ? {} : { traceContext: redactedTraceContext },
|
|
527
|
+
openedAt,
|
|
528
|
+
updatedAt: openedAt,
|
|
529
|
+
currentSequence: 0,
|
|
530
|
+
eventCount: 0,
|
|
531
|
+
artifactCount: 0
|
|
532
|
+
};
|
|
533
|
+
await writeJsonFileExclusive(this.sessionManifestPath(sessionId), session);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const patched = {
|
|
537
|
+
...existing,
|
|
538
|
+
profile: config.profile,
|
|
539
|
+
...redactedLabels === void 0 ? {} : { labels: redactedLabels },
|
|
540
|
+
...redactedTraceContext === void 0 ? {} : { traceContext: redactedTraceContext },
|
|
541
|
+
updatedAt: Math.max(existing.updatedAt, openedAt)
|
|
542
|
+
};
|
|
543
|
+
await writeJsonFileAtomic(this.sessionManifestPath(sessionId), patched);
|
|
544
|
+
});
|
|
545
|
+
return new FilesystemSessionSink(this, sessionId);
|
|
546
|
+
}
|
|
547
|
+
async getSession(sessionId) {
|
|
548
|
+
const manifestPath = this.sessionManifestPath(sessionId);
|
|
549
|
+
if (!await pathExists(manifestPath)) {
|
|
550
|
+
return void 0;
|
|
551
|
+
}
|
|
552
|
+
return readJsonFile(manifestPath);
|
|
553
|
+
}
|
|
554
|
+
async appendEvent(sessionId, input) {
|
|
555
|
+
const [event] = await this.appendEvents(sessionId, [input]);
|
|
556
|
+
if (event === void 0) {
|
|
557
|
+
throw new Error(`failed to append observation event for session ${sessionId}`);
|
|
558
|
+
}
|
|
559
|
+
return event;
|
|
560
|
+
}
|
|
561
|
+
async appendEvents(sessionId, input) {
|
|
562
|
+
if (input.length === 0) {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
return withFilesystemLock(this.sessionLockPath(sessionId), async () => {
|
|
566
|
+
const session = await this.reconcileSessionManifest(sessionId);
|
|
567
|
+
if (session === void 0) {
|
|
568
|
+
throw new Error(`observation session ${sessionId} was not found`);
|
|
569
|
+
}
|
|
570
|
+
const redactor = this.redactors.get(sessionId) ?? createObservationRedactor(void 0);
|
|
571
|
+
const events = [];
|
|
572
|
+
let sequence = session.currentSequence;
|
|
573
|
+
let updatedAt = session.updatedAt;
|
|
574
|
+
for (const raw of input) {
|
|
575
|
+
sequence += 1;
|
|
576
|
+
const createdAt = normalizeTimestamp("createdAt", raw.createdAt);
|
|
577
|
+
const context = normalizeObservationContext(raw.context);
|
|
578
|
+
const redactedData = raw.data === void 0 ? void 0 : redactor.redactJson(toCanonicalJsonValue(raw.data));
|
|
579
|
+
const redactedError = redactor.redactError(raw.error);
|
|
580
|
+
const event = {
|
|
581
|
+
eventId: normalizeNonEmptyString(
|
|
582
|
+
"eventId",
|
|
583
|
+
raw.eventId ?? `observation:${sessionId}:${String(sequence).padStart(12, "0")}`
|
|
584
|
+
),
|
|
585
|
+
sessionId,
|
|
586
|
+
sequence,
|
|
587
|
+
kind: raw.kind,
|
|
588
|
+
phase: raw.phase,
|
|
589
|
+
createdAt,
|
|
590
|
+
correlationId: normalizeNonEmptyString("correlationId", raw.correlationId),
|
|
591
|
+
...raw.spanId === void 0 ? {} : { spanId: normalizeNonEmptyString("spanId", raw.spanId) },
|
|
592
|
+
...raw.parentSpanId === void 0 ? {} : { parentSpanId: normalizeNonEmptyString("parentSpanId", raw.parentSpanId) },
|
|
593
|
+
...context === void 0 ? {} : { context },
|
|
594
|
+
...redactedData === void 0 ? {} : { data: redactedData },
|
|
595
|
+
...redactedError === void 0 ? {} : { error: redactedError },
|
|
596
|
+
...raw.artifactIds === void 0 || raw.artifactIds.length === 0 ? {} : { artifactIds: [...raw.artifactIds] }
|
|
597
|
+
};
|
|
598
|
+
await writeJsonFileExclusive(
|
|
599
|
+
path7.join(this.sessionEventsDirectory(sessionId), eventFileName(sequence)),
|
|
600
|
+
event
|
|
601
|
+
);
|
|
602
|
+
updatedAt = Math.max(updatedAt, createdAt);
|
|
603
|
+
events.push(event);
|
|
604
|
+
}
|
|
605
|
+
await writeJsonFileAtomic(this.sessionManifestPath(sessionId), {
|
|
606
|
+
...session,
|
|
607
|
+
currentSequence: sequence,
|
|
608
|
+
eventCount: session.eventCount + events.length,
|
|
609
|
+
updatedAt
|
|
610
|
+
});
|
|
611
|
+
return events;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
async writeArtifact(sessionId, input) {
|
|
615
|
+
return withFilesystemLock(this.sessionLockPath(sessionId), async () => {
|
|
616
|
+
const session = await this.reconcileSessionManifest(sessionId);
|
|
617
|
+
if (session === void 0) {
|
|
618
|
+
throw new Error(`observation session ${sessionId} was not found`);
|
|
619
|
+
}
|
|
620
|
+
const redactor = this.redactors.get(sessionId) ?? createObservationRedactor(void 0);
|
|
621
|
+
const createdAt = normalizeTimestamp("createdAt", input.createdAt);
|
|
622
|
+
const context = normalizeObservationContext(input.context);
|
|
623
|
+
const redactedStorageKey = input.storageKey === void 0 ? void 0 : redactor.redactText(input.storageKey);
|
|
624
|
+
const redactedMetadata = input.metadata === void 0 ? void 0 : redactor.redactJson(toCanonicalJsonValue(input.metadata));
|
|
625
|
+
const artifact = {
|
|
626
|
+
artifactId: normalizeNonEmptyString("artifactId", input.artifactId),
|
|
627
|
+
sessionId,
|
|
628
|
+
kind: input.kind,
|
|
629
|
+
createdAt,
|
|
630
|
+
...context === void 0 ? {} : { context },
|
|
631
|
+
...input.mediaType === void 0 ? {} : { mediaType: input.mediaType },
|
|
632
|
+
...input.byteLength === void 0 ? {} : { byteLength: input.byteLength },
|
|
633
|
+
...input.sha256 === void 0 ? {} : { sha256: input.sha256 },
|
|
634
|
+
...input.opensteerArtifactId === void 0 ? {} : { opensteerArtifactId: input.opensteerArtifactId },
|
|
635
|
+
...redactedStorageKey === void 0 ? {} : { storageKey: redactedStorageKey },
|
|
636
|
+
...redactedMetadata === void 0 ? {} : { metadata: redactedMetadata }
|
|
637
|
+
};
|
|
638
|
+
const artifactPath = this.sessionArtifactPath(sessionId, artifact.artifactId);
|
|
639
|
+
if (!await pathExists(artifactPath)) {
|
|
640
|
+
await writeJsonFileExclusive(artifactPath, artifact);
|
|
641
|
+
await writeJsonFileAtomic(this.sessionManifestPath(sessionId), {
|
|
642
|
+
...session,
|
|
643
|
+
artifactCount: session.artifactCount + 1,
|
|
644
|
+
updatedAt: Math.max(session.updatedAt, createdAt)
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return artifact;
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
async listEvents(sessionId, input = {}) {
|
|
651
|
+
const directoryPath = this.sessionEventsDirectory(sessionId);
|
|
652
|
+
if (!await pathExists(directoryPath)) {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
const files = await listJsonFiles(directoryPath);
|
|
656
|
+
const events = await Promise.all(
|
|
657
|
+
files.map((fileName) => readJsonFile(path7.join(directoryPath, fileName)))
|
|
658
|
+
);
|
|
659
|
+
const filtered = events.filter((event) => {
|
|
660
|
+
if (input.kind !== void 0 && event.kind !== input.kind) return false;
|
|
661
|
+
if (input.phase !== void 0 && event.phase !== input.phase) return false;
|
|
662
|
+
if (input.correlationId !== void 0 && event.correlationId !== input.correlationId) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (input.pageRef !== void 0 && event.context?.pageRef !== input.pageRef) return false;
|
|
666
|
+
if (input.afterSequence !== void 0 && event.sequence <= input.afterSequence) return false;
|
|
667
|
+
if (input.from !== void 0 && event.createdAt < input.from) return false;
|
|
668
|
+
if (input.to !== void 0 && event.createdAt > input.to) return false;
|
|
669
|
+
return true;
|
|
670
|
+
});
|
|
671
|
+
if (input.limit === void 0 || filtered.length <= input.limit) {
|
|
672
|
+
return filtered;
|
|
673
|
+
}
|
|
674
|
+
return filtered.slice(-input.limit);
|
|
675
|
+
}
|
|
676
|
+
async listArtifacts(sessionId, input = {}) {
|
|
677
|
+
const directoryPath = this.sessionArtifactsDirectory(sessionId);
|
|
678
|
+
if (!await pathExists(directoryPath)) {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
const files = await listJsonFiles(directoryPath);
|
|
682
|
+
const artifacts = await Promise.all(
|
|
683
|
+
files.map(
|
|
684
|
+
(fileName) => readJsonFile(path7.join(directoryPath, fileName))
|
|
685
|
+
)
|
|
686
|
+
);
|
|
687
|
+
const filtered = artifacts.filter((artifact) => {
|
|
688
|
+
if (input.kind !== void 0 && artifact.kind !== input.kind) return false;
|
|
689
|
+
if (input.pageRef !== void 0 && artifact.context?.pageRef !== input.pageRef) return false;
|
|
690
|
+
return true;
|
|
691
|
+
});
|
|
692
|
+
if (input.limit === void 0 || filtered.length <= input.limit) {
|
|
693
|
+
return filtered;
|
|
694
|
+
}
|
|
695
|
+
return filtered.slice(-input.limit);
|
|
696
|
+
}
|
|
697
|
+
async getArtifact(sessionId, artifactId) {
|
|
698
|
+
const artifactPath = this.sessionArtifactPath(sessionId, artifactId);
|
|
699
|
+
if (!await pathExists(artifactPath)) {
|
|
700
|
+
return void 0;
|
|
701
|
+
}
|
|
702
|
+
return readJsonFile(artifactPath);
|
|
703
|
+
}
|
|
704
|
+
async closeSession(sessionId, _reason) {
|
|
705
|
+
await withFilesystemLock(this.sessionLockPath(sessionId), async () => {
|
|
706
|
+
const session = await this.reconcileSessionManifest(sessionId);
|
|
707
|
+
if (session === void 0 || session.closedAt !== void 0) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const now = Date.now();
|
|
711
|
+
await writeJsonFileAtomic(this.sessionManifestPath(sessionId), {
|
|
712
|
+
...session,
|
|
713
|
+
updatedAt: Math.max(session.updatedAt, now),
|
|
714
|
+
closedAt: now
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
this.redactors.delete(sessionId);
|
|
718
|
+
}
|
|
719
|
+
async writeArtifactFromManifest(sessionId, manifest, kind, metadata) {
|
|
720
|
+
return this.writeArtifact(sessionId, {
|
|
721
|
+
artifactId: manifest.artifactId,
|
|
722
|
+
kind,
|
|
723
|
+
createdAt: manifest.createdAt,
|
|
724
|
+
context: manifest.scope,
|
|
725
|
+
mediaType: manifest.mediaType,
|
|
726
|
+
byteLength: manifest.byteLength,
|
|
727
|
+
sha256: manifest.sha256,
|
|
728
|
+
opensteerArtifactId: manifest.artifactId,
|
|
729
|
+
storageKey: manifestToExternalBinaryLocation(this.rootPath, manifest).uri,
|
|
730
|
+
...metadata === void 0 ? {} : { metadata }
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
async ensureArtifactLinked(sessionId, manifest) {
|
|
734
|
+
const existing = await this.getArtifact(sessionId, manifest.artifactId);
|
|
735
|
+
if (existing !== void 0) {
|
|
736
|
+
return existing;
|
|
737
|
+
}
|
|
738
|
+
const kind = toObservationArtifactKind(manifest.kind);
|
|
739
|
+
return this.writeArtifactFromManifest(sessionId, manifest, kind);
|
|
740
|
+
}
|
|
741
|
+
async hydrateArtifactManifests(artifactIds) {
|
|
742
|
+
return (await Promise.all(
|
|
743
|
+
artifactIds.map(async (artifactId) => this.artifacts.getManifest(artifactId))
|
|
744
|
+
)).filter((value) => value !== void 0);
|
|
745
|
+
}
|
|
746
|
+
sessionDirectory(sessionId) {
|
|
747
|
+
return path7.join(this.sessionsDirectory, encodePathSegment(sessionId));
|
|
748
|
+
}
|
|
749
|
+
sessionManifestPath(sessionId) {
|
|
750
|
+
return path7.join(this.sessionDirectory(sessionId), "session.json");
|
|
751
|
+
}
|
|
752
|
+
sessionEventsDirectory(sessionId) {
|
|
753
|
+
return path7.join(this.sessionDirectory(sessionId), "events");
|
|
754
|
+
}
|
|
755
|
+
sessionArtifactsDirectory(sessionId) {
|
|
756
|
+
return path7.join(this.sessionDirectory(sessionId), "artifacts");
|
|
757
|
+
}
|
|
758
|
+
sessionArtifactPath(sessionId, artifactId) {
|
|
759
|
+
return path7.join(
|
|
760
|
+
this.sessionArtifactsDirectory(sessionId),
|
|
761
|
+
`${encodePathSegment(artifactId)}.json`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
sessionLockPath(sessionId) {
|
|
765
|
+
return path7.join(this.sessionDirectory(sessionId), ".lock");
|
|
766
|
+
}
|
|
767
|
+
async reconcileSessionManifest(sessionId) {
|
|
768
|
+
const session = await this.getSession(sessionId);
|
|
769
|
+
if (session === void 0) {
|
|
770
|
+
return void 0;
|
|
771
|
+
}
|
|
772
|
+
const [hasEventDirectory, hasArtifactDirectory] = await Promise.all([
|
|
773
|
+
pathExists(this.sessionEventsDirectory(sessionId)),
|
|
774
|
+
pathExists(this.sessionArtifactsDirectory(sessionId))
|
|
775
|
+
]);
|
|
776
|
+
const [eventFiles, artifactFiles] = await Promise.all([
|
|
777
|
+
hasEventDirectory ? listJsonFiles(this.sessionEventsDirectory(sessionId)) : Promise.resolve([]),
|
|
778
|
+
hasArtifactDirectory ? listJsonFiles(this.sessionArtifactsDirectory(sessionId)) : Promise.resolve([])
|
|
779
|
+
]);
|
|
780
|
+
const currentSequence = eventFiles.reduce((maxSequence, fileName) => {
|
|
781
|
+
const parsed = Number.parseInt(fileName.replace(/\.json$/u, ""), 10);
|
|
782
|
+
return Number.isFinite(parsed) ? Math.max(maxSequence, parsed) : maxSequence;
|
|
783
|
+
}, 0);
|
|
784
|
+
const eventCount = eventFiles.length;
|
|
785
|
+
const artifactCount = artifactFiles.length;
|
|
786
|
+
if (session.currentSequence === currentSequence && session.eventCount === eventCount && session.artifactCount === artifactCount) {
|
|
787
|
+
return session;
|
|
788
|
+
}
|
|
789
|
+
const [events, artifacts] = await Promise.all([
|
|
790
|
+
Promise.all(
|
|
791
|
+
eventFiles.map(
|
|
792
|
+
(fileName) => readJsonFile(
|
|
793
|
+
path7.join(this.sessionEventsDirectory(sessionId), fileName)
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
),
|
|
797
|
+
Promise.all(
|
|
798
|
+
artifactFiles.map(
|
|
799
|
+
(fileName) => readJsonFile(
|
|
800
|
+
path7.join(this.sessionArtifactsDirectory(sessionId), fileName)
|
|
801
|
+
)
|
|
802
|
+
)
|
|
803
|
+
)
|
|
804
|
+
]);
|
|
805
|
+
const updatedAt = Math.max(
|
|
806
|
+
session.openedAt,
|
|
807
|
+
session.closedAt ?? 0,
|
|
808
|
+
...events.map((event) => event.createdAt),
|
|
809
|
+
...artifacts.map((artifact) => artifact.createdAt)
|
|
810
|
+
);
|
|
811
|
+
const reconciled = {
|
|
812
|
+
...session,
|
|
813
|
+
currentSequence,
|
|
814
|
+
eventCount,
|
|
815
|
+
artifactCount,
|
|
816
|
+
updatedAt
|
|
817
|
+
};
|
|
818
|
+
await writeJsonFileAtomic(this.sessionManifestPath(sessionId), reconciled);
|
|
819
|
+
return reconciled;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
function toObservationArtifactKind(kind) {
|
|
823
|
+
switch (kind) {
|
|
824
|
+
case "screenshot":
|
|
825
|
+
return "screenshot";
|
|
826
|
+
case "dom-snapshot":
|
|
827
|
+
return "dom-snapshot";
|
|
828
|
+
case "html-snapshot":
|
|
829
|
+
return "html-snapshot";
|
|
830
|
+
default:
|
|
831
|
+
return "other";
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function createObservationStore(rootPath, artifacts) {
|
|
835
|
+
return new FilesystemObservationStoreImpl(rootPath, artifacts);
|
|
836
|
+
}
|
|
837
|
+
function normalizeTags(tags) {
|
|
838
|
+
if (tags === void 0) {
|
|
839
|
+
return [];
|
|
840
|
+
}
|
|
841
|
+
return Array.from(new Set(tags.map((tag) => normalizeNonEmptyString("tag", tag)))).sort(
|
|
842
|
+
(left, right) => left.localeCompare(right)
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
function normalizeProvenance2(provenance) {
|
|
846
|
+
if (provenance === void 0) {
|
|
847
|
+
return void 0;
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
source: normalizeNonEmptyString("provenance.source", provenance.source),
|
|
851
|
+
...provenance.sourceId === void 0 ? {} : { sourceId: normalizeNonEmptyString("provenance.sourceId", provenance.sourceId) },
|
|
852
|
+
...provenance.capturedAt === void 0 ? {} : { capturedAt: normalizeTimestamp("provenance.capturedAt", provenance.capturedAt) },
|
|
853
|
+
...provenance.notes === void 0 ? {} : { notes: normalizeNonEmptyString("provenance.notes", provenance.notes) }
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function normalizeFreshness(freshness) {
|
|
857
|
+
if (freshness === void 0) {
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
...freshness.lastValidatedAt === void 0 ? {} : {
|
|
862
|
+
lastValidatedAt: normalizeTimestamp(
|
|
863
|
+
"freshness.lastValidatedAt",
|
|
864
|
+
freshness.lastValidatedAt
|
|
865
|
+
)
|
|
866
|
+
},
|
|
867
|
+
...freshness.staleAt === void 0 ? {} : { staleAt: normalizeTimestamp("freshness.staleAt", freshness.staleAt) },
|
|
868
|
+
...freshness.expiresAt === void 0 ? {} : { expiresAt: normalizeTimestamp("freshness.expiresAt", freshness.expiresAt) }
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function compareByCreatedAtAndId(left, right) {
|
|
872
|
+
if (left.createdAt !== right.createdAt) {
|
|
873
|
+
return right.createdAt - left.createdAt;
|
|
874
|
+
}
|
|
875
|
+
return left.id.localeCompare(right.id);
|
|
876
|
+
}
|
|
877
|
+
var FilesystemRegistryStore = class {
|
|
878
|
+
constructor(rootPath, registryRelativePath) {
|
|
879
|
+
this.registryRelativePath = registryRelativePath;
|
|
880
|
+
const basePath = path7.join(rootPath, ...registryRelativePath);
|
|
881
|
+
this.recordsDirectory = path7.join(basePath, "records");
|
|
882
|
+
this.indexesDirectory = path7.join(basePath, "indexes", "by-key");
|
|
883
|
+
}
|
|
884
|
+
recordsDirectory;
|
|
885
|
+
indexesDirectory;
|
|
886
|
+
async initialize() {
|
|
887
|
+
await ensureDirectory(this.recordsDirectory);
|
|
888
|
+
await ensureDirectory(this.indexesDirectory);
|
|
889
|
+
}
|
|
890
|
+
async getById(id) {
|
|
891
|
+
const recordPath = this.recordPath(id);
|
|
892
|
+
if (!await pathExists(recordPath)) {
|
|
893
|
+
return void 0;
|
|
894
|
+
}
|
|
895
|
+
return readJsonFile(recordPath);
|
|
896
|
+
}
|
|
897
|
+
async resolve(input) {
|
|
898
|
+
const key = normalizeNonEmptyString("key", input.key);
|
|
899
|
+
if (input.version !== void 0) {
|
|
900
|
+
return this.resolveIndexedRecord(key, normalizeNonEmptyString("version", input.version));
|
|
901
|
+
}
|
|
902
|
+
const matches = (await this.readAllRecords()).filter((record) => record.key === key);
|
|
903
|
+
matches.sort(compareByCreatedAtAndId);
|
|
904
|
+
return matches[0];
|
|
905
|
+
}
|
|
906
|
+
async writeRecord(record) {
|
|
907
|
+
return withFilesystemLock(this.writeLockPath(), async () => {
|
|
908
|
+
if (await this.getById(record.id) !== void 0) {
|
|
909
|
+
throw new Error(`registry record ${record.id} already exists`);
|
|
910
|
+
}
|
|
911
|
+
const indexPath = this.indexPath(record.key, record.version);
|
|
912
|
+
if (await pathExists(indexPath)) {
|
|
913
|
+
const indexedRecord = await readJsonFile(indexPath);
|
|
914
|
+
throw new Error(
|
|
915
|
+
`registry record ${record.key}@${record.version} already exists as ${indexedRecord.id}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
const exactMatch = await this.findExactRecord(record.key, record.version);
|
|
919
|
+
if (exactMatch !== void 0) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`registry record ${record.key}@${record.version} already exists as ${exactMatch.id}`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
try {
|
|
925
|
+
await writeJsonFileExclusive(this.recordPath(record.id), record);
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if (isAlreadyExistsError(error)) {
|
|
928
|
+
throw new Error(`registry record ${record.id} already exists`);
|
|
929
|
+
}
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
await writeJsonFileExclusive(indexPath, {
|
|
934
|
+
id: record.id
|
|
935
|
+
});
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (isAlreadyExistsError(error)) {
|
|
938
|
+
throw new Error(`registry record ${record.key}@${record.version} already exists`);
|
|
939
|
+
}
|
|
940
|
+
throw error;
|
|
941
|
+
}
|
|
942
|
+
return record;
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
readAllRecords() {
|
|
946
|
+
return this.readRecordsFromDirectory();
|
|
947
|
+
}
|
|
948
|
+
async readRecordsFromDirectory() {
|
|
949
|
+
const files = await listJsonFiles(this.recordsDirectory);
|
|
950
|
+
const records = await Promise.all(
|
|
951
|
+
files.map((fileName) => readJsonFile(path7.join(this.recordsDirectory, fileName)))
|
|
952
|
+
);
|
|
953
|
+
records.sort(compareByCreatedAtAndId);
|
|
954
|
+
return records;
|
|
955
|
+
}
|
|
956
|
+
async findExactRecord(key, version) {
|
|
957
|
+
const records = await this.readAllRecords();
|
|
958
|
+
return records.find((record) => record.key === key && record.version === version);
|
|
959
|
+
}
|
|
960
|
+
async resolveIndexedRecord(key, version) {
|
|
961
|
+
const indexPath = this.indexPath(key, version);
|
|
962
|
+
if (!await pathExists(indexPath)) {
|
|
963
|
+
const exactMatches = (await this.readAllRecords()).filter(
|
|
964
|
+
(record2) => record2.key === key && record2.version === version
|
|
965
|
+
);
|
|
966
|
+
if (exactMatches.length <= 1) {
|
|
967
|
+
return exactMatches[0];
|
|
968
|
+
}
|
|
969
|
+
throw new Error(
|
|
970
|
+
`registry contains multiple records for ${key}@${version} without an index entry`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const indexedRecord = await readJsonFile(indexPath);
|
|
974
|
+
const record = await this.getById(indexedRecord.id);
|
|
975
|
+
if (record === void 0) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`registry index ${key}@${version} points to missing record ${indexedRecord.id}`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return record;
|
|
981
|
+
}
|
|
982
|
+
recordPath(id) {
|
|
983
|
+
return path7.join(this.recordsDirectory, `${encodePathSegment(id)}.json`);
|
|
984
|
+
}
|
|
985
|
+
indexPath(key, version) {
|
|
986
|
+
return path7.join(
|
|
987
|
+
this.indexesDirectory,
|
|
988
|
+
encodePathSegment(key),
|
|
989
|
+
`${encodePathSegment(version)}.json`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
writeLockPath() {
|
|
993
|
+
return path7.join(path7.dirname(this.recordsDirectory), ".write.lock");
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
var FilesystemDescriptorRegistry = class extends FilesystemRegistryStore {
|
|
997
|
+
constructor(rootPath) {
|
|
998
|
+
super(rootPath, ["registry", "descriptors"]);
|
|
999
|
+
}
|
|
1000
|
+
async write(input) {
|
|
1001
|
+
const id = normalizeNonEmptyString("id", input.id ?? `descriptor:${randomUUID()}`);
|
|
1002
|
+
const key = normalizeNonEmptyString("key", input.key);
|
|
1003
|
+
const version = normalizeNonEmptyString("version", input.version);
|
|
1004
|
+
const createdAt = normalizeTimestamp("createdAt", input.createdAt ?? Date.now());
|
|
1005
|
+
const updatedAt = normalizeTimestamp("updatedAt", input.updatedAt ?? createdAt);
|
|
1006
|
+
if (updatedAt < createdAt) {
|
|
1007
|
+
throw new RangeError("updatedAt must be greater than or equal to createdAt");
|
|
1008
|
+
}
|
|
1009
|
+
const payload = input.payload;
|
|
1010
|
+
const contentHash = sha256Hex(Buffer.from(canonicalJsonString(payload), "utf8"));
|
|
1011
|
+
const provenance = normalizeProvenance2(input.provenance);
|
|
1012
|
+
const record = {
|
|
1013
|
+
id,
|
|
1014
|
+
key,
|
|
1015
|
+
version,
|
|
1016
|
+
createdAt,
|
|
1017
|
+
updatedAt,
|
|
1018
|
+
contentHash,
|
|
1019
|
+
tags: normalizeTags(input.tags),
|
|
1020
|
+
...provenance === void 0 ? {} : { provenance },
|
|
1021
|
+
payload
|
|
1022
|
+
};
|
|
1023
|
+
return this.writeRecord(record);
|
|
1024
|
+
}
|
|
1025
|
+
async list(input = {}) {
|
|
1026
|
+
const key = input.key === void 0 ? void 0 : normalizeNonEmptyString("key", input.key);
|
|
1027
|
+
const records = await this.readAllRecords();
|
|
1028
|
+
return key === void 0 ? records : records.filter((record) => record.key === key);
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
var FilesystemRequestPlanRegistry = class extends FilesystemRegistryStore {
|
|
1032
|
+
constructor(rootPath) {
|
|
1033
|
+
super(rootPath, ["registry", "request-plans"]);
|
|
1034
|
+
}
|
|
1035
|
+
async write(input) {
|
|
1036
|
+
const id = normalizeNonEmptyString("id", input.id ?? `request-plan:${randomUUID()}`);
|
|
1037
|
+
const key = normalizeNonEmptyString("key", input.key);
|
|
1038
|
+
const version = normalizeNonEmptyString("version", input.version);
|
|
1039
|
+
const createdAt = normalizeTimestamp("createdAt", input.createdAt ?? Date.now());
|
|
1040
|
+
const updatedAt = normalizeTimestamp("updatedAt", input.updatedAt ?? createdAt);
|
|
1041
|
+
if (updatedAt < createdAt) {
|
|
1042
|
+
throw new RangeError("updatedAt must be greater than or equal to createdAt");
|
|
1043
|
+
}
|
|
1044
|
+
const payload = input.payload;
|
|
1045
|
+
const contentHash = sha256Hex(Buffer.from(canonicalJsonString(payload), "utf8"));
|
|
1046
|
+
const provenance = normalizeProvenance2(input.provenance);
|
|
1047
|
+
const freshness = normalizeFreshness(input.freshness);
|
|
1048
|
+
const record = {
|
|
1049
|
+
id,
|
|
1050
|
+
key,
|
|
1051
|
+
version,
|
|
1052
|
+
createdAt,
|
|
1053
|
+
updatedAt,
|
|
1054
|
+
contentHash,
|
|
1055
|
+
tags: normalizeTags(input.tags),
|
|
1056
|
+
...provenance === void 0 ? {} : { provenance },
|
|
1057
|
+
payload,
|
|
1058
|
+
...freshness === void 0 ? {} : { freshness }
|
|
1059
|
+
};
|
|
1060
|
+
return this.writeRecord(record);
|
|
1061
|
+
}
|
|
1062
|
+
async list(input = {}) {
|
|
1063
|
+
const key = input.key === void 0 ? void 0 : normalizeNonEmptyString("key", input.key);
|
|
1064
|
+
const records = await this.readAllRecords();
|
|
1065
|
+
return key === void 0 ? records : records.filter((record) => record.key === key);
|
|
1066
|
+
}
|
|
1067
|
+
async updateFreshness(input) {
|
|
1068
|
+
const id = normalizeNonEmptyString("id", input.id);
|
|
1069
|
+
return withFilesystemLock(this.writeLockPath(), async () => {
|
|
1070
|
+
const existing = await this.getById(id);
|
|
1071
|
+
if (existing === void 0) {
|
|
1072
|
+
throw new Error(`registry record ${id} was not found`);
|
|
1073
|
+
}
|
|
1074
|
+
const nextFreshness = normalizeFreshness(input.freshness ?? existing.freshness);
|
|
1075
|
+
const nextUpdatedAt = normalizeTimestamp(
|
|
1076
|
+
"updatedAt",
|
|
1077
|
+
input.updatedAt ?? Math.max(Date.now(), existing.updatedAt)
|
|
1078
|
+
);
|
|
1079
|
+
if (nextUpdatedAt < existing.createdAt) {
|
|
1080
|
+
throw new RangeError("updatedAt must be greater than or equal to createdAt");
|
|
1081
|
+
}
|
|
1082
|
+
const nextRecord = {
|
|
1083
|
+
...existing,
|
|
1084
|
+
updatedAt: nextUpdatedAt,
|
|
1085
|
+
...nextFreshness === void 0 ? {} : { freshness: nextFreshness }
|
|
1086
|
+
};
|
|
1087
|
+
await writeJsonFileAtomic(this.recordPath(id), nextRecord);
|
|
1088
|
+
return nextRecord;
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
var FilesystemInteractionTraceRegistry = class extends FilesystemRegistryStore {
|
|
1093
|
+
constructor(rootPath) {
|
|
1094
|
+
super(rootPath, ["registry", "interaction-traces"]);
|
|
1095
|
+
}
|
|
1096
|
+
async write(input) {
|
|
1097
|
+
const id = normalizeNonEmptyString("id", input.id ?? `interaction-trace:${randomUUID()}`);
|
|
1098
|
+
const key = normalizeNonEmptyString("key", input.key);
|
|
1099
|
+
const version = normalizeNonEmptyString("version", input.version);
|
|
1100
|
+
const createdAt = normalizeTimestamp("createdAt", input.createdAt ?? Date.now());
|
|
1101
|
+
const updatedAt = normalizeTimestamp("updatedAt", input.updatedAt ?? createdAt);
|
|
1102
|
+
if (updatedAt < createdAt) {
|
|
1103
|
+
throw new RangeError("updatedAt must be greater than or equal to createdAt");
|
|
1104
|
+
}
|
|
1105
|
+
const payload = input.payload;
|
|
1106
|
+
const contentHash = sha256Hex(Buffer.from(canonicalJsonString(payload), "utf8"));
|
|
1107
|
+
const provenance = normalizeProvenance2(input.provenance);
|
|
1108
|
+
const record = {
|
|
1109
|
+
id,
|
|
1110
|
+
key,
|
|
1111
|
+
version,
|
|
1112
|
+
createdAt,
|
|
1113
|
+
updatedAt,
|
|
1114
|
+
contentHash,
|
|
1115
|
+
tags: normalizeTags(input.tags),
|
|
1116
|
+
...provenance === void 0 ? {} : { provenance },
|
|
1117
|
+
payload
|
|
1118
|
+
};
|
|
1119
|
+
return this.writeRecord(record);
|
|
1120
|
+
}
|
|
1121
|
+
async list(input = {}) {
|
|
1122
|
+
const key = input.key === void 0 ? void 0 : normalizeNonEmptyString("key", input.key);
|
|
1123
|
+
const records = await this.readAllRecords();
|
|
1124
|
+
return key === void 0 ? records : records.filter((record) => record.key === key);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
function createDescriptorRegistry(rootPath) {
|
|
1128
|
+
return new FilesystemDescriptorRegistry(rootPath);
|
|
1129
|
+
}
|
|
1130
|
+
function createRequestPlanRegistry(rootPath) {
|
|
1131
|
+
return new FilesystemRequestPlanRegistry(rootPath);
|
|
1132
|
+
}
|
|
1133
|
+
function createInteractionTraceRegistry(rootPath) {
|
|
1134
|
+
return new FilesystemInteractionTraceRegistry(rootPath);
|
|
1135
|
+
}
|
|
1136
|
+
var TAG_DELIMITER = "";
|
|
1137
|
+
var NODE_SQLITE_SPECIFIER = `node:${"sqlite"}`;
|
|
1138
|
+
var SAVED_NETWORK_SQLITE_SUPPORT_ERROR = "Saved-network operations require Node's built-in SQLite support. Use a Node runtime with node:sqlite enabled.";
|
|
1139
|
+
var SqliteSavedNetworkStore = class {
|
|
1140
|
+
databasePath;
|
|
1141
|
+
database;
|
|
1142
|
+
directoryInitialization;
|
|
1143
|
+
databaseInitialization;
|
|
1144
|
+
constructor(rootPath) {
|
|
1145
|
+
this.databasePath = path7.join(rootPath, "registry", "saved-network.sqlite");
|
|
1146
|
+
}
|
|
1147
|
+
async initialize() {
|
|
1148
|
+
await this.ensureDatabaseDirectory();
|
|
1149
|
+
}
|
|
1150
|
+
async save(records, options) {
|
|
1151
|
+
const database = await this.requireDatabase();
|
|
1152
|
+
const upsertRecord = database.prepare(buildSavedNetworkUpsertSql(options.bodyWriteMode));
|
|
1153
|
+
const insertTag = database.prepare(`
|
|
1154
|
+
INSERT OR IGNORE INTO saved_network_tags (record_id, tag)
|
|
1155
|
+
VALUES (@record_id, @tag)
|
|
1156
|
+
`);
|
|
1157
|
+
return withSqliteTransaction(database, () => {
|
|
1158
|
+
let savedCount = 0;
|
|
1159
|
+
for (const entry of records) {
|
|
1160
|
+
const url = new URL(entry.record.url);
|
|
1161
|
+
const pageRefKey = entry.record.pageRef ?? "";
|
|
1162
|
+
upsertRecord.run({
|
|
1163
|
+
record_id: entry.recordId,
|
|
1164
|
+
request_id: entry.record.requestId,
|
|
1165
|
+
session_ref: entry.record.sessionRef,
|
|
1166
|
+
page_ref: entry.record.pageRef ?? null,
|
|
1167
|
+
page_ref_key: pageRefKey,
|
|
1168
|
+
frame_ref: entry.record.frameRef ?? null,
|
|
1169
|
+
document_ref: entry.record.documentRef ?? null,
|
|
1170
|
+
capture: entry.capture ?? null,
|
|
1171
|
+
method: entry.record.method,
|
|
1172
|
+
method_lc: entry.record.method.toLowerCase(),
|
|
1173
|
+
url: entry.record.url,
|
|
1174
|
+
url_lc: entry.record.url.toLowerCase(),
|
|
1175
|
+
hostname: url.hostname,
|
|
1176
|
+
hostname_lc: url.hostname.toLowerCase(),
|
|
1177
|
+
path: url.pathname,
|
|
1178
|
+
path_lc: url.pathname.toLowerCase(),
|
|
1179
|
+
status: entry.record.status ?? null,
|
|
1180
|
+
status_text: entry.record.statusText ?? null,
|
|
1181
|
+
resource_type: entry.record.resourceType,
|
|
1182
|
+
navigation_request: entry.record.navigationRequest ? 1 : 0,
|
|
1183
|
+
request_headers_json: JSON.stringify(entry.record.requestHeaders),
|
|
1184
|
+
response_headers_json: JSON.stringify(entry.record.responseHeaders),
|
|
1185
|
+
request_body_json: stringifyOptional(entry.record.requestBody),
|
|
1186
|
+
response_body_json: stringifyOptional(entry.record.responseBody),
|
|
1187
|
+
initiator_json: stringifyOptional(entry.record.initiator),
|
|
1188
|
+
timing_json: stringifyOptional(entry.record.timing),
|
|
1189
|
+
transfer_json: stringifyOptional(entry.record.transfer),
|
|
1190
|
+
source_json: stringifyOptional(entry.record.source),
|
|
1191
|
+
capture_state: entry.record.captureState ?? "complete",
|
|
1192
|
+
request_body_state: entry.record.requestBodyState ?? (entry.record.requestBody === void 0 ? "skipped" : "complete"),
|
|
1193
|
+
response_body_state: entry.record.responseBodyState ?? (entry.record.responseBody === void 0 ? "skipped" : "complete"),
|
|
1194
|
+
request_body_skip_reason: entry.record.requestBodySkipReason ?? null,
|
|
1195
|
+
response_body_skip_reason: entry.record.responseBodySkipReason ?? null,
|
|
1196
|
+
request_body_error: entry.record.requestBodyError ?? null,
|
|
1197
|
+
response_body_error: entry.record.responseBodyError ?? null,
|
|
1198
|
+
redirect_from_request_id: entry.record.redirectFromRequestId ?? null,
|
|
1199
|
+
redirect_to_request_id: entry.record.redirectToRequestId ?? null,
|
|
1200
|
+
saved_at: entry.savedAt ?? Date.now()
|
|
1201
|
+
});
|
|
1202
|
+
const tags = new Set(entry.tags ?? []);
|
|
1203
|
+
if (options.tag !== void 0) {
|
|
1204
|
+
tags.add(options.tag);
|
|
1205
|
+
}
|
|
1206
|
+
for (const currentTag of tags) {
|
|
1207
|
+
const result = insertTag.run({
|
|
1208
|
+
record_id: entry.recordId,
|
|
1209
|
+
tag: currentTag
|
|
1210
|
+
});
|
|
1211
|
+
savedCount += result.changes ?? 0;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return savedCount;
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
async tagByFilter(filter, tag) {
|
|
1218
|
+
const database = await this.requireDatabase();
|
|
1219
|
+
const { whereSql, parameters } = buildSavedNetworkWhere(filter);
|
|
1220
|
+
const selectRecords = database.prepare(
|
|
1221
|
+
`
|
|
1222
|
+
SELECT r.record_id
|
|
1223
|
+
FROM saved_network_records r
|
|
1224
|
+
${whereSql}
|
|
1225
|
+
`
|
|
1226
|
+
);
|
|
1227
|
+
const insertTag = database.prepare(`
|
|
1228
|
+
INSERT OR IGNORE INTO saved_network_tags (record_id, tag)
|
|
1229
|
+
VALUES (@record_id, @tag)
|
|
1230
|
+
`);
|
|
1231
|
+
return withSqliteTransaction(database, () => {
|
|
1232
|
+
let taggedCount = 0;
|
|
1233
|
+
const rows = selectRecords.all(
|
|
1234
|
+
...parameters
|
|
1235
|
+
);
|
|
1236
|
+
for (const row of rows) {
|
|
1237
|
+
const recordId = row.record_id;
|
|
1238
|
+
if (typeof recordId !== "string") {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
const result = insertTag.run({
|
|
1242
|
+
record_id: recordId,
|
|
1243
|
+
tag
|
|
1244
|
+
});
|
|
1245
|
+
taggedCount += result.changes ?? 0;
|
|
1246
|
+
}
|
|
1247
|
+
return taggedCount;
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
async query(input = {}) {
|
|
1251
|
+
const database = await this.requireDatabase();
|
|
1252
|
+
const limit = Math.max(1, Math.min(input.limit ?? 50, 1e3));
|
|
1253
|
+
const { whereSql, parameters } = buildSavedNetworkWhere(input);
|
|
1254
|
+
const rows = database.prepare(
|
|
1255
|
+
`
|
|
1256
|
+
SELECT
|
|
1257
|
+
r.*,
|
|
1258
|
+
GROUP_CONCAT(t.tag, '${TAG_DELIMITER}') AS tags
|
|
1259
|
+
FROM saved_network_records r
|
|
1260
|
+
LEFT JOIN saved_network_tags t
|
|
1261
|
+
ON t.record_id = r.record_id
|
|
1262
|
+
${whereSql}
|
|
1263
|
+
GROUP BY r.record_id
|
|
1264
|
+
ORDER BY r.saved_at DESC, r.record_id ASC
|
|
1265
|
+
LIMIT ?
|
|
1266
|
+
`
|
|
1267
|
+
).all(
|
|
1268
|
+
...parameters,
|
|
1269
|
+
limit
|
|
1270
|
+
);
|
|
1271
|
+
return rows.map((row) => inflateSavedNetworkRow(row, input.includeBodies ?? false));
|
|
1272
|
+
}
|
|
1273
|
+
async getByRecordId(recordId, options = {}) {
|
|
1274
|
+
const [record] = await this.query({
|
|
1275
|
+
recordId,
|
|
1276
|
+
...options.includeBodies === void 0 ? {} : { includeBodies: options.includeBodies },
|
|
1277
|
+
limit: 1
|
|
1278
|
+
});
|
|
1279
|
+
return record;
|
|
1280
|
+
}
|
|
1281
|
+
async clear(input = {}) {
|
|
1282
|
+
const database = await this.requireDatabase();
|
|
1283
|
+
const countAll = database.prepare(`SELECT COUNT(*) AS cleared FROM saved_network_records`);
|
|
1284
|
+
const deleteAllRecords = database.prepare(`DELETE FROM saved_network_records`);
|
|
1285
|
+
const { whereSql, parameters } = buildSavedNetworkWhere(input);
|
|
1286
|
+
const countFiltered = database.prepare(`
|
|
1287
|
+
SELECT COUNT(*) AS cleared
|
|
1288
|
+
FROM saved_network_records r
|
|
1289
|
+
${whereSql}
|
|
1290
|
+
`);
|
|
1291
|
+
const deleteFiltered = database.prepare(`
|
|
1292
|
+
DELETE FROM saved_network_records
|
|
1293
|
+
WHERE record_id IN (
|
|
1294
|
+
SELECT r.record_id
|
|
1295
|
+
FROM saved_network_records r
|
|
1296
|
+
${whereSql}
|
|
1297
|
+
)
|
|
1298
|
+
`);
|
|
1299
|
+
return withSqliteTransaction(database, () => {
|
|
1300
|
+
if (input.capture === void 0 && input.tag === void 0) {
|
|
1301
|
+
const cleared2 = countAll.get().cleared;
|
|
1302
|
+
deleteAllRecords.run();
|
|
1303
|
+
return cleared2;
|
|
1304
|
+
}
|
|
1305
|
+
const args = parameters;
|
|
1306
|
+
const cleared = countFiltered.get(...args).cleared;
|
|
1307
|
+
deleteFiltered.run(...args);
|
|
1308
|
+
return cleared;
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
async *iterateBatches(options = {}) {
|
|
1312
|
+
const database = await this.requireDatabase();
|
|
1313
|
+
const batchSize = Math.max(1, Math.min(options.batchSize ?? 500, 1e3));
|
|
1314
|
+
let cursor;
|
|
1315
|
+
while (true) {
|
|
1316
|
+
const rows = database.prepare(
|
|
1317
|
+
`
|
|
1318
|
+
SELECT
|
|
1319
|
+
r.*,
|
|
1320
|
+
GROUP_CONCAT(t.tag, '${TAG_DELIMITER}') AS tags
|
|
1321
|
+
FROM saved_network_records r
|
|
1322
|
+
LEFT JOIN saved_network_tags t
|
|
1323
|
+
ON t.record_id = r.record_id
|
|
1324
|
+
${cursor === void 0 ? "" : "WHERE r.saved_at > ? OR (r.saved_at = ? AND r.record_id > ?)"}
|
|
1325
|
+
GROUP BY r.record_id
|
|
1326
|
+
ORDER BY r.saved_at ASC, r.record_id ASC
|
|
1327
|
+
LIMIT ?
|
|
1328
|
+
`
|
|
1329
|
+
).all(
|
|
1330
|
+
...cursor === void 0 ? [] : [cursor.savedAt, cursor.savedAt, cursor.recordId],
|
|
1331
|
+
batchSize
|
|
1332
|
+
);
|
|
1333
|
+
if (rows.length === 0) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
yield rows.map((row) => inflateSavedNetworkRow(row, options.includeBodies ?? true));
|
|
1337
|
+
const lastRow = rows.at(-1);
|
|
1338
|
+
if (lastRow === void 0) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
cursor = {
|
|
1342
|
+
savedAt: lastRow.saved_at,
|
|
1343
|
+
recordId: lastRow.record_id
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
close() {
|
|
1348
|
+
if (this.database !== void 0) {
|
|
1349
|
+
closeSqliteDatabase(this.database);
|
|
1350
|
+
this.database = void 0;
|
|
1351
|
+
this.databaseInitialization = void 0;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async requireDatabase() {
|
|
1355
|
+
if (this.database) {
|
|
1356
|
+
return this.database;
|
|
1357
|
+
}
|
|
1358
|
+
this.databaseInitialization ??= this.openDatabase();
|
|
1359
|
+
try {
|
|
1360
|
+
return await this.databaseInitialization;
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
this.databaseInitialization = void 0;
|
|
1363
|
+
throw error;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async openDatabase() {
|
|
1367
|
+
await this.ensureDatabaseDirectory();
|
|
1368
|
+
let DatabaseSync;
|
|
1369
|
+
try {
|
|
1370
|
+
({ DatabaseSync } = await import(NODE_SQLITE_SPECIFIER));
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
throw normalizeSqliteImportError(error);
|
|
1373
|
+
}
|
|
1374
|
+
const database = new DatabaseSync(this.databasePath);
|
|
1375
|
+
try {
|
|
1376
|
+
this.configureDatabase(database);
|
|
1377
|
+
this.database = database;
|
|
1378
|
+
return database;
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
closeSqliteDatabase(database);
|
|
1381
|
+
throw error;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
async ensureDatabaseDirectory() {
|
|
1385
|
+
this.directoryInitialization ??= ensureDirectory(path7.dirname(this.databasePath)).catch(
|
|
1386
|
+
(error) => {
|
|
1387
|
+
this.directoryInitialization = void 0;
|
|
1388
|
+
throw error;
|
|
1389
|
+
}
|
|
1390
|
+
);
|
|
1391
|
+
await this.directoryInitialization;
|
|
1392
|
+
}
|
|
1393
|
+
configureDatabase(database) {
|
|
1394
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
1395
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
1396
|
+
database.exec(`
|
|
1397
|
+
CREATE TABLE IF NOT EXISTS saved_network_records (
|
|
1398
|
+
record_id TEXT PRIMARY KEY,
|
|
1399
|
+
request_id TEXT NOT NULL,
|
|
1400
|
+
session_ref TEXT NOT NULL,
|
|
1401
|
+
page_ref TEXT,
|
|
1402
|
+
page_ref_key TEXT NOT NULL,
|
|
1403
|
+
frame_ref TEXT,
|
|
1404
|
+
document_ref TEXT,
|
|
1405
|
+
capture TEXT,
|
|
1406
|
+
method TEXT NOT NULL,
|
|
1407
|
+
method_lc TEXT NOT NULL,
|
|
1408
|
+
url TEXT NOT NULL,
|
|
1409
|
+
url_lc TEXT NOT NULL,
|
|
1410
|
+
hostname TEXT NOT NULL,
|
|
1411
|
+
hostname_lc TEXT NOT NULL,
|
|
1412
|
+
path TEXT NOT NULL,
|
|
1413
|
+
path_lc TEXT NOT NULL,
|
|
1414
|
+
status INTEGER,
|
|
1415
|
+
status_text TEXT,
|
|
1416
|
+
resource_type TEXT NOT NULL,
|
|
1417
|
+
navigation_request INTEGER NOT NULL,
|
|
1418
|
+
request_headers_json TEXT NOT NULL,
|
|
1419
|
+
response_headers_json TEXT NOT NULL,
|
|
1420
|
+
request_body_json TEXT,
|
|
1421
|
+
response_body_json TEXT,
|
|
1422
|
+
initiator_json TEXT,
|
|
1423
|
+
timing_json TEXT,
|
|
1424
|
+
transfer_json TEXT,
|
|
1425
|
+
source_json TEXT,
|
|
1426
|
+
capture_state TEXT NOT NULL,
|
|
1427
|
+
request_body_state TEXT NOT NULL,
|
|
1428
|
+
response_body_state TEXT NOT NULL,
|
|
1429
|
+
request_body_skip_reason TEXT,
|
|
1430
|
+
response_body_skip_reason TEXT,
|
|
1431
|
+
request_body_error TEXT,
|
|
1432
|
+
response_body_error TEXT,
|
|
1433
|
+
redirect_from_request_id TEXT,
|
|
1434
|
+
redirect_to_request_id TEXT,
|
|
1435
|
+
saved_at INTEGER NOT NULL
|
|
1436
|
+
);
|
|
1437
|
+
|
|
1438
|
+
CREATE TABLE IF NOT EXISTS saved_network_tags (
|
|
1439
|
+
record_id TEXT NOT NULL REFERENCES saved_network_records(record_id) ON DELETE CASCADE,
|
|
1440
|
+
tag TEXT NOT NULL,
|
|
1441
|
+
PRIMARY KEY (record_id, tag)
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
CREATE INDEX IF NOT EXISTS saved_network_tags_tag
|
|
1445
|
+
ON saved_network_tags (tag);
|
|
1446
|
+
`);
|
|
1447
|
+
database.exec(`DROP INDEX IF EXISTS saved_network_records_scope_request`);
|
|
1448
|
+
database.exec(`
|
|
1449
|
+
CREATE INDEX IF NOT EXISTS saved_network_records_scope_request
|
|
1450
|
+
ON saved_network_records (session_ref, page_ref_key, request_id)
|
|
1451
|
+
`);
|
|
1452
|
+
database.exec(`
|
|
1453
|
+
CREATE INDEX IF NOT EXISTS saved_network_records_saved_at
|
|
1454
|
+
ON saved_network_records (saved_at DESC)
|
|
1455
|
+
`);
|
|
1456
|
+
database.exec(`
|
|
1457
|
+
CREATE INDEX IF NOT EXISTS saved_network_records_capture
|
|
1458
|
+
ON saved_network_records (capture)
|
|
1459
|
+
`);
|
|
1460
|
+
this.ensureColumn(
|
|
1461
|
+
database,
|
|
1462
|
+
"saved_network_records",
|
|
1463
|
+
"capture_state",
|
|
1464
|
+
"TEXT NOT NULL DEFAULT 'complete'"
|
|
1465
|
+
);
|
|
1466
|
+
this.ensureColumn(database, "saved_network_records", "capture", "TEXT");
|
|
1467
|
+
this.ensureColumn(
|
|
1468
|
+
database,
|
|
1469
|
+
"saved_network_records",
|
|
1470
|
+
"request_body_state",
|
|
1471
|
+
"TEXT NOT NULL DEFAULT 'skipped'"
|
|
1472
|
+
);
|
|
1473
|
+
this.ensureColumn(
|
|
1474
|
+
database,
|
|
1475
|
+
"saved_network_records",
|
|
1476
|
+
"response_body_state",
|
|
1477
|
+
"TEXT NOT NULL DEFAULT 'skipped'"
|
|
1478
|
+
);
|
|
1479
|
+
this.ensureColumn(database, "saved_network_records", "request_body_skip_reason", "TEXT");
|
|
1480
|
+
this.ensureColumn(database, "saved_network_records", "response_body_skip_reason", "TEXT");
|
|
1481
|
+
this.ensureColumn(database, "saved_network_records", "request_body_error", "TEXT");
|
|
1482
|
+
this.ensureColumn(database, "saved_network_records", "response_body_error", "TEXT");
|
|
1483
|
+
}
|
|
1484
|
+
ensureColumn(database, table, column, definition) {
|
|
1485
|
+
const rows = database.prepare(`PRAGMA table_info(${table})`).all();
|
|
1486
|
+
if (rows.some((row) => row.name === column)) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
database.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
function buildSavedNetworkWhere(input) {
|
|
1493
|
+
const clauses = [];
|
|
1494
|
+
const parameters = [];
|
|
1495
|
+
if (input.pageRef !== void 0) {
|
|
1496
|
+
clauses.push("r.page_ref_key = ?");
|
|
1497
|
+
parameters.push(input.pageRef);
|
|
1498
|
+
}
|
|
1499
|
+
if (input.recordId !== void 0) {
|
|
1500
|
+
clauses.push("r.record_id = ?");
|
|
1501
|
+
parameters.push(input.recordId);
|
|
1502
|
+
}
|
|
1503
|
+
if (input.requestId !== void 0) {
|
|
1504
|
+
clauses.push("r.request_id = ?");
|
|
1505
|
+
parameters.push(input.requestId);
|
|
1506
|
+
}
|
|
1507
|
+
if (input.capture !== void 0) {
|
|
1508
|
+
clauses.push("r.capture = ?");
|
|
1509
|
+
parameters.push(input.capture);
|
|
1510
|
+
}
|
|
1511
|
+
if (input.tag !== void 0) {
|
|
1512
|
+
clauses.push(`
|
|
1513
|
+
EXISTS (
|
|
1514
|
+
SELECT 1
|
|
1515
|
+
FROM saved_network_tags exact_tag
|
|
1516
|
+
WHERE exact_tag.record_id = r.record_id
|
|
1517
|
+
AND exact_tag.tag = ?
|
|
1518
|
+
)
|
|
1519
|
+
`);
|
|
1520
|
+
parameters.push(input.tag);
|
|
1521
|
+
}
|
|
1522
|
+
if (input.url !== void 0) {
|
|
1523
|
+
clauses.push("instr(r.url_lc, ?) > 0");
|
|
1524
|
+
parameters.push(input.url.toLowerCase());
|
|
1525
|
+
}
|
|
1526
|
+
if (input.hostname !== void 0) {
|
|
1527
|
+
clauses.push("instr(r.hostname_lc, ?) > 0");
|
|
1528
|
+
parameters.push(input.hostname.toLowerCase());
|
|
1529
|
+
}
|
|
1530
|
+
if (input.path !== void 0) {
|
|
1531
|
+
clauses.push("instr(r.path_lc, ?) > 0");
|
|
1532
|
+
parameters.push(input.path.toLowerCase());
|
|
1533
|
+
}
|
|
1534
|
+
if (input.method !== void 0) {
|
|
1535
|
+
clauses.push("instr(r.method_lc, ?) > 0");
|
|
1536
|
+
parameters.push(input.method.toLowerCase());
|
|
1537
|
+
}
|
|
1538
|
+
if (input.status !== void 0) {
|
|
1539
|
+
clauses.push("instr(lower(COALESCE(CAST(r.status AS TEXT), '')), ?) > 0");
|
|
1540
|
+
parameters.push(String(input.status).toLowerCase());
|
|
1541
|
+
}
|
|
1542
|
+
if (input.resourceType !== void 0) {
|
|
1543
|
+
clauses.push("r.resource_type = ?");
|
|
1544
|
+
parameters.push(input.resourceType);
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
whereSql: clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`,
|
|
1548
|
+
parameters
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function buildSavedNetworkUpsertSql(bodyWriteMode) {
|
|
1552
|
+
const bodyUpdateSql = bodyWriteMode === "authoritative" ? `
|
|
1553
|
+
request_body_json = excluded.request_body_json,
|
|
1554
|
+
response_body_json = excluded.response_body_json,
|
|
1555
|
+
request_body_state = excluded.request_body_state,
|
|
1556
|
+
response_body_state = excluded.response_body_state,
|
|
1557
|
+
request_body_skip_reason = excluded.request_body_skip_reason,
|
|
1558
|
+
response_body_skip_reason = excluded.response_body_skip_reason,
|
|
1559
|
+
request_body_error = excluded.request_body_error,
|
|
1560
|
+
response_body_error = excluded.response_body_error,
|
|
1561
|
+
` : "";
|
|
1562
|
+
return `
|
|
1563
|
+
INSERT INTO saved_network_records (
|
|
1564
|
+
record_id,
|
|
1565
|
+
request_id,
|
|
1566
|
+
session_ref,
|
|
1567
|
+
page_ref,
|
|
1568
|
+
page_ref_key,
|
|
1569
|
+
frame_ref,
|
|
1570
|
+
document_ref,
|
|
1571
|
+
capture,
|
|
1572
|
+
method,
|
|
1573
|
+
method_lc,
|
|
1574
|
+
url,
|
|
1575
|
+
url_lc,
|
|
1576
|
+
hostname,
|
|
1577
|
+
hostname_lc,
|
|
1578
|
+
path,
|
|
1579
|
+
path_lc,
|
|
1580
|
+
status,
|
|
1581
|
+
status_text,
|
|
1582
|
+
resource_type,
|
|
1583
|
+
navigation_request,
|
|
1584
|
+
request_headers_json,
|
|
1585
|
+
response_headers_json,
|
|
1586
|
+
request_body_json,
|
|
1587
|
+
response_body_json,
|
|
1588
|
+
initiator_json,
|
|
1589
|
+
timing_json,
|
|
1590
|
+
transfer_json,
|
|
1591
|
+
source_json,
|
|
1592
|
+
capture_state,
|
|
1593
|
+
request_body_state,
|
|
1594
|
+
response_body_state,
|
|
1595
|
+
request_body_skip_reason,
|
|
1596
|
+
response_body_skip_reason,
|
|
1597
|
+
request_body_error,
|
|
1598
|
+
response_body_error,
|
|
1599
|
+
redirect_from_request_id,
|
|
1600
|
+
redirect_to_request_id,
|
|
1601
|
+
saved_at
|
|
1602
|
+
) VALUES (
|
|
1603
|
+
@record_id,
|
|
1604
|
+
@request_id,
|
|
1605
|
+
@session_ref,
|
|
1606
|
+
@page_ref,
|
|
1607
|
+
@page_ref_key,
|
|
1608
|
+
@frame_ref,
|
|
1609
|
+
@document_ref,
|
|
1610
|
+
@capture,
|
|
1611
|
+
@method,
|
|
1612
|
+
@method_lc,
|
|
1613
|
+
@url,
|
|
1614
|
+
@url_lc,
|
|
1615
|
+
@hostname,
|
|
1616
|
+
@hostname_lc,
|
|
1617
|
+
@path,
|
|
1618
|
+
@path_lc,
|
|
1619
|
+
@status,
|
|
1620
|
+
@status_text,
|
|
1621
|
+
@resource_type,
|
|
1622
|
+
@navigation_request,
|
|
1623
|
+
@request_headers_json,
|
|
1624
|
+
@response_headers_json,
|
|
1625
|
+
@request_body_json,
|
|
1626
|
+
@response_body_json,
|
|
1627
|
+
@initiator_json,
|
|
1628
|
+
@timing_json,
|
|
1629
|
+
@transfer_json,
|
|
1630
|
+
@source_json,
|
|
1631
|
+
@capture_state,
|
|
1632
|
+
@request_body_state,
|
|
1633
|
+
@response_body_state,
|
|
1634
|
+
@request_body_skip_reason,
|
|
1635
|
+
@response_body_skip_reason,
|
|
1636
|
+
@request_body_error,
|
|
1637
|
+
@response_body_error,
|
|
1638
|
+
@redirect_from_request_id,
|
|
1639
|
+
@redirect_to_request_id,
|
|
1640
|
+
@saved_at
|
|
1641
|
+
)
|
|
1642
|
+
ON CONFLICT(record_id) DO UPDATE SET
|
|
1643
|
+
page_ref = excluded.page_ref,
|
|
1644
|
+
page_ref_key = excluded.page_ref_key,
|
|
1645
|
+
frame_ref = excluded.frame_ref,
|
|
1646
|
+
document_ref = excluded.document_ref,
|
|
1647
|
+
capture = excluded.capture,
|
|
1648
|
+
method = excluded.method,
|
|
1649
|
+
method_lc = excluded.method_lc,
|
|
1650
|
+
url = excluded.url,
|
|
1651
|
+
url_lc = excluded.url_lc,
|
|
1652
|
+
hostname = excluded.hostname,
|
|
1653
|
+
hostname_lc = excluded.hostname_lc,
|
|
1654
|
+
path = excluded.path,
|
|
1655
|
+
path_lc = excluded.path_lc,
|
|
1656
|
+
status = excluded.status,
|
|
1657
|
+
status_text = excluded.status_text,
|
|
1658
|
+
resource_type = excluded.resource_type,
|
|
1659
|
+
navigation_request = excluded.navigation_request,
|
|
1660
|
+
request_headers_json = excluded.request_headers_json,
|
|
1661
|
+
response_headers_json = excluded.response_headers_json,
|
|
1662
|
+
${bodyUpdateSql} initiator_json = excluded.initiator_json,
|
|
1663
|
+
timing_json = excluded.timing_json,
|
|
1664
|
+
transfer_json = excluded.transfer_json,
|
|
1665
|
+
source_json = excluded.source_json,
|
|
1666
|
+
capture_state = excluded.capture_state,
|
|
1667
|
+
redirect_from_request_id = excluded.redirect_from_request_id,
|
|
1668
|
+
redirect_to_request_id = excluded.redirect_to_request_id,
|
|
1669
|
+
saved_at = MIN(saved_network_records.saved_at, excluded.saved_at)
|
|
1670
|
+
`;
|
|
1671
|
+
}
|
|
1672
|
+
function inflateSavedNetworkRow(row, includeBodies) {
|
|
1673
|
+
const requestBody = includeBodies && row.request_body_json !== null ? JSON.parse(row.request_body_json) : void 0;
|
|
1674
|
+
const responseBody = includeBodies && row.response_body_json !== null ? JSON.parse(row.response_body_json) : void 0;
|
|
1675
|
+
const record = {
|
|
1676
|
+
kind: "http",
|
|
1677
|
+
requestId: row.request_id,
|
|
1678
|
+
sessionRef: row.session_ref,
|
|
1679
|
+
method: row.method,
|
|
1680
|
+
url: row.url,
|
|
1681
|
+
requestHeaders: JSON.parse(row.request_headers_json),
|
|
1682
|
+
responseHeaders: JSON.parse(row.response_headers_json),
|
|
1683
|
+
resourceType: row.resource_type,
|
|
1684
|
+
navigationRequest: row.navigation_request === 1,
|
|
1685
|
+
captureState: row.capture_state,
|
|
1686
|
+
requestBodyState: row.request_body_state,
|
|
1687
|
+
responseBodyState: row.response_body_state
|
|
1688
|
+
};
|
|
1689
|
+
if (row.page_ref !== null) {
|
|
1690
|
+
record.pageRef = row.page_ref;
|
|
1691
|
+
}
|
|
1692
|
+
if (row.frame_ref !== null) {
|
|
1693
|
+
record.frameRef = row.frame_ref;
|
|
1694
|
+
}
|
|
1695
|
+
if (row.document_ref !== null) {
|
|
1696
|
+
record.documentRef = row.document_ref;
|
|
1697
|
+
}
|
|
1698
|
+
if (row.status !== null) {
|
|
1699
|
+
record.status = row.status;
|
|
1700
|
+
}
|
|
1701
|
+
if (row.status_text !== null) {
|
|
1702
|
+
record.statusText = row.status_text;
|
|
1703
|
+
}
|
|
1704
|
+
if (row.redirect_from_request_id !== null) {
|
|
1705
|
+
record.redirectFromRequestId = row.redirect_from_request_id;
|
|
1706
|
+
}
|
|
1707
|
+
if (row.redirect_to_request_id !== null) {
|
|
1708
|
+
record.redirectToRequestId = row.redirect_to_request_id;
|
|
1709
|
+
}
|
|
1710
|
+
if (row.initiator_json !== null) {
|
|
1711
|
+
record.initiator = JSON.parse(row.initiator_json);
|
|
1712
|
+
}
|
|
1713
|
+
if (row.timing_json !== null) {
|
|
1714
|
+
record.timing = JSON.parse(row.timing_json);
|
|
1715
|
+
}
|
|
1716
|
+
if (row.transfer_json !== null) {
|
|
1717
|
+
record.transfer = JSON.parse(row.transfer_json);
|
|
1718
|
+
}
|
|
1719
|
+
if (row.source_json !== null) {
|
|
1720
|
+
record.source = JSON.parse(row.source_json);
|
|
1721
|
+
}
|
|
1722
|
+
if (row.request_body_skip_reason !== null) {
|
|
1723
|
+
record.requestBodySkipReason = row.request_body_skip_reason;
|
|
1724
|
+
}
|
|
1725
|
+
if (row.response_body_skip_reason !== null) {
|
|
1726
|
+
record.responseBodySkipReason = row.response_body_skip_reason;
|
|
1727
|
+
}
|
|
1728
|
+
if (row.request_body_error !== null) {
|
|
1729
|
+
record.requestBodyError = row.request_body_error;
|
|
1730
|
+
}
|
|
1731
|
+
if (row.response_body_error !== null) {
|
|
1732
|
+
record.responseBodyError = row.response_body_error;
|
|
1733
|
+
}
|
|
1734
|
+
if (requestBody !== void 0) {
|
|
1735
|
+
record.requestBody = requestBody;
|
|
1736
|
+
}
|
|
1737
|
+
if (responseBody !== void 0) {
|
|
1738
|
+
record.responseBody = responseBody;
|
|
1739
|
+
}
|
|
1740
|
+
return {
|
|
1741
|
+
recordId: row.record_id,
|
|
1742
|
+
...row.capture === null ? {} : { capture: row.capture },
|
|
1743
|
+
...row.tags === null || row.tags.length === 0 ? {} : { tags: row.tags.split(TAG_DELIMITER).filter((tag) => tag.length > 0) },
|
|
1744
|
+
savedAt: row.saved_at,
|
|
1745
|
+
record
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function stringifyOptional(value) {
|
|
1749
|
+
return value === void 0 ? null : JSON.stringify(value);
|
|
1750
|
+
}
|
|
1751
|
+
function normalizeSqliteImportError(error) {
|
|
1752
|
+
if (error instanceof Error && error.code === "ERR_UNKNOWN_BUILTIN_MODULE" && error.message.includes(NODE_SQLITE_SPECIFIER)) {
|
|
1753
|
+
return new Error(SAVED_NETWORK_SQLITE_SUPPORT_ERROR, {
|
|
1754
|
+
cause: error
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1758
|
+
}
|
|
1759
|
+
function closeSqliteDatabase(database) {
|
|
1760
|
+
try {
|
|
1761
|
+
database.close();
|
|
1762
|
+
} catch {
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function withSqliteTransaction(database, task) {
|
|
1766
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1767
|
+
try {
|
|
1768
|
+
const result = task();
|
|
1769
|
+
database.exec("COMMIT");
|
|
1770
|
+
return result;
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
database.exec("ROLLBACK");
|
|
1773
|
+
throw error;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
function createSavedNetworkStore(rootPath) {
|
|
1777
|
+
return new SqliteSavedNetworkStore(rootPath);
|
|
1778
|
+
}
|
|
1779
|
+
function normalizeContext(context) {
|
|
1780
|
+
return {
|
|
1781
|
+
...context?.sessionRef === void 0 ? {} : { sessionRef: context.sessionRef },
|
|
1782
|
+
...context?.pageRef === void 0 ? {} : { pageRef: context.pageRef },
|
|
1783
|
+
...context?.frameRef === void 0 ? {} : { frameRef: context.frameRef },
|
|
1784
|
+
...context?.documentRef === void 0 ? {} : { documentRef: context.documentRef },
|
|
1785
|
+
...context?.documentEpoch === void 0 ? {} : { documentEpoch: context.documentEpoch }
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function sequenceFileName(sequence) {
|
|
1789
|
+
return `${String(sequence).padStart(12, "0")}.json`;
|
|
1790
|
+
}
|
|
1791
|
+
var FilesystemTraceStore = class {
|
|
1792
|
+
constructor(rootPath, artifacts) {
|
|
1793
|
+
this.rootPath = rootPath;
|
|
1794
|
+
this.artifacts = artifacts;
|
|
1795
|
+
this.runsDirectory = path7.join(this.rootPath, "traces", "runs");
|
|
1796
|
+
}
|
|
1797
|
+
runsDirectory;
|
|
1798
|
+
async initialize() {
|
|
1799
|
+
await ensureDirectory(this.runsDirectory);
|
|
1800
|
+
}
|
|
1801
|
+
async createRun(input = {}) {
|
|
1802
|
+
const runId = normalizeNonEmptyString("runId", input.runId ?? `run:${randomUUID()}`);
|
|
1803
|
+
const manifestPath = this.runManifestPath(runId);
|
|
1804
|
+
const createdAt = normalizeTimestamp("createdAt", input.createdAt ?? Date.now());
|
|
1805
|
+
const manifest = {
|
|
1806
|
+
runId,
|
|
1807
|
+
createdAt,
|
|
1808
|
+
updatedAt: createdAt,
|
|
1809
|
+
entryCount: 0
|
|
1810
|
+
};
|
|
1811
|
+
await ensureDirectory(this.runEntriesDirectory(runId));
|
|
1812
|
+
try {
|
|
1813
|
+
await writeJsonFileExclusive(manifestPath, manifest);
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
if (isAlreadyExistsError(error)) {
|
|
1816
|
+
throw new Error(`trace run ${runId} already exists`);
|
|
1817
|
+
}
|
|
1818
|
+
throw error;
|
|
1819
|
+
}
|
|
1820
|
+
return manifest;
|
|
1821
|
+
}
|
|
1822
|
+
async getRun(runId) {
|
|
1823
|
+
const manifestPath = this.runManifestPath(runId);
|
|
1824
|
+
if (!await pathExists(manifestPath)) {
|
|
1825
|
+
return void 0;
|
|
1826
|
+
}
|
|
1827
|
+
return readJsonFile(manifestPath);
|
|
1828
|
+
}
|
|
1829
|
+
async append(runId, input) {
|
|
1830
|
+
const startedAt = normalizeTimestamp("startedAt", input.startedAt);
|
|
1831
|
+
const completedAt = normalizeTimestamp("completedAt", input.completedAt);
|
|
1832
|
+
if (completedAt < startedAt) {
|
|
1833
|
+
throw new RangeError("completedAt must be greater than or equal to startedAt");
|
|
1834
|
+
}
|
|
1835
|
+
if (input.outcome === "error" && input.error === void 0) {
|
|
1836
|
+
throw new TypeError("error traces must include an error payload");
|
|
1837
|
+
}
|
|
1838
|
+
if (input.outcome === "ok" && input.error !== void 0) {
|
|
1839
|
+
throw new TypeError("successful traces must not include an error payload");
|
|
1840
|
+
}
|
|
1841
|
+
return withFilesystemLock(this.runWriteLockPath(runId), async () => {
|
|
1842
|
+
const manifest = await this.getRun(runId);
|
|
1843
|
+
if (manifest === void 0) {
|
|
1844
|
+
throw new Error(`trace run ${runId} was not found`);
|
|
1845
|
+
}
|
|
1846
|
+
const sequence = manifest.entryCount + 1;
|
|
1847
|
+
const traceId = normalizeNonEmptyString(
|
|
1848
|
+
"traceId",
|
|
1849
|
+
input.traceId ?? `trace:${runId}:${String(sequence).padStart(12, "0")}`
|
|
1850
|
+
);
|
|
1851
|
+
const stepId = normalizeNonEmptyString(
|
|
1852
|
+
"stepId",
|
|
1853
|
+
input.stepId ?? `step:${runId}:${String(sequence).padStart(12, "0")}`
|
|
1854
|
+
);
|
|
1855
|
+
const entry = {
|
|
1856
|
+
runId,
|
|
1857
|
+
sequence,
|
|
1858
|
+
traceId,
|
|
1859
|
+
stepId,
|
|
1860
|
+
operation: normalizeNonEmptyString("operation", input.operation),
|
|
1861
|
+
outcome: input.outcome,
|
|
1862
|
+
startedAt,
|
|
1863
|
+
completedAt,
|
|
1864
|
+
durationMs: completedAt - startedAt,
|
|
1865
|
+
context: normalizeContext(input.context),
|
|
1866
|
+
events: [...input.events ?? []],
|
|
1867
|
+
...input.artifacts === void 0 || input.artifacts.length === 0 ? {} : { artifacts: [...input.artifacts] },
|
|
1868
|
+
...input.data === void 0 ? {} : { data: input.data },
|
|
1869
|
+
...input.error === void 0 ? {} : { error: input.error }
|
|
1870
|
+
};
|
|
1871
|
+
await writeJsonFileExclusive(
|
|
1872
|
+
path7.join(this.runEntriesDirectory(runId), sequenceFileName(sequence)),
|
|
1873
|
+
entry
|
|
1874
|
+
);
|
|
1875
|
+
await writeJsonFileAtomic(this.runManifestPath(runId), {
|
|
1876
|
+
...manifest,
|
|
1877
|
+
updatedAt: Math.max(manifest.updatedAt, completedAt),
|
|
1878
|
+
entryCount: sequence
|
|
1879
|
+
});
|
|
1880
|
+
return entry;
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
async listEntries(runId) {
|
|
1884
|
+
const entriesDirectory = this.runEntriesDirectory(runId);
|
|
1885
|
+
if (!await pathExists(entriesDirectory)) {
|
|
1886
|
+
return [];
|
|
1887
|
+
}
|
|
1888
|
+
const files = await listJsonFiles(entriesDirectory);
|
|
1889
|
+
return Promise.all(
|
|
1890
|
+
files.map(
|
|
1891
|
+
(fileName) => readJsonFile(path7.join(entriesDirectory, fileName))
|
|
1892
|
+
)
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
async getEntry(runId, traceId) {
|
|
1896
|
+
return (await this.listEntries(runId)).find((entry) => entry.traceId === traceId);
|
|
1897
|
+
}
|
|
1898
|
+
toProtocolTraceRecord(entry) {
|
|
1899
|
+
return {
|
|
1900
|
+
traceId: entry.traceId,
|
|
1901
|
+
stepId: entry.stepId,
|
|
1902
|
+
operation: entry.operation,
|
|
1903
|
+
outcome: entry.outcome,
|
|
1904
|
+
startedAt: entry.startedAt,
|
|
1905
|
+
completedAt: entry.completedAt,
|
|
1906
|
+
durationMs: entry.durationMs,
|
|
1907
|
+
context: entry.context,
|
|
1908
|
+
events: entry.events,
|
|
1909
|
+
...entry.artifacts === void 0 ? {} : { artifacts: entry.artifacts },
|
|
1910
|
+
...entry.data === void 0 ? {} : { data: entry.data },
|
|
1911
|
+
...entry.error === void 0 ? {} : { error: entry.error }
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
async readProtocolTraceBundle(runId, traceId, options = {}) {
|
|
1915
|
+
const entry = await this.getEntry(runId, traceId);
|
|
1916
|
+
if (entry === void 0) {
|
|
1917
|
+
return void 0;
|
|
1918
|
+
}
|
|
1919
|
+
const trace = this.toProtocolTraceRecord(entry);
|
|
1920
|
+
if (entry.artifacts === void 0 || entry.artifacts.length === 0) {
|
|
1921
|
+
return { trace };
|
|
1922
|
+
}
|
|
1923
|
+
const artifacts = [];
|
|
1924
|
+
for (const reference of entry.artifacts) {
|
|
1925
|
+
const artifact = await this.artifacts.toProtocolArtifact(
|
|
1926
|
+
reference.artifactId,
|
|
1927
|
+
options.artifactDelivery === void 0 ? {} : { delivery: options.artifactDelivery }
|
|
1928
|
+
);
|
|
1929
|
+
if (artifact === void 0) {
|
|
1930
|
+
throw new Error(`trace ${traceId} references missing artifact ${reference.artifactId}`);
|
|
1931
|
+
}
|
|
1932
|
+
artifacts.push(artifact);
|
|
1933
|
+
}
|
|
1934
|
+
return { trace, artifacts };
|
|
1935
|
+
}
|
|
1936
|
+
runDirectory(runId) {
|
|
1937
|
+
return path7.join(this.runsDirectory, encodeURIComponent(runId));
|
|
1938
|
+
}
|
|
1939
|
+
runEntriesDirectory(runId) {
|
|
1940
|
+
return path7.join(this.runDirectory(runId), "entries");
|
|
1941
|
+
}
|
|
1942
|
+
runManifestPath(runId) {
|
|
1943
|
+
return path7.join(this.runDirectory(runId), "manifest.json");
|
|
1944
|
+
}
|
|
1945
|
+
runWriteLockPath(runId) {
|
|
1946
|
+
return path7.join(this.runDirectory(runId), ".append.lock");
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
function createTraceStore(rootPath, artifacts) {
|
|
1950
|
+
return new FilesystemTraceStore(rootPath, artifacts);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// ../runtime-core/src/root.ts
|
|
1954
|
+
var OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT = "opensteer-workspace";
|
|
1955
|
+
var OPENSTEER_FILESYSTEM_WORKSPACE_VERSION = 2;
|
|
1956
|
+
function normalizeWorkspaceId(workspace) {
|
|
1957
|
+
return encodePathSegment(workspace);
|
|
1958
|
+
}
|
|
1959
|
+
function resolveFilesystemWorkspacePath(input) {
|
|
1960
|
+
return path7.join(
|
|
1961
|
+
input.rootDir,
|
|
1962
|
+
".opensteer",
|
|
1963
|
+
"workspaces",
|
|
1964
|
+
normalizeWorkspaceId(input.workspace)
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
async function createFilesystemOpensteerWorkspace(options) {
|
|
1968
|
+
await ensureDirectory(options.rootPath);
|
|
1969
|
+
const manifestPath = path7.join(options.rootPath, "workspace.json");
|
|
1970
|
+
const browserPath = path7.join(options.rootPath, "browser");
|
|
1971
|
+
const browserManifestPath = path7.join(browserPath, "manifest.json");
|
|
1972
|
+
const browserUserDataDir = path7.join(browserPath, "user-data");
|
|
1973
|
+
const livePath = path7.join(options.rootPath, "live");
|
|
1974
|
+
const liveLocalPath = path7.join(livePath, "local.json");
|
|
1975
|
+
const liveCloudPath = path7.join(livePath, "cloud.json");
|
|
1976
|
+
const artifactsPath = path7.join(options.rootPath, "artifacts");
|
|
1977
|
+
const tracesPath = path7.join(options.rootPath, "traces");
|
|
1978
|
+
const observationsPath = path7.join(options.rootPath, "observations");
|
|
1979
|
+
const registryPath = path7.join(options.rootPath, "registry");
|
|
1980
|
+
const lockPath = path7.join(options.rootPath, ".lock");
|
|
1981
|
+
let manifest;
|
|
1982
|
+
if (await pathExists(manifestPath)) {
|
|
1983
|
+
manifest = await readJsonFile(manifestPath);
|
|
1984
|
+
if (manifest.layout !== OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT) {
|
|
1985
|
+
throw new Error(
|
|
1986
|
+
`workspace ${options.rootPath} is not an ${OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT} layout`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
if (manifest.version !== OPENSTEER_FILESYSTEM_WORKSPACE_VERSION) {
|
|
1990
|
+
throw new Error(
|
|
1991
|
+
`workspace ${options.rootPath} uses unsupported version ${String(manifest.version)}`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
if (manifest.paths.observations === void 0) {
|
|
1995
|
+
manifest = {
|
|
1996
|
+
...manifest,
|
|
1997
|
+
updatedAt: Date.now(),
|
|
1998
|
+
paths: {
|
|
1999
|
+
...manifest.paths,
|
|
2000
|
+
observations: "observations"
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
await writeJsonFileAtomic(manifestPath, manifest);
|
|
2004
|
+
}
|
|
2005
|
+
} else {
|
|
2006
|
+
const createdAt = normalizeTimestamp("createdAt", options.createdAt ?? Date.now());
|
|
2007
|
+
manifest = {
|
|
2008
|
+
layout: OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT,
|
|
2009
|
+
version: OPENSTEER_FILESYSTEM_WORKSPACE_VERSION,
|
|
2010
|
+
scope: options.scope ?? (options.workspace === void 0 ? "temporary" : "workspace"),
|
|
2011
|
+
...options.workspace === void 0 ? {} : { workspace: options.workspace },
|
|
2012
|
+
createdAt,
|
|
2013
|
+
updatedAt: createdAt,
|
|
2014
|
+
paths: {
|
|
2015
|
+
browser: "browser",
|
|
2016
|
+
live: "live",
|
|
2017
|
+
artifacts: "artifacts",
|
|
2018
|
+
traces: "traces",
|
|
2019
|
+
observations: "observations",
|
|
2020
|
+
registry: "registry"
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
await writeJsonFileAtomic(manifestPath, manifest);
|
|
2024
|
+
}
|
|
2025
|
+
await Promise.all([
|
|
2026
|
+
ensureDirectory(browserPath),
|
|
2027
|
+
ensureDirectory(browserUserDataDir),
|
|
2028
|
+
ensureDirectory(livePath),
|
|
2029
|
+
ensureDirectory(artifactsPath),
|
|
2030
|
+
ensureDirectory(tracesPath),
|
|
2031
|
+
ensureDirectory(observationsPath),
|
|
2032
|
+
ensureDirectory(registryPath)
|
|
2033
|
+
]);
|
|
2034
|
+
const artifacts = createArtifactStore(options.rootPath);
|
|
2035
|
+
await artifacts.initialize();
|
|
2036
|
+
const descriptors = createDescriptorRegistry(options.rootPath);
|
|
2037
|
+
await descriptors.initialize();
|
|
2038
|
+
const requestPlans = createRequestPlanRegistry(options.rootPath);
|
|
2039
|
+
await requestPlans.initialize();
|
|
2040
|
+
const savedNetwork = createSavedNetworkStore(options.rootPath);
|
|
2041
|
+
await savedNetwork.initialize();
|
|
2042
|
+
const interactionTraces = createInteractionTraceRegistry(options.rootPath);
|
|
2043
|
+
await interactionTraces.initialize();
|
|
2044
|
+
const traces = createTraceStore(options.rootPath, artifacts);
|
|
2045
|
+
await traces.initialize();
|
|
2046
|
+
const observations = createObservationStore(options.rootPath, artifacts);
|
|
2047
|
+
await observations.initialize();
|
|
2048
|
+
return {
|
|
2049
|
+
rootPath: options.rootPath,
|
|
2050
|
+
manifestPath,
|
|
2051
|
+
manifest,
|
|
2052
|
+
browserPath,
|
|
2053
|
+
browserManifestPath,
|
|
2054
|
+
browserUserDataDir,
|
|
2055
|
+
livePath,
|
|
2056
|
+
liveLocalPath,
|
|
2057
|
+
liveCloudPath,
|
|
2058
|
+
artifactsPath,
|
|
2059
|
+
tracesPath,
|
|
2060
|
+
observationsPath,
|
|
2061
|
+
registryPath,
|
|
2062
|
+
lockPath,
|
|
2063
|
+
artifacts,
|
|
2064
|
+
traces,
|
|
2065
|
+
observations,
|
|
2066
|
+
registry: {
|
|
2067
|
+
descriptors,
|
|
2068
|
+
requestPlans,
|
|
2069
|
+
savedNetwork,
|
|
2070
|
+
interactionTraces
|
|
2071
|
+
},
|
|
2072
|
+
lock(task) {
|
|
2073
|
+
return withFilesystemLock(lockPath, task);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// ../runtime-core/src/internal/engine-selection.ts
|
|
2079
|
+
var OPENSTEER_ENGINE_NAMES = ["playwright", "abp"];
|
|
2080
|
+
var DEFAULT_OPENSTEER_ENGINE = "playwright";
|
|
2081
|
+
function resolveOpensteerEngineName(input = {}) {
|
|
2082
|
+
if (input.requested !== void 0) {
|
|
2083
|
+
return normalizeOpensteerEngineName(input.requested, "--engine");
|
|
2084
|
+
}
|
|
2085
|
+
if (input.environment !== void 0) {
|
|
2086
|
+
return normalizeOpensteerEngineName(input.environment, "OPENSTEER_ENGINE");
|
|
2087
|
+
}
|
|
2088
|
+
return DEFAULT_OPENSTEER_ENGINE;
|
|
2089
|
+
}
|
|
2090
|
+
function normalizeOpensteerEngineName(value, source = "engine") {
|
|
2091
|
+
const normalized = value.trim().toLowerCase();
|
|
2092
|
+
if (normalized === "playwright" || normalized === "abp") {
|
|
2093
|
+
return normalized;
|
|
2094
|
+
}
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
`${source} must be one of ${OPENSTEER_ENGINE_NAMES.join(", ")}; received "${value}".`
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
function assertSupportedEngineOptions(input) {
|
|
2100
|
+
if (input.engineName !== "abp") {
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
if (typeof input.browser === "object" && input.browser !== null && input.browser.mode === "attach") {
|
|
2104
|
+
throw new Error(
|
|
2105
|
+
'ABP engine does not support browser.mode="attach". Use the Playwright engine for attach flows.'
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const unsupportedContextOptionNames = listUnsupportedContextOptionNames(input.context);
|
|
2109
|
+
if (unsupportedContextOptionNames.length === 0) {
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
throw new Error(
|
|
2113
|
+
`ABP engine does not support ${unsupportedContextOptionNames.join(", ")}. Supported ABP context options: context.viewport.`
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
function toAbpLaunchOptions(input) {
|
|
2117
|
+
const mapped = {};
|
|
2118
|
+
if (input.launch?.headless !== void 0) {
|
|
2119
|
+
mapped.headless = input.launch.headless;
|
|
2120
|
+
}
|
|
2121
|
+
const args = mergeManagedLaunchArgs(input.launch?.args, input.context?.viewport);
|
|
2122
|
+
if (args !== void 0) {
|
|
2123
|
+
mapped.args = args;
|
|
2124
|
+
}
|
|
2125
|
+
if (input.launch?.executablePath !== void 0) {
|
|
2126
|
+
mapped.browserExecutablePath = input.launch.executablePath;
|
|
2127
|
+
}
|
|
2128
|
+
if (input.userDataDir !== void 0) {
|
|
2129
|
+
mapped.userDataDir = input.userDataDir;
|
|
2130
|
+
}
|
|
2131
|
+
if (input.sessionDir !== void 0) {
|
|
2132
|
+
mapped.sessionDir = input.sessionDir;
|
|
2133
|
+
}
|
|
2134
|
+
return Object.keys(mapped).length === 0 ? void 0 : mapped;
|
|
2135
|
+
}
|
|
2136
|
+
function listUnsupportedContextOptionNames(options) {
|
|
2137
|
+
if (options === void 0) {
|
|
2138
|
+
return [];
|
|
2139
|
+
}
|
|
2140
|
+
return Object.entries(options).filter(([key, value]) => value !== void 0 && key !== "viewport").map(([key]) => `context.${key}`);
|
|
2141
|
+
}
|
|
2142
|
+
function mergeManagedLaunchArgs(args, viewport) {
|
|
2143
|
+
const filtered = stripWindowSizeArgs(args);
|
|
2144
|
+
if (viewport === void 0 || viewport === null) {
|
|
2145
|
+
return filtered.length === 0 ? void 0 : filtered;
|
|
2146
|
+
}
|
|
2147
|
+
return [...filtered, `--window-size=${viewport.width},${viewport.height}`];
|
|
2148
|
+
}
|
|
2149
|
+
function stripWindowSizeArgs(args) {
|
|
2150
|
+
if (args === void 0) {
|
|
2151
|
+
return [];
|
|
2152
|
+
}
|
|
2153
|
+
const filtered = [];
|
|
2154
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2155
|
+
const argument = args[index];
|
|
2156
|
+
if (argument === "--window-size") {
|
|
2157
|
+
index += 1;
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
if (argument.startsWith("--window-size=")) {
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
filtered.push(argument);
|
|
2164
|
+
}
|
|
2165
|
+
return filtered;
|
|
2166
|
+
}
|
|
2167
|
+
var CHROME_SINGLETON_ARTIFACTS = [
|
|
2168
|
+
"SingletonCookie",
|
|
2169
|
+
"SingletonLock",
|
|
2170
|
+
"SingletonSocket",
|
|
2171
|
+
"DevToolsActivePort",
|
|
2172
|
+
"lockfile"
|
|
2173
|
+
];
|
|
2174
|
+
async function clearChromeSingletonEntries(userDataDir) {
|
|
2175
|
+
await Promise.all(
|
|
2176
|
+
CHROME_SINGLETON_ARTIFACTS.map(
|
|
2177
|
+
(entry) => rm(join(userDataDir, entry), {
|
|
2178
|
+
recursive: true,
|
|
2179
|
+
force: true
|
|
2180
|
+
}).catch(() => void 0)
|
|
2181
|
+
)
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
async function sanitizeChromeProfile(userDataDir) {
|
|
2185
|
+
const entries = await readdir(userDataDir).catch(() => []);
|
|
2186
|
+
const profileDirs = entries.filter(
|
|
2187
|
+
(entry) => entry === "Default" || /^Profile \d+$/i.test(entry)
|
|
2188
|
+
);
|
|
2189
|
+
await Promise.all(profileDirs.map((dir) => sanitizeProfilePreferences(userDataDir, dir)));
|
|
2190
|
+
}
|
|
2191
|
+
async function sanitizeProfilePreferences(userDataDir, profileDir) {
|
|
2192
|
+
const prefsPath = join(userDataDir, profileDir, "Preferences");
|
|
2193
|
+
try {
|
|
2194
|
+
const raw = await readFile(prefsPath, "utf8");
|
|
2195
|
+
const prefs = JSON.parse(raw);
|
|
2196
|
+
const profile = prefs.profile ?? {};
|
|
2197
|
+
if (profile.exit_type === "Normal" && profile.exited_cleanly === true) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
profile.exit_type = "Normal";
|
|
2201
|
+
profile.exited_cleanly = true;
|
|
2202
|
+
prefs.profile = profile;
|
|
2203
|
+
await writeFile(prefsPath, JSON.stringify(prefs), "utf8");
|
|
2204
|
+
await rm(join(userDataDir, profileDir, "Secure Preferences"), { force: true }).catch(
|
|
2205
|
+
() => void 0
|
|
2206
|
+
);
|
|
2207
|
+
} catch {
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
var CHROME_SINGLETON_ENTRIES = new Set(CHROME_SINGLETON_ARTIFACTS);
|
|
2211
|
+
var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
2212
|
+
"Crash Reports",
|
|
2213
|
+
"Crashpad",
|
|
2214
|
+
"BrowserMetrics",
|
|
2215
|
+
"GrShaderCache",
|
|
2216
|
+
"ShaderCache",
|
|
2217
|
+
"GraphiteDawnCache",
|
|
2218
|
+
"component_crx_cache",
|
|
2219
|
+
"Crowd Deny",
|
|
2220
|
+
"hyphen-data",
|
|
2221
|
+
"OnDeviceHeadSuggestModel",
|
|
2222
|
+
"OptimizationGuidePredictionModels",
|
|
2223
|
+
"Segmentation Platform",
|
|
2224
|
+
"SmartCardDeviceNames",
|
|
2225
|
+
"WidevineCdm",
|
|
2226
|
+
"pnacl"
|
|
2227
|
+
]);
|
|
2228
|
+
var SESSION_ROOT_FILES = /* @__PURE__ */ new Set(["Local State"]);
|
|
2229
|
+
var SESSION_SKIPPED_PROFILE_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
2230
|
+
"Cache",
|
|
2231
|
+
"Code Cache",
|
|
2232
|
+
"GPUCache",
|
|
2233
|
+
"Service Worker",
|
|
2234
|
+
"File System",
|
|
2235
|
+
"blob_storage",
|
|
2236
|
+
"Network"
|
|
2237
|
+
]);
|
|
2238
|
+
async function createBrowserProfileSnapshot(input) {
|
|
2239
|
+
const sourceUserDataDir = resolve(expandHome(input.sourceUserDataDir));
|
|
2240
|
+
const targetUserDataDir = resolve(expandHome(input.targetUserDataDir));
|
|
2241
|
+
const profileDirectory = input.profileDirectory?.trim();
|
|
2242
|
+
const copyMode = input.copyMode;
|
|
2243
|
+
await mkdir(targetUserDataDir, { recursive: true });
|
|
2244
|
+
await clearChromeSingletonEntries(targetUserDataDir);
|
|
2245
|
+
if (profileDirectory) {
|
|
2246
|
+
const sourceProfileDir = join(sourceUserDataDir, profileDirectory);
|
|
2247
|
+
if (!existsSync(sourceProfileDir)) {
|
|
2248
|
+
throw new Error(
|
|
2249
|
+
`Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
await cp(sourceProfileDir, join(targetUserDataDir, profileDirectory), {
|
|
2253
|
+
recursive: true,
|
|
2254
|
+
filter: (candidate) => shouldCopyEntry({
|
|
2255
|
+
candidatePath: candidate,
|
|
2256
|
+
copyMode,
|
|
2257
|
+
rootPath: sourceProfileDir
|
|
2258
|
+
})
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
await copyRootLevelEntries({
|
|
2262
|
+
copyMode,
|
|
2263
|
+
sourceUserDataDir,
|
|
2264
|
+
targetUserDataDir,
|
|
2265
|
+
...profileDirectory === void 0 ? {} : { selectedProfileDirectory: profileDirectory }
|
|
2266
|
+
});
|
|
2267
|
+
await clearChromeSingletonEntries(targetUserDataDir);
|
|
2268
|
+
await sanitizeChromeProfile(targetUserDataDir);
|
|
2269
|
+
}
|
|
2270
|
+
async function copyRootLevelEntries(input) {
|
|
2271
|
+
const entries = await readdir(input.sourceUserDataDir).catch(() => []);
|
|
2272
|
+
for (const entry of entries) {
|
|
2273
|
+
if (CHROME_SINGLETON_ENTRIES.has(entry) || entry === input.selectedProfileDirectory) {
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
const sourcePath = join(input.sourceUserDataDir, entry);
|
|
2277
|
+
const targetPath = join(input.targetUserDataDir, entry);
|
|
2278
|
+
const entryStat = await stat(sourcePath).catch(() => null);
|
|
2279
|
+
if (!entryStat) {
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
if (entryStat.isFile()) {
|
|
2283
|
+
if (input.copyMode === "session" && !SESSION_ROOT_FILES.has(entry)) {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
await copyFile(sourcePath, targetPath).catch(() => void 0);
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
if (!entryStat.isDirectory()) {
|
|
2290
|
+
continue;
|
|
2291
|
+
}
|
|
2292
|
+
if (SKIPPED_ROOT_DIRECTORIES.has(entry) || isProfileDirectory(input.sourceUserDataDir, entry)) {
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
if (input.copyMode === "session") {
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
await cp(sourcePath, targetPath, {
|
|
2299
|
+
recursive: true,
|
|
2300
|
+
filter: (candidate) => shouldCopyEntry({
|
|
2301
|
+
candidatePath: candidate,
|
|
2302
|
+
copyMode: input.copyMode,
|
|
2303
|
+
rootPath: sourcePath
|
|
2304
|
+
})
|
|
2305
|
+
}).catch(() => void 0);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
function isProfileDirectory(userDataDir, entry) {
|
|
2309
|
+
return existsSync(join(userDataDir, entry, "Preferences"));
|
|
2310
|
+
}
|
|
2311
|
+
function shouldCopyEntry(input) {
|
|
2312
|
+
const entryName = input.candidatePath.split("/").at(-1)?.split("\\").at(-1) ?? input.candidatePath;
|
|
2313
|
+
if (CHROME_SINGLETON_ENTRIES.has(entryName)) {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
if (input.copyMode !== "session") {
|
|
2317
|
+
return true;
|
|
2318
|
+
}
|
|
2319
|
+
const relativePath = relative(input.rootPath, input.candidatePath);
|
|
2320
|
+
if (relativePath.length === 0) {
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
const firstSegment = relativePath.split("/").at(0)?.split("\\").at(0) ?? relativePath;
|
|
2324
|
+
return !SESSION_SKIPPED_PROFILE_DIRECTORIES.has(firstSegment);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/local-browser/stealth-init-script.ts
|
|
2328
|
+
function generateStealthInitScript(profile) {
|
|
2329
|
+
const encodedProfile = JSON.stringify({
|
|
2330
|
+
...profile,
|
|
2331
|
+
platformString: getPlatformString(profile.platform),
|
|
2332
|
+
userAgentData: buildUserAgentData(profile)
|
|
2333
|
+
});
|
|
2334
|
+
return `(() => {
|
|
2335
|
+
const profile = ${encodedProfile};
|
|
2336
|
+
var define = function(target, key, value) {
|
|
2337
|
+
Object.defineProperty(target, key, {
|
|
2338
|
+
configurable: true,
|
|
2339
|
+
get: typeof value === 'function' ? value : function() { return value; },
|
|
2340
|
+
});
|
|
2341
|
+
};
|
|
2342
|
+
|
|
2343
|
+
// --- navigator / screen mirrors for future pages ---
|
|
2344
|
+
if (navigator.webdriver === true) {
|
|
2345
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
2346
|
+
configurable: true,
|
|
2347
|
+
get: function() { return false; },
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
define(Navigator.prototype, 'platform', profile.platformString);
|
|
2351
|
+
define(Navigator.prototype, 'userAgent', profile.userAgent);
|
|
2352
|
+
define(Navigator.prototype, 'language', profile.locale);
|
|
2353
|
+
define(Navigator.prototype, 'languages', [profile.locale, 'en']);
|
|
2354
|
+
define(Navigator.prototype, 'maxTouchPoints', profile.maxTouchPoints);
|
|
2355
|
+
define(window, 'devicePixelRatio', profile.devicePixelRatio);
|
|
2356
|
+
define(window.screen, 'width', profile.screenResolution.width);
|
|
2357
|
+
define(window.screen, 'height', profile.screenResolution.height);
|
|
2358
|
+
define(window.screen, 'availWidth', profile.screenResolution.width);
|
|
2359
|
+
define(window.screen, 'availHeight', profile.screenResolution.height - 40);
|
|
2360
|
+
define(window.screen, 'colorDepth', 24);
|
|
2361
|
+
define(window.screen, 'pixelDepth', 24);
|
|
2362
|
+
define(Navigator.prototype, 'userAgentData', {
|
|
2363
|
+
brands: profile.userAgentData.brands,
|
|
2364
|
+
mobile: false,
|
|
2365
|
+
platform: profile.userAgentData.platform,
|
|
2366
|
+
toJSON: function() {
|
|
2367
|
+
return {
|
|
2368
|
+
brands: this.brands,
|
|
2369
|
+
mobile: this.mobile,
|
|
2370
|
+
platform: this.platform,
|
|
2371
|
+
};
|
|
2372
|
+
},
|
|
2373
|
+
getHighEntropyValues: async function(hints) {
|
|
2374
|
+
var source = {
|
|
2375
|
+
architecture: profile.userAgentData.architecture,
|
|
2376
|
+
bitness: profile.userAgentData.bitness,
|
|
2377
|
+
brands: profile.userAgentData.brands,
|
|
2378
|
+
fullVersionList: profile.userAgentData.fullVersionList,
|
|
2379
|
+
mobile: false,
|
|
2380
|
+
model: '',
|
|
2381
|
+
platform: profile.userAgentData.platform,
|
|
2382
|
+
platformVersion: profile.userAgentData.platformVersion,
|
|
2383
|
+
uaFullVersion: profile.browserVersion,
|
|
2384
|
+
wow64: false,
|
|
2385
|
+
};
|
|
2386
|
+
var values = {};
|
|
2387
|
+
for (var i = 0; i < hints.length; i++) {
|
|
2388
|
+
var hint = hints[i];
|
|
2389
|
+
if (Object.prototype.hasOwnProperty.call(source, hint)) {
|
|
2390
|
+
values[hint] = source[hint];
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return values;
|
|
2394
|
+
},
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
|
2398
|
+
var originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
|
|
2399
|
+
Intl.DateTimeFormat.prototype.resolvedOptions = function() {
|
|
2400
|
+
var options = originalResolvedOptions.call(this);
|
|
2401
|
+
options.timeZone = profile.timezoneId;
|
|
2402
|
+
return options;
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if (Date.prototype.getTimezoneOffset) {
|
|
2407
|
+
var originalGetTimezoneOffset = Date.prototype.getTimezoneOffset;
|
|
2408
|
+
var calculateTimezoneOffset = function(date) {
|
|
2409
|
+
try {
|
|
2410
|
+
var formatter = new Intl.DateTimeFormat('en-US', {
|
|
2411
|
+
timeZone: profile.timezoneId,
|
|
2412
|
+
hour12: false,
|
|
2413
|
+
year: 'numeric',
|
|
2414
|
+
month: '2-digit',
|
|
2415
|
+
day: '2-digit',
|
|
2416
|
+
hour: '2-digit',
|
|
2417
|
+
minute: '2-digit',
|
|
2418
|
+
second: '2-digit',
|
|
2419
|
+
});
|
|
2420
|
+
var parts = formatter.formatToParts(date);
|
|
2421
|
+
var values = {};
|
|
2422
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2423
|
+
if (parts[i].type !== 'literal') {
|
|
2424
|
+
values[parts[i].type] = Number(parts[i].value);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
var utcTime = Date.UTC(
|
|
2428
|
+
values.year,
|
|
2429
|
+
values.month - 1,
|
|
2430
|
+
values.day,
|
|
2431
|
+
values.hour,
|
|
2432
|
+
values.minute,
|
|
2433
|
+
values.second,
|
|
2434
|
+
);
|
|
2435
|
+
return Math.round((date.getTime() - utcTime) / 60000);
|
|
2436
|
+
} catch {
|
|
2437
|
+
return originalGetTimezoneOffset.call(date);
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
Date.prototype.getTimezoneOffset = function() {
|
|
2441
|
+
return calculateTimezoneOffset(this);
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// --- CDP Runtime.enable leak defense ---
|
|
2446
|
+
var _wrap = function(name) {
|
|
2447
|
+
var orig = console[name];
|
|
2448
|
+
if (typeof orig !== 'function') return;
|
|
2449
|
+
console[name] = new Proxy(orig, {
|
|
2450
|
+
apply: function(target, thisArg, args) {
|
|
2451
|
+
for (var i = 0; i < args.length; i++) {
|
|
2452
|
+
if (args[i] instanceof Error) {
|
|
2453
|
+
var d = Object.getOwnPropertyDescriptor(args[i], 'stack');
|
|
2454
|
+
if (d && typeof d.get === 'function') return undefined;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return Reflect.apply(target, thisArg, args);
|
|
2458
|
+
},
|
|
2459
|
+
});
|
|
2460
|
+
};
|
|
2461
|
+
['debug', 'log', 'info', 'error', 'warn', 'trace', 'dir'].forEach(_wrap);
|
|
2462
|
+
|
|
2463
|
+
// --- Canvas fingerprint noise ---
|
|
2464
|
+
var seedNoise = function(seed, input) {
|
|
2465
|
+
var value = Math.sin(seed + input * 12.9898) * 43758.5453;
|
|
2466
|
+
return value - Math.floor(value);
|
|
2467
|
+
};
|
|
2468
|
+
if (HTMLCanvasElement.prototype.toDataURL) {
|
|
2469
|
+
var originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
2470
|
+
HTMLCanvasElement.prototype.toDataURL = function() {
|
|
2471
|
+
var context = this.getContext('2d');
|
|
2472
|
+
if (context) {
|
|
2473
|
+
var x = Math.min(1, Math.max(0, this.width - 1));
|
|
2474
|
+
var y = Math.min(1, Math.max(0, this.height - 1));
|
|
2475
|
+
var imageData = context.getImageData(x, y, 1, 1);
|
|
2476
|
+
imageData.data[0] = Math.max(0, Math.min(255, imageData.data[0] + Math.floor(seedNoise(profile.canvasNoiseSeed, 1) * 2)));
|
|
2477
|
+
context.putImageData(imageData, x, y);
|
|
2478
|
+
}
|
|
2479
|
+
return originalToDataURL.apply(this, arguments);
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// --- WebGL vendor/renderer spoofing ---
|
|
2484
|
+
if (typeof WebGLRenderingContext !== 'undefined') {
|
|
2485
|
+
var originalGetParameter = WebGLRenderingContext.prototype.getParameter;
|
|
2486
|
+
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
|
2487
|
+
if (parameter === 37445) return profile.webglVendor;
|
|
2488
|
+
if (parameter === 37446) return profile.webglRenderer;
|
|
2489
|
+
return originalGetParameter.call(this, parameter);
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// --- AudioBuffer fingerprint noise ---
|
|
2494
|
+
if (typeof AudioBuffer !== 'undefined' && typeof AnalyserNode !== 'undefined') {
|
|
2495
|
+
var originalGetFloatFrequencyData = AnalyserNode.prototype.getFloatFrequencyData;
|
|
2496
|
+
AnalyserNode.prototype.getFloatFrequencyData = function(array) {
|
|
2497
|
+
originalGetFloatFrequencyData.call(this, array);
|
|
2498
|
+
for (var index = 0; index < array.length; index += 16) {
|
|
2499
|
+
array[index] += (seedNoise(profile.audioNoiseSeed, index) - 0.5) * 0.0001;
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// --- Font availability spoofing ---
|
|
2505
|
+
if (document.fonts && typeof document.fonts.check === 'function') {
|
|
2506
|
+
var originalCheck = document.fonts.check.bind(document.fonts);
|
|
2507
|
+
document.fonts.check = function(font, text) {
|
|
2508
|
+
var family = String(font).match(/["']([^"']+)["']/)?.[1] || String(font).split(/\\s+/).at(-1);
|
|
2509
|
+
if (family && profile.fonts.includes(family)) {
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
return originalCheck(font, text);
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
})();`;
|
|
2516
|
+
}
|
|
2517
|
+
function buildUserAgentData(profile) {
|
|
2518
|
+
const majorVersion = profile.browserVersion.split(".")[0] ?? "136";
|
|
2519
|
+
const platformData = {
|
|
2520
|
+
macos: { platform: "macOS", platformVersion: "14.4.0", architecture: "arm" },
|
|
2521
|
+
windows: { platform: "Windows", platformVersion: "15.0.0", architecture: "x86" },
|
|
2522
|
+
linux: { platform: "Linux", platformVersion: "6.5.0", architecture: "x86" }
|
|
2523
|
+
};
|
|
2524
|
+
const platformInfo = platformData[profile.platform];
|
|
2525
|
+
return {
|
|
2526
|
+
architecture: platformInfo.architecture,
|
|
2527
|
+
bitness: "64",
|
|
2528
|
+
brands: [
|
|
2529
|
+
{ brand: "Chromium", version: majorVersion },
|
|
2530
|
+
...profile.browserBrand === "edge" ? [{ brand: "Microsoft Edge", version: majorVersion }] : [{ brand: "Google Chrome", version: majorVersion }],
|
|
2531
|
+
{ brand: "Not-A.Brand", version: "99" }
|
|
2532
|
+
],
|
|
2533
|
+
fullVersionList: [
|
|
2534
|
+
{ brand: "Chromium", version: profile.browserVersion },
|
|
2535
|
+
...profile.browserBrand === "edge" ? [{ brand: "Microsoft Edge", version: profile.browserVersion }] : [{ brand: "Google Chrome", version: profile.browserVersion }],
|
|
2536
|
+
{ brand: "Not-A.Brand", version: "99.0.0.0" }
|
|
2537
|
+
],
|
|
2538
|
+
platform: platformInfo.platform,
|
|
2539
|
+
platformVersion: platformInfo.platformVersion
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
function getPlatformString(platform) {
|
|
2543
|
+
return platform === "macos" ? "MacIntel" : platform === "windows" ? "Win32" : "Linux x86_64";
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// src/local-browser/stealth.ts
|
|
2547
|
+
var STEALTH_INIT_SCRIPT = `(() => {
|
|
2548
|
+
// Override navigator.webdriver only if Chrome reports automation.
|
|
2549
|
+
// --disable-blink-features=AutomationControlled should prevent this, but some
|
|
2550
|
+
// Chrome builds still set webdriver=true when --remote-debugging-port is active.
|
|
2551
|
+
if (navigator.webdriver === true) {
|
|
2552
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
2553
|
+
get: function() { return false; },
|
|
2554
|
+
configurable: true,
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// Neutralize CDP Runtime.enable leak detection.
|
|
2559
|
+
//
|
|
2560
|
+
// Playwright enables the CDP Runtime domain for page.evaluate(). Bot detectors
|
|
2561
|
+
// exploit this: they create an Error with a user-defined getter on its 'stack'
|
|
2562
|
+
// property, then pass that Error to a console method. When Runtime.enable is
|
|
2563
|
+
// active, Chrome serializes the Error for the Runtime.consoleAPICalled event,
|
|
2564
|
+
// which triggers the getter \u2014 proving CDP is present.
|
|
2565
|
+
//
|
|
2566
|
+
// Defense: wrap console methods in a Proxy that detects this specific pattern
|
|
2567
|
+
// (an Error whose 'stack' is an accessor, not a data property) and suppresses
|
|
2568
|
+
// the call. Normal Errors have a plain data-property 'stack', so legitimate
|
|
2569
|
+
// console usage is unaffected.
|
|
2570
|
+
var _wrap = function(name) {
|
|
2571
|
+
var orig = console[name];
|
|
2572
|
+
if (typeof orig !== 'function') return;
|
|
2573
|
+
console[name] = new Proxy(orig, {
|
|
2574
|
+
apply: function(target, thisArg, args) {
|
|
2575
|
+
for (var i = 0; i < args.length; i++) {
|
|
2576
|
+
if (args[i] instanceof Error) {
|
|
2577
|
+
var d = Object.getOwnPropertyDescriptor(args[i], 'stack');
|
|
2578
|
+
if (d && typeof d.get === 'function') return undefined;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return Reflect.apply(target, thisArg, args);
|
|
2582
|
+
},
|
|
2583
|
+
});
|
|
2584
|
+
};
|
|
2585
|
+
['debug', 'log', 'info', 'error', 'warn', 'trace', 'dir'].forEach(_wrap);
|
|
2586
|
+
})();`;
|
|
2587
|
+
async function injectBrowserStealthScripts(context, input = {}) {
|
|
2588
|
+
if (input.profile !== void 0) {
|
|
2589
|
+
await installContextNetworkHeaders(context, input.profile);
|
|
2590
|
+
await installCdpStealthOverrides(context, input.profile, input.page);
|
|
2591
|
+
}
|
|
2592
|
+
if (typeof context.addInitScript === "function") {
|
|
2593
|
+
await context.addInitScript({
|
|
2594
|
+
content: input.profile === void 0 ? STEALTH_INIT_SCRIPT : generateStealthInitScript(input.profile)
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function buildUserAgentMetadata(profile) {
|
|
2599
|
+
const majorVersion = profile.browserVersion.split(".")[0] ?? "136";
|
|
2600
|
+
const brands = [
|
|
2601
|
+
{ brand: "Chromium", version: majorVersion },
|
|
2602
|
+
...profile.browserBrand === "edge" ? [{ brand: "Microsoft Edge", version: majorVersion }] : [{ brand: "Google Chrome", version: majorVersion }],
|
|
2603
|
+
{ brand: "Not-A.Brand", version: "99" }
|
|
2604
|
+
];
|
|
2605
|
+
const fullVersionList = [
|
|
2606
|
+
{ brand: "Chromium", version: profile.browserVersion },
|
|
2607
|
+
...profile.browserBrand === "edge" ? [{ brand: "Microsoft Edge", version: profile.browserVersion }] : [{ brand: "Google Chrome", version: profile.browserVersion }],
|
|
2608
|
+
{ brand: "Not-A.Brand", version: "99.0.0.0" }
|
|
2609
|
+
];
|
|
2610
|
+
const platformMap = {
|
|
2611
|
+
// Chromium keeps the reduced macOS UA token frozen to Intel even on Apple Silicon.
|
|
2612
|
+
macos: { platform: "macOS", platformVersion: "14.4.0", architecture: "arm" },
|
|
2613
|
+
windows: { platform: "Windows", platformVersion: "15.0.0", architecture: "x86" },
|
|
2614
|
+
linux: { platform: "Linux", platformVersion: "6.5.0", architecture: "x86" }
|
|
2615
|
+
};
|
|
2616
|
+
const platformInfo = platformMap[profile.platform];
|
|
2617
|
+
return {
|
|
2618
|
+
brands,
|
|
2619
|
+
fullVersionList,
|
|
2620
|
+
platform: platformInfo.platform,
|
|
2621
|
+
platformVersion: platformInfo.platformVersion,
|
|
2622
|
+
architecture: platformInfo.architecture,
|
|
2623
|
+
model: "",
|
|
2624
|
+
mobile: false,
|
|
2625
|
+
bitness: "64",
|
|
2626
|
+
wow64: false
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
async function installCdpStealthOverrides(context, profile, initialPage) {
|
|
2630
|
+
const pages = initialPage === void 0 ? context.pages() : Array.from(/* @__PURE__ */ new Set([initialPage, ...context.pages()]));
|
|
2631
|
+
await Promise.all(pages.map((page) => applyPageOverrides(context, page, profile)));
|
|
2632
|
+
const appliedPages = /* @__PURE__ */ new WeakSet();
|
|
2633
|
+
const applyFuturePageOverrides = async (page) => {
|
|
2634
|
+
if (appliedPages.has(page)) {
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
appliedPages.add(page);
|
|
2638
|
+
await applyPageOverrides(context, page, profile);
|
|
2639
|
+
};
|
|
2640
|
+
if (typeof context.on === "function") {
|
|
2641
|
+
context.on("page", applyFuturePageOverrides);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
async function installContextNetworkHeaders(context, profile) {
|
|
2645
|
+
if (typeof context.setExtraHTTPHeaders !== "function") {
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
await context.setExtraHTTPHeaders(buildStealthRequestHeaders(profile)).catch(() => void 0);
|
|
2649
|
+
}
|
|
2650
|
+
async function applyPageOverrides(context, page, profile) {
|
|
2651
|
+
const contextWithCdp = context;
|
|
2652
|
+
if (typeof contextWithCdp.newCDPSession !== "function") {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
let cdp;
|
|
2656
|
+
try {
|
|
2657
|
+
cdp = await contextWithCdp.newCDPSession(page);
|
|
2658
|
+
} catch {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
try {
|
|
2662
|
+
await applyCdpStealthCommands((method, params) => cdp.send(method, params), profile);
|
|
2663
|
+
} catch {
|
|
2664
|
+
} finally {
|
|
2665
|
+
await cdp.detach().catch(() => void 0);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
async function applyCdpStealthCommands(send, profile) {
|
|
2669
|
+
await send("Network.setUserAgentOverride", {
|
|
2670
|
+
userAgent: profile.userAgent,
|
|
2671
|
+
acceptLanguage: `${profile.locale},en;q=0.9`,
|
|
2672
|
+
platform: getPlatformString2(profile.platform),
|
|
2673
|
+
userAgentMetadata: buildUserAgentMetadata(profile)
|
|
2674
|
+
});
|
|
2675
|
+
await send("Emulation.setDeviceMetricsOverride", {
|
|
2676
|
+
width: profile.viewport.width,
|
|
2677
|
+
height: profile.viewport.height,
|
|
2678
|
+
deviceScaleFactor: profile.devicePixelRatio,
|
|
2679
|
+
mobile: false,
|
|
2680
|
+
screenWidth: profile.screenResolution.width,
|
|
2681
|
+
screenHeight: profile.screenResolution.height
|
|
2682
|
+
});
|
|
2683
|
+
await send("Emulation.setLocaleOverride", {
|
|
2684
|
+
locale: profile.locale
|
|
2685
|
+
}).catch(() => void 0);
|
|
2686
|
+
await send("Emulation.setTimezoneOverride", {
|
|
2687
|
+
timezoneId: profile.timezoneId
|
|
2688
|
+
}).catch(() => void 0);
|
|
2689
|
+
}
|
|
2690
|
+
function buildStealthRequestHeaders(profile) {
|
|
2691
|
+
const metadata = buildUserAgentMetadata(profile);
|
|
2692
|
+
return {
|
|
2693
|
+
"Accept-Language": `${profile.locale},en;q=0.9`,
|
|
2694
|
+
"Sec-CH-UA": metadata.brands.map(formatClientHintBrand).join(", "),
|
|
2695
|
+
"Sec-CH-UA-Mobile": "?0",
|
|
2696
|
+
"Sec-CH-UA-Platform": `"${metadata.platform}"`,
|
|
2697
|
+
"User-Agent": profile.userAgent
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
function formatClientHintBrand(brand) {
|
|
2701
|
+
return `"${brand.brand}";v="${brand.version}"`;
|
|
2702
|
+
}
|
|
2703
|
+
function getPlatformString2(platform) {
|
|
2704
|
+
return platform === "macos" ? "MacIntel" : platform === "windows" ? "Win32" : "Linux x86_64";
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// src/local-browser/stealth-profiles.ts
|
|
2708
|
+
var PROFILE_PRESETS = [
|
|
2709
|
+
{
|
|
2710
|
+
platform: "macos",
|
|
2711
|
+
browserBrand: "chrome",
|
|
2712
|
+
browserVersion: "136.0.7103.93",
|
|
2713
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
2714
|
+
viewport: { width: 1440, height: 900 },
|
|
2715
|
+
screenResolution: { width: 1512, height: 982 },
|
|
2716
|
+
devicePixelRatio: 2,
|
|
2717
|
+
maxTouchPoints: 0,
|
|
2718
|
+
webglVendor: "Apple",
|
|
2719
|
+
webglRenderer: "Apple M2",
|
|
2720
|
+
fonts: ["SF Pro Text", "Helvetica Neue", "Arial", "Menlo", "Apple Color Emoji"],
|
|
2721
|
+
locale: "en-US",
|
|
2722
|
+
timezoneId: "America/Los_Angeles"
|
|
2723
|
+
},
|
|
2724
|
+
{
|
|
2725
|
+
platform: "windows",
|
|
2726
|
+
browserBrand: "chrome",
|
|
2727
|
+
browserVersion: "136.0.7103.93",
|
|
2728
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
2729
|
+
viewport: { width: 1536, height: 864 },
|
|
2730
|
+
screenResolution: { width: 1920, height: 1080 },
|
|
2731
|
+
devicePixelRatio: 1.25,
|
|
2732
|
+
maxTouchPoints: 0,
|
|
2733
|
+
webglVendor: "Google Inc. (Intel)",
|
|
2734
|
+
webglRenderer: "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
2735
|
+
fonts: ["Segoe UI", "Arial", "Calibri", "Consolas", "Segoe UI Emoji"],
|
|
2736
|
+
locale: "en-US",
|
|
2737
|
+
timezoneId: "America/New_York"
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
platform: "windows",
|
|
2741
|
+
browserBrand: "edge",
|
|
2742
|
+
browserVersion: "136.0.3240.50",
|
|
2743
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.3240.50",
|
|
2744
|
+
viewport: { width: 1536, height: 864 },
|
|
2745
|
+
screenResolution: { width: 1920, height: 1080 },
|
|
2746
|
+
devicePixelRatio: 1.25,
|
|
2747
|
+
maxTouchPoints: 0,
|
|
2748
|
+
webglVendor: "Google Inc. (Intel)",
|
|
2749
|
+
webglRenderer: "ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
2750
|
+
fonts: ["Segoe UI", "Arial", "Calibri", "Consolas", "Segoe UI Emoji"],
|
|
2751
|
+
locale: "en-US",
|
|
2752
|
+
timezoneId: "America/Chicago"
|
|
2753
|
+
},
|
|
2754
|
+
{
|
|
2755
|
+
platform: "linux",
|
|
2756
|
+
browserBrand: "chrome",
|
|
2757
|
+
browserVersion: "136.0.7103.93",
|
|
2758
|
+
userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
2759
|
+
viewport: { width: 1366, height: 768 },
|
|
2760
|
+
screenResolution: { width: 1366, height: 768 },
|
|
2761
|
+
devicePixelRatio: 1,
|
|
2762
|
+
maxTouchPoints: 0,
|
|
2763
|
+
webglVendor: "Google Inc. (Mesa)",
|
|
2764
|
+
webglRenderer: "ANGLE (AMD, AMD Radeon Graphics, OpenGL 4.6)",
|
|
2765
|
+
fonts: ["Noto Sans", "Ubuntu", "DejaVu Sans", "Liberation Sans", "Noto Color Emoji"],
|
|
2766
|
+
locale: "en-US",
|
|
2767
|
+
timezoneId: "UTC"
|
|
2768
|
+
}
|
|
2769
|
+
];
|
|
2770
|
+
function generateStealthProfile(overrides = {}) {
|
|
2771
|
+
const preset = pickStealthProfilePreset(overrides);
|
|
2772
|
+
return {
|
|
2773
|
+
id: overrides.id ?? `stealth:${preset.platform}:${preset.browserBrand}:${Math.random().toString(36).slice(2, 10)}`,
|
|
2774
|
+
platform: overrides.platform ?? preset.platform,
|
|
2775
|
+
browserBrand: overrides.browserBrand ?? preset.browserBrand,
|
|
2776
|
+
browserVersion: overrides.browserVersion ?? preset.browserVersion,
|
|
2777
|
+
userAgent: overrides.userAgent ?? preset.userAgent,
|
|
2778
|
+
viewport: overrides.viewport ?? preset.viewport,
|
|
2779
|
+
screenResolution: overrides.screenResolution ?? preset.screenResolution,
|
|
2780
|
+
devicePixelRatio: overrides.devicePixelRatio ?? preset.devicePixelRatio,
|
|
2781
|
+
maxTouchPoints: overrides.maxTouchPoints ?? preset.maxTouchPoints,
|
|
2782
|
+
webglVendor: overrides.webglVendor ?? preset.webglVendor,
|
|
2783
|
+
webglRenderer: overrides.webglRenderer ?? preset.webglRenderer,
|
|
2784
|
+
fonts: overrides.fonts ?? preset.fonts,
|
|
2785
|
+
canvasNoiseSeed: overrides.canvasNoiseSeed ?? Math.floor(Math.random() * 1e6),
|
|
2786
|
+
audioNoiseSeed: overrides.audioNoiseSeed ?? Math.floor(Math.random() * 1e6),
|
|
2787
|
+
locale: overrides.locale ?? preset.locale,
|
|
2788
|
+
timezoneId: overrides.timezoneId ?? preset.timezoneId
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
function pickStealthProfilePreset(overrides) {
|
|
2792
|
+
const candidates = PROFILE_PRESETS.filter(
|
|
2793
|
+
(preset) => (overrides.platform === void 0 || preset.platform === overrides.platform) && (overrides.browserBrand === void 0 || preset.browserBrand === overrides.browserBrand)
|
|
2794
|
+
);
|
|
2795
|
+
const pool = candidates.length > 0 ? candidates : PROFILE_PRESETS;
|
|
2796
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// src/local-view/preferences.ts
|
|
2800
|
+
var OPENSTEER_LOCAL_VIEW_PREFERENCES_LAYOUT = "opensteer-local-view-preferences";
|
|
2801
|
+
var OPENSTEER_LOCAL_VIEW_PREFERENCES_VERSION = 1;
|
|
2802
|
+
async function resolveLocalViewMode() {
|
|
2803
|
+
const preferences = await readLocalViewPreferences();
|
|
2804
|
+
return preferences?.mode ?? "auto";
|
|
2805
|
+
}
|
|
2806
|
+
async function setLocalViewMode(mode) {
|
|
2807
|
+
return writeLocalViewPreferences(mode);
|
|
2808
|
+
}
|
|
2809
|
+
async function readLocalViewPreferences() {
|
|
2810
|
+
const preferencesPath = resolveLocalViewPreferencesPath();
|
|
2811
|
+
if (!await pathExists(preferencesPath)) {
|
|
2812
|
+
return void 0;
|
|
2813
|
+
}
|
|
2814
|
+
const parsed = await readJsonFile(preferencesPath);
|
|
2815
|
+
return isPersistedLocalViewPreferences(parsed) ? parsed : void 0;
|
|
2816
|
+
}
|
|
2817
|
+
async function writeLocalViewPreferences(mode) {
|
|
2818
|
+
const preferences = {
|
|
2819
|
+
layout: OPENSTEER_LOCAL_VIEW_PREFERENCES_LAYOUT,
|
|
2820
|
+
version: OPENSTEER_LOCAL_VIEW_PREFERENCES_VERSION,
|
|
2821
|
+
mode,
|
|
2822
|
+
updatedAt: Date.now()
|
|
2823
|
+
};
|
|
2824
|
+
await writeJsonFileAtomic(resolveLocalViewPreferencesPath(), preferences);
|
|
2825
|
+
return preferences;
|
|
2826
|
+
}
|
|
2827
|
+
function isPersistedLocalViewPreferences(value) {
|
|
2828
|
+
return value?.layout === OPENSTEER_LOCAL_VIEW_PREFERENCES_LAYOUT && value.version === OPENSTEER_LOCAL_VIEW_PREFERENCES_VERSION && (value.mode === "auto" || value.mode === "manual") && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
|
|
2829
|
+
}
|
|
2830
|
+
var LOCK_OWNER_FILE = "owner.json";
|
|
2831
|
+
var LOCK_RECLAIMER_DIR = "reclaimer";
|
|
2832
|
+
var LOCK_RETRY_DELAY_MS = 50;
|
|
2833
|
+
async function acquireDirLock(lockDirPath) {
|
|
2834
|
+
while (true) {
|
|
2835
|
+
const releaseLock = await tryAcquireDirLock(lockDirPath);
|
|
2836
|
+
if (releaseLock) {
|
|
2837
|
+
return releaseLock;
|
|
2838
|
+
}
|
|
2839
|
+
await sleep(LOCK_RETRY_DELAY_MS);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
async function tryAcquireDirLock(lockDirPath) {
|
|
2843
|
+
await mkdir(dirname(lockDirPath), { recursive: true });
|
|
2844
|
+
while (true) {
|
|
2845
|
+
const tempLockDirPath = `${lockDirPath}-${String(process.pid)}-${String(CURRENT_PROCESS_OWNER.processStartedAtMs)}-${randomUUID()}`;
|
|
2846
|
+
try {
|
|
2847
|
+
await mkdir(tempLockDirPath);
|
|
2848
|
+
await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_OWNER);
|
|
2849
|
+
try {
|
|
2850
|
+
await rename(tempLockDirPath, lockDirPath);
|
|
2851
|
+
break;
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
|
|
2854
|
+
throw error;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
} finally {
|
|
2858
|
+
await rm(tempLockDirPath, {
|
|
2859
|
+
recursive: true,
|
|
2860
|
+
force: true
|
|
2861
|
+
}).catch(() => void 0);
|
|
2862
|
+
}
|
|
2863
|
+
const owner = await readLockOwner(lockDirPath);
|
|
2864
|
+
if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
return null;
|
|
2868
|
+
}
|
|
2869
|
+
return async () => {
|
|
2870
|
+
await rm(lockDirPath, {
|
|
2871
|
+
recursive: true,
|
|
2872
|
+
force: true
|
|
2873
|
+
}).catch(() => void 0);
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
function getErrorCode(error) {
|
|
2877
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
2878
|
+
}
|
|
2879
|
+
function wasDirPublishedByAnotherProcess(error, targetDirPath) {
|
|
2880
|
+
const code = getErrorCode(error);
|
|
2881
|
+
return existsSync(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
|
|
2882
|
+
}
|
|
2883
|
+
async function writeLockOwner(lockDirPath, owner) {
|
|
2884
|
+
await writeFile(join(lockDirPath, LOCK_OWNER_FILE), JSON.stringify(owner));
|
|
2885
|
+
}
|
|
2886
|
+
async function readLockOwner(lockDirPath) {
|
|
2887
|
+
return readLockParticipant(join(lockDirPath, LOCK_OWNER_FILE));
|
|
2888
|
+
}
|
|
2889
|
+
async function readLockParticipant(filePath) {
|
|
2890
|
+
return (await readLockParticipantRecord(filePath)).owner;
|
|
2891
|
+
}
|
|
2892
|
+
async function readLockParticipantRecord(filePath) {
|
|
2893
|
+
try {
|
|
2894
|
+
const raw = await readFile(filePath, "utf8");
|
|
2895
|
+
return {
|
|
2896
|
+
exists: true,
|
|
2897
|
+
owner: parseProcessOwner(JSON.parse(raw))
|
|
2898
|
+
};
|
|
2899
|
+
} catch (error) {
|
|
2900
|
+
return {
|
|
2901
|
+
exists: getErrorCode(error) !== "ENOENT",
|
|
2902
|
+
owner: null
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
async function readLockReclaimerRecord(lockDirPath) {
|
|
2907
|
+
return readLockParticipantRecord(join(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE));
|
|
2908
|
+
}
|
|
2909
|
+
async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
|
|
2910
|
+
if (!await tryAcquireLockReclaimer(lockDirPath)) {
|
|
2911
|
+
return false;
|
|
2912
|
+
}
|
|
2913
|
+
let reclaimed = false;
|
|
2914
|
+
try {
|
|
2915
|
+
const owner = await readLockOwner(lockDirPath);
|
|
2916
|
+
if (!processOwnersEqual(owner, expectedOwner)) {
|
|
2917
|
+
return false;
|
|
2918
|
+
}
|
|
2919
|
+
if (owner && await getProcessLiveness(owner) !== "dead") {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
await rm(lockDirPath, {
|
|
2923
|
+
recursive: true,
|
|
2924
|
+
force: true
|
|
2925
|
+
}).catch(() => void 0);
|
|
2926
|
+
reclaimed = !existsSync(lockDirPath);
|
|
2927
|
+
return reclaimed;
|
|
2928
|
+
} finally {
|
|
2929
|
+
if (!reclaimed) {
|
|
2930
|
+
await rm(buildLockReclaimerDirPath(lockDirPath), {
|
|
2931
|
+
recursive: true,
|
|
2932
|
+
force: true
|
|
2933
|
+
}).catch(() => void 0);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
async function tryAcquireLockReclaimer(lockDirPath) {
|
|
2938
|
+
const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
|
|
2939
|
+
while (true) {
|
|
2940
|
+
const tempReclaimerDirPath = `${reclaimerDirPath}-${String(process.pid)}-${String(CURRENT_PROCESS_OWNER.processStartedAtMs)}-${randomUUID()}`;
|
|
2941
|
+
try {
|
|
2942
|
+
await mkdir(tempReclaimerDirPath);
|
|
2943
|
+
await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_OWNER);
|
|
2944
|
+
try {
|
|
2945
|
+
await rename(tempReclaimerDirPath, reclaimerDirPath);
|
|
2946
|
+
return true;
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
2949
|
+
return false;
|
|
2950
|
+
}
|
|
2951
|
+
if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
|
|
2952
|
+
throw error;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
} catch (error) {
|
|
2956
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
2957
|
+
return false;
|
|
2958
|
+
}
|
|
2959
|
+
throw error;
|
|
2960
|
+
} finally {
|
|
2961
|
+
await rm(tempReclaimerDirPath, {
|
|
2962
|
+
recursive: true,
|
|
2963
|
+
force: true
|
|
2964
|
+
}).catch(() => void 0);
|
|
2965
|
+
}
|
|
2966
|
+
const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
|
|
2967
|
+
if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
|
|
2971
|
+
return false;
|
|
2972
|
+
}
|
|
2973
|
+
await rm(reclaimerDirPath, {
|
|
2974
|
+
recursive: true,
|
|
2975
|
+
force: true
|
|
2976
|
+
}).catch(() => void 0);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
function buildLockReclaimerDirPath(lockDirPath) {
|
|
2980
|
+
return join(lockDirPath, LOCK_RECLAIMER_DIR);
|
|
2981
|
+
}
|
|
2982
|
+
async function sleep(ms) {
|
|
2983
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// src/local-view/service.ts
|
|
2987
|
+
var LOCAL_VIEW_STARTUP_TIMEOUT_MS = 1e4;
|
|
2988
|
+
var LOCAL_VIEW_STARTUP_POLL_MS = 100;
|
|
2989
|
+
var LOCAL_VIEW_STOP_TIMEOUT_MS = 1e4;
|
|
2990
|
+
async function ensureLocalViewServiceRunning() {
|
|
2991
|
+
const current = await readReachableLocalViewServiceState();
|
|
2992
|
+
if (current !== void 0) {
|
|
2993
|
+
return current;
|
|
2994
|
+
}
|
|
2995
|
+
const releaseLock = await acquireDirLock(resolveLocalViewServiceLockDir());
|
|
2996
|
+
try {
|
|
2997
|
+
const lockedState = await readReachableLocalViewServiceState();
|
|
2998
|
+
if (lockedState !== void 0) {
|
|
2999
|
+
return lockedState;
|
|
3000
|
+
}
|
|
3001
|
+
await spawnLocalViewService();
|
|
3002
|
+
const started = await waitForLocalViewService();
|
|
3003
|
+
if (!started) {
|
|
3004
|
+
throw new Error("Timed out while starting the local view service.");
|
|
3005
|
+
}
|
|
3006
|
+
return started;
|
|
3007
|
+
} finally {
|
|
3008
|
+
await releaseLock();
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
async function stopLocalViewService() {
|
|
3012
|
+
const state = await readLocalViewServiceState();
|
|
3013
|
+
if (state === void 0 || !await isLocalViewServiceStateLive(state)) {
|
|
3014
|
+
await clearLocalViewServiceState(
|
|
3015
|
+
state === void 0 ? void 0 : { pid: state.pid, token: state.token }
|
|
3016
|
+
);
|
|
3017
|
+
return false;
|
|
3018
|
+
}
|
|
3019
|
+
const liveState = state;
|
|
3020
|
+
const stopRequested = await requestLocalViewServiceStop(liveState).catch(() => false);
|
|
3021
|
+
if (!stopRequested && await getLocalViewServiceStateLiveness(liveState) !== "dead") {
|
|
3022
|
+
process.kill(liveState.pid);
|
|
3023
|
+
}
|
|
3024
|
+
await waitForLocalViewServiceStop(liveState);
|
|
3025
|
+
await clearLocalViewServiceState({ pid: liveState.pid, token: liveState.token });
|
|
3026
|
+
return true;
|
|
3027
|
+
}
|
|
3028
|
+
function buildLocalViewSessionUrl(args) {
|
|
3029
|
+
if (!args.sessionId) {
|
|
3030
|
+
return args.baseUrl;
|
|
3031
|
+
}
|
|
3032
|
+
return `${args.baseUrl}#session=${encodeURIComponent(args.sessionId)}`;
|
|
3033
|
+
}
|
|
3034
|
+
async function requestLocalViewServiceStop(state) {
|
|
3035
|
+
const response = await fetch(new URL("/api/service/stop", state.url), {
|
|
3036
|
+
method: "POST",
|
|
3037
|
+
headers: {
|
|
3038
|
+
"x-opensteer-local-token": state.token
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
return response.ok;
|
|
3042
|
+
}
|
|
3043
|
+
async function waitForLocalViewServiceStop(state) {
|
|
3044
|
+
const deadline = Date.now() + LOCAL_VIEW_STOP_TIMEOUT_MS;
|
|
3045
|
+
while (Date.now() < deadline) {
|
|
3046
|
+
if (await getLocalViewServiceStateLiveness(state) === "dead") {
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
await delay(LOCAL_VIEW_STARTUP_POLL_MS);
|
|
3050
|
+
}
|
|
3051
|
+
throw new Error("Timed out while stopping the local view service.");
|
|
3052
|
+
}
|
|
3053
|
+
function spawnLocalViewService() {
|
|
3054
|
+
const command = resolveLocalViewSpawnCommand();
|
|
3055
|
+
const child = spawn(command.executable, command.args, {
|
|
3056
|
+
cwd: process.cwd(),
|
|
3057
|
+
env: {
|
|
3058
|
+
...process.env,
|
|
3059
|
+
...command.env ?? {},
|
|
3060
|
+
OPENSTEER_LOCAL_VIEW_BOOT_TOKEN: process.env.OPENSTEER_LOCAL_VIEW_BOOT_TOKEN ?? randomBytes(24).toString("hex")
|
|
3061
|
+
},
|
|
3062
|
+
detached: process.platform !== "win32",
|
|
3063
|
+
stdio: "ignore"
|
|
3064
|
+
});
|
|
3065
|
+
child.unref();
|
|
3066
|
+
}
|
|
3067
|
+
async function waitForLocalViewService() {
|
|
3068
|
+
const deadline = Date.now() + LOCAL_VIEW_STARTUP_TIMEOUT_MS;
|
|
3069
|
+
while (Date.now() < deadline) {
|
|
3070
|
+
const state = await readReachableLocalViewServiceState();
|
|
3071
|
+
if (state !== void 0) {
|
|
3072
|
+
return state;
|
|
3073
|
+
}
|
|
3074
|
+
await delay(LOCAL_VIEW_STARTUP_POLL_MS);
|
|
3075
|
+
}
|
|
3076
|
+
return void 0;
|
|
3077
|
+
}
|
|
3078
|
+
async function readReachableLocalViewServiceState() {
|
|
3079
|
+
const state = await readLocalViewServiceState();
|
|
3080
|
+
if (state === void 0 || !await isLocalViewServiceStateLive(state)) {
|
|
3081
|
+
return void 0;
|
|
3082
|
+
}
|
|
3083
|
+
return await isLocalViewServiceReachable(state.url, state.token) ? state : void 0;
|
|
3084
|
+
}
|
|
3085
|
+
async function isLocalViewServiceReachable(baseUrl, token) {
|
|
3086
|
+
try {
|
|
3087
|
+
const response = await fetch(new URL("/api/health", baseUrl), {
|
|
3088
|
+
headers: {
|
|
3089
|
+
"x-opensteer-local-token": token
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
return response.ok;
|
|
3093
|
+
} catch {
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
function resolveLocalViewSpawnCommand() {
|
|
3098
|
+
const moduleDir = path7.dirname(fileURLToPath(import.meta.url));
|
|
3099
|
+
const distServicePath = findExistingPath([
|
|
3100
|
+
path7.join(moduleDir, "local-view", "serve-entry.js"),
|
|
3101
|
+
path7.join(moduleDir, "serve-entry.js"),
|
|
3102
|
+
path7.join(moduleDir, "..", "local-view", "serve-entry.js")
|
|
3103
|
+
]);
|
|
3104
|
+
if (distServicePath) {
|
|
3105
|
+
return {
|
|
3106
|
+
executable: process.execPath,
|
|
3107
|
+
args: [distServicePath]
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
const distCliPath = findExistingPath([
|
|
3111
|
+
path7.join(moduleDir, "cli", "bin.js"),
|
|
3112
|
+
path7.join(moduleDir, "..", "cli", "bin.js")
|
|
3113
|
+
]);
|
|
3114
|
+
if (distCliPath) {
|
|
3115
|
+
return {
|
|
3116
|
+
executable: process.execPath,
|
|
3117
|
+
args: [distCliPath, "view", "serve"]
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
const srcServicePath = findExistingPath([
|
|
3121
|
+
path7.join(moduleDir, "serve-entry.ts"),
|
|
3122
|
+
path7.join(moduleDir, "..", "local-view", "serve-entry.ts"),
|
|
3123
|
+
path7.join(moduleDir, "..", "src", "local-view", "serve-entry.ts")
|
|
3124
|
+
]);
|
|
3125
|
+
if (srcServicePath) {
|
|
3126
|
+
const require2 = createRequire(import.meta.url);
|
|
3127
|
+
const tsxLoaderPath = require2.resolve("tsx");
|
|
3128
|
+
const tsconfigPath = findNearestTsconfig(path7.resolve(moduleDir, "..", "..", ".."));
|
|
3129
|
+
return {
|
|
3130
|
+
executable: process.execPath,
|
|
3131
|
+
args: ["--import", tsxLoaderPath, srcServicePath],
|
|
3132
|
+
...tsconfigPath ? { env: { TSX_TSCONFIG_PATH: tsconfigPath } } : {}
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
const srcCliPath = findExistingPath([
|
|
3136
|
+
path7.join(moduleDir, "..", "cli", "bin.ts"),
|
|
3137
|
+
path7.join(moduleDir, "..", "src", "cli", "bin.ts")
|
|
3138
|
+
]);
|
|
3139
|
+
if (srcCliPath) {
|
|
3140
|
+
const require2 = createRequire(import.meta.url);
|
|
3141
|
+
const tsxLoaderPath = require2.resolve("tsx");
|
|
3142
|
+
const tsconfigPath = findNearestTsconfig(path7.resolve(moduleDir, "..", "..", ".."));
|
|
3143
|
+
return {
|
|
3144
|
+
executable: process.execPath,
|
|
3145
|
+
args: ["--import", tsxLoaderPath, srcCliPath, "view", "serve"],
|
|
3146
|
+
...tsconfigPath ? { env: { TSX_TSCONFIG_PATH: tsconfigPath } } : {}
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
throw new Error(`Could not resolve the Opensteer CLI entrypoint from ${moduleDir}.`);
|
|
3150
|
+
}
|
|
3151
|
+
function findExistingPath(candidates) {
|
|
3152
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
3153
|
+
}
|
|
3154
|
+
function findNearestTsconfig(startDir) {
|
|
3155
|
+
let currentDir = startDir;
|
|
3156
|
+
while (true) {
|
|
3157
|
+
const candidate = path7.join(currentDir, "tsconfig.json");
|
|
3158
|
+
if (existsSync(candidate)) {
|
|
3159
|
+
return candidate;
|
|
3160
|
+
}
|
|
3161
|
+
const parentDir = path7.dirname(currentDir);
|
|
3162
|
+
if (parentDir === currentDir) {
|
|
3163
|
+
return void 0;
|
|
3164
|
+
}
|
|
3165
|
+
currentDir = parentDir;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
function delay(ms) {
|
|
3169
|
+
return new Promise((resolve2) => {
|
|
3170
|
+
setTimeout(resolve2, ms);
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// src/local-view/registration.ts
|
|
3175
|
+
async function bestEffortRegisterLocalViewSession(input) {
|
|
3176
|
+
try {
|
|
3177
|
+
const manifest = createLocalViewSessionManifest(input);
|
|
3178
|
+
await writeLocalViewSessionManifest(manifest);
|
|
3179
|
+
if (await resolveLocalViewMode() === "auto") {
|
|
3180
|
+
void ensureLocalViewServiceRunning().catch(() => void 0);
|
|
3181
|
+
}
|
|
3182
|
+
return manifest;
|
|
3183
|
+
} catch {
|
|
3184
|
+
return void 0;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
async function bestEffortUnregisterLocalViewSession(sessionId) {
|
|
3188
|
+
if (!sessionId) {
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
await deleteLocalViewSessionManifest(sessionId).catch(() => void 0);
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
// src/browser-manager.ts
|
|
3195
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
3196
|
+
var DEVTOOLS_POLL_INTERVAL_MS = 50;
|
|
3197
|
+
var TEMPORARY_WORKSPACE_PREFIX = "opensteer-temporary-workspace-";
|
|
3198
|
+
var BROWSER_CLOSE_TIMEOUT_MS = 5e3;
|
|
3199
|
+
var OpensteerBrowserManager = class {
|
|
3200
|
+
mode;
|
|
3201
|
+
engineName;
|
|
3202
|
+
rootPath;
|
|
3203
|
+
workspace;
|
|
3204
|
+
cleanupRootOnDisconnect;
|
|
3205
|
+
browserOptions;
|
|
3206
|
+
launchOptions;
|
|
3207
|
+
contextOptions;
|
|
3208
|
+
workspaceStore;
|
|
3209
|
+
constructor(options = {}) {
|
|
3210
|
+
this.workspace = normalizeWorkspace(options.workspace);
|
|
3211
|
+
this.mode = resolveBrowserMode(this.workspace, options.browser);
|
|
3212
|
+
this.browserOptions = isAttachBrowserOptions(options.browser) ? options.browser : void 0;
|
|
3213
|
+
this.launchOptions = options.launch;
|
|
3214
|
+
this.contextOptions = normalizeBrowserContextOptions(options.context);
|
|
3215
|
+
this.engineName = options.engineName ?? DEFAULT_OPENSTEER_ENGINE;
|
|
3216
|
+
assertSupportedEngineOptions({
|
|
3217
|
+
engineName: this.engineName,
|
|
3218
|
+
...options.browser === void 0 ? {} : { browser: options.browser },
|
|
3219
|
+
...this.contextOptions === void 0 ? {} : { context: this.contextOptions }
|
|
3220
|
+
});
|
|
3221
|
+
this.rootPath = options.rootPath ?? (this.workspace === void 0 ? path7.join(tmpdir(), `${TEMPORARY_WORKSPACE_PREFIX}${randomUUID()}`) : resolveFilesystemWorkspacePath({
|
|
3222
|
+
rootDir: path7.resolve(options.rootDir ?? process.cwd()),
|
|
3223
|
+
workspace: this.workspace
|
|
3224
|
+
}));
|
|
3225
|
+
this.cleanupRootOnDisconnect = this.workspace === void 0;
|
|
3226
|
+
}
|
|
3227
|
+
async createEngine() {
|
|
3228
|
+
if (this.mode === "persistent") {
|
|
3229
|
+
const effectiveEngine = await this.resolveLivePersistentEngineName() ?? this.engineName;
|
|
3230
|
+
if (effectiveEngine === "abp") {
|
|
3231
|
+
return this.createAbpEngine();
|
|
3232
|
+
}
|
|
3233
|
+
return this.createPersistentEngine();
|
|
3234
|
+
}
|
|
3235
|
+
if (this.engineName === "abp") {
|
|
3236
|
+
return this.createAbpEngine();
|
|
3237
|
+
}
|
|
3238
|
+
if (this.mode === "temporary") {
|
|
3239
|
+
return this.createTemporaryEngine();
|
|
3240
|
+
}
|
|
3241
|
+
if (this.mode === "attach") {
|
|
3242
|
+
return this.createAttachEngine();
|
|
3243
|
+
}
|
|
3244
|
+
return this.createPersistentEngine();
|
|
3245
|
+
}
|
|
3246
|
+
async status() {
|
|
3247
|
+
if (this.mode === "temporary") {
|
|
3248
|
+
return {
|
|
3249
|
+
mode: "temporary",
|
|
3250
|
+
engine: this.engineName,
|
|
3251
|
+
live: false
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
const liveRecord = await this.readLivePersistentBrowser(await this.ensureWorkspaceStore());
|
|
3255
|
+
return {
|
|
3256
|
+
mode: this.mode,
|
|
3257
|
+
engine: liveRecord?.engine ?? this.engineName,
|
|
3258
|
+
...this.workspace === void 0 ? {} : { workspace: this.workspace },
|
|
3259
|
+
live: liveRecord !== void 0
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
async clonePersistentBrowser(input) {
|
|
3263
|
+
this.requirePersistentMode("clone");
|
|
3264
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3265
|
+
return workspace.lock(async () => {
|
|
3266
|
+
await this.assertPersistentBrowserClosed(workspace);
|
|
3267
|
+
await rm(workspace.browserPath, { recursive: true, force: true });
|
|
3268
|
+
await ensureDirectory(workspace.browserUserDataDir);
|
|
3269
|
+
await clearChromeSingletonEntries(workspace.browserUserDataDir);
|
|
3270
|
+
await createBrowserProfileSnapshot({
|
|
3271
|
+
sourceUserDataDir: input.sourceUserDataDir,
|
|
3272
|
+
targetUserDataDir: workspace.browserUserDataDir,
|
|
3273
|
+
...input.sourceProfileDirectory === void 0 ? {} : { profileDirectory: input.sourceProfileDirectory },
|
|
3274
|
+
copyMode: "full"
|
|
3275
|
+
});
|
|
3276
|
+
const manifest = {
|
|
3277
|
+
mode: "persistent",
|
|
3278
|
+
createdAt: Date.now(),
|
|
3279
|
+
updatedAt: Date.now(),
|
|
3280
|
+
userDataDir: "browser/user-data",
|
|
3281
|
+
bootstrap: {
|
|
3282
|
+
kind: "cloneLocalProfile",
|
|
3283
|
+
sourceUserDataDir: path7.resolve(input.sourceUserDataDir),
|
|
3284
|
+
...input.sourceProfileDirectory === void 0 ? {} : { sourceProfileDirectory: input.sourceProfileDirectory }
|
|
3285
|
+
}
|
|
3286
|
+
};
|
|
3287
|
+
await writeJsonFileAtomic(workspace.browserManifestPath, manifest);
|
|
3288
|
+
return manifest;
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
async reset() {
|
|
3292
|
+
this.requirePersistentMode("reset");
|
|
3293
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3294
|
+
await workspace.lock(async () => {
|
|
3295
|
+
await this.closePersistentBrowser(workspace);
|
|
3296
|
+
await rm(resolveAbpSessionDir(workspace), { recursive: true, force: true });
|
|
3297
|
+
await rm(workspace.browserPath, { recursive: true, force: true });
|
|
3298
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local");
|
|
3299
|
+
await ensureDirectory(workspace.browserUserDataDir);
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
async delete() {
|
|
3303
|
+
this.requirePersistentMode("delete");
|
|
3304
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3305
|
+
await workspace.lock(async () => {
|
|
3306
|
+
await this.closePersistentBrowser(workspace);
|
|
3307
|
+
await rm(resolveAbpSessionDir(workspace), { recursive: true, force: true });
|
|
3308
|
+
await rm(workspace.browserPath, { recursive: true, force: true });
|
|
3309
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local");
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
async close() {
|
|
3313
|
+
if (this.mode !== "persistent") {
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3317
|
+
await workspace.lock(async () => {
|
|
3318
|
+
await this.closePersistentBrowser(workspace);
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
async createAbpEngine() {
|
|
3322
|
+
if (this.mode === "attach") {
|
|
3323
|
+
throw new Error(
|
|
3324
|
+
'ABP engine does not support browser.mode="attach". Use the Playwright engine for attach flows.'
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
if (this.mode === "temporary") {
|
|
3328
|
+
return this.createTemporaryAbpEngine();
|
|
3329
|
+
}
|
|
3330
|
+
return this.createPersistentAbpEngine();
|
|
3331
|
+
}
|
|
3332
|
+
async createTemporaryAbpEngine() {
|
|
3333
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3334
|
+
await clearChromeSingletonEntries(workspace.browserUserDataDir);
|
|
3335
|
+
const { createAbpBrowserCoreEngine } = await loadAbpModule();
|
|
3336
|
+
const launch = toAbpLaunchOptions({
|
|
3337
|
+
...this.launchOptions === void 0 ? {} : { launch: this.launchOptions },
|
|
3338
|
+
...this.contextOptions === void 0 ? {} : { context: this.contextOptions },
|
|
3339
|
+
userDataDir: workspace.browserUserDataDir,
|
|
3340
|
+
sessionDir: resolveAbpSessionDir(workspace)
|
|
3341
|
+
});
|
|
3342
|
+
return await createAbpBrowserCoreEngine(
|
|
3343
|
+
launch === void 0 ? {} : { launch }
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
async createPersistentAbpEngine() {
|
|
3347
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3348
|
+
return workspace.lock(async () => {
|
|
3349
|
+
const live = await this.readLivePersistentBrowser(workspace);
|
|
3350
|
+
if (live !== void 0) {
|
|
3351
|
+
if (live.engine !== "abp") {
|
|
3352
|
+
throw new Error(
|
|
3353
|
+
`workspace "${this.workspace}" already has a live ${live.engine} browser. Close it before reopening with engine "abp".`
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
await bestEffortRegisterLocalViewSession({
|
|
3357
|
+
rootPath: workspace.rootPath,
|
|
3358
|
+
...this.workspace === void 0 ? {} : { workspace: this.workspace },
|
|
3359
|
+
live: toPersistedLocalBrowserSessionRecord(this.workspace, live),
|
|
3360
|
+
ownership: "owned"
|
|
3361
|
+
});
|
|
3362
|
+
return this.createAdoptedAbpEngine(live);
|
|
3363
|
+
}
|
|
3364
|
+
await this.ensurePersistentBrowserManifest(workspace);
|
|
3365
|
+
await clearChromeSingletonEntries(workspace.browserUserDataDir);
|
|
3366
|
+
const { launchAbpProcess } = await loadAbpModule();
|
|
3367
|
+
const launch = toAbpLaunchOptions({
|
|
3368
|
+
...this.launchOptions === void 0 ? {} : { launch: this.launchOptions },
|
|
3369
|
+
...this.contextOptions === void 0 ? {} : { context: this.contextOptions },
|
|
3370
|
+
userDataDir: workspace.browserUserDataDir,
|
|
3371
|
+
sessionDir: resolveAbpSessionDir(workspace)
|
|
3372
|
+
});
|
|
3373
|
+
const launched = await launchAbpProcess({
|
|
3374
|
+
port: await allocateEphemeralPort(),
|
|
3375
|
+
userDataDir: workspace.browserUserDataDir,
|
|
3376
|
+
sessionDir: resolveAbpSessionDir(workspace),
|
|
3377
|
+
headless: launch?.headless ?? true,
|
|
3378
|
+
args: launch?.args ?? [],
|
|
3379
|
+
verbose: false,
|
|
3380
|
+
...launch?.browserExecutablePath === void 0 ? {} : { browserExecutablePath: launch.browserExecutablePath }
|
|
3381
|
+
});
|
|
3382
|
+
const liveRecord = {
|
|
3383
|
+
mode: "persistent",
|
|
3384
|
+
engine: "abp",
|
|
3385
|
+
baseUrl: launched.baseUrl,
|
|
3386
|
+
remoteDebuggingUrl: launched.remoteDebuggingUrl,
|
|
3387
|
+
pid: launched.process.pid ?? 0,
|
|
3388
|
+
startedAt: Date.now(),
|
|
3389
|
+
userDataDir: workspace.browserUserDataDir,
|
|
3390
|
+
sessionDir: resolveAbpSessionDir(workspace),
|
|
3391
|
+
...launch?.browserExecutablePath === void 0 ? {} : { executablePath: launch.browserExecutablePath }
|
|
3392
|
+
};
|
|
3393
|
+
await this.writeLivePersistentBrowser(workspace, liveRecord);
|
|
3394
|
+
const persistedLiveRecord = toPersistedLocalBrowserSessionRecord(this.workspace, liveRecord);
|
|
3395
|
+
await bestEffortRegisterLocalViewSession({
|
|
3396
|
+
rootPath: workspace.rootPath,
|
|
3397
|
+
...this.workspace === void 0 ? {} : { workspace: this.workspace },
|
|
3398
|
+
live: persistedLiveRecord,
|
|
3399
|
+
ownership: "owned"
|
|
3400
|
+
});
|
|
3401
|
+
try {
|
|
3402
|
+
return await this.createAdoptedAbpEngine(liveRecord);
|
|
3403
|
+
} catch (error) {
|
|
3404
|
+
await this.unregisterLocalViewSessionForRecord(workspace.rootPath, persistedLiveRecord);
|
|
3405
|
+
await terminateProcess(launched.process.pid ?? 0).catch(() => void 0);
|
|
3406
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
|
|
3407
|
+
throw error;
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
async createAdoptedAbpEngine(live) {
|
|
3412
|
+
if (live.baseUrl === void 0 || live.remoteDebuggingUrl === void 0) {
|
|
3413
|
+
throw new Error("workspace live browser record is missing ABP connection metadata.");
|
|
3414
|
+
}
|
|
3415
|
+
const { createAbpBrowserCoreEngine } = await loadAbpModule();
|
|
3416
|
+
return await createAbpBrowserCoreEngine({
|
|
3417
|
+
browser: {
|
|
3418
|
+
baseUrl: live.baseUrl,
|
|
3419
|
+
remoteDebuggingUrl: live.remoteDebuggingUrl
|
|
3420
|
+
}
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
async createTemporaryEngine() {
|
|
3424
|
+
const userDataDir = await mkdtemp(path7.join(tmpdir(), "opensteer-temporary-browser-"));
|
|
3425
|
+
await clearChromeSingletonEntries(userDataDir);
|
|
3426
|
+
const launched = await launchOwnedBrowser({
|
|
3427
|
+
userDataDir,
|
|
3428
|
+
...this.launchOptions === void 0 ? {} : { launch: this.launchOptions },
|
|
3429
|
+
...this.contextOptions?.viewport === void 0 ? {} : { viewport: this.contextOptions.viewport }
|
|
3430
|
+
});
|
|
3431
|
+
const temporaryLiveRecord = {
|
|
3432
|
+
layout: "opensteer-session",
|
|
3433
|
+
version: 1,
|
|
3434
|
+
provider: "local",
|
|
3435
|
+
engine: "playwright",
|
|
3436
|
+
endpoint: launched.endpoint,
|
|
3437
|
+
pid: launched.pid,
|
|
3438
|
+
startedAt: Date.now(),
|
|
3439
|
+
updatedAt: Date.now(),
|
|
3440
|
+
executablePath: launched.executablePath,
|
|
3441
|
+
userDataDir
|
|
3442
|
+
};
|
|
3443
|
+
await writePersistedSessionRecord(this.rootPath, temporaryLiveRecord);
|
|
3444
|
+
const localViewManifest = await bestEffortRegisterLocalViewSession({
|
|
3445
|
+
rootPath: this.rootPath,
|
|
3446
|
+
live: temporaryLiveRecord,
|
|
3447
|
+
ownership: "owned"
|
|
3448
|
+
});
|
|
3449
|
+
try {
|
|
3450
|
+
return await this.createAttachedEngine({
|
|
3451
|
+
endpoint: launched.endpoint,
|
|
3452
|
+
freshTab: false,
|
|
3453
|
+
onDispose: async () => {
|
|
3454
|
+
await bestEffortUnregisterLocalViewSession(localViewManifest?.sessionId);
|
|
3455
|
+
await clearPersistedSessionRecord(this.rootPath, "local").catch(() => void 0);
|
|
3456
|
+
await terminateProcess(launched.pid).catch(() => void 0);
|
|
3457
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3458
|
+
}
|
|
3459
|
+
});
|
|
3460
|
+
} catch (error) {
|
|
3461
|
+
await bestEffortUnregisterLocalViewSession(localViewManifest?.sessionId);
|
|
3462
|
+
await clearPersistedSessionRecord(this.rootPath, "local").catch(() => void 0);
|
|
3463
|
+
await terminateProcess(launched.pid).catch(() => void 0);
|
|
3464
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3465
|
+
throw error;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
async createAttachEngine() {
|
|
3469
|
+
const endpoint = await resolveAttachEndpoint(this.browserOptions);
|
|
3470
|
+
return this.createAttachedEngine({
|
|
3471
|
+
endpoint,
|
|
3472
|
+
...this.browserOptions?.headers === void 0 ? {} : { headers: this.browserOptions.headers },
|
|
3473
|
+
freshTab: this.browserOptions?.freshTab ?? true,
|
|
3474
|
+
onDispose: async () => void 0
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
async createPersistentEngine() {
|
|
3478
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3479
|
+
return workspace.lock(async () => {
|
|
3480
|
+
const live = await this.readLivePersistentBrowser(workspace);
|
|
3481
|
+
if (live) {
|
|
3482
|
+
if (live.engine !== "playwright") {
|
|
3483
|
+
throw new Error(
|
|
3484
|
+
`workspace "${this.workspace}" already has a live ${live.engine} browser. Close it before reopening with engine "playwright".`
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
if (live.endpoint === void 0) {
|
|
3488
|
+
throw new Error("workspace live browser record is missing a DevTools endpoint.");
|
|
3489
|
+
}
|
|
3490
|
+
await bestEffortRegisterLocalViewSession({
|
|
3491
|
+
rootPath: workspace.rootPath,
|
|
3492
|
+
...this.workspace === void 0 ? {} : { workspace: this.workspace },
|
|
3493
|
+
live: toPersistedLocalBrowserSessionRecord(this.workspace, live),
|
|
3494
|
+
ownership: "owned"
|
|
3495
|
+
});
|
|
3496
|
+
return this.createAttachedEngine({
|
|
3497
|
+
endpoint: live.endpoint,
|
|
3498
|
+
freshTab: false,
|
|
3499
|
+
onDispose: async () => void 0
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
await this.ensurePersistentBrowserManifest(workspace);
|
|
3503
|
+
const launched = await launchOwnedBrowser({
|
|
3504
|
+
userDataDir: workspace.browserUserDataDir,
|
|
3505
|
+
...this.launchOptions === void 0 ? {} : { launch: this.launchOptions },
|
|
3506
|
+
...this.contextOptions?.viewport === void 0 ? {} : { viewport: this.contextOptions.viewport }
|
|
3507
|
+
});
|
|
3508
|
+
const liveRecord = {
|
|
3509
|
+
mode: "persistent",
|
|
3510
|
+
engine: "playwright",
|
|
3511
|
+
endpoint: launched.endpoint,
|
|
3512
|
+
pid: launched.pid,
|
|
3513
|
+
startedAt: Date.now(),
|
|
3514
|
+
executablePath: launched.executablePath,
|
|
3515
|
+
userDataDir: workspace.browserUserDataDir
|
|
3516
|
+
};
|
|
3517
|
+
await this.writeLivePersistentBrowser(workspace, liveRecord);
|
|
3518
|
+
const persistedLiveRecord = toPersistedLocalBrowserSessionRecord(this.workspace, liveRecord);
|
|
3519
|
+
await bestEffortRegisterLocalViewSession({
|
|
3520
|
+
rootPath: workspace.rootPath,
|
|
3521
|
+
...this.workspace === void 0 ? {} : { workspace: this.workspace },
|
|
3522
|
+
live: persistedLiveRecord,
|
|
3523
|
+
ownership: "owned"
|
|
3524
|
+
});
|
|
3525
|
+
try {
|
|
3526
|
+
return await this.createAttachedEngine({
|
|
3527
|
+
endpoint: launched.endpoint,
|
|
3528
|
+
freshTab: false,
|
|
3529
|
+
onDispose: async () => void 0
|
|
3530
|
+
});
|
|
3531
|
+
} catch (error) {
|
|
3532
|
+
await this.unregisterLocalViewSessionForRecord(workspace.rootPath, persistedLiveRecord);
|
|
3533
|
+
await terminateProcess(launched.pid).catch(() => void 0);
|
|
3534
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
|
|
3535
|
+
throw error;
|
|
3536
|
+
}
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
async createAttachedEngine(input) {
|
|
3540
|
+
const browser = await connectPlaywrightChromiumBrowser({
|
|
3541
|
+
url: input.endpoint,
|
|
3542
|
+
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
3543
|
+
});
|
|
3544
|
+
try {
|
|
3545
|
+
const context = browser.contexts()[0];
|
|
3546
|
+
if (!context) {
|
|
3547
|
+
throw new Error("Connected browser did not expose a Chromium browser context.");
|
|
3548
|
+
}
|
|
3549
|
+
const page = input.freshTab || context.pages()[0] === void 0 ? await context.newPage() : context.pages()[0];
|
|
3550
|
+
await page.bringToFront?.();
|
|
3551
|
+
const stealthProfile = resolveStealthProfile(this.contextOptions?.stealthProfile);
|
|
3552
|
+
await injectBrowserStealthScripts(
|
|
3553
|
+
context,
|
|
3554
|
+
stealthProfile === void 0 ? {} : {
|
|
3555
|
+
profile: stealthProfile,
|
|
3556
|
+
page
|
|
3557
|
+
}
|
|
3558
|
+
);
|
|
3559
|
+
const engine = await createPlaywrightBrowserCoreEngine({
|
|
3560
|
+
browser,
|
|
3561
|
+
attachedContext: context,
|
|
3562
|
+
attachedPage: page,
|
|
3563
|
+
closeAttachedContextOnSessionClose: false,
|
|
3564
|
+
closeBrowserOnDispose: false,
|
|
3565
|
+
...this.contextOptions === void 0 ? {} : { context: toEngineBrowserContextOptions(this.contextOptions) }
|
|
3566
|
+
});
|
|
3567
|
+
const originalDispose = engine.dispose?.bind(engine);
|
|
3568
|
+
const originalAsyncDispose = engine[Symbol.asyncDispose]?.bind(engine);
|
|
3569
|
+
let disposed = false;
|
|
3570
|
+
const disposeConnection = async () => {
|
|
3571
|
+
if (disposed) {
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
disposed = true;
|
|
3575
|
+
try {
|
|
3576
|
+
await originalDispose?.();
|
|
3577
|
+
} finally {
|
|
3578
|
+
await disconnectPlaywrightChromiumBrowser(browser).catch(() => void 0);
|
|
3579
|
+
await input.onDispose().catch(() => void 0);
|
|
3580
|
+
}
|
|
3581
|
+
};
|
|
3582
|
+
engine.dispose = disposeConnection;
|
|
3583
|
+
engine[Symbol.asyncDispose] = async () => {
|
|
3584
|
+
if (disposed) {
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
disposed = true;
|
|
3588
|
+
try {
|
|
3589
|
+
await originalAsyncDispose?.();
|
|
3590
|
+
} finally {
|
|
3591
|
+
await disconnectPlaywrightChromiumBrowser(browser).catch(() => void 0);
|
|
3592
|
+
await input.onDispose().catch(() => void 0);
|
|
3593
|
+
}
|
|
3594
|
+
};
|
|
3595
|
+
return engine;
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
await disconnectPlaywrightChromiumBrowser(browser).catch(() => void 0);
|
|
3598
|
+
throw error;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
async ensureWorkspaceStore() {
|
|
3602
|
+
this.workspaceStore ??= await createFilesystemOpensteerWorkspace({
|
|
3603
|
+
rootPath: this.rootPath,
|
|
3604
|
+
...this.workspace === void 0 ? {} : { workspace: this.workspace },
|
|
3605
|
+
scope: this.workspace === void 0 ? "temporary" : "workspace"
|
|
3606
|
+
});
|
|
3607
|
+
return this.workspaceStore;
|
|
3608
|
+
}
|
|
3609
|
+
async ensurePersistentBrowserManifest(workspace) {
|
|
3610
|
+
const existing = await this.readBrowserManifest(workspace);
|
|
3611
|
+
if (existing) {
|
|
3612
|
+
return existing;
|
|
3613
|
+
}
|
|
3614
|
+
const now = Date.now();
|
|
3615
|
+
const manifest = {
|
|
3616
|
+
mode: "persistent",
|
|
3617
|
+
createdAt: now,
|
|
3618
|
+
updatedAt: now,
|
|
3619
|
+
userDataDir: "browser/user-data",
|
|
3620
|
+
bootstrap: {
|
|
3621
|
+
kind: "empty"
|
|
3622
|
+
}
|
|
3623
|
+
};
|
|
3624
|
+
await ensureDirectory(workspace.browserUserDataDir);
|
|
3625
|
+
await writeJsonFileAtomic(workspace.browserManifestPath, manifest);
|
|
3626
|
+
return manifest;
|
|
3627
|
+
}
|
|
3628
|
+
async readBrowserManifest(workspace) {
|
|
3629
|
+
if (!await pathExists(workspace.browserManifestPath)) {
|
|
3630
|
+
return void 0;
|
|
3631
|
+
}
|
|
3632
|
+
return readJsonFile(workspace.browserManifestPath);
|
|
3633
|
+
}
|
|
3634
|
+
async readLivePersistentBrowser(workspace) {
|
|
3635
|
+
const live = await this.readStoredLiveBrowser(workspace);
|
|
3636
|
+
if (live === void 0) {
|
|
3637
|
+
return void 0;
|
|
3638
|
+
}
|
|
3639
|
+
if (!isProcessRunning(live.pid)) {
|
|
3640
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
|
|
3641
|
+
return void 0;
|
|
3642
|
+
}
|
|
3643
|
+
if (live.engine === "playwright") {
|
|
3644
|
+
if (live.endpoint === void 0 || !await isEndpointReachable(live.endpoint)) {
|
|
3645
|
+
throw new Error(
|
|
3646
|
+
`workspace "${this.workspace}" browser process ${String(live.pid)} is still running, but its DevTools endpoint is unavailable`
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
return live;
|
|
3650
|
+
}
|
|
3651
|
+
if (live.baseUrl === void 0 || !await isAbpBaseUrlReachable(live.baseUrl)) {
|
|
3652
|
+
throw new Error(
|
|
3653
|
+
`workspace "${this.workspace}" browser process ${String(live.pid)} is still running, but its ABP endpoint is unavailable`
|
|
3654
|
+
);
|
|
3655
|
+
}
|
|
3656
|
+
return live;
|
|
3657
|
+
}
|
|
3658
|
+
async readStoredLiveBrowser(workspace) {
|
|
3659
|
+
const live = await readPersistedLocalBrowserSessionRecord(workspace.rootPath);
|
|
3660
|
+
return live === void 0 ? void 0 : toWorkspaceLiveBrowserRecord(live);
|
|
3661
|
+
}
|
|
3662
|
+
async resolveLivePersistentEngineName() {
|
|
3663
|
+
if (this.mode !== "persistent") {
|
|
3664
|
+
return void 0;
|
|
3665
|
+
}
|
|
3666
|
+
const workspace = await this.ensureWorkspaceStore();
|
|
3667
|
+
const live = await this.readStoredLiveBrowser(workspace);
|
|
3668
|
+
return live?.engine;
|
|
3669
|
+
}
|
|
3670
|
+
async assertPersistentBrowserClosed(workspace) {
|
|
3671
|
+
if (await this.readLivePersistentBrowser(workspace) !== void 0) {
|
|
3672
|
+
throw new Error(
|
|
3673
|
+
`workspace "${this.workspace}" already has a live browser. Close it before changing the saved profile.`
|
|
3674
|
+
);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
async closePersistentBrowser(workspace) {
|
|
3678
|
+
const live = await this.readStoredLiveBrowser(workspace);
|
|
3679
|
+
if (!live) {
|
|
3680
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
await this.unregisterLocalViewSessionForRecord(
|
|
3684
|
+
workspace.rootPath,
|
|
3685
|
+
toPersistedLocalBrowserSessionRecord(this.workspace, live)
|
|
3686
|
+
);
|
|
3687
|
+
if (live.engine === "playwright") {
|
|
3688
|
+
if (live.endpoint !== void 0) {
|
|
3689
|
+
await requestBrowserClose(live.endpoint).catch(() => void 0);
|
|
3690
|
+
}
|
|
3691
|
+
if (await waitForProcessExit(live.pid, BROWSER_CLOSE_TIMEOUT_MS)) {
|
|
3692
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
await terminateProcess(live.pid).catch(() => void 0);
|
|
3696
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
await terminateProcess(live.pid).catch(() => void 0);
|
|
3700
|
+
await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
|
|
3701
|
+
}
|
|
3702
|
+
async writeLivePersistentBrowser(workspace, live) {
|
|
3703
|
+
await writePersistedSessionRecord(
|
|
3704
|
+
workspace.rootPath,
|
|
3705
|
+
toPersistedLocalBrowserSessionRecord(this.workspace, live)
|
|
3706
|
+
);
|
|
3707
|
+
}
|
|
3708
|
+
requirePersistentMode(method) {
|
|
3709
|
+
if (this.mode !== "persistent" || this.workspace === void 0) {
|
|
3710
|
+
throw new Error(`browser.${method}() requires a persistent workspace browser.`);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
async unregisterLocalViewSessionForRecord(rootPath, record) {
|
|
3714
|
+
await bestEffortUnregisterLocalViewSession(
|
|
3715
|
+
buildLocalViewSessionId({
|
|
3716
|
+
rootPath,
|
|
3717
|
+
pid: record.pid,
|
|
3718
|
+
startedAt: record.startedAt
|
|
3719
|
+
})
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
function normalizeWorkspace(workspace) {
|
|
3724
|
+
const normalized = workspace?.trim();
|
|
3725
|
+
return normalized === void 0 || normalized.length === 0 ? void 0 : normalized;
|
|
3726
|
+
}
|
|
3727
|
+
function toPersistedLocalBrowserSessionRecord(workspace, live) {
|
|
3728
|
+
return {
|
|
3729
|
+
layout: "opensteer-session",
|
|
3730
|
+
version: 1,
|
|
3731
|
+
provider: "local",
|
|
3732
|
+
...workspace === void 0 ? {} : { workspace },
|
|
3733
|
+
engine: live.engine,
|
|
3734
|
+
...live.endpoint === void 0 ? {} : { endpoint: live.endpoint },
|
|
3735
|
+
...live.baseUrl === void 0 ? {} : { baseUrl: live.baseUrl },
|
|
3736
|
+
...live.remoteDebuggingUrl === void 0 ? {} : { remoteDebuggingUrl: live.remoteDebuggingUrl },
|
|
3737
|
+
...live.sessionDir === void 0 ? {} : { sessionDir: live.sessionDir },
|
|
3738
|
+
pid: live.pid,
|
|
3739
|
+
startedAt: live.startedAt,
|
|
3740
|
+
updatedAt: Date.now(),
|
|
3741
|
+
...live.executablePath === void 0 ? {} : { executablePath: live.executablePath },
|
|
3742
|
+
userDataDir: live.userDataDir
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
function toWorkspaceLiveBrowserRecord(record) {
|
|
3746
|
+
return {
|
|
3747
|
+
mode: "persistent",
|
|
3748
|
+
engine: record.engine,
|
|
3749
|
+
...record.endpoint === void 0 ? {} : { endpoint: record.endpoint },
|
|
3750
|
+
...record.baseUrl === void 0 ? {} : { baseUrl: record.baseUrl },
|
|
3751
|
+
...record.remoteDebuggingUrl === void 0 ? {} : { remoteDebuggingUrl: record.remoteDebuggingUrl },
|
|
3752
|
+
...record.sessionDir === void 0 ? {} : { sessionDir: record.sessionDir },
|
|
3753
|
+
pid: record.pid,
|
|
3754
|
+
startedAt: record.startedAt,
|
|
3755
|
+
...record.executablePath === void 0 ? {} : { executablePath: record.executablePath },
|
|
3756
|
+
userDataDir: record.userDataDir
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
function resolveBrowserMode(workspace, browser) {
|
|
3760
|
+
if (browser === void 0) {
|
|
3761
|
+
return workspace === void 0 ? "temporary" : "persistent";
|
|
3762
|
+
}
|
|
3763
|
+
if (browser === "temporary" || browser === "persistent") {
|
|
3764
|
+
return browser;
|
|
3765
|
+
}
|
|
3766
|
+
return "attach";
|
|
3767
|
+
}
|
|
3768
|
+
function isAttachBrowserOptions(browser) {
|
|
3769
|
+
return typeof browser === "object" && browser !== null && browser.mode === "attach";
|
|
3770
|
+
}
|
|
3771
|
+
async function resolveAttachEndpoint(browser) {
|
|
3772
|
+
const endpoint = browser?.endpoint?.trim();
|
|
3773
|
+
if (endpoint && endpoint.length > 0) {
|
|
3774
|
+
return endpoint;
|
|
3775
|
+
}
|
|
3776
|
+
const selection = await selectAttachBrowserCandidate({
|
|
3777
|
+
timeoutMs: DEFAULT_TIMEOUT_MS
|
|
3778
|
+
});
|
|
3779
|
+
return selection.endpoint;
|
|
3780
|
+
}
|
|
3781
|
+
async function launchOwnedBrowser(input) {
|
|
3782
|
+
await ensureDirectory(input.userDataDir);
|
|
3783
|
+
await clearChromeSingletonEntries(input.userDataDir);
|
|
3784
|
+
await sanitizeChromeProfile(input.userDataDir);
|
|
3785
|
+
const requestedRemoteDebuggingPort = readRequestedRemoteDebuggingPort(input.launch?.args);
|
|
3786
|
+
const executablePath = resolveChromeExecutablePath(input.launch?.executablePath);
|
|
3787
|
+
const args = buildChromeArgs(
|
|
3788
|
+
input.userDataDir,
|
|
3789
|
+
input.launch,
|
|
3790
|
+
input.viewport,
|
|
3791
|
+
requestedRemoteDebuggingPort
|
|
3792
|
+
);
|
|
3793
|
+
const child = spawn(executablePath, args, {
|
|
3794
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
3795
|
+
detached: process.platform !== "win32"
|
|
3796
|
+
});
|
|
3797
|
+
child.unref();
|
|
3798
|
+
const stderrLines = [];
|
|
3799
|
+
child.stderr?.setEncoding("utf8");
|
|
3800
|
+
child.stderr?.on("data", (chunk) => {
|
|
3801
|
+
stderrLines.push(String(chunk));
|
|
3802
|
+
});
|
|
3803
|
+
child.stderr?.unref?.();
|
|
3804
|
+
const endpoint = await waitForDevToolsEndpoint({
|
|
3805
|
+
userDataDir: input.userDataDir,
|
|
3806
|
+
timeoutMs: input.launch?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
3807
|
+
childExited: async () => child.exitCode,
|
|
3808
|
+
stderrLines,
|
|
3809
|
+
...requestedRemoteDebuggingPort !== void 0 && requestedRemoteDebuggingPort > 0 ? { requestedRemoteDebuggingPort } : {}
|
|
3810
|
+
}).catch(async (error) => {
|
|
3811
|
+
child.kill("SIGKILL");
|
|
3812
|
+
throw error;
|
|
3813
|
+
});
|
|
3814
|
+
return {
|
|
3815
|
+
endpoint,
|
|
3816
|
+
pid: child.pid ?? 0,
|
|
3817
|
+
executablePath
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
function buildChromeArgs(userDataDir, launch, viewport, requestedRemoteDebuggingPort) {
|
|
3821
|
+
const isHeadless = launch?.headless ?? true;
|
|
3822
|
+
const args = [
|
|
3823
|
+
...requestedRemoteDebuggingPort === void 0 ? ["--remote-debugging-port=0"] : [],
|
|
3824
|
+
"--no-first-run",
|
|
3825
|
+
"--no-default-browser-check",
|
|
3826
|
+
"--disable-blink-features=AutomationControlled",
|
|
3827
|
+
"--disable-background-networking",
|
|
3828
|
+
"--disable-backgrounding-occluded-windows",
|
|
3829
|
+
"--disable-component-update",
|
|
3830
|
+
"--disable-default-apps",
|
|
3831
|
+
"--disable-hang-monitor",
|
|
3832
|
+
"--disable-popup-blocking",
|
|
3833
|
+
"--disable-prompt-on-repost",
|
|
3834
|
+
"--disable-sync",
|
|
3835
|
+
"--disable-infobars",
|
|
3836
|
+
"--disable-features=Translate",
|
|
3837
|
+
"--enable-features=NetworkService,NetworkServiceInProcess",
|
|
3838
|
+
"--password-store=basic",
|
|
3839
|
+
"--use-mock-keychain",
|
|
3840
|
+
`--user-data-dir=${userDataDir}`
|
|
3841
|
+
];
|
|
3842
|
+
if (isHeadless) {
|
|
3843
|
+
args.push("--headless=new");
|
|
3844
|
+
}
|
|
3845
|
+
const hasUserWindowSize = (launch?.args ?? []).some((entry) => entry.startsWith("--window-size"));
|
|
3846
|
+
if (!hasUserWindowSize) {
|
|
3847
|
+
const width = viewport?.width ?? 1440;
|
|
3848
|
+
const height = viewport?.height ?? 900;
|
|
3849
|
+
args.push(
|
|
3850
|
+
isHeadless ? `--window-size=${String(width)},${String(height)}` : `--window-size=${String(width)},${String(height + 150)}`
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
args.push(...launch?.args ?? []);
|
|
3854
|
+
if (!(launch?.args ?? []).some((entry) => !entry.startsWith("-"))) {
|
|
3855
|
+
args.push("about:blank");
|
|
3856
|
+
}
|
|
3857
|
+
return args;
|
|
3858
|
+
}
|
|
3859
|
+
async function waitForDevToolsEndpoint(input) {
|
|
3860
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
3861
|
+
while (Date.now() < deadline) {
|
|
3862
|
+
const activePort = readDevToolsActivePort(input.userDataDir);
|
|
3863
|
+
if (activePort) {
|
|
3864
|
+
try {
|
|
3865
|
+
const inspected = await inspectCdpEndpoint({
|
|
3866
|
+
endpoint: `http://127.0.0.1:${String(activePort.port)}`,
|
|
3867
|
+
timeoutMs: Math.min(2e3, input.timeoutMs)
|
|
3868
|
+
});
|
|
3869
|
+
return inspected.endpoint;
|
|
3870
|
+
} catch {
|
|
3871
|
+
return `ws://127.0.0.1:${String(activePort.port)}${activePort.webSocketPath}`;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
if (input.requestedRemoteDebuggingPort !== void 0) {
|
|
3875
|
+
const endpoint = await tryInspectRemoteDebuggingPort(
|
|
3876
|
+
input.requestedRemoteDebuggingPort,
|
|
3877
|
+
input.timeoutMs
|
|
3878
|
+
);
|
|
3879
|
+
if (endpoint !== void 0) {
|
|
3880
|
+
return endpoint;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
const exitCode = await input.childExited();
|
|
3884
|
+
if (exitCode !== null) {
|
|
3885
|
+
throw new Error(formatChromeLaunchError(input.stderrLines));
|
|
3886
|
+
}
|
|
3887
|
+
await sleep2(DEVTOOLS_POLL_INTERVAL_MS);
|
|
3888
|
+
}
|
|
3889
|
+
throw new Error(formatChromeLaunchError(input.stderrLines));
|
|
3890
|
+
}
|
|
3891
|
+
function readRequestedRemoteDebuggingPort(args) {
|
|
3892
|
+
if (args === void 0 || args.length === 0) {
|
|
3893
|
+
return void 0;
|
|
3894
|
+
}
|
|
3895
|
+
let explicitFlagFound = false;
|
|
3896
|
+
let port;
|
|
3897
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3898
|
+
const entry = args[index];
|
|
3899
|
+
if (entry === "--remote-debugging-port") {
|
|
3900
|
+
explicitFlagFound = true;
|
|
3901
|
+
const next = args[index + 1];
|
|
3902
|
+
if (next !== void 0) {
|
|
3903
|
+
port = parseRemoteDebuggingPort(next);
|
|
3904
|
+
index += 1;
|
|
3905
|
+
}
|
|
3906
|
+
continue;
|
|
3907
|
+
}
|
|
3908
|
+
if (entry.startsWith("--remote-debugging-port=")) {
|
|
3909
|
+
explicitFlagFound = true;
|
|
3910
|
+
port = parseRemoteDebuggingPort(entry.slice("--remote-debugging-port=".length));
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
return explicitFlagFound ? port : void 0;
|
|
3914
|
+
}
|
|
3915
|
+
function parseRemoteDebuggingPort(value) {
|
|
3916
|
+
const trimmed = value.trim();
|
|
3917
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
3918
|
+
return void 0;
|
|
3919
|
+
}
|
|
3920
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
3921
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
3922
|
+
return void 0;
|
|
3923
|
+
}
|
|
3924
|
+
return parsed;
|
|
3925
|
+
}
|
|
3926
|
+
async function tryInspectRemoteDebuggingPort(port, timeoutMs) {
|
|
3927
|
+
try {
|
|
3928
|
+
const inspected = await inspectCdpEndpoint({
|
|
3929
|
+
endpoint: `http://127.0.0.1:${String(port)}`,
|
|
3930
|
+
timeoutMs: Math.min(2e3, timeoutMs)
|
|
3931
|
+
});
|
|
3932
|
+
return inspected.endpoint;
|
|
3933
|
+
} catch {
|
|
3934
|
+
return void 0;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
function formatChromeLaunchError(stderrLines) {
|
|
3938
|
+
const collapsed = stderrLines.join("").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3939
|
+
if (collapsed.length === 0) {
|
|
3940
|
+
return "Chrome failed to launch before exposing a DevTools endpoint.";
|
|
3941
|
+
}
|
|
3942
|
+
return `Chrome failed to launch before exposing a DevTools endpoint.
|
|
3943
|
+
${collapsed.slice(-5).join("\n")}`;
|
|
3944
|
+
}
|
|
3945
|
+
async function isEndpointReachable(endpoint) {
|
|
3946
|
+
try {
|
|
3947
|
+
await inspectCdpEndpoint({
|
|
3948
|
+
endpoint,
|
|
3949
|
+
timeoutMs: 1500
|
|
3950
|
+
});
|
|
3951
|
+
return true;
|
|
3952
|
+
} catch {
|
|
3953
|
+
return false;
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
async function isAbpBaseUrlReachable(baseUrl) {
|
|
3957
|
+
try {
|
|
3958
|
+
const response = await fetch(`${baseUrl}/browser/status`, {
|
|
3959
|
+
signal: AbortSignal.timeout(1500)
|
|
3960
|
+
});
|
|
3961
|
+
return response.ok;
|
|
3962
|
+
} catch {
|
|
3963
|
+
return false;
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
async function terminateProcess(pid) {
|
|
3967
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
try {
|
|
3971
|
+
process.kill(pid, "SIGTERM");
|
|
3972
|
+
} catch {
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
if (await waitForProcessExit(pid, BROWSER_CLOSE_TIMEOUT_MS)) {
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
try {
|
|
3979
|
+
process.kill(pid, "SIGKILL");
|
|
3980
|
+
} catch {
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
async function requestBrowserClose(endpoint) {
|
|
3985
|
+
await new Promise((resolve2, reject) => {
|
|
3986
|
+
const socket = new WebSocket(endpoint);
|
|
3987
|
+
const timeout = setTimeout(() => {
|
|
3988
|
+
socket.close();
|
|
3989
|
+
reject(new Error("Timed out waiting for Browser.close."));
|
|
3990
|
+
}, BROWSER_CLOSE_TIMEOUT_MS);
|
|
3991
|
+
let settled = false;
|
|
3992
|
+
const finish = (error) => {
|
|
3993
|
+
if (settled) {
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
settled = true;
|
|
3997
|
+
clearTimeout(timeout);
|
|
3998
|
+
if (error) {
|
|
3999
|
+
reject(error);
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
resolve2();
|
|
4003
|
+
};
|
|
4004
|
+
socket.addEventListener("open", () => {
|
|
4005
|
+
socket.send(JSON.stringify({ id: 1, method: "Browser.close" }));
|
|
4006
|
+
});
|
|
4007
|
+
socket.addEventListener("message", (event) => {
|
|
4008
|
+
try {
|
|
4009
|
+
const message = JSON.parse(String(event.data));
|
|
4010
|
+
if (message.id !== 1) {
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
finish(message.error?.message === void 0 ? void 0 : new Error(message.error.message));
|
|
4014
|
+
} catch (error) {
|
|
4015
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
4016
|
+
}
|
|
4017
|
+
});
|
|
4018
|
+
socket.addEventListener("close", () => {
|
|
4019
|
+
finish();
|
|
4020
|
+
});
|
|
4021
|
+
socket.addEventListener("error", () => {
|
|
4022
|
+
finish(new Error("Failed to send Browser.close."));
|
|
4023
|
+
});
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
4027
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
4028
|
+
return true;
|
|
4029
|
+
}
|
|
4030
|
+
const deadline = Date.now() + timeoutMs;
|
|
4031
|
+
while (Date.now() < deadline) {
|
|
4032
|
+
if (!isProcessRunning(pid)) {
|
|
4033
|
+
return true;
|
|
4034
|
+
}
|
|
4035
|
+
await sleep2(50);
|
|
4036
|
+
}
|
|
4037
|
+
return !isProcessRunning(pid);
|
|
4038
|
+
}
|
|
4039
|
+
function resolveAbpSessionDir(workspace) {
|
|
4040
|
+
return path7.join(workspace.livePath, "abp-session");
|
|
4041
|
+
}
|
|
4042
|
+
async function allocateEphemeralPort() {
|
|
4043
|
+
const { allocatePort } = await loadAbpModule();
|
|
4044
|
+
return allocatePort();
|
|
4045
|
+
}
|
|
4046
|
+
async function loadAbpModule() {
|
|
4047
|
+
try {
|
|
4048
|
+
return await import('@opensteer/engine-abp');
|
|
4049
|
+
} catch (error) {
|
|
4050
|
+
if (isMissingPackageError(error, "@opensteer/engine-abp")) {
|
|
4051
|
+
throw new Error(
|
|
4052
|
+
'ABP engine selected but "@opensteer/engine-abp" is not installed. Install it to use --engine abp or OPENSTEER_ENGINE=abp.'
|
|
4053
|
+
);
|
|
4054
|
+
}
|
|
4055
|
+
throw error;
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
function isMissingPackageError(error, packageName) {
|
|
4059
|
+
if (!(error instanceof Error)) {
|
|
4060
|
+
return false;
|
|
4061
|
+
}
|
|
4062
|
+
return error.message.includes(`Cannot find package '${packageName}'`) || error.message.includes(`Cannot find module '${packageName}'`) || error.message.includes(`Cannot find module "${packageName}"`);
|
|
4063
|
+
}
|
|
4064
|
+
function normalizeBrowserContextOptions(context) {
|
|
4065
|
+
const stealthProfile = resolveStealthProfile(context?.stealthProfile);
|
|
4066
|
+
const locale = context?.locale ?? stealthProfile?.locale;
|
|
4067
|
+
const timezoneId = context?.timezoneId ?? stealthProfile?.timezoneId;
|
|
4068
|
+
const userAgent = context?.userAgent ?? stealthProfile?.userAgent;
|
|
4069
|
+
return {
|
|
4070
|
+
...context ?? {},
|
|
4071
|
+
...stealthProfile === void 0 ? {} : { stealthProfile },
|
|
4072
|
+
...locale === void 0 ? {} : { locale },
|
|
4073
|
+
...timezoneId === void 0 ? {} : { timezoneId },
|
|
4074
|
+
...userAgent === void 0 ? {} : { userAgent },
|
|
4075
|
+
viewport: context?.viewport ?? stealthProfile?.viewport ?? {
|
|
4076
|
+
width: 1440,
|
|
4077
|
+
height: 900
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
function toEngineBrowserContextOptions(context) {
|
|
4082
|
+
const { stealthProfile: _stealthProfile, ...engineContext } = context;
|
|
4083
|
+
return engineContext;
|
|
4084
|
+
}
|
|
4085
|
+
function resolveStealthProfile(input) {
|
|
4086
|
+
if (input === void 0) {
|
|
4087
|
+
return void 0;
|
|
4088
|
+
}
|
|
4089
|
+
if (isStealthProfile(input)) {
|
|
4090
|
+
return input;
|
|
4091
|
+
}
|
|
4092
|
+
return generateStealthProfile(input);
|
|
4093
|
+
}
|
|
4094
|
+
function isStealthProfile(input) {
|
|
4095
|
+
return input.id !== void 0 && input.platform !== void 0 && input.browserBrand !== void 0 && input.browserVersion !== void 0 && input.userAgent !== void 0 && input.viewport !== void 0 && input.screenResolution !== void 0 && input.devicePixelRatio !== void 0 && input.maxTouchPoints !== void 0 && input.webglVendor !== void 0 && input.webglRenderer !== void 0 && input.fonts !== void 0 && input.canvasNoiseSeed !== void 0 && input.audioNoiseSeed !== void 0 && input.locale !== void 0 && input.timezoneId !== void 0;
|
|
4096
|
+
}
|
|
4097
|
+
async function sleep2(ms) {
|
|
4098
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
export { DEFAULT_OPENSTEER_ENGINE, OPENSTEER_ENGINE_NAMES, OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT, OPENSTEER_FILESYSTEM_WORKSPACE_VERSION, OpensteerBrowserManager, assertSupportedEngineOptions, buildLocalViewSessionUrl, createArtifactStore, createFilesystemOpensteerWorkspace, createObservationStore, ensureLocalViewServiceRunning, manifestToExternalBinaryLocation, normalizeObservabilityConfig, normalizeObservationContext, normalizeOpensteerEngineName, normalizeWorkspaceId, resolveFilesystemWorkspacePath, resolveOpensteerEngineName, setLocalViewMode, stopLocalViewService };
|
|
4102
|
+
//# sourceMappingURL=chunk-2TIVULZY.js.map
|
|
4103
|
+
//# sourceMappingURL=chunk-2TIVULZY.js.map
|