@steerpane/sdk 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +109 -2
- package/dist/client.d.ts +409 -0
- package/dist/client.js +506 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +49 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/playwright.d.ts +16 -0
- package/dist/playwright.js +20 -0
- package/dist/types.d.ts +454 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/webhooks.d.ts +17 -0
- package/dist/webhooks.js +64 -0
- package/package.json +29 -4
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/** Response shapes. Field names are the API's own, and fields the API adds later pass straight through. */
|
|
2
|
+
export type Json = Record<string, unknown>;
|
|
3
|
+
export type SessionStatus = "queued" | "starting" | "running" | "idle" | "human_control" | "awaiting_help" | "stopped" | "error";
|
|
4
|
+
export type RunStatus = "queued" | "running" | "awaiting_human" | "completed" | "failed" | "cancelled";
|
|
5
|
+
export type HandbackOutcome = "done" | "not_done";
|
|
6
|
+
export interface Page<T> {
|
|
7
|
+
data: T[];
|
|
8
|
+
next_cursor: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface Failure {
|
|
11
|
+
category: string;
|
|
12
|
+
retryable: boolean;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Session extends Json {
|
|
16
|
+
id: string;
|
|
17
|
+
org_id: string;
|
|
18
|
+
project_id: string | null;
|
|
19
|
+
profile_id: string | null;
|
|
20
|
+
profile_name: string;
|
|
21
|
+
end_user_id: string | null;
|
|
22
|
+
status: SessionStatus;
|
|
23
|
+
controller: "agent" | "human";
|
|
24
|
+
controller_user_id: string | null;
|
|
25
|
+
agent_paused: boolean;
|
|
26
|
+
agent_name: string | null;
|
|
27
|
+
connected_via: string | null;
|
|
28
|
+
agent_client: Json | null;
|
|
29
|
+
current_url: string;
|
|
30
|
+
page_title: string;
|
|
31
|
+
region: string;
|
|
32
|
+
node_id: string;
|
|
33
|
+
container_id: string;
|
|
34
|
+
started_at: string | null;
|
|
35
|
+
ended_at: string | null;
|
|
36
|
+
cpu: number;
|
|
37
|
+
memory_mb: number;
|
|
38
|
+
network_kbps: number;
|
|
39
|
+
action_count: number;
|
|
40
|
+
queued_commands: number;
|
|
41
|
+
/** Only in the dashboard's (cookie) view, never for API tokens. */
|
|
42
|
+
queue?: Json[];
|
|
43
|
+
/** Only while `status` is "queued" (a `profile_wait` session waiting for its profile); 1 is next. */
|
|
44
|
+
queue_position?: number | null;
|
|
45
|
+
help_request: Json | null;
|
|
46
|
+
error_message: string | null;
|
|
47
|
+
recording: boolean;
|
|
48
|
+
thumbnail_hue: number;
|
|
49
|
+
profile_color: unknown;
|
|
50
|
+
config: Json;
|
|
51
|
+
cdp_url: string;
|
|
52
|
+
live_url: string;
|
|
53
|
+
watch_url: string;
|
|
54
|
+
always_on: boolean;
|
|
55
|
+
restarting: boolean;
|
|
56
|
+
restart_at: string | null;
|
|
57
|
+
last_handback: {
|
|
58
|
+
outcome: HandbackOutcome | null;
|
|
59
|
+
note: string | null;
|
|
60
|
+
queued?: "replay" | "discard" | null;
|
|
61
|
+
at: string;
|
|
62
|
+
} | null;
|
|
63
|
+
failure: Failure | null;
|
|
64
|
+
}
|
|
65
|
+
export interface EndUser extends Json {
|
|
66
|
+
id: string;
|
|
67
|
+
org_id: string;
|
|
68
|
+
external_id: string;
|
|
69
|
+
name: string | null;
|
|
70
|
+
metadata: Record<string, string>;
|
|
71
|
+
disabled: boolean;
|
|
72
|
+
disabled_at: string | null;
|
|
73
|
+
created_at: string | null;
|
|
74
|
+
updated_at: string | null;
|
|
75
|
+
}
|
|
76
|
+
export interface Profile extends Json {
|
|
77
|
+
id: string;
|
|
78
|
+
org_id: string;
|
|
79
|
+
end_user_id: string | null;
|
|
80
|
+
name: string;
|
|
81
|
+
description: string | null;
|
|
82
|
+
icon: string;
|
|
83
|
+
color: number;
|
|
84
|
+
/** The saved proxy settings without the password; `has_password` says whether one is stored. */
|
|
85
|
+
proxy: Json;
|
|
86
|
+
region: string;
|
|
87
|
+
fingerprint: Json;
|
|
88
|
+
user_agent: string;
|
|
89
|
+
behaviour: Json;
|
|
90
|
+
extensions: string[];
|
|
91
|
+
tags: string[];
|
|
92
|
+
logged_in_sites: Json[];
|
|
93
|
+
/** Always `[]` in lists; the cookies of one profile come with `get`. */
|
|
94
|
+
cookies: Json[];
|
|
95
|
+
storage_bytes: number;
|
|
96
|
+
cookie_count: number;
|
|
97
|
+
last_used_at: string | null;
|
|
98
|
+
created_at: string | null;
|
|
99
|
+
created_by: string;
|
|
100
|
+
session_count: number;
|
|
101
|
+
}
|
|
102
|
+
export interface FetchResult extends Json {
|
|
103
|
+
id: string;
|
|
104
|
+
url: string;
|
|
105
|
+
final_url: string;
|
|
106
|
+
status: number;
|
|
107
|
+
format: string;
|
|
108
|
+
content_type: string;
|
|
109
|
+
/** "" when the page has no title; never null. */
|
|
110
|
+
title: string;
|
|
111
|
+
content: string;
|
|
112
|
+
headers: Record<string, string>;
|
|
113
|
+
size_bytes: number;
|
|
114
|
+
latency_ms: number;
|
|
115
|
+
redirects: string[];
|
|
116
|
+
created_at: string;
|
|
117
|
+
mode: string;
|
|
118
|
+
engine: string;
|
|
119
|
+
links?: string[];
|
|
120
|
+
metadata?: Json;
|
|
121
|
+
/** Set when the page was a wall (consent, bot check, CAPTCHA) rather than content. */
|
|
122
|
+
blocked_by?: string | null;
|
|
123
|
+
next_step?: string | null;
|
|
124
|
+
degraded?: boolean | null;
|
|
125
|
+
degraded_reason?: string | null;
|
|
126
|
+
deep_reason?: string | null;
|
|
127
|
+
rendered_by?: string | null;
|
|
128
|
+
}
|
|
129
|
+
export interface SearchResponse extends Json {
|
|
130
|
+
id: string;
|
|
131
|
+
query: string;
|
|
132
|
+
results: {
|
|
133
|
+
title: string;
|
|
134
|
+
url: string;
|
|
135
|
+
snippet: string;
|
|
136
|
+
published_at?: string | null;
|
|
137
|
+
}[];
|
|
138
|
+
provider: string;
|
|
139
|
+
latency_ms: number;
|
|
140
|
+
created_at: string;
|
|
141
|
+
}
|
|
142
|
+
export interface Capture extends Json {
|
|
143
|
+
id: string;
|
|
144
|
+
kind: "screenshot" | "pdf";
|
|
145
|
+
file_id: string;
|
|
146
|
+
/** Authenticated URL of the stored file. */
|
|
147
|
+
url: string;
|
|
148
|
+
/** Signed link that needs no token. */
|
|
149
|
+
download_url: string;
|
|
150
|
+
download_expires_at: string;
|
|
151
|
+
download_curl: string;
|
|
152
|
+
name: string;
|
|
153
|
+
mime: string;
|
|
154
|
+
size: number;
|
|
155
|
+
/** null for PDFs. */
|
|
156
|
+
width: number | null;
|
|
157
|
+
height: number | null;
|
|
158
|
+
page_url: string;
|
|
159
|
+
final_url: string;
|
|
160
|
+
title: string;
|
|
161
|
+
status: number | null;
|
|
162
|
+
engine: string;
|
|
163
|
+
latency_ms: number;
|
|
164
|
+
created_at: string;
|
|
165
|
+
expires_at: string | null;
|
|
166
|
+
base64?: string | null;
|
|
167
|
+
}
|
|
168
|
+
export interface CrawlJob extends Json {
|
|
169
|
+
id: string;
|
|
170
|
+
url: string;
|
|
171
|
+
status: "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
172
|
+
engine: string;
|
|
173
|
+
options: Json;
|
|
174
|
+
progress: {
|
|
175
|
+
done: number;
|
|
176
|
+
total: number;
|
|
177
|
+
failed: number;
|
|
178
|
+
};
|
|
179
|
+
error: string | null;
|
|
180
|
+
project_id: string | null;
|
|
181
|
+
created_at: string | null;
|
|
182
|
+
started_at: string | null;
|
|
183
|
+
finished_at: string | null;
|
|
184
|
+
/** Only on `get` (the page of results); the list and `start` leave these out. */
|
|
185
|
+
pages?: Json[];
|
|
186
|
+
/** Pass back to `get` for the next page of `pages`. A string, or null on the last page. */
|
|
187
|
+
next_cursor?: string | null;
|
|
188
|
+
/** Left out until a `check_links` crawl has stored its report; never null. */
|
|
189
|
+
link_check?: {
|
|
190
|
+
checked: number;
|
|
191
|
+
ok: number;
|
|
192
|
+
unverified: number;
|
|
193
|
+
blocked: number;
|
|
194
|
+
truncated: boolean;
|
|
195
|
+
broken: {
|
|
196
|
+
url: string;
|
|
197
|
+
found_on: string;
|
|
198
|
+
status: number | null;
|
|
199
|
+
error: string | null;
|
|
200
|
+
}[];
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export interface AgentRun extends Json {
|
|
204
|
+
id: string;
|
|
205
|
+
agent_id: string | null;
|
|
206
|
+
agent_name: string;
|
|
207
|
+
task: string;
|
|
208
|
+
status: RunStatus;
|
|
209
|
+
session_id: string | null;
|
|
210
|
+
profile_id: string | null;
|
|
211
|
+
project_id: string | null;
|
|
212
|
+
model: string;
|
|
213
|
+
max_steps: number;
|
|
214
|
+
step_count: number;
|
|
215
|
+
tokens_in: number;
|
|
216
|
+
tokens_out: number;
|
|
217
|
+
cost_usd: number;
|
|
218
|
+
started_at: string;
|
|
219
|
+
ended_at: string | null;
|
|
220
|
+
duration_sec: number;
|
|
221
|
+
variables: Json;
|
|
222
|
+
limits: Json;
|
|
223
|
+
result: unknown;
|
|
224
|
+
error: string | null;
|
|
225
|
+
failure: Failure | null;
|
|
226
|
+
/** Only when the run was read with its steps. */
|
|
227
|
+
steps?: Json[] | null;
|
|
228
|
+
}
|
|
229
|
+
export interface StoredFile extends Json {
|
|
230
|
+
id: string;
|
|
231
|
+
name: string;
|
|
232
|
+
mime: string;
|
|
233
|
+
size_bytes: number;
|
|
234
|
+
/** null while an upload is still pending. */
|
|
235
|
+
sha256: string | null;
|
|
236
|
+
source: "upload" | "download" | "api" | "embed";
|
|
237
|
+
status: "pending" | "ready";
|
|
238
|
+
session_id: string | null;
|
|
239
|
+
project_id: string | null;
|
|
240
|
+
created_by_user_id: string | null;
|
|
241
|
+
created_by_name: string | null;
|
|
242
|
+
token_id: string | null;
|
|
243
|
+
created_at: string;
|
|
244
|
+
expires_at: string | null;
|
|
245
|
+
content_url: string;
|
|
246
|
+
}
|
|
247
|
+
export interface UploadUrl extends Json {
|
|
248
|
+
file: StoredFile;
|
|
249
|
+
file_id: string;
|
|
250
|
+
upload_url: string;
|
|
251
|
+
method: string;
|
|
252
|
+
headers: Record<string, string>;
|
|
253
|
+
expires_at: string;
|
|
254
|
+
curl: string;
|
|
255
|
+
complete_url: string;
|
|
256
|
+
requires_complete: boolean;
|
|
257
|
+
max_bytes: number;
|
|
258
|
+
}
|
|
259
|
+
export interface EmbedLink extends Json {
|
|
260
|
+
id: string;
|
|
261
|
+
url: string;
|
|
262
|
+
expires_at: string | null;
|
|
263
|
+
mode: "read_only" | "interactive" | "interactive_takeover";
|
|
264
|
+
/** The fields below come with `list` and `revoke`; a freshly made link has just the four above. */
|
|
265
|
+
name?: string;
|
|
266
|
+
session_id?: string;
|
|
267
|
+
allowed_origins?: string[];
|
|
268
|
+
hide_toolbar?: boolean;
|
|
269
|
+
show_control?: boolean;
|
|
270
|
+
end_user?: Json | null;
|
|
271
|
+
theme?: string;
|
|
272
|
+
revoked_at?: string | null;
|
|
273
|
+
created_at?: string;
|
|
274
|
+
created_by?: string;
|
|
275
|
+
views?: number;
|
|
276
|
+
status?: string;
|
|
277
|
+
}
|
|
278
|
+
export interface Webhook extends Json {
|
|
279
|
+
id: string;
|
|
280
|
+
org_id: string;
|
|
281
|
+
project_id: string | null;
|
|
282
|
+
url: string;
|
|
283
|
+
description: string | null;
|
|
284
|
+
events: string[];
|
|
285
|
+
headers: {
|
|
286
|
+
key: string;
|
|
287
|
+
value: string;
|
|
288
|
+
}[];
|
|
289
|
+
enabled: boolean;
|
|
290
|
+
disabled_reason: string | null;
|
|
291
|
+
success_rate: number;
|
|
292
|
+
last_delivery_at: string | null;
|
|
293
|
+
created_at: string | null;
|
|
294
|
+
/** On create and rotate, and for callers who may change webhooks; not in lists. */
|
|
295
|
+
secret?: string | null;
|
|
296
|
+
}
|
|
297
|
+
export interface WebhookDelivery extends Json {
|
|
298
|
+
id: string;
|
|
299
|
+
webhook_id: string;
|
|
300
|
+
event: string;
|
|
301
|
+
event_id: string;
|
|
302
|
+
/** 0 when the request never got an answer. */
|
|
303
|
+
status_code: number;
|
|
304
|
+
success: boolean;
|
|
305
|
+
duration_ms: number;
|
|
306
|
+
attempt: number;
|
|
307
|
+
delivered_at: string | null;
|
|
308
|
+
next_attempt_at: string | null;
|
|
309
|
+
request_headers: Json;
|
|
310
|
+
request_body: Json;
|
|
311
|
+
response_body: string;
|
|
312
|
+
error: string | null;
|
|
313
|
+
test: boolean;
|
|
314
|
+
}
|
|
315
|
+
export interface Credential extends Json {
|
|
316
|
+
id: string;
|
|
317
|
+
org_id: string;
|
|
318
|
+
name: string;
|
|
319
|
+
domain: string;
|
|
320
|
+
username: string;
|
|
321
|
+
has_totp: boolean;
|
|
322
|
+
profile_ids: string[];
|
|
323
|
+
token_ids: string[];
|
|
324
|
+
last_used_at: string | null;
|
|
325
|
+
created_at: string | null;
|
|
326
|
+
}
|
|
327
|
+
export interface HumanResult extends Json {
|
|
328
|
+
status: "handed_back" | "timeout" | "stopped";
|
|
329
|
+
note: string | null;
|
|
330
|
+
outcome: HandbackOutcome | null;
|
|
331
|
+
}
|
|
332
|
+
/** A rule of the action guard. `id` is yours; give at least one of `selector`, `role`, `name` (a case-insensitive regex on the accessible name or visible text). */
|
|
333
|
+
export interface ActionGuardRule {
|
|
334
|
+
id: string;
|
|
335
|
+
selector?: string;
|
|
336
|
+
role?: string;
|
|
337
|
+
name?: string;
|
|
338
|
+
frames?: "all" | "top";
|
|
339
|
+
}
|
|
340
|
+
export interface ActionGuard extends Json {
|
|
341
|
+
rules: ActionGuardRule[];
|
|
342
|
+
}
|
|
343
|
+
export interface ActionGuardUnlock extends Json {
|
|
344
|
+
token: string;
|
|
345
|
+
expires_at: string;
|
|
346
|
+
/** Frames whose document had already started when the guard was installed (or that have no guard): a page script may have registered a listener ahead of it. Optional: older servers omit it. */
|
|
347
|
+
late_targets?: number;
|
|
348
|
+
}
|
|
349
|
+
export interface HelpRequest extends Json {
|
|
350
|
+
request_id: string;
|
|
351
|
+
status: string;
|
|
352
|
+
session_id: string;
|
|
353
|
+
}
|
|
354
|
+
/** A computer (or shared machine) that runs sessions for you. `scope` is `user`, `org` or `end_user`. */
|
|
355
|
+
export interface Device extends Json {
|
|
356
|
+
id: string;
|
|
357
|
+
name: string;
|
|
358
|
+
scope: "user" | "org" | "end_user";
|
|
359
|
+
owner_user_id: string | null;
|
|
360
|
+
end_user_id: string | null;
|
|
361
|
+
platform: string;
|
|
362
|
+
agent_version: string;
|
|
363
|
+
chrome_version: string;
|
|
364
|
+
modes: string[];
|
|
365
|
+
capacity: number;
|
|
366
|
+
connected: boolean;
|
|
367
|
+
extension_connected: boolean;
|
|
368
|
+
last_seen_at: string | null;
|
|
369
|
+
created_at: string | null;
|
|
370
|
+
revoked_at: string | null;
|
|
371
|
+
}
|
|
372
|
+
/** A single-use code, shown once, that pairs a device agent (`XXXX-XXXX`). */
|
|
373
|
+
export interface DevicePairingCode extends Json {
|
|
374
|
+
code: string;
|
|
375
|
+
expires_at: string | null;
|
|
376
|
+
scope: string;
|
|
377
|
+
end_user_id: string | null;
|
|
378
|
+
}
|
|
379
|
+
/** What `sessions.solveCaptcha` returns. `status` is `solved`, `unsolved`, `none` (no CAPTCHA found), `no_model`, `browser_unavailable`, `in_progress` (call again), or a human-help status when `onFailure` handed it to a person. */
|
|
380
|
+
export interface CaptchaSolveResult extends Json {
|
|
381
|
+
status: string;
|
|
382
|
+
}
|
|
383
|
+
/** Arguments of the MCP tool `wait_for_code`. Pass `sessionId` to have the code typed into the page without it passing through you, or `returnCode` to get it back. */
|
|
384
|
+
export interface WaitForCodeOptions {
|
|
385
|
+
sessionId?: string;
|
|
386
|
+
profileId?: string;
|
|
387
|
+
credentialId?: string;
|
|
388
|
+
/** The site the code is for, such as `acme.com`. Needed unless the code source or the credential names it. */
|
|
389
|
+
site?: string;
|
|
390
|
+
sender?: string;
|
|
391
|
+
subject?: string;
|
|
392
|
+
/** Overall wait, up to 600 (default 120). */
|
|
393
|
+
timeoutS?: number;
|
|
394
|
+
/** Also accept a code received up to this long before the wait began, up to 600 (default 120). */
|
|
395
|
+
maxAgeS?: number;
|
|
396
|
+
/** The code field, or the boxes when there is one per digit. Default: the page's one-time-code field. */
|
|
397
|
+
selector?: string;
|
|
398
|
+
frame?: string;
|
|
399
|
+
/** Press Enter after entering the code. */
|
|
400
|
+
submit?: boolean;
|
|
401
|
+
/** Return the code to you instead of entering it. Without `sessionId` this is the only way to use a code. */
|
|
402
|
+
returnCode?: boolean;
|
|
403
|
+
/** A code held from an earlier `received_not_entered` result: try entering it again. */
|
|
404
|
+
codeId?: string;
|
|
405
|
+
}
|
|
406
|
+
/** `wait_for_code` result. `waitForCode` keeps calling while the status is `waiting`, so these are the final ones. */
|
|
407
|
+
export type WaitForCodeResult = (Json & {
|
|
408
|
+
status: "entered";
|
|
409
|
+
code_id: string;
|
|
410
|
+
code_returned: false;
|
|
411
|
+
sender?: string;
|
|
412
|
+
channel?: string;
|
|
413
|
+
received_at?: string;
|
|
414
|
+
expires_at?: string;
|
|
415
|
+
next_step?: string;
|
|
416
|
+
}) | (Json & {
|
|
417
|
+
status: "received";
|
|
418
|
+
code: string;
|
|
419
|
+
code_id: string;
|
|
420
|
+
code_returned: true;
|
|
421
|
+
sender?: string;
|
|
422
|
+
channel?: string;
|
|
423
|
+
received_at?: string;
|
|
424
|
+
expires_at?: string;
|
|
425
|
+
}) | (Json & {
|
|
426
|
+
status: "received_not_entered";
|
|
427
|
+
code_id: string;
|
|
428
|
+
error: {
|
|
429
|
+
code: string;
|
|
430
|
+
message: string;
|
|
431
|
+
};
|
|
432
|
+
next_step?: string;
|
|
433
|
+
}) | (Json & {
|
|
434
|
+
status: "timeout";
|
|
435
|
+
started_at?: string;
|
|
436
|
+
source_problems?: unknown;
|
|
437
|
+
next_step?: string;
|
|
438
|
+
}) | (Json & {
|
|
439
|
+
status: "session_ended" | "no_source";
|
|
440
|
+
next_step?: string;
|
|
441
|
+
});
|
|
442
|
+
/** Result of `actions.secretFill`. `value` is always `"••••••"`. `typed: true` (or a 409 whose `details.typed` is set) means the value is already in the page: do not send it again.
|
|
443
|
+
* `warning` says why when the call succeeded but a later step (Enter, the action guard) failed. */
|
|
444
|
+
export interface SecretFillResult extends Json {
|
|
445
|
+
ok: boolean;
|
|
446
|
+
kind: "password" | "code";
|
|
447
|
+
site: string;
|
|
448
|
+
selector: string;
|
|
449
|
+
value: string;
|
|
450
|
+
submitted: boolean;
|
|
451
|
+
frame_origin: string;
|
|
452
|
+
typed?: boolean;
|
|
453
|
+
warning?: string;
|
|
454
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.2.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = "0.2.0";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SteerpaneError } from "./errors.js";
|
|
2
|
+
export declare const SIGNATURE_HEADER = "Steerpane-Signature";
|
|
3
|
+
export declare const DELIVERY_HEADER = "Steerpane-Delivery";
|
|
4
|
+
export declare class WebhookSignatureError extends SteerpaneError {
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
export interface VerifyOptions {
|
|
8
|
+
toleranceS?: number;
|
|
9
|
+
/** Unix seconds; for tests. */
|
|
10
|
+
now?: number;
|
|
11
|
+
}
|
|
12
|
+
/** The signature header's value, or a request's headers: a `Headers` object, or Node's `req.headers`. */
|
|
13
|
+
export type SignatureSource = string | null | undefined | Headers | Record<string, string | string[] | undefined>;
|
|
14
|
+
/** True when `source` holds a fresh, valid signature of `rawBody`. Pass the body as received, not re-encoded JSON. */
|
|
15
|
+
export declare function verifySignature(rawBody: string | Uint8Array, source: SignatureSource, secret: string, options?: VerifyOptions): boolean;
|
|
16
|
+
/** Check the signature and return the parsed event, or throw `WebhookSignatureError`. */
|
|
17
|
+
export declare function constructEvent<T = Record<string, unknown>>(rawBody: string | Uint8Array, source: SignatureSource, secret: string, options?: VerifyOptions): T;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check a webhook's `Steerpane-Signature` header.
|
|
3
|
+
*
|
|
4
|
+
* The header is `t=<unix seconds>,v1=<hex>`, where the hex is HMAC-SHA256 of `<t>.<raw body>` with the endpoint's secret. For 24 hours after
|
|
5
|
+
* a secret rotation it carries one `v1` per secret, and any match is good. Deliveries can repeat: deduplicate on the event's `id`.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
8
|
+
import { SteerpaneError } from "./errors.js";
|
|
9
|
+
export const SIGNATURE_HEADER = "Steerpane-Signature";
|
|
10
|
+
export const DELIVERY_HEADER = "Steerpane-Delivery";
|
|
11
|
+
/** What the header was called before the rename. Read only when `SIGNATURE_HEADER` is absent. */
|
|
12
|
+
const LEGACY_SIGNATURE_HEADER = "Conduit-Signature";
|
|
13
|
+
const DEFAULT_TOLERANCE_S = 300;
|
|
14
|
+
export class WebhookSignatureError extends SteerpaneError {
|
|
15
|
+
name = "WebhookSignatureError";
|
|
16
|
+
}
|
|
17
|
+
function signatureValue(source) {
|
|
18
|
+
if (source === null || source === undefined || typeof source === "string")
|
|
19
|
+
return source;
|
|
20
|
+
const names = [SIGNATURE_HEADER, LEGACY_SIGNATURE_HEADER];
|
|
21
|
+
if (typeof Headers !== "undefined" && source instanceof Headers) {
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
const value = source.get(name);
|
|
24
|
+
if (value)
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const lowered = new Map(Object.entries(source).map(([k, v]) => [k.toLowerCase(), v]));
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
const value = lowered.get(name.toLowerCase());
|
|
32
|
+
if (value)
|
|
33
|
+
return Array.isArray(value) ? value[0] : value;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/** True when `source` holds a fresh, valid signature of `rawBody`. Pass the body as received, not re-encoded JSON. */
|
|
38
|
+
export function verifySignature(rawBody, source, secret, options = {}) {
|
|
39
|
+
const header = signatureValue(source);
|
|
40
|
+
if (!header || !secret)
|
|
41
|
+
return false;
|
|
42
|
+
const pairs = header.split(",").map((p) => {
|
|
43
|
+
const i = p.indexOf("=");
|
|
44
|
+
return i < 0 ? ["", ""] : [p.slice(0, i).trim(), p.slice(i + 1).trim()];
|
|
45
|
+
});
|
|
46
|
+
const timestamp = pairs.find(([k]) => k === "t")?.[1];
|
|
47
|
+
const signatures = pairs.filter(([k]) => k === "v1").map(([, v]) => v);
|
|
48
|
+
if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
const now = options.now ?? Date.now() / 1000;
|
|
51
|
+
if (Math.abs(now - Number(timestamp)) > (options.toleranceS ?? DEFAULT_TOLERANCE_S))
|
|
52
|
+
return false;
|
|
53
|
+
const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
|
|
54
|
+
return signatures.some((signature) => {
|
|
55
|
+
const given = Buffer.from(signature, "hex");
|
|
56
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Check the signature and return the parsed event, or throw `WebhookSignatureError`. */
|
|
60
|
+
export function constructEvent(rawBody, source, secret, options = {}) {
|
|
61
|
+
if (!verifySignature(rawBody, source, secret, options))
|
|
62
|
+
throw new WebhookSignatureError("Webhook signature is missing, invalid or too old");
|
|
63
|
+
return JSON.parse(typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8"));
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerpane/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Node client for Steerpane: remote browsers for AI agents, with human takeover.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://steerpane.app",
|
|
7
|
+
"repository": { "type": "git", "url": "git+https://github.com/Steerpane/sdks.git", "directory": "node" },
|
|
8
|
+
"keywords": ["browser", "automation", "agents", "playwright", "cdp", "mcp"],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
17
|
+
"engines": { "node": ">=20" },
|
|
18
|
+
"publishConfig": { "access": "public" },
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "node --test test/*.test.mjs",
|
|
22
|
+
"prepack": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": { "playwright-core": ">=1.45" },
|
|
25
|
+
"peerDependenciesMeta": { "playwright-core": { "optional": true } },
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"playwright-core": "^1.50.0",
|
|
29
|
+
"typescript": "^5.6.0"
|
|
30
|
+
}
|
|
31
|
+
}
|