@kontextmind/kxm 0.7.134 → 0.7.135
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +11 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +3 -2
- package/docs/guides/agent-skills.md +1 -1
- package/docs/guides/browser-automation.md +44 -17
- package/docs/kb/how-credentials-retrieved-safely.md +19 -8
- package/docs/kb/how-to-connect-playwright-to-steel.md +28 -9
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +12 -11
- package/docs/kb/why-automation-opened-different-browser.md +10 -7
- package/docs/prompts/browser-diagnose-recover.md +2 -2
- package/docs/prompts/browser-start.md +3 -3
- package/docs/reference/configuration.md +9 -5
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +164 -30
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +3 -3
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +3 -2
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +7 -6
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +19 -8
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +11 -8
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +19 -4
- package/plugins/kxm/src/browser.ts +257 -31
- package/plugins/kxm/src/mcp-server.ts +1 -1
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* takeover, MFA, and the live session viewer (`KXM_BROWSER=steel`).
|
|
7
7
|
* Manages remote Steel sessions, CDP endpoints, human takeover handoffs,
|
|
8
8
|
* pass-cli credential references, and automated cleanup without leaking secrets.
|
|
9
|
+
*
|
|
10
|
+
* Hosts behind Authentik forward auth (the KontextMind Steel proxies) accept
|
|
11
|
+
* an app password only as `Authorization: Basic`. A Bearer token is refused.
|
|
12
|
+
* `STEEL_API_KEY` remains a legacy shim: `x-steel-api-key` and `?apiKey=`.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { execSync } from "node:child_process";
|
|
@@ -136,11 +140,156 @@ export interface AnnotationFeedback {
|
|
|
136
140
|
|
|
137
141
|
export interface SteelConfig {
|
|
138
142
|
apiUrl: string;
|
|
143
|
+
/** Legacy Steel key. Omitted once Authentik Basic auth is configured. */
|
|
139
144
|
apiKey?: string | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* Full `Authorization` value, for example `Basic <base64>`.
|
|
147
|
+
* Present when Authentik app-password auth is configured.
|
|
148
|
+
*/
|
|
149
|
+
authorization?: string | undefined;
|
|
140
150
|
uiUrl?: string | undefined;
|
|
141
151
|
timeoutMs?: number | undefined;
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
/** Inputs for {@link resolveSteelConfig}. Environment variables fill anything omitted. */
|
|
155
|
+
export interface SteelConfigOverrides extends Partial<SteelConfig> {
|
|
156
|
+
/** Full Authorization value, or a bare base64 credential. Wins over the other auth inputs. */
|
|
157
|
+
authHeader?: string | undefined;
|
|
158
|
+
/** `base64(user:token)`, with or without a leading `Basic `. */
|
|
159
|
+
authBasic?: string | undefined;
|
|
160
|
+
/** Authentik username, for example `svc-steel`. Used with `authToken`. */
|
|
161
|
+
authUser?: string | undefined;
|
|
162
|
+
/** Authentik app password. Used with `authUser`. */
|
|
163
|
+
authToken?: string | undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class SteelAuthConfigError extends Error {
|
|
167
|
+
constructor(message: string) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = "SteelAuthConfigError";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Authentik challenged the request with a redirect. The message never includes credentials. */
|
|
174
|
+
export class SteelAuthRedirectError extends Error {
|
|
175
|
+
readonly status: number;
|
|
176
|
+
readonly host: string;
|
|
177
|
+
|
|
178
|
+
constructor(status: number, host: string) {
|
|
179
|
+
super(
|
|
180
|
+
`Steel request was redirected (${status}) to ${host}. ` +
|
|
181
|
+
"Send Authorization: Basic via STEEL_AUTH_BASIC or STEEL_AUTH_USER and STEEL_AUTH_TOKEN. " +
|
|
182
|
+
"A Bearer token is not accepted.",
|
|
183
|
+
);
|
|
184
|
+
this.name = "SteelAuthRedirectError";
|
|
185
|
+
this.status = status;
|
|
186
|
+
this.host = host;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const LEGACY_STEEL_AUTH_WARNING =
|
|
191
|
+
"kxm: STEEL_API_KEY is deprecated for Steel. Authentik forward auth accepts app passwords only as Authorization: Basic. " +
|
|
192
|
+
"Set STEEL_AUTH_BASIC, or STEEL_AUTH_USER and STEEL_AUTH_TOKEN. " +
|
|
193
|
+
"The legacy x-steel-api-key header and apiKey query parameter remain for the temporary proxy shim.\n";
|
|
194
|
+
|
|
195
|
+
let legacySteelAuthWarned = false;
|
|
196
|
+
|
|
197
|
+
/** Test hook. Production calls warn at most once per process. */
|
|
198
|
+
export function resetLegacySteelAuthWarningForTests(): void {
|
|
199
|
+
legacySteelAuthWarned = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function warnLegacySteelAuth(): void {
|
|
203
|
+
if (legacySteelAuthWarned) return;
|
|
204
|
+
legacySteelAuthWarned = true;
|
|
205
|
+
process.stderr.write(LEGACY_STEEL_AUTH_WARNING);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function firstNonEmpty(...values: Array<string | undefined>): string | undefined {
|
|
209
|
+
for (const value of values) {
|
|
210
|
+
const trimmed = value?.trim();
|
|
211
|
+
if (trimmed) return trimmed;
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeAuthorization(raw: string): string {
|
|
217
|
+
if (/[\r\n]/.test(raw)) {
|
|
218
|
+
throw new SteelAuthConfigError("Steel authorization value contains a line break.");
|
|
219
|
+
}
|
|
220
|
+
const value = raw.trim();
|
|
221
|
+
if (!value) {
|
|
222
|
+
throw new SteelAuthConfigError("Steel authorization value is empty.");
|
|
223
|
+
}
|
|
224
|
+
const basicPrefix = /^basic\s+(.+)$/i.exec(value);
|
|
225
|
+
if (basicPrefix) {
|
|
226
|
+
const credential = basicPrefix[1] ?? "";
|
|
227
|
+
if (!credential || /\s/.test(credential)) {
|
|
228
|
+
throw new SteelAuthConfigError("Steel Basic credential must be a single base64 token.");
|
|
229
|
+
}
|
|
230
|
+
return `Basic ${credential}`;
|
|
231
|
+
}
|
|
232
|
+
if (/\s/.test(value)) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
return `Basic ${value}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the Authentik `Authorization` value.
|
|
240
|
+
* Precedence: auth header override, then `STEEL_AUTH_BASIC`, then user + token.
|
|
241
|
+
* Returns undefined when none of those are set so the legacy API key can apply.
|
|
242
|
+
*/
|
|
243
|
+
export function resolveSteelAuthorization(overrides?: SteelConfigOverrides): string | undefined {
|
|
244
|
+
const header = firstNonEmpty(overrides?.authHeader, overrides?.authorization, process.env.STEEL_AUTH_HEADER);
|
|
245
|
+
if (header) return normalizeAuthorization(header);
|
|
246
|
+
|
|
247
|
+
const basic = firstNonEmpty(overrides?.authBasic, process.env.STEEL_AUTH_BASIC);
|
|
248
|
+
if (basic) return normalizeAuthorization(basic);
|
|
249
|
+
|
|
250
|
+
const user = firstNonEmpty(overrides?.authUser, process.env.STEEL_AUTH_USER);
|
|
251
|
+
const token = firstNonEmpty(overrides?.authToken, process.env.STEEL_AUTH_TOKEN);
|
|
252
|
+
if (user || token) {
|
|
253
|
+
if (!user || !token) {
|
|
254
|
+
throw new SteelAuthConfigError(
|
|
255
|
+
"Steel Basic auth needs both STEEL_AUTH_USER and STEEL_AUTH_TOKEN, or STEEL_AUTH_BASIC.",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return `Basic ${Buffer.from(`${user}:${token}`, "utf8").toString("base64")}`;
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Headers for Steel HTTP and for Playwright `chromium.connectOverCDP(url, { headers })`.
|
|
265
|
+
* Basic auth wins and does not attach the legacy API key.
|
|
266
|
+
*/
|
|
267
|
+
export function steelRequestHeaders(config: Pick<SteelConfig, "authorization" | "apiKey">): Record<string, string> {
|
|
268
|
+
if (config.authorization) {
|
|
269
|
+
return { Authorization: config.authorization };
|
|
270
|
+
}
|
|
271
|
+
if (config.apiKey) {
|
|
272
|
+
return { "x-steel-api-key": config.apiKey };
|
|
273
|
+
}
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function steelAuthRedirectError(res: {
|
|
278
|
+
status: number;
|
|
279
|
+
headers: { get(name: string): string | null };
|
|
280
|
+
}): SteelAuthRedirectError {
|
|
281
|
+
let host = "the identity provider";
|
|
282
|
+
const location = res.headers.get("location");
|
|
283
|
+
if (location) {
|
|
284
|
+
try {
|
|
285
|
+
host = new URL(location, "https://id.kxmd.dev").host;
|
|
286
|
+
} catch {
|
|
287
|
+
host = "the identity provider";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return new SteelAuthRedirectError(res.status, host);
|
|
291
|
+
}
|
|
292
|
+
|
|
144
293
|
export function resolvePassCliApiKey(
|
|
145
294
|
execFn: (cmd: string) => string = (cmd) =>
|
|
146
295
|
execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 5000 }),
|
|
@@ -178,14 +327,24 @@ export function resolvePassCliApiKey(
|
|
|
178
327
|
/**
|
|
179
328
|
* Resolve Steel configuration from environment or pass-cli.
|
|
180
329
|
* Does not write secrets to disk or logs.
|
|
330
|
+
*
|
|
331
|
+
* Authentik Basic auth (`STEEL_AUTH_HEADER`, `STEEL_AUTH_BASIC`, or
|
|
332
|
+
* `STEEL_AUTH_USER` + `STEEL_AUTH_TOKEN`) wins over `STEEL_API_KEY`.
|
|
333
|
+
* The legacy key is kept only when no Basic credential is configured, and
|
|
334
|
+
* a one-time deprecation warning is written to stderr.
|
|
181
335
|
*/
|
|
182
|
-
export function resolveSteelConfig(overrides?:
|
|
336
|
+
export function resolveSteelConfig(overrides?: SteelConfigOverrides): SteelConfig {
|
|
183
337
|
const apiUrl =
|
|
184
338
|
overrides?.apiUrl ||
|
|
185
339
|
process.env.STEEL_API_URL ||
|
|
186
340
|
"https://steel.kontextmind.com";
|
|
187
341
|
|
|
188
|
-
const
|
|
342
|
+
const authorization = resolveSteelAuthorization(overrides);
|
|
343
|
+
let apiKey: string | undefined;
|
|
344
|
+
if (!authorization) {
|
|
345
|
+
apiKey = overrides?.apiKey || process.env.STEEL_API_KEY || resolvePassCliApiKey();
|
|
346
|
+
if (apiKey) warnLegacySteelAuth();
|
|
347
|
+
}
|
|
189
348
|
|
|
190
349
|
const uiUrl =
|
|
191
350
|
overrides?.uiUrl ||
|
|
@@ -196,6 +355,7 @@ export function resolveSteelConfig(overrides?: Partial<SteelConfig>): SteelConfi
|
|
|
196
355
|
return {
|
|
197
356
|
apiUrl: apiUrl.replace(/\/$/, ""),
|
|
198
357
|
apiKey,
|
|
358
|
+
authorization,
|
|
199
359
|
uiUrl,
|
|
200
360
|
timeoutMs: overrides?.timeoutMs || 300000, // 5 minutes default
|
|
201
361
|
};
|
|
@@ -203,6 +363,7 @@ export function resolveSteelConfig(overrides?: Partial<SteelConfig>): SteelConfi
|
|
|
203
363
|
|
|
204
364
|
/**
|
|
205
365
|
* Format a remote CDP connection URL for Playwright or agent-browser.
|
|
366
|
+
* The legacy `apiKey` query parameter is added only when Basic auth is unset.
|
|
206
367
|
*/
|
|
207
368
|
export function formatCDPEndpoint(session: Pick<SteelSession, "id" | "websocketUrl">, config: SteelConfig): string {
|
|
208
369
|
const baseApi = config.apiUrl;
|
|
@@ -213,13 +374,34 @@ export function formatCDPEndpoint(session: Pick<SteelSession, "id" | "websocketU
|
|
|
213
374
|
|
|
214
375
|
const searchParams = new URLSearchParams();
|
|
215
376
|
searchParams.set("sessionId", session.id);
|
|
216
|
-
if (config.apiKey) {
|
|
377
|
+
if (!config.authorization && config.apiKey) {
|
|
217
378
|
searchParams.set("apiKey", config.apiKey);
|
|
218
379
|
}
|
|
219
380
|
|
|
220
381
|
return `${wsProtocol}//${host}/v1/devtools?${searchParams.toString()}`;
|
|
221
382
|
}
|
|
222
383
|
|
|
384
|
+
export interface SteelCdpConnect {
|
|
385
|
+
/** WebSocket URL. Credentials stay out of it when Authentik Basic auth is set. */
|
|
386
|
+
url: string;
|
|
387
|
+
/** Pass as the second argument to `chromium.connectOverCDP(url, { headers })`. */
|
|
388
|
+
headers: Record<string, string>;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* CDP URL plus the headers Playwright must send on the WebSocket handshake.
|
|
393
|
+
* Call `chromium.connectOverCDP(url, { headers })`. Do not put the credential in the URL.
|
|
394
|
+
*/
|
|
395
|
+
export function formatCDPConnect(
|
|
396
|
+
session: Pick<SteelSession, "id" | "websocketUrl">,
|
|
397
|
+
config: SteelConfig,
|
|
398
|
+
): SteelCdpConnect {
|
|
399
|
+
return {
|
|
400
|
+
url: formatCDPEndpoint(session, config),
|
|
401
|
+
headers: steelRequestHeaders(config),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
223
405
|
export const DEFAULT_OBSCURA_CDP_URL = "http://127.0.0.1:9222";
|
|
224
406
|
const DEFAULT_OBSCURA_PORT = 9222;
|
|
225
407
|
|
|
@@ -249,32 +431,64 @@ export function resolveObscuraCdpEndpoint(): string {
|
|
|
249
431
|
}
|
|
250
432
|
|
|
251
433
|
/**
|
|
252
|
-
* Playwright CDP
|
|
253
|
-
* Obscura by default. `KXM_BROWSER=steel` uses {@link
|
|
434
|
+
* Playwright CDP target.
|
|
435
|
+
* Obscura by default, with empty headers. `KXM_BROWSER=steel` uses {@link formatCDPConnect}.
|
|
254
436
|
*/
|
|
255
|
-
export function
|
|
437
|
+
export function resolveBrowserCdpConnect(
|
|
256
438
|
session?: Pick<SteelSession, "id" | "websocketUrl">,
|
|
257
439
|
config?: SteelConfig,
|
|
258
|
-
):
|
|
440
|
+
): SteelCdpConnect {
|
|
259
441
|
const browser = (process.env.KXM_BROWSER ?? "").trim().toLowerCase();
|
|
260
442
|
if (browser === "" || browser === "obscura") {
|
|
261
|
-
return resolveObscuraCdpEndpoint();
|
|
443
|
+
return { url: resolveObscuraCdpEndpoint(), headers: {} };
|
|
262
444
|
}
|
|
263
445
|
if (browser === "steel") {
|
|
264
446
|
if (!session?.id) {
|
|
265
447
|
throw new Error("KXM_BROWSER=steel requires a Steel session id");
|
|
266
448
|
}
|
|
267
|
-
return
|
|
449
|
+
return formatCDPConnect(session, config ?? resolveSteelConfig());
|
|
268
450
|
}
|
|
269
451
|
throw new Error(`Unsupported KXM_BROWSER value ${JSON.stringify(process.env.KXM_BROWSER)}; expected "obscura" or "steel"`);
|
|
270
452
|
}
|
|
271
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Playwright CDP endpoint.
|
|
456
|
+
* Obscura by default. `KXM_BROWSER=steel` returns the URL from {@link formatCDPConnect}.
|
|
457
|
+
* Pass {@link resolveBrowserCdpConnect} headers into `chromium.connectOverCDP`, or call {@link connectBrowserOverCdp}.
|
|
458
|
+
*/
|
|
459
|
+
export function resolveBrowserCdpEndpoint(
|
|
460
|
+
session?: Pick<SteelSession, "id" | "websocketUrl">,
|
|
461
|
+
config?: SteelConfig,
|
|
462
|
+
): string {
|
|
463
|
+
return resolveBrowserCdpConnect(session, config).url;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Connect Playwright over CDP.
|
|
468
|
+
* Obscura is called with the URL only. `KXM_BROWSER=steel` passes Authentik or legacy Steel headers on the handshake.
|
|
469
|
+
*/
|
|
470
|
+
export async function connectBrowserOverCdp<T>(
|
|
471
|
+
connectOverCDP: (url: string, options?: { headers?: Record<string, string> }) => Promise<T>,
|
|
472
|
+
session?: Pick<SteelSession, "id" | "websocketUrl">,
|
|
473
|
+
config?: SteelConfig,
|
|
474
|
+
): Promise<T> {
|
|
475
|
+
const { url, headers } = resolveBrowserCdpConnect(session, config);
|
|
476
|
+
if (Object.keys(headers).length === 0) return connectOverCDP(url);
|
|
477
|
+
return connectOverCDP(url, { headers });
|
|
478
|
+
}
|
|
479
|
+
|
|
272
480
|
/**
|
|
273
481
|
* Redact sensitive API keys and tokens from URLs and objects for logging.
|
|
274
482
|
*/
|
|
275
483
|
export function sanitizeLogOutput<T>(input: T): T {
|
|
276
484
|
if (typeof input === "string") {
|
|
277
|
-
|
|
485
|
+
const redacted = input
|
|
486
|
+
.replace(/apiKey=[^&\s]+/gi, "apiKey=[REDACTED]")
|
|
487
|
+
.replace(/([?&]authorization=)[^&\s]+/gi, "$1[REDACTED]")
|
|
488
|
+
.replace(/authorization:\s*(?:basic\s+)?\S+/gi, "authorization: [REDACTED]")
|
|
489
|
+
.replace(/\bBasic\s+(?:[A-Za-z0-9+/]*[+/=0-9][A-Za-z0-9+/]*={0,2})/g, "Basic [REDACTED]")
|
|
490
|
+
.replace(/steel_[a-f0-9]+/g, "steel_[REDACTED]");
|
|
491
|
+
return redacted as unknown as T;
|
|
278
492
|
}
|
|
279
493
|
if (Array.isArray(input)) {
|
|
280
494
|
return input.map(sanitizeLogOutput) as unknown as T;
|
|
@@ -360,7 +574,7 @@ export class SteelClient {
|
|
|
360
574
|
private config: SteelConfig;
|
|
361
575
|
private activeSessions = new Map<string, SteelSession>();
|
|
362
576
|
|
|
363
|
-
constructor(config?:
|
|
577
|
+
constructor(config?: SteelConfigOverrides) {
|
|
364
578
|
this.config = resolveSteelConfig(config);
|
|
365
579
|
}
|
|
366
580
|
|
|
@@ -368,14 +582,31 @@ export class SteelClient {
|
|
|
368
582
|
return { ...this.config };
|
|
369
583
|
}
|
|
370
584
|
|
|
585
|
+
/**
|
|
586
|
+
* URL and headers for `chromium.connectOverCDP(url, { headers })`.
|
|
587
|
+
* The URL omits credentials when Authentik Basic auth is configured.
|
|
588
|
+
*/
|
|
589
|
+
cdpConnectOptions(session: Pick<SteelSession, "id" | "websocketUrl">): SteelCdpConnect {
|
|
590
|
+
return formatCDPConnect(session, this.config);
|
|
591
|
+
}
|
|
592
|
+
|
|
371
593
|
private headers(): Record<string, string> {
|
|
372
|
-
|
|
594
|
+
return {
|
|
373
595
|
"Content-Type": "application/json",
|
|
596
|
+
...steelRequestHeaders(this.config),
|
|
374
597
|
};
|
|
375
|
-
|
|
376
|
-
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private async steelFetch(url: string, init: RequestInit): Promise<Response> {
|
|
601
|
+
const headers = {
|
|
602
|
+
...this.headers(),
|
|
603
|
+
...(init.headers as Record<string, string> | undefined),
|
|
604
|
+
};
|
|
605
|
+
const res = await fetch(url, { ...init, headers, redirect: "manual" });
|
|
606
|
+
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
607
|
+
throw steelAuthRedirectError(res);
|
|
377
608
|
}
|
|
378
|
-
return
|
|
609
|
+
return res;
|
|
379
610
|
}
|
|
380
611
|
|
|
381
612
|
/**
|
|
@@ -397,14 +628,13 @@ export class SteelClient {
|
|
|
397
628
|
body.proxy = options.proxy;
|
|
398
629
|
}
|
|
399
630
|
|
|
400
|
-
const res = await
|
|
631
|
+
const res = await this.steelFetch(`${this.config.apiUrl}/v1/sessions`, {
|
|
401
632
|
method: "POST",
|
|
402
|
-
headers: this.headers(),
|
|
403
633
|
body: JSON.stringify(body),
|
|
404
634
|
});
|
|
405
635
|
|
|
406
636
|
if (!res.ok) {
|
|
407
|
-
const errText = await res.text();
|
|
637
|
+
const errText = sanitizeLogOutput(await res.text());
|
|
408
638
|
throw new Error(`Failed to create Steel session (${res.status}): ${errText}`);
|
|
409
639
|
}
|
|
410
640
|
|
|
@@ -432,9 +662,8 @@ export class SteelClient {
|
|
|
432
662
|
* Get details of an existing session.
|
|
433
663
|
*/
|
|
434
664
|
async getSession(sessionId: string): Promise<SteelSession | null> {
|
|
435
|
-
const res = await
|
|
665
|
+
const res = await this.steelFetch(`${this.config.apiUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
436
666
|
method: "GET",
|
|
437
|
-
headers: this.headers(),
|
|
438
667
|
});
|
|
439
668
|
|
|
440
669
|
if (res.status === 404) {
|
|
@@ -563,9 +792,8 @@ export class SteelClient {
|
|
|
563
792
|
*/
|
|
564
793
|
async releaseSession(sessionId: string): Promise<boolean> {
|
|
565
794
|
try {
|
|
566
|
-
const res = await
|
|
795
|
+
const res = await this.steelFetch(`${this.config.apiUrl}/v1/sessions/${encodeURIComponent(sessionId)}/release`, {
|
|
567
796
|
method: "POST",
|
|
568
|
-
headers: this.headers(),
|
|
569
797
|
});
|
|
570
798
|
|
|
571
799
|
const session = this.activeSessions.get(sessionId);
|
|
@@ -576,7 +804,8 @@ export class SteelClient {
|
|
|
576
804
|
}
|
|
577
805
|
this.activeSessions.delete(sessionId);
|
|
578
806
|
return res.ok;
|
|
579
|
-
} catch {
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (error instanceof SteelAuthRedirectError) throw error;
|
|
580
809
|
this.activeSessions.delete(sessionId);
|
|
581
810
|
return false;
|
|
582
811
|
}
|
|
@@ -586,14 +815,13 @@ export class SteelClient {
|
|
|
586
815
|
* Perform a direct stateless scrape without manual session management.
|
|
587
816
|
*/
|
|
588
817
|
async scrape(url: string): Promise<ScrapeResult> {
|
|
589
|
-
const res = await
|
|
818
|
+
const res = await this.steelFetch(`${this.config.apiUrl}/v1/scrape`, {
|
|
590
819
|
method: "POST",
|
|
591
|
-
headers: this.headers(),
|
|
592
820
|
body: JSON.stringify({ url }),
|
|
593
821
|
});
|
|
594
822
|
|
|
595
823
|
if (!res.ok) {
|
|
596
|
-
const err = await res.text();
|
|
824
|
+
const err = sanitizeLogOutput(await res.text());
|
|
597
825
|
throw new Error(`Scrape failed (${res.status}): ${err}`);
|
|
598
826
|
}
|
|
599
827
|
|
|
@@ -604,14 +832,13 @@ export class SteelClient {
|
|
|
604
832
|
* Perform a direct screenshot action.
|
|
605
833
|
*/
|
|
606
834
|
async screenshot(url: string, fullPage = false): Promise<ScreenshotResult> {
|
|
607
|
-
const res = await
|
|
835
|
+
const res = await this.steelFetch(`${this.config.apiUrl}/v1/screenshot`, {
|
|
608
836
|
method: "POST",
|
|
609
|
-
headers: this.headers(),
|
|
610
837
|
body: JSON.stringify({ url, fullPage }),
|
|
611
838
|
});
|
|
612
839
|
|
|
613
840
|
if (!res.ok) {
|
|
614
|
-
const err = await res.text();
|
|
841
|
+
const err = sanitizeLogOutput(await res.text());
|
|
615
842
|
throw new Error(`Screenshot failed (${res.status}): ${err}`);
|
|
616
843
|
}
|
|
617
844
|
|
|
@@ -622,9 +849,8 @@ export class SteelClient {
|
|
|
622
849
|
* Detect and list orphaned or timed-out active sessions.
|
|
623
850
|
*/
|
|
624
851
|
async checkOrphanedSessions(maxIdleMs = 600000): Promise<string[]> {
|
|
625
|
-
const res = await
|
|
852
|
+
const res = await this.steelFetch(`${this.config.apiUrl}/v1/sessions`, {
|
|
626
853
|
method: "GET",
|
|
627
|
-
headers: this.headers(),
|
|
628
854
|
});
|
|
629
855
|
|
|
630
856
|
if (!res.ok) {
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.135";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|