@scalebrowser/sdk 0.3.0 → 0.4.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/README.md CHANGED
@@ -59,50 +59,50 @@ try {
59
59
 
60
60
  Every `/v1` endpoint is a typed method on `ScalebrowserClient`:
61
61
 
62
- - **Profiles** — `listProfiles`, `getProfile`, `createProfile`, `updateProfile`,
62
+ - **Profiles**: `listProfiles`, `getProfile`, `createProfile`, `updateProfile`,
63
63
  `deleteProfile`, `startProfile`, `stopProfile`
64
- - **Bulk** — `bulkCreateProfiles`, `bulkStart`, `bulkStop`, `bulkDelete`,
64
+ - **Bulk**: `bulkCreateProfiles`, `bulkStart`, `bulkStop`, `bulkDelete`,
65
65
  `bulkAssignProxy`
66
- - **Groups / Presets** — `listGroups`/`createGroup`/`getGroup`/`updateGroup`/`deleteGroup`,
66
+ - **Groups / Presets**: `listGroups`/`createGroup`/`getGroup`/`updateGroup`/`deleteGroup`,
67
67
  `listPresets`/`createPreset`/`getPreset`/`updatePreset`/`deletePreset`,
68
68
  `getPersonaConstraints`. A preset is `config` (what the profiles do:
69
69
  `geo_mode`, `proxy_id`, …) plus `constraints` (what they are: `country`, which
70
70
  pins the persona's language, timezone and voices). Both are typed and the daemon
71
- refuses an unknown key with a 400 — read the valid regions from
71
+ refuses an unknown key with a 400, so read the valid regions from
72
72
  `getPersonaConstraints()` rather than hardcoding them.
73
- - **Proxies** — `listProxies`, `createProxy`, `getProxy`, `updateProxy`,
73
+ - **Proxies**: `listProxies`, `createProxy`, `getProxy`, `updateProxy`,
74
74
  `deleteProxy`, `checkProxy`, `checkProxyConfig` (probe a config before you save
75
- it; pass `id` to reuse an existing proxy's stored credentials) — credentials
75
+ it; pass `id` to reuse an existing proxy's stored credentials). Credentials
76
76
  are write-only, never returned
77
- - **Extensions** — `listExtensions`, `attachExtension`, `detachExtension`, plus
77
+ - **Extensions**: `listExtensions`, `attachExtension`, `detachExtension`, plus
78
78
  the daemon-wide library (`uploadExtension`, `getLibraryExtension`,
79
79
  `deleteLibraryExtension`). An attached package IS loaded into the browser at
80
80
  launch, under the canonical Web-Store id its own key derives
81
- - **Credentials** — `listCredentials`, `putCredential`, `revealCredential`
81
+ - **Credentials**: `listCredentials`, `putCredential`, `revealCredential`
82
82
  (needs the vault password), `exportCredentials`, `importCredentials`
83
- - **Cookies** — `revealCookies`, the one route a cookie VALUE leaves through,
83
+ - **Cookies**: `revealCookies`, the one route a cookie VALUE leaves through,
84
84
  behind the same vault password
85
- - **Sessions** — `exportSession`, `importSession`
86
- - **Mailboxes** — `listInboxes`, `createInbox`, `updateInbox`, `deleteInbox`,
87
- `getInboxBindings`, `bindInbox`, `unbindInbox` — where a profile's
85
+ - **Sessions**: `exportSession`, `importSession`
86
+ - **Mailboxes**: `listInboxes`, `createInbox`, `updateInbox`, `deleteInbox`,
87
+ `getInboxBindings`, `bindInbox`, `unbindInbox`, which is where a profile's
88
88
  confirmation codes arrive
89
- - **Passkeys** — `listPasskeys`, `deletePasskey`. Metadata only: the private key
89
+ - **Passkeys**: `listPasskeys`, `deletePasskey`. Metadata only: the private key
90
90
  has no field and no endpoint
91
- - **Agent runs** — `listRuns`, `getRun`, `listRunSteps`, `getRunShot`,
91
+ - **Agent runs**: `listRuns`, `getRun`, `listRunSteps`, `getRunShot`,
92
92
  `getActivity`. Read-only, all of it
93
- - **Interruptions** — `listInterruptionLocks`, `setInterruptionLock`,
94
- `listInterruptionRules`, `setInterruptionRule`, `deleteInterruptionRule` — who
93
+ - **Interruptions**: `listInterruptionLocks`, `setInterruptionLock`,
94
+ `listInterruptionRules`, `setInterruptionRule`, `deleteInterruptionRule`: who
95
95
  may answer when the browser asks something
96
- - **Artifacts** — `putArtifact` (hand the daemon a file to upload later),
96
+ - **Artifacts**: `putArtifact` (hand the daemon a file to upload later),
97
97
  `getArtifact` (fetch a screenshot, download or saved PDF as bytes)
98
- - **Input** — `sendInput`
99
- - **Metrics / Account / Health** — `getMetrics`, `getAccount`, `health`, `ready`.
98
+ - **Input**: `sendInput`
99
+ - **Metrics / Account / Health**: `getMetrics`, `getAccount`, `health`, `ready`.
100
100
  The first two answer 404 on a daemon without them, which the SDK treats as
101
101
  information rather than as an error: metrics are then derived from profile
102
102
  state, and the account reads `licensed: false`, which is what "self-hosted, no
103
103
  control plane" means
104
104
 
105
- Errors map to typed exceptions carrying the daemon's numeric code (4001–4010):
105
+ Errors map to typed exceptions carrying the daemon's numeric code (4001–4012):
106
106
 
107
107
  ```ts
108
108
  import { ApiError, ErrorCode } from '@scalebrowser/sdk';
@@ -111,7 +111,7 @@ try {
111
111
  await sb.startProfile(id);
112
112
  } catch (err) {
113
113
  if (err instanceof ApiError && err.code === ErrorCode.Capacity) {
114
- // 4003 — capacity exceeded
114
+ // 4003: capacity exceeded
115
115
  }
116
116
  }
117
117
  ```
@@ -136,11 +136,11 @@ for await (const e of sb.events()) {
136
136
 
137
137
  `CdpSession` (returned by `connectCdp` / `launch`) gives you:
138
138
 
139
- - `send<T>(method, params?)` — raw CDP command, awaited by id (concurrent-safe)
139
+ - `send<T>(method, params?)`: raw CDP command, awaited by id (concurrent-safe)
140
140
  - `navigate(url, { wait, timeout })`, `evaluate<T>(expr, { isolated })`
141
141
  - `createIsolatedWorld(frameId?, worldName?)`
142
142
  - `attachToPage({ targetId?, create?, url? })`
143
- - `on(method, cb)` / `events()` — CDP event subscription scoped to the session
143
+ - `on(method, cb)` / `events()`: CDP event subscription scoped to the session
144
144
  - `humanizeMove` / `humanizeClick` / `humanizeType` / `humanizeScroll`
145
145
  - `close()`
146
146
 
@@ -149,6 +149,12 @@ for await (const e of sb.events()) {
149
149
  - Bearer token is sent on every request and the SSE/CDP handshakes; it is
150
150
  **never logged** (the client's inspect output masks it).
151
151
  - Proxy credentials are injected by the daemon and never travel over CDP.
152
+ - **The CDP endpoint is guarded by the OS user, not by that token.** The engine's
153
+ DevTools port has no authentication of its own: it answers a bogus bearer with
154
+ `200`, measured, so the engine drops any connection whose peer process runs as
155
+ a different user. Nothing to configure and nothing to pass: your process is the
156
+ one that started the profile, so it is on the allowed side. A helper running as
157
+ another account will not get in, by design.
152
158
 
153
159
  ## Develop
154
160
 
package/dist/index.cjs CHANGED
@@ -21,7 +21,17 @@ var ErrorCode = {
21
21
  * Distinct from Capacity (4003 = this machine is full, wait): the caller's move
22
22
  * is to stop a running browser anywhere or upgrade the plan. */
23
23
  ConcurrencyLimit: 4008,
24
- Unauthorized: 4010
24
+ Unauthorized: 4010,
25
+ /** No account is connected to this machine, so nothing may run (DEC-014).
26
+ * Distinct from Unauthorized (4010 = the daemon's own bearer token is wrong, so
27
+ * re-read the token): 4011 means the machine needs to be signed in, and no token
28
+ * a client holds can substitute for that. Connect it in the desktop app. */
29
+ NotSignedIn: 4011,
30
+ /** This ONE profile is open on another machine of the same account (DEC-015).
31
+ * Distinct from ConcurrencyLimit (4008 = the plan is full, buy more or free any
32
+ * browser): the caller's move here costs nothing. Close it on that machine, or
33
+ * take it over from the app running there. */
34
+ ProfileInUse: 4012
25
35
  };
26
36
  var CODE_LABELS = {
27
37
  4001: "Not found",
@@ -31,7 +41,9 @@ var CODE_LABELS = {
31
41
  4005: "Launch preflight failed",
32
42
  4006: "Profile already running",
33
43
  4008: "Concurrent-browser limit reached (subscription-wide)",
34
- 4010: "Unauthorized"
44
+ 4010: "Unauthorized",
45
+ 4011: "No account connected to this machine",
46
+ 4012: "This profile is open on another machine"
35
47
  };
36
48
  var ScalebrowserError = class extends Error {
37
49
  constructor(message) {
@@ -133,7 +145,7 @@ var ClientCore = class {
133
145
  return text ? JSON.parse(text) : void 0;
134
146
  }
135
147
  /**
136
- * A route that answers with bytes, not JSON — artifacts and run stills.
148
+ * A route that answers with bytes, not JSON: artifacts and run stills.
137
149
  *
138
150
  * Separate from {@link ClientCore.request} rather than a flag on it, because the two
139
151
  * differ in the error path as well: a failed byte fetch still answers JSON,
@@ -251,7 +263,7 @@ var ControlApi = class extends ClientCore {
251
263
  });
252
264
  }
253
265
  /**
254
- * Fetch one artifact's bytes — a screenshot, a download, a saved PDF.
266
+ * Fetch one artifact's bytes: a screenshot, a download, a saved PDF.
255
267
  *
256
268
  * A miss is a plain 404 whether the id never existed, expired, or belonged to
257
269
  * a released lease: the caller's next move is the same in all three cases.
@@ -264,7 +276,7 @@ var ControlApi = class extends ClientCore {
264
276
  * Licence state for display. The launch gate stays the authority on starts.
265
277
  *
266
278
  * A daemon predating the route answers 404, and that is not an error worth
267
- * throwing — but it is read pessimistically: nothing can be shown to be
279
+ * throwing, but it is read pessimistically: nothing can be shown to be
268
280
  * connected or permitted. Same shape of tolerance as {@link getMetrics}.
269
281
  */
270
282
  async getAccount(signal) {
@@ -279,7 +291,7 @@ var ControlApi = class extends ClientCore {
279
291
  health(signal) {
280
292
  return this.request("GET", "/health", void 0, { signal });
281
293
  }
282
- /** "Can it serve?" Local facts only — the control plane is deliberately not consulted. */
294
+ /** "Can it serve?" Local facts only; the control plane is deliberately not consulted. */
283
295
  ready(signal) {
284
296
  return this.request("GET", "/health/ready", void 0, { signal });
285
297
  }
@@ -291,7 +303,7 @@ var ExtensionsApi = class extends ClientCore {
291
303
  //
292
304
  // Upload a `.crx`; the daemon lifts its public key out of the CRX header and
293
305
  // writes it into the unpacked `manifest.json`, so every profile loads the
294
- // package under its CANONICAL Web-Store id. A key-less package is refused — it
306
+ // package under its CANONICAL Web-Store id. A key-less package is refused, since it
295
307
  // would take a path-derived id, identical across the fleet.
296
308
  /** The library, newest first. */
297
309
  listLibraryExtensions(signal) {
@@ -304,7 +316,7 @@ var ExtensionsApi = class extends ClientCore {
304
316
  uploadExtension(crx) {
305
317
  return this.request("POST", "/v1/extensions", crx);
306
318
  }
307
- /** One library entry — the most recently added version. */
319
+ /** One library entry: the most recently added version. */
308
320
  getLibraryExtension(id, signal) {
309
321
  return this.request("GET", `/v1/extensions/${enc(id)}`, void 0, { signal });
310
322
  }
@@ -315,7 +327,7 @@ var ExtensionsApi = class extends ClientCore {
315
327
  /**
316
328
  * Remove ONE stocked version.
317
329
  *
318
- * Assignments survive — they name the extension, not the version — unless this
330
+ * Assignments survive, because they name the extension and not the version, unless this
319
331
  * was the last version, in which case they go with it.
320
332
  */
321
333
  deleteLibraryExtensionVersion(id, version) {
@@ -354,14 +366,14 @@ var IdentityApi = class extends ClientCore {
354
366
  // ── Platform credentials ────────────────────────────────────────────────────
355
367
  //
356
368
  // Stored logins, so an agent can sign a profile into a platform WITHOUT ever
357
- // holding the password — the daemon decrypts and types it. Only `revealCredential`
369
+ // holding the password: the daemon decrypts and types it. Only `revealCredential`
358
370
  // and the bundle calls return plaintext, and every one of them needs the vault
359
371
  // password: the bearer token alone is not enough, because the MCP agent layer
360
372
  // authenticates with exactly the same one.
361
373
  //
362
374
  // `platform` is whatever you have: the service's domain ("discord.com"), its
363
375
  // name ("Discord") or a host under it ("www.discord.com"). The daemon resolves
364
- // all of them to ONE key — the registrable domain — and every response echoes
376
+ // all of them to ONE key (the registrable domain), and every response echoes
365
377
  // that key, so `listCredentials` and a later `deleteCredential` agree. Pass a
366
378
  // whole sign-in URL only to the MCP tools: here the platform is a path segment,
367
379
  // and a `/` in it is refused rather than read as a path.
@@ -377,7 +389,7 @@ var IdentityApi = class extends ClientCore {
377
389
  /**
378
390
  * Store or update one login.
379
391
  *
380
- * An omitted secret keeps its stored value rather than clearing it — an edit form
392
+ * An omitted secret keeps its stored value rather than clearing it, because an edit form
381
393
  * never saw the password in the clear, so it cannot resend one; delete the row to
382
394
  * remove a credential. `totp_secret` may be pasted exactly as a site presents it
383
395
  * (grouped, lower case): it is normalised and test-decoded by the daemon, so a
@@ -410,7 +422,7 @@ var IdentityApi = class extends ClientCore {
410
422
  *
411
423
  * `password` is deliberately separate from the vault password: a backup gets
412
424
  * handed to another machine, and that must not also hand over daemon access. This
413
- * is the answer to a lost master key — a daemon-generated password exists nowhere
425
+ * is the answer to a lost master key: a daemon-generated password exists nowhere
414
426
  * else.
415
427
  */
416
428
  exportCredentials(profileId, vaultPassword, password) {
@@ -482,6 +494,72 @@ var IdentityApi = class extends ClientCore {
482
494
  deletePasskey(profileId, credentialId) {
483
495
  return this.request("DELETE", `/v1/profiles/${enc(profileId)}/passkeys/${enc(credentialId)}`);
484
496
  }
497
+ // ── Secret Layer ───────────────────────────────────────────────────────────
498
+ //
499
+ // Values a profile holds that no agent ever reads. Nothing here returns a
500
+ // value, and that is not an oversight: the wrapper below starts the command
501
+ // INSIDE the daemon precisely so that no route has to hand plaintext out.
502
+ /** The entries this profile keeps, with the placeholder that stands for each. */
503
+ listSecrets(profileId, signal) {
504
+ return this.request(
505
+ "GET",
506
+ `/v1/profiles/${enc(profileId)}/secrets`,
507
+ void 0,
508
+ { signal }
509
+ );
510
+ }
511
+ /** Change a title, a binding, or the state. Never the label. */
512
+ updateSecret(profileId, secretId, body) {
513
+ return this.request(
514
+ "PATCH",
515
+ `/v1/profiles/${enc(profileId)}/secrets/${enc(secretId)}`,
516
+ body
517
+ );
518
+ }
519
+ /** Delete an entry. The reply names the runs it appeared in. */
520
+ deleteSecret(profileId, secretId) {
521
+ return this.request(
522
+ "DELETE",
523
+ `/v1/profiles/${enc(profileId)}/secrets/${enc(secretId)}`
524
+ );
525
+ }
526
+ /**
527
+ * Filter a text you hold against this profile's values.
528
+ *
529
+ * The SDK path exists because censoring only makes sense where a MODEL is
530
+ * reading: your own code is not, and a placeholder in the middle of it would
531
+ * break parsing. So the daemon offers the filter as a call rather than
532
+ * applying it to a channel it does not own.
533
+ */
534
+ scrubText(profileId, text) {
535
+ return this.request(
536
+ "POST",
537
+ `/v1/profiles/${enc(profileId)}/secrets/scrub`,
538
+ { text }
539
+ );
540
+ }
541
+ /**
542
+ * Run a command with named values in its environment.
543
+ *
544
+ * The daemon starts the child, so no value crosses a socket. At least one
545
+ * `env` entry is required: without the rule an `npm publish` would carry every
546
+ * value of the profile.
547
+ */
548
+ runWithSecrets(profileId, body) {
549
+ return this.request(
550
+ "POST",
551
+ `/v1/profiles/${enc(profileId)}/secrets/run`,
552
+ body
553
+ );
554
+ }
555
+ /** Is the secret layer on? It is by default. */
556
+ getSecretLayer(signal) {
557
+ return this.request("GET", "/v1/secret-layer", void 0, { signal });
558
+ }
559
+ /** Turn the secret layer on or off for the whole daemon. */
560
+ setSecretLayer(enabled) {
561
+ return this.request("POST", "/v1/secret-layer", { enabled });
562
+ }
485
563
  // ── Cookies ────────────────────────────────────────────────────────────────
486
564
  /**
487
565
  * The one door a cookie VALUE leaves through, and it needs the vault password.
@@ -508,7 +586,7 @@ var RunsApi = class extends ClientCore {
508
586
  getRun(runId, signal) {
509
587
  return this.request("GET", `/v1/runs/${enc(runId)}`, void 0, { signal });
510
588
  }
511
- /** A run's steps, oldest first — the order they happened in. */
589
+ /** A run's steps, oldest first: the order they happened in. */
512
590
  listRunSteps(runId, params = {}, signal) {
513
591
  return this.request("GET", `/v1/runs/${enc(runId)}/steps`, void 0, {
514
592
  query: { limit: params.limit, offset: params.offset },
@@ -713,7 +791,7 @@ var CdpConnection = class {
713
791
  return () => this.#subscribers.delete(cb);
714
792
  }
715
793
  /** Register a callback fired once when the connection closes (immediately if
716
- * it already has) — event streams use this to end instead of hanging. */
794
+ * it already has); event streams use this to end instead of hanging. */
717
795
  onClose(cb) {
718
796
  if (this.#closeReason) {
719
797
  cb();
@@ -997,7 +1075,7 @@ var ScalebrowserClient = class extends ClientCore {
997
1075
  });
998
1076
  }
999
1077
  /**
1000
- * Every id matching `params`, unpaged — what "act on all matches" needs.
1078
+ * Every id matching `params`, unpaged: what "act on all matches" needs.
1001
1079
  *
1002
1080
  * `listProfiles` is paged, so acting on "everything" from it only ever covers the
1003
1081
  * page you fetched. Asking for ids costs one request and no persona payloads.
@@ -1033,6 +1111,14 @@ var ScalebrowserClient = class extends ClientCore {
1033
1111
  stopProfile(id) {
1034
1112
  return this.request("POST", `/v1/profiles/${enc(id)}/stop`, {});
1035
1113
  }
1114
+ /** Ask for a profile that is open on another machine of this account.
1115
+ *
1116
+ * Closes nothing here and nothing there: the control plane revokes the holder's
1117
+ * right, the holder closes its own browser at its next heartbeat. Start only
1118
+ * after this answered `free`. Errors: 4012 when it is held elsewhere. */
1119
+ takeoverProfile(id) {
1120
+ return this.request("POST", `/v1/profiles/${enc(id)}/takeover`, {});
1121
+ }
1036
1122
  // ── Bulk (PRD §D2) ───────────────────────────────────────────────────────
1037
1123
  bulkCreateProfiles(body) {
1038
1124
  return this.request("POST", "/v1/bulk/profiles", body);
@@ -1130,7 +1216,7 @@ var ScalebrowserClient = class extends ClientCore {
1130
1216
  return this.request("POST", `/v1/proxies/${enc(id)}/check`, {});
1131
1217
  }
1132
1218
  /**
1133
- * Probe a proxy config WITHOUT persisting it — validate before you commit.
1219
+ * Probe a proxy config WITHOUT persisting it: validate before you commit.
1134
1220
  * Pass `id` (and omit the credentials) to probe an existing proxy's stored,
1135
1221
  * write-only credentials in an edit form.
1136
1222
  */