cdp-toolkit 1.6.0 → 1.7.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.
@@ -39,8 +39,9 @@ import type {
39
39
  BrowserCookie, BrowserDriver, DriverUid, ElementLocator, NavigateResult, PageDriver, PageInfo, SnapshotNode,
40
40
  } from "./driver.ts";
41
41
  import type { TargetSelector } from "./types.ts";
42
- import { assertLeaseOk, claimLease, defaultLabel, leaseTtlMs, releaseLeaseFor, type LeaseBackend, type LeaseToken } from "./leases.ts";
42
+ import { assertLeaseOk, claimLease, defaultLabel, leaseTtlMs, releaseLeaseFor, requireLease, type LeaseBackend, type LeaseToken } from "./leases.ts";
43
43
  import { newTrackedPage, originIndex, type PageOrigin } from "./origins.ts";
44
+ import { reapStaleAgentTabs, type ReapedTab } from "./reap.ts";
44
45
 
45
46
  const ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
46
47
  const STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
@@ -70,7 +71,9 @@ async function withPage<T>(driver: BrowserDriver, target: TargetSelector | undef
70
71
  }
71
72
  }
72
73
 
73
- async function resolvePage(driver: BrowserDriver, selector?: TargetSelector): Promise<PageInfo> {
74
+ /** Exported for leases-tools.ts's release_page{target}, which needs exactly this
75
+ * gate: resolving through here is what authorizes the release. */
76
+ export async function resolvePage(driver: BrowserDriver, selector?: TargetSelector): Promise<PageInfo> {
74
77
  const pages = await driver.listPages();
75
78
  const hit = await pickPage(driver, pages, selector);
76
79
  // THE third choke point. close_page and select_page resolve here and nowhere
@@ -93,8 +96,17 @@ async function resolvePage(driver: BrowserDriver, selector?: TargetSelector): Pr
93
96
  * every branch resolves against page-type targets only EXCEPT a bare-id lookup, which (like
94
97
  * legacy resolveTarget's `targets.find`) searches the FULL unfiltered listing, so passing an
95
98
  * exact non-page targetId (a worker, an iframe) still resolves.
99
+ *
100
+ * THE GATE-FREE HALF of resolvePage, and exported for the one caller that needs
101
+ * resolution WITHOUT authorization: claim_page{target}. Everything else must go
102
+ * through resolvePage. Claiming cannot run assertLeaseOk, because under
103
+ * CDP_REQUIRE_LEASE the gate AUTO-ACQUIRES a lease on an unleased tab, so a
104
+ * claim routed through it would take the tab twice — once implicitly with an
105
+ * auto lease, then again explicitly, and the second claim would collide with
106
+ * the first. Refusing a tab another process holds is still enforced, one layer
107
+ * down, by claimLease's own conflict check.
96
108
  */
97
- async function pickPage(driver: BrowserDriver, pages: PageInfo[], selector?: TargetSelector): Promise<PageInfo> {
109
+ export async function pickPage(driver: BrowserDriver, pages: PageInfo[], selector?: TargetSelector): Promise<PageInfo> {
98
110
  if (selector === undefined || selector === "active") {
99
111
  if (!pages.length) throw new SharedToolError("no page targets open");
100
112
  return pages[0]!;
@@ -176,8 +188,15 @@ async function reapSet(driver: BrowserDriver, pages: PageInfo[], all?: boolean):
176
188
  }
177
189
  }
178
190
 
179
- export async function listPages(driver: BrowserDriver, args: { all?: boolean } = {}): Promise<{ pages: ListedPage[]; count: number }> {
180
- const pages = await driver.listPages({ all: args.all });
191
+ export async function listPages(
192
+ driver: BrowserDriver,
193
+ args: { all?: boolean } = {},
194
+ ): Promise<{ pages: ListedPage[]; count: number; reaped?: ReapedTab[] }> {
195
+ // Reap FIRST, so a tab this call is about to close never appears in the
196
+ // listing it returns. Under strict mode only: see requireLease.
197
+ const reaped = requireLease() ? await reapStaleAgentTabs(driver, backendOf(driver)) : [];
198
+ const reapedIds = new Set(reaped.map((r) => r.targetId));
199
+ const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
181
200
  // Never let provenance break the listing: originIndex does not throw, and a
182
201
  // missing or unreadable ledger yields an empty map, so every page falls back
183
202
  // to origin "unknown" exactly as a pre-ledger consumer always saw.
@@ -188,7 +207,10 @@ export async function listPages(driver: BrowserDriver, args: { all?: boolean } =
188
207
  if (rec.unreadable !== undefined) return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
189
208
  return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
190
209
  });
191
- return { pages: annotated, count: annotated.length };
210
+ // `reaped` is present only when it has something to say, so the common-case
211
+ // shape is byte-identical to 1.4.0. Silently closing a tab is not acceptable:
212
+ // if a read closed something, the read says so.
213
+ return { pages: annotated, count: annotated.length, ...(reaped.length ? { reaped } : {}) };
192
214
  }
193
215
 
194
216
  export async function newPage(
@@ -199,12 +221,22 @@ export async function newPage(
199
221
  // Creates the tab AND writes its creation record. The record outlives any
200
222
  // lease taken below, which is the point: a released tab is still an agent's.
201
223
  const p = await newTrackedPage(driver, backendOf(driver), { url: args.url, label });
202
- if (args.claim !== true) return { targetId: p.id, url: p.url };
224
+ // Under strict mode a tab nobody holds is a tab nobody may drive, so creating
225
+ // one without claiming it would hand back a target the very next call has to
226
+ // auto-acquire anyway. Doing it here means the tab is never briefly unowned
227
+ // AND the caller is handed a token it can release with, which matters because
228
+ // an auto-acquired lease never returns one.
229
+ const explicit = args.claim === true;
230
+ if (!explicit && !requireLease()) return { targetId: p.id, url: p.url };
203
231
  // Atomic in the sense that matters: the tab is claimed before this call
204
232
  // returns, so no other agent can see it unclaimed and take it first.
205
233
  const { record, token } = await claimLease(backendOf(driver), p.id, {
206
234
  label,
207
235
  ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
236
+ // An explicit claim:true stays explicit under strict mode: the caller asked
237
+ // for the strong tier and must keep getting it, or turning strict on would
238
+ // silently WEAKEN every existing claim:true call site to pid-only.
239
+ auto: !explicit,
208
240
  });
209
241
  return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
210
242
  }