@reclaimprotocol/agent 0.1.22 → 0.1.24

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.
Files changed (163) hide show
  1. package/README.md +55 -20
  2. package/lib/authoring/backend.d.ts +13 -10
  3. package/lib/authoring/tools.js +106 -9
  4. package/lib/cdp/browser-proxy.d.ts +78 -0
  5. package/lib/cdp/browser-proxy.js +186 -0
  6. package/lib/cdp/capture-engine.js +8 -7
  7. package/lib/cdp/client.d.ts +10 -0
  8. package/lib/cdp/geo.d.ts +10 -0
  9. package/lib/cdp/geo.js +36 -0
  10. package/lib/cdp/index.d.ts +1 -0
  11. package/lib/cdp/index.js +1 -0
  12. package/lib/live/cloudflared.d.ts +15 -0
  13. package/lib/live/cloudflared.js +268 -0
  14. package/lib/live/page.d.ts +20 -0
  15. package/lib/live/page.js +134 -0
  16. package/lib/live/tunnel.d.ts +23 -0
  17. package/lib/live/tunnel.js +120 -0
  18. package/lib/live/viewer.d.ts +28 -0
  19. package/lib/live/viewer.js +232 -0
  20. package/lib/mcp/api/operations.gen.js +34 -26
  21. package/lib/mcp/api/to-zod.d.ts +1 -1
  22. package/lib/mcp/api/to-zod.js +1 -1
  23. package/lib/mcp/api/tools.js +1 -1
  24. package/lib/mcp/instructions.d.ts +15 -2
  25. package/lib/mcp/instructions.js +101 -38
  26. package/lib/mcp/start.js +4 -3
  27. package/lib/mcp/tools/agent/analyze.js +5 -1
  28. package/lib/mcp/tools/agent/attach.d.ts +8 -1
  29. package/lib/mcp/tools/agent/attach.js +166 -22
  30. package/lib/mcp/tools/agent/build.d.ts +13 -8
  31. package/lib/mcp/tools/agent/build.js +22 -11
  32. package/lib/mcp/tools/agent/capture.js +5 -1
  33. package/lib/mcp/tools/agent/eval.js +3 -1
  34. package/lib/mcp/tools/agent/find.js +6 -5
  35. package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
  36. package/lib/mcp/tools/agent/how-it-works.js +57 -17
  37. package/lib/mcp/tools/agent/live-view.d.ts +21 -0
  38. package/lib/mcp/tools/agent/live-view.js +153 -0
  39. package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
  40. package/lib/mcp/tools/agent/pod-proof.js +64 -0
  41. package/lib/mcp/tools/agent/propose.js +29 -3
  42. package/lib/mcp/tools/agent/proxy.d.ts +3 -0
  43. package/lib/mcp/tools/agent/proxy.js +199 -0
  44. package/lib/mcp/tools/agent/publish.js +49 -77
  45. package/lib/mcp/tools/agent/session.d.ts +2 -1
  46. package/lib/mcp/tools/agent/session.js +26 -7
  47. package/lib/mcp/tools/agent/shared.js +17 -8
  48. package/lib/mcp/tools/authenticate/authenticate.d.ts +11 -0
  49. package/lib/mcp/tools/authenticate/authenticate.js +143 -121
  50. package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -3
  51. package/lib/mcp/tools/authenticate/builder-access.js +8 -3
  52. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  53. package/lib/mcp/tools/mode/mode.js +26 -7
  54. package/lib/old/client.d.ts +9 -0
  55. package/lib/old/client.js +19 -1
  56. package/lib/old/instructions.d.ts +9 -1
  57. package/lib/old/instructions.js +4 -6
  58. package/lib/old/login-cdp.d.ts +2 -2
  59. package/lib/old/login-cdp.js +46 -6
  60. package/lib/old/to-register.d.ts +20 -8
  61. package/lib/old/to-register.js +7 -1
  62. package/lib/old/tools/authenticate.js +9 -1
  63. package/lib/old/tools/get-provider-info.js +6 -5
  64. package/lib/old/tools/issue-app-credentials.js +3 -2
  65. package/lib/old/tools/me-providers.js +3 -1
  66. package/lib/old/tools/publish.d.ts +1 -1
  67. package/lib/old/tools/publish.js +132 -129
  68. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  69. package/lib/old/tools/update-provider-metadata.js +1 -1
  70. package/lib/old/tools/verify-sdk-setup.js +1 -1
  71. package/lib/proof/attestor.d.ts +3 -3
  72. package/lib/proof/debug-response-dump.js +4 -4
  73. package/lib/proof/index.d.ts +1 -0
  74. package/lib/proof/index.js +1 -0
  75. package/lib/proof/page-replay.d.ts +36 -0
  76. package/lib/proof/page-replay.js +76 -0
  77. package/lib/proof/pod-prover.d.ts +47 -0
  78. package/lib/proof/pod-prover.js +155 -0
  79. package/lib/proof/replay.d.ts +28 -0
  80. package/lib/proof/replay.js +39 -18
  81. package/lib/provider/dom-xpath.js +11 -9
  82. package/lib/provider/draft.d.ts +13 -8
  83. package/lib/provider/draft.js +47 -13
  84. package/lib/provider/injection-doc.d.ts +23 -1
  85. package/lib/provider/injection-doc.js +34 -21
  86. package/lib/provider/match-redaction-warning.d.ts +2 -2
  87. package/lib/provider/match-redaction-warning.js +3 -3
  88. package/lib/provider/matcher.js +122 -12
  89. package/lib/provider/secrets.d.ts +3 -3
  90. package/lib/provider/secrets.js +3 -3
  91. package/lib/provider/to-version.d.ts +12 -9
  92. package/lib/provider/to-version.js +16 -4
  93. package/lib/tools/registry.d.ts +5 -5
  94. package/lib/tools/registry.js +5 -5
  95. package/package.json +4 -3
  96. package/samples/demo-server/README.md +7 -6
  97. package/skill/SKILL.builder.md +356 -189
  98. package/skill/SKILL.common.md +314 -79
  99. package/skill/SKILL.old.md +340 -155
  100. package/skill/builder-mode.md +58 -17
  101. package/skill/hash-validation.md +170 -0
  102. package/skill/interception.md +235 -0
  103. package/skill/js-sdk-integration.md +13 -7
  104. package/skill/old-mode.md +53 -12
  105. package/skill/user-script.md +61 -0
  106. package/src/authoring/backend.ts +13 -10
  107. package/src/authoring/tools.ts +128 -9
  108. package/src/cdp/capture-engine.ts +8 -7
  109. package/src/cdp/client.ts +8 -0
  110. package/src/cdp/geo.ts +52 -0
  111. package/src/cdp/index.ts +1 -0
  112. package/src/consts.ts +1 -0
  113. package/src/live/cloudflared.ts +348 -0
  114. package/src/live/page.ts +145 -0
  115. package/src/live/tunnel.ts +157 -0
  116. package/src/live/viewer.ts +293 -0
  117. package/src/mcp/api/operations.gen.ts +34 -26
  118. package/src/mcp/api/to-zod.ts +1 -1
  119. package/src/mcp/api/tools.ts +1 -1
  120. package/src/mcp/instructions.ts +121 -48
  121. package/src/mcp/start.ts +4 -3
  122. package/src/mcp/tools/agent/analyze.ts +5 -1
  123. package/src/mcp/tools/agent/attach.ts +194 -22
  124. package/src/mcp/tools/agent/build.ts +26 -15
  125. package/src/mcp/tools/agent/capture.ts +5 -1
  126. package/src/mcp/tools/agent/eval.ts +4 -1
  127. package/src/mcp/tools/agent/find.ts +6 -5
  128. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  129. package/src/mcp/tools/agent/live-view.ts +198 -0
  130. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  131. package/src/mcp/tools/agent/propose.ts +31 -3
  132. package/src/mcp/tools/agent/publish.ts +54 -78
  133. package/src/mcp/tools/agent/session.ts +26 -5
  134. package/src/mcp/tools/agent/shared.ts +19 -8
  135. package/src/mcp/tools/authenticate/authenticate.ts +167 -165
  136. package/src/mcp/tools/authenticate/builder-access.ts +8 -3
  137. package/src/mcp/tools/mode/mode.ts +28 -7
  138. package/src/old/client.ts +30 -1
  139. package/src/old/instructions.ts +4 -10
  140. package/src/old/login-cdp.ts +50 -6
  141. package/src/old/to-register.ts +28 -8
  142. package/src/old/tools/authenticate.ts +9 -1
  143. package/src/old/tools/get-provider-info.ts +6 -5
  144. package/src/old/tools/issue-app-credentials.ts +3 -2
  145. package/src/old/tools/me-providers.ts +3 -1
  146. package/src/old/tools/publish.ts +140 -130
  147. package/src/old/tools/update-provider-metadata.ts +1 -1
  148. package/src/old/tools/verify-sdk-setup.ts +1 -1
  149. package/src/proof/attestor.ts +3 -3
  150. package/src/proof/debug-response-dump.ts +4 -4
  151. package/src/proof/index.ts +8 -0
  152. package/src/proof/page-replay.ts +131 -0
  153. package/src/proof/pod-prover.ts +231 -0
  154. package/src/proof/replay.ts +60 -19
  155. package/src/provider/dom-xpath.ts +11 -9
  156. package/src/provider/draft.ts +56 -13
  157. package/src/provider/injection-doc.ts +37 -24
  158. package/src/provider/match-redaction-warning.ts +3 -3
  159. package/src/provider/matcher.ts +151 -10
  160. package/src/provider/secrets.ts +3 -3
  161. package/src/provider/to-version.ts +30 -13
  162. package/src/tools/registry.ts +5 -5
  163. package/skill/injection-api.md +0 -335
@@ -0,0 +1,61 @@
1
+ # User scripts (`jsUserScripts` / `customInjection`)
2
+
3
+ A provider can inject **plain JavaScript** that runs in the page before every
4
+ page load. Builder stores it as `webSettings.jsUserScripts`; old-devtools stores
5
+ it as `customInjection`. Identical semantics, different field name.
6
+
7
+ ## What the script is for
8
+
9
+ The verification client **already intercepts the page's network requests
10
+ automatically** — the script does not need to capture, forward, or "claim" them.
11
+ Its job is to get the page into the state where the target request happens,
12
+ using standard browser APIs:
13
+
14
+ - **Navigate** — `window.location.href = '…'`, `location.assign(…)`, the
15
+ history API.
16
+ - **Interact** — select an element and `.click()` it, submit a form, dispatch
17
+ events.
18
+ - **Wait** — poll the DOM until login/MFA completes or content loads, then
19
+ trigger the next step.
20
+
21
+ Write it like any page script. It runs before EVERY page load, so keep it
22
+ idempotent, cheap, and guarded against firing at the wrong time.
23
+
24
+ Both backends run it the same way: before every page load, including before the
25
+ first navigation to `initialUrl`/`loginUrl`.
26
+
27
+ ## `window.Reclaim` — rarely needed
28
+
29
+ `window.Reclaim` is an OPTIONAL bridge for a handful of edge cases. Most
30
+ providers never touch it, and **it does not do navigation** — use the standard
31
+ browser APIs above for that.
32
+
33
+ **On the Builder verification client it is installed but INERT.** Both
34
+ `window.Reclaim` and `window.reclaimInterceptor` are injected before every
35
+ document load, so a call won't throw — but the run never drains the
36
+ `Reclaim` outbox and never delivers traffic to
37
+ `reclaimInterceptor.addResponseMiddleware`, so nothing you send is acted on
38
+ and no middleware ever fires. Write Builder-targeted scripts in plain browser
39
+ JS unconditionally, and let the capture loop find the request itself.
40
+
41
+ Don't reach for it by default. When an edge case genuinely requires it, consult
42
+ the authoritative API surface rather than guessing:
43
+ <https://github.com/reclaimprotocol/provider-web-script-dev/blob/main/src/support/env.d.ts>
44
+
45
+ ## Declaring the requests the script fires
46
+
47
+ Requests the script triggers beyond the proven one (for example, pagination) are
48
+ declared as `allowedJsRequests` (Builder) / `allowedInjectedRequestData`
49
+ (old-devtools). Same concept, different field name.
50
+
51
+ ⚠️ **Before populating them to gate `verifyProof(proof, { providerId })`**, read
52
+ topic `hash-validation` — the declared `url` is hashed VERBATIM, so a
53
+ session-unique URL there makes `verifyProof` reject every real proof. Leaving
54
+ the field empty is better than populating it wrong.
55
+
56
+ ## Related topics
57
+
58
+ - `interception` — `injectionType`/`interceptorType`, live interception, and the
59
+ document-replay flag.
60
+ - `hash-validation` — `bodySniff`, and how `verifyProof({ providerId })` gates
61
+ on the provider's declared spec.
@@ -85,18 +85,21 @@ export interface AuthoringBackend {
85
85
  /** This session's drafts (propose → replay/run_proof). */
86
86
  readonly drafts: DraftStore
87
87
  /** Prove a drafted recipe. Local: attestor SDK (rich claim view). Remote: TEE
88
- * `/reclaim/prove` (lean, secret-free summary). Returns the environment's
89
- * display shape. A backend may record the proven recipe internally (e.g.
90
- * the cloud publish step) — that side effect is the backend's, not ours. */
88
+ * `/reclaim/prove` (lean, secret-free summary). Returns the environment's
89
+ * display shape. A backend may record the proven recipe internally (for
90
+ * example, the cloud publish step) — that side effect is the backend's, not
91
+ * ours. */
91
92
  prove(draft: Draft, opts?: ProveOptions): Promise<unknown>
92
93
  /**
93
- * Validate/replay a drafted recipe before proving. With `withoutSecrets` it
94
- * is the AUTH-BOUND check: re-issue the request stripped of the captured
95
- * secrets (local: a Node re-fetch without them; remote: an anonymous `fetch`
96
- * in the Popcorn page, `credentials: 'omit'`) and report whether the values
97
- * are STILL extractable — i.e. the endpoint is public. Without it, validate
98
- * extraction for the authenticated request. The shape is environment-specific
99
- * and passed straight through as the tool's result.
94
+ * Validate/replay a drafted recipe before proving. Both local and remote
95
+ * backends re-issue the request FROM INSIDE the live attached page (a
96
+ * `fetch` run over CDP), never a detached out-of-band call, so replay
97
+ * always reflects the real browser session. With `withoutSecrets` it is
98
+ * the AUTH-BOUND check: re-issue anonymously (`credentials: 'omit'`, no
99
+ * secret headers) and report whether the values are STILL extractable —
100
+ * that is, the endpoint is public. Without it, validate extraction for the
101
+ * authenticated request. The shape is environment-specific and passed
102
+ * straight through as the tool's result.
100
103
  */
101
104
  replay(draft: Draft, opts?: { withoutSecrets?: boolean }): Promise<unknown>
102
105
  }
@@ -116,7 +116,10 @@ export function authoringTools(
116
116
  defineTool<Record<string, unknown>>(
117
117
  {
118
118
  name: 'navigate',
119
- description: 'Navigate the browser tab to a URL, waiting for load.',
119
+ description:
120
+ 'Navigate the captured tab to a URL and wait for its load event. '
121
+ + 'Returns the URL the tab actually landed on, so you can detect a '
122
+ + 'redirect to a login page.',
120
123
  inputSchema: objectSchema(
121
124
  { url: { type: 'string' }, timeoutMs: { type: 'number' } },
122
125
  ['url'],
@@ -144,8 +147,10 @@ export function authoringTools(
144
147
  {
145
148
  name: 'eval_in_page',
146
149
  description:
147
- 'Evaluate a JavaScript expression in the page and return its value '
148
- + '(use to read the displayed value, check login state, etc.).',
150
+ 'Evaluate a JavaScript expression in the page and return its value. '
151
+ + 'Use it to read the value being proven out of the DOM, to check '
152
+ + 'login state, or to trigger the target request (for example, '
153
+ + '`location.reload()`).',
149
154
  inputSchema: objectSchema(
150
155
  { expression: { type: 'string' } }, ['expression'],
151
156
  ),
@@ -156,8 +161,10 @@ export function authoringTools(
156
161
  {
157
162
  name: 'wait_for_page',
158
163
  description:
159
- 'Poll a JavaScript expression until it is truthy or the timeout '
160
- + 'elapses (e.g. wait for the user to finish logging in).',
164
+ 'Poll a JavaScript expression once a second until it is truthy or '
165
+ + 'the timeout elapses. Use it to wait for content to appear. To '
166
+ + 'wait for a login, use wait_for_login instead — page JavaScript '
167
+ + 'cannot see the httpOnly session cookie.',
161
168
  inputSchema: objectSchema(
162
169
  { expression: { type: 'string' }, timeoutMs: { type: 'number' } },
163
170
  ['expression'],
@@ -179,6 +186,111 @@ export function authoringTools(
179
186
  return { ready: false }
180
187
  },
181
188
  ),
189
+ defineTool<Record<string, unknown>>(
190
+ {
191
+ name: 'test_user_script',
192
+ description:
193
+ 'Dry-run a USER SCRIPT (the `jsUserScripts` / `customInjection` '
194
+ + 'field) in the LIVE attached tab, BEFORE publishing. Installs '
195
+ + '`script` via CDP so it runs on the next navigation exactly as '
196
+ + 'the real field would, then reports whether it ran and what it '
197
+ + 'threw. With `navigateUrl`, it navigates there and removes the '
198
+ + 'script once the result is read — self-cleaning, safe to repeat. '
199
+ + 'WITHOUT `navigateUrl` the script stays installed (navigate '
200
+ + 'yourself, then re-call this tool or read '
201
+ + '`window.__reclaimUserScriptResult` via eval_in_page) and keeps '
202
+ + 'running on every later navigation until a call WITH '
203
+ + '`navigateUrl` removes it — so don\'t stack manual-mode calls. '
204
+ + 'Guide: how_it_works({ topic: "user-script" }).',
205
+ inputSchema: objectSchema(
206
+ {
207
+ script: { type: 'string' },
208
+ navigateUrl: { type: 'string' },
209
+ timeoutMs: { type: 'number' },
210
+ },
211
+ ['script'],
212
+ ),
213
+ },
214
+ async(args) => {
215
+ const backend = resolveBackend(args)
216
+ const script = String(args.script)
217
+ // The user's script is spliced in as a STRING and compiled at
218
+ // runtime via `new Function`, not as raw top-level source: a
219
+ // syntax error in it would otherwise fail to parse the entire
220
+ // wrapper (result init, error listener, and try/catch included),
221
+ // so the tool would report ran:false / errors:[] instead of the
222
+ // actual SyntaxError.
223
+ const wrapped = `
224
+ window.__reclaimUserScriptResult = { ran: false, errors: [] };
225
+ window.addEventListener('error', function(e) {
226
+ window.__reclaimUserScriptResult.errors.push(String(e.message));
227
+ });
228
+ try {
229
+ (new Function(${JSON.stringify(script)}))();
230
+ window.__reclaimUserScriptResult.ran = true;
231
+ } catch(e) {
232
+ window.__reclaimUserScriptResult.errors.push(
233
+ String((e && e.message) || e),
234
+ );
235
+ }
236
+ `
237
+ const installed = (await backend.sendCdp(
238
+ 'Page.addScriptToEvaluateOnNewDocument', { source: wrapped },
239
+ )) as { identifier?: string }
240
+
241
+ if(typeof args.navigateUrl !== 'string') {
242
+ return {
243
+ installed: true,
244
+ note:
245
+ 'Installed for the NEXT navigation only — it did not run on '
246
+ + 'the current page, and stays installed (runs on every later '
247
+ + 'navigation too) until a follow-up call with navigateUrl '
248
+ + 'removes it. Navigate now (for example, with the navigate '
249
+ + 'tool), then read window.__reclaimUserScriptResult via '
250
+ + 'eval_in_page, or call '
251
+ + 'test_user_script again with navigateUrl.',
252
+ }
253
+ }
254
+
255
+ try {
256
+ const timeoutMs = Number(args.timeoutMs) || 30_000
257
+ await backend.sendCdp('Page.enable')
258
+ const load = backend.waitForEvent('Page.loadEventFired', timeoutMs)
259
+ const nav = (await backend.sendCdp(
260
+ 'Page.navigate', { url: String(args.navigateUrl) },
261
+ )) as { errorText?: string }
262
+ if(nav.errorText) {
263
+ return { installed: true, navigated: false, error: nav.errorText }
264
+ }
265
+
266
+ await load
267
+ const { value, error } = await evalInPage(
268
+ backend, 'window.__reclaimUserScriptResult',
269
+ )
270
+ if(error) {
271
+ return { installed: true, navigated: true, error }
272
+ }
273
+
274
+ const result =
275
+ value as { ran?: boolean, errors?: string[] } | undefined
276
+ return {
277
+ installed: true,
278
+ navigated: true,
279
+ ran: result?.ran ?? false,
280
+ errors: result?.errors ?? [],
281
+ }
282
+ } finally {
283
+ // One-shot test: remove it so it doesn't keep running (and
284
+ // potentially redirecting the tab) on every later navigation.
285
+ if(installed.identifier) {
286
+ await backend.sendCdp(
287
+ 'Page.removeScriptToEvaluateOnNewDocument',
288
+ { identifier: installed.identifier },
289
+ )
290
+ }
291
+ }
292
+ },
293
+ ),
182
294
  defineTool<Record<string, unknown>>(
183
295
  {
184
296
  name: 'get_cookies',
@@ -186,7 +298,8 @@ export function authoringTools(
186
298
  'List the NAMES of cookies currently set for the page (values are '
187
299
  + 'NEVER returned). The most reliable, language- and DOM-independent '
188
300
  + 'login signal: after the user logs in, an auth/session cookie '
189
- + 'appears (e.g. session/sid/token/auth/jwt/_user/logged_in). '
301
+ + 'appears (for example, session, sid, token, auth, jwt, _user, '
302
+ + 'logged_in). '
190
303
  + 'Includes httpOnly cookies the page JS cannot see.',
191
304
  inputSchema: objectSchema({}),
192
305
  },
@@ -242,8 +355,8 @@ export function authoringTools(
242
355
  'userFirstVisit', 'cf_', '_dd_', 'bcookie', 'bscookie', 'lidc',
243
356
  ].join('|') + ')', 'i')
244
357
  // A real session/auth cookie name. Deliberately NOT bare `token` — a
245
- // challenge/CSRF token (e.g. LinkedIn `chp_token`) contains it and is
246
- // set BEFORE auth completes. Any new httpOnly cookie is also a
358
+ // challenge/CSRF token (for example, LinkedIn `chp_token`) contains it
359
+ // and is set BEFORE auth completes. Any new httpOnly cookie is also a
247
360
  // candidate (server-set session cookies are usually httpOnly).
248
361
  const SESSION = /sess|sid|jwt|_session|auth|logged.?in|remember|li_at/i
249
362
  // Auth/challenge URL paths — while the page is still on one, login is
@@ -321,7 +434,13 @@ export function authoringTools(
321
434
  name: 'run_proof',
322
435
  description:
323
436
  'Prove the drafted recipe (zkTLS). Only call after replay_request '
324
- + 'looks right. Returns the proof result.',
437
+ + 'looks right. Returns the proof result. NOTE: unlike '
438
+ + 'replay_request (which re-issues from inside the live attached '
439
+ + 'browser tab), the ATTESTOR calls the target from its OWN '
440
+ + 'network/IP, separate from the browser\'s. A request that '
441
+ + 'replayed fine can still fail here for geo/IP-bound content '
442
+ + '(country-locked pages, IP-allowlisted APIs) — if that happens, '
443
+ + 'it\'s a property of the site, not a bug in the draft.',
325
444
  inputSchema: objectSchema({ draftId: { type: 'string' } }, ['draftId']),
326
445
  },
327
446
  // The backend shapes its own result: the cloud returns a lean,
@@ -89,9 +89,10 @@ export function isStaticAsset(url: string): boolean {
89
89
  }
90
90
 
91
91
  /** Response Content-Types for static assets. The URL-extension filter misses
92
- * extension-less bundler/CDN URLs (e.g. `/assets/3b8f1c`), so we also drop by
93
- * MIME at response time. `text/html`, `application/json`, and `text/plain`
94
- * are NOT here — those are the document/API shapes recipes match. */
92
+ * extension-less bundler/CDN URLs (for example, `/assets/3b8f1c`), so we also
93
+ * drop by MIME at response time. `text/html`, `application/json`, and
94
+ * `text/plain` are NOT here — those are the document/API shapes recipes
95
+ * match. */
95
96
  const STATIC_ASSET_MIME_EXACT = new Set([
96
97
  'text/css',
97
98
  'application/javascript', 'text/javascript', 'application/x-javascript',
@@ -272,9 +273,9 @@ export function createCaptureEngine(opts: CaptureEngineOptions): CaptureEngine {
272
273
  function onResponseReceived(p: ResponseReceivedLike) {
273
274
  const f = inFlight.get(p.requestId)
274
275
  if(!f) {
275
- // No in-flight entry → the request was skipped (e.g. a static asset)
276
- // or never seen. Discard any buffered response extra-info so a skipped
277
- // asset can't leave a dangling entry.
276
+ // No in-flight entry → the request was skipped (for example, a static
277
+ // asset) or never seen. Discard any buffered response extra-info so a
278
+ // skipped asset can't leave a dangling entry.
278
279
  pendingResponseExtra.delete(p.requestId)
279
280
  return
280
281
  }
@@ -339,7 +340,7 @@ export function createCaptureEngine(opts: CaptureEngineOptions): CaptureEngine {
339
340
  r.finishedAt = Date.now()
340
341
  } catch{
341
342
  // Body might be unavailable for
342
- // some content-types (e.g. websocket). Skip.
343
+ // some content-types (for example, websocket). Skip.
343
344
  }
344
345
  }
345
346
  }
package/src/cdp/client.ts CHANGED
@@ -30,6 +30,14 @@ interface CdpTabEventMap {
30
30
  headers: Record<string, string>
31
31
  }
32
32
  'Network.loadingFinished': { requestId: string }
33
+ /** Screencast frames, for the live view (`live/viewer.ts`). Each carries a
34
+ * `sessionId` that MUST be acked with `Page.screencastFrameAck`, or CDP
35
+ * stops sending. */
36
+ 'Page.screencastFrame': {
37
+ data: string
38
+ sessionId: number
39
+ metadata?: { timestamp?: number }
40
+ }
33
41
  }
34
42
 
35
43
  export interface CdpTabClient {
package/src/cdp/geo.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Country-code suggestion for a country-routed Popcorn browser session, when
3
+ * the caller doesn't name one explicitly. `attach_browser` returns this as the
4
+ * suggestion to confirm with the developer — the calling machine's own
5
+ * public-IP country, resolved the same way reclaim-portal's `ip-geolocation`
6
+ * util does.
7
+ */
8
+ import { LOGGER } from '../logger.ts'
9
+
10
+ export interface IpGeolocation {
11
+ countryCode: string
12
+ country?: string
13
+ region?: string
14
+ city?: string
15
+ }
16
+
17
+ const IPINFO_URL = 'https://ipinfo.io/json'
18
+ const FALLBACK_COUNTRY_CODE = 'US'
19
+
20
+ /** Resolve the calling machine's own public-IP country. Never throws — a
21
+ * lookup failure just loses the "best guess" convenience, falling back to
22
+ * {@link FALLBACK_COUNTRY_CODE} rather than blocking applying a proxy. */
23
+ export async function suggestCountryFromIp(
24
+ fetchImpl: typeof fetch = fetch,
25
+ ): Promise<IpGeolocation> {
26
+ try {
27
+ const res = await fetchImpl(IPINFO_URL)
28
+ if(!res.ok) {
29
+ throw new Error(`ipinfo.io ${res.status}`)
30
+ }
31
+
32
+ const data = await res.json() as {
33
+ country?: string
34
+ region?: string
35
+ city?: string
36
+ }
37
+ const countryCode = data.country?.toUpperCase()
38
+ if(!countryCode) {
39
+ throw new Error('ipinfo.io response had no country')
40
+ }
41
+
42
+ return {
43
+ countryCode,
44
+ country: data.country,
45
+ region: data.region,
46
+ city: data.city,
47
+ }
48
+ } catch(err) {
49
+ LOGGER.warn({ err }, 'suggestCountryFromIp failed; using fallback')
50
+ return { countryCode: FALLBACK_COUNTRY_CODE }
51
+ }
52
+ }
package/src/cdp/index.ts CHANGED
@@ -4,3 +4,4 @@
4
4
  export * from './capture-engine.ts'
5
5
  export { startCapture } from './capture.ts'
6
6
  export { BodyStore } from './eviction.ts'
7
+ export * from './geo.ts'
package/src/consts.ts CHANGED
@@ -224,3 +224,4 @@ export function oldEthUid(): string | undefined {
224
224
  export function oldLoginUrl(): string | undefined {
225
225
  return process.env.RECLAIM_OLD_LOGIN_URL
226
226
  }
227
+