@reclaimprotocol/agent 0.1.23 → 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 (152) hide show
  1. package/README.md +55 -20
  2. package/lib/authoring/backend.d.ts +5 -4
  3. package/lib/authoring/tools.js +30 -30
  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 +4 -2
  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 +47 -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/authenticate/authenticate.js +26 -13
  48. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  49. package/lib/mcp/tools/mode/mode.js +10 -22
  50. package/lib/old/client.d.ts +1 -1
  51. package/lib/old/client.js +1 -1
  52. package/lib/old/instructions.d.ts +9 -1
  53. package/lib/old/instructions.js +4 -6
  54. package/lib/old/login-cdp.d.ts +2 -2
  55. package/lib/old/login-cdp.js +46 -6
  56. package/lib/old/to-register.d.ts +3 -2
  57. package/lib/old/to-register.js +1 -1
  58. package/lib/old/tools/authenticate.js +9 -1
  59. package/lib/old/tools/get-provider-info.js +6 -5
  60. package/lib/old/tools/issue-app-credentials.js +3 -2
  61. package/lib/old/tools/publish.d.ts +1 -1
  62. package/lib/old/tools/publish.js +90 -131
  63. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  64. package/lib/old/tools/update-provider-metadata.js +1 -1
  65. package/lib/old/tools/verify-sdk-setup.js +1 -1
  66. package/lib/proof/attestor.d.ts +3 -3
  67. package/lib/proof/debug-response-dump.js +4 -4
  68. package/lib/proof/index.d.ts +1 -0
  69. package/lib/proof/index.js +1 -0
  70. package/lib/proof/page-replay.d.ts +1 -1
  71. package/lib/proof/page-replay.js +1 -1
  72. package/lib/proof/pod-prover.d.ts +47 -0
  73. package/lib/proof/pod-prover.js +155 -0
  74. package/lib/provider/dom-xpath.js +11 -9
  75. package/lib/provider/draft.d.ts +10 -7
  76. package/lib/provider/draft.js +11 -9
  77. package/lib/provider/injection-doc.d.ts +23 -1
  78. package/lib/provider/injection-doc.js +34 -21
  79. package/lib/provider/match-redaction-warning.d.ts +2 -2
  80. package/lib/provider/match-redaction-warning.js +3 -3
  81. package/lib/provider/matcher.js +13 -12
  82. package/lib/provider/secrets.d.ts +3 -3
  83. package/lib/provider/secrets.js +3 -3
  84. package/lib/provider/to-version.d.ts +7 -6
  85. package/lib/provider/to-version.js +3 -3
  86. package/lib/tools/registry.d.ts +5 -5
  87. package/lib/tools/registry.js +5 -5
  88. package/package.json +3 -3
  89. package/samples/demo-server/README.md +7 -6
  90. package/skill/SKILL.builder.md +356 -189
  91. package/skill/SKILL.common.md +323 -97
  92. package/skill/SKILL.old.md +340 -155
  93. package/skill/builder-mode.md +58 -17
  94. package/skill/hash-validation.md +170 -0
  95. package/skill/interception.md +235 -0
  96. package/skill/js-sdk-integration.md +13 -7
  97. package/skill/old-mode.md +60 -21
  98. package/skill/user-script.md +61 -0
  99. package/src/authoring/backend.ts +5 -4
  100. package/src/authoring/tools.ts +31 -30
  101. package/src/cdp/capture-engine.ts +8 -7
  102. package/src/cdp/client.ts +8 -0
  103. package/src/cdp/geo.ts +52 -0
  104. package/src/cdp/index.ts +1 -0
  105. package/src/consts.ts +1 -0
  106. package/src/live/cloudflared.ts +348 -0
  107. package/src/live/page.ts +145 -0
  108. package/src/live/tunnel.ts +157 -0
  109. package/src/live/viewer.ts +293 -0
  110. package/src/mcp/api/operations.gen.ts +34 -26
  111. package/src/mcp/api/to-zod.ts +1 -1
  112. package/src/mcp/api/tools.ts +1 -1
  113. package/src/mcp/instructions.ts +121 -48
  114. package/src/mcp/start.ts +4 -3
  115. package/src/mcp/tools/agent/analyze.ts +5 -1
  116. package/src/mcp/tools/agent/attach.ts +194 -22
  117. package/src/mcp/tools/agent/build.ts +26 -15
  118. package/src/mcp/tools/agent/capture.ts +5 -1
  119. package/src/mcp/tools/agent/eval.ts +4 -1
  120. package/src/mcp/tools/agent/find.ts +6 -5
  121. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  122. package/src/mcp/tools/agent/live-view.ts +198 -0
  123. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  124. package/src/mcp/tools/agent/propose.ts +4 -2
  125. package/src/mcp/tools/agent/publish.ts +52 -77
  126. package/src/mcp/tools/agent/session.ts +26 -5
  127. package/src/mcp/tools/authenticate/authenticate.ts +28 -13
  128. package/src/mcp/tools/mode/mode.ts +10 -22
  129. package/src/old/client.ts +2 -2
  130. package/src/old/instructions.ts +4 -10
  131. package/src/old/login-cdp.ts +50 -6
  132. package/src/old/to-register.ts +3 -2
  133. package/src/old/tools/authenticate.ts +9 -1
  134. package/src/old/tools/get-provider-info.ts +6 -5
  135. package/src/old/tools/issue-app-credentials.ts +3 -2
  136. package/src/old/tools/publish.ts +95 -131
  137. package/src/old/tools/update-provider-metadata.ts +1 -1
  138. package/src/old/tools/verify-sdk-setup.ts +1 -1
  139. package/src/proof/attestor.ts +3 -3
  140. package/src/proof/debug-response-dump.ts +4 -4
  141. package/src/proof/index.ts +8 -0
  142. package/src/proof/page-replay.ts +1 -1
  143. package/src/proof/pod-prover.ts +231 -0
  144. package/src/provider/dom-xpath.ts +11 -9
  145. package/src/provider/draft.ts +13 -10
  146. package/src/provider/injection-doc.ts +37 -24
  147. package/src/provider/match-redaction-warning.ts +3 -3
  148. package/src/provider/matcher.ts +13 -12
  149. package/src/provider/secrets.ts +3 -3
  150. package/src/provider/to-version.ts +11 -9
  151. package/src/tools/registry.ts +5 -5
  152. package/skill/injection-api.md +0 -364
@@ -8,7 +8,7 @@ import type { ReclaimOldClient, UpdateProviderMetadataBody } from '../client.ts'
8
8
  * `create_provider_version_from_capture` paths: it does NOT touch
9
9
  * `providerConfig`/versions at all, and only the fields you pass here
10
10
  * change — everything else on the provider is left untouched (no need to
11
- * re-supply `requestData`/`customInjection`/etc., and
11
+ * re-supply `requestData`/`customInjection`, and so on, and
12
12
  * no new version is created).
13
13
  *
14
14
  * The backend 403s if you don't own the provider (and aren't an admin) —
@@ -122,7 +122,7 @@ export function verifySdkSetupTool(client: ReclaimOldClient): RegisteredTool {
122
122
  : 'PUBLIC_URL not set — fine if you only read the proof from the ' +
123
123
  'frontend flow, but the async webhook callback needs a ' +
124
124
  'publicly-reachable URL or it will never fire. Get one via a ' +
125
- 'tunnel (e.g. `ngrok http <port>`) or by deploying to a ' +
125
+ 'tunnel (for example, `ngrok http <port>`) or by deploying to a ' +
126
126
  'dev/staging server you already have — ask the user which they ' +
127
127
  'prefer.',
128
128
  })
@@ -53,13 +53,13 @@ export interface RunAttestorProofOptions {
53
53
  /** When resolving the key from env/.env/file, the address the caller expects
54
54
  * to sign with (sanity check). Ignored when `ownerPrivateKey` is given. */
55
55
  ownerAddress?: string
56
- /** Sign with this key directly (e.g. the app/VC's ephemeral per-session key)
57
- * instead of resolving from env/.env/file. */
56
+ /** Sign with this key directly (for example, the app or VC's ephemeral
57
+ * per-session key) instead of resolving from env/.env/file. */
58
58
  ownerPrivateKey?: string
59
59
  /** ZK engine for `createClaimOnAttestor`. Defaults to `'stwo'` — a pure-WASM
60
60
  * prover that runs everywhere (no native shared library, so it works on
61
61
  * Windows where `'gnark'` fails to load its `.dll`). Server-side paths that
62
- * want gnark's speed pin it explicitly (e.g. the verify runner). */
62
+ * want gnark's speed pin it explicitly (for example, the verify runner). */
63
63
  zkEngine?: 'gnark' | 'snarkjs' | 'stwo'
64
64
  /** Extract the structured {@link AttestorClaim} (claim + attestor signature)
65
65
  * onto the result for nested-proof construction. */
@@ -1,10 +1,10 @@
1
1
  // Debug helper (gated by RECLAIM_AGENT_DUMP_RESPONSE): capture the EXACT
2
2
  // response bytes the attestor validates a claim against, plus a per-match
3
3
  // presence check. A "Response does not contain X" failure means the attestor's
4
- // server-side re-fetch differs from what the browser (replay) saw — e.g. GitHub
5
- // embeds nav/userMenu data for browser requests but not for the attestor's raw
6
- // fetch. Seeing the attestor's actual response is the only way to tell what it
7
- // really returned.
4
+ // server-side re-fetch differs from what the browser (replay) saw — for
5
+ // example, GitHub embeds nav/userMenu data for browser requests but not for the
6
+ // attestor's raw fetch. Seeing the attestor's actual response is the only way
7
+ // to tell what it really returned.
8
8
  //
9
9
  // On each getResponseRedactions call it writes the response to
10
10
  // `attestor-last-response.html` in the process CWD and logs the byte count +,
@@ -15,6 +15,14 @@ export {
15
15
  parseClaim,
16
16
  resolveAttestorUrl,
17
17
  } from './proof-core.ts'
18
+ export {
19
+ type HttpParams,
20
+ type HttpSecretParams,
21
+ type PopcornProveOptions,
22
+ popcornProve,
23
+ popcornValidateExtraction,
24
+ type ValidateExtractionResult,
25
+ } from './pod-prover.ts'
18
26
  export { ensureZkFiles } from './zk-files.ts'
19
27
  // Re-exported attestor-core claim primitive so the agent is the single
20
28
  // integration point with @reclaimprotocol/attestor-core across the builder.
@@ -102,7 +102,7 @@ function authenticatedPageFetch(
102
102
  * (`authenticatedPageFetch`) so cookies are the real browser's, which a
103
103
  * detached call could never reproduce without manually re-deriving them.
104
104
  * WITH `withoutSecrets` (the auth-bound check — "does this still work with
105
- * no credentials, i.e. is it actually public"), replay instead through the
105
+ * no credentials, that is, is it actually public"), replay instead through the
106
106
  * detached Node-side `replayProvider`: the whole point of that check is to
107
107
  * prove NO secret rides along, and the live page's own global `fetch` can't
108
108
  * give that guarantee — a site (or a script installed via test_user_script)
@@ -0,0 +1,231 @@
1
+ /**
2
+ * PopcornPodProver — run the zkTLS proof inside a Popcorn browser session's
3
+ * OWN TEE pod instead of the in-process attestor SDK (`runAttestorProof`).
4
+ * POSTs the recipe + captured secrets to the pod's `/reclaim/prove` (the same
5
+ * attestor TEE+MPC protocol) and re-shapes the response into the shared
6
+ * {@link ProofResult}, so every caller consumes proof results identically
7
+ * regardless of which path produced them.
8
+ *
9
+ * This is the pod's OWN api for creating a proof from a live session — used
10
+ * by both the app's server-driven verification runner and the MCP's
11
+ * `run_proof_via_pod` tool. Proving here (rather than locally) matters
12
+ * specifically when the session is country-routed: Popcorn allocated the
13
+ * browser with an exit country, and this is the SAME enclave holding that
14
+ * browser's TLS session, so the proof reaches the target from the identical
15
+ * exit IP the browser used — a local attestor SDK proof runs from the
16
+ * app/agent's OWN network instead.
17
+ *
18
+ * Contract (verified against ../popcorn-images oapi.go + ../reclaim-tee):
19
+ * POST {apiUrl}/reclaim/prove body { provider_params_json, config_json? }
20
+ * provider_params_json = JSON of { name, params, secretParams, context? }
21
+ * → 200 { session_id, claim, signature } (ReclaimProveResult, snake_case):
22
+ * claim has provider/parameters/owner/timestamp_s/context/identifier/epoch;
23
+ * signature has attestor_address + claim_signature(b64) +
24
+ * result_signature(b64). A proof failure/timeout is a 500 whose `message`
25
+ * carries the reason.
26
+ *
27
+ * The pod API is unauthenticated (gateway/session is the trust boundary); call
28
+ * it over the same internal host used for CDP. Consumers verify
29
+ * `claim_signature` only (see `@reclaimprotocol/client`'s `verifyNestedProof`),
30
+ * so this hands back a {@link AttestorClaim} whose hex `claimSignature` the
31
+ * shared `wrapProofAsJws` (app-side) turns into the nested result proof.
32
+ * `result_signature` is unused by any consumer.
33
+ */
34
+ import { bytesToHex } from '@noble/hashes/utils.js'
35
+ import type { ProviderParams } from '@reclaimprotocol/attestor-core'
36
+ import type { AttestorClaim, ProofResult } from './attestor.ts'
37
+
38
+ /** Attestor HTTP provider params — the recipe the agent synthesis produces. */
39
+ export type HttpParams = ProviderParams<'http'>
40
+
41
+ /** Captured secrets for the replay, exactly as `/reclaim/prove` wants them. */
42
+ export interface HttpSecretParams {
43
+ cookieStr?: string
44
+ authorisationHeader?: string
45
+ headers?: Record<string, string>
46
+ paramValues?: Record<string, string>
47
+ }
48
+
49
+ /**
50
+ * The pod's `/reclaim/prove` 200 body (ReclaimProveResult, snake_case; fields
51
+ * optional because we validate). Note `timestamp_s` and the whole `signature`
52
+ * object are snake_case; matching them is exactly what fixes the false
53
+ * "no claim/signature".
54
+ */
55
+ interface ReclaimProveResponse {
56
+ session_id?: string
57
+ claim?: {
58
+ provider?: string
59
+ parameters?: string
60
+ owner?: string
61
+ timestamp_s?: number
62
+ context?: string
63
+ identifier?: string
64
+ epoch?: number
65
+ }
66
+ signature?: {
67
+ attestor_address?: string
68
+ claim_signature?: string
69
+ result_signature?: string
70
+ }
71
+ }
72
+
73
+ export interface PopcornProveOptions {
74
+ /** Pod API base (the session's `apiUrl`). */
75
+ apiUrl: string
76
+ params: HttpParams
77
+ secretParams: HttpSecretParams
78
+ /** Session context, stringified into the claim context if given. */
79
+ context?: Record<string, unknown>
80
+ fetchImpl?: typeof fetch
81
+ }
82
+
83
+ export interface ValidateExtractionResult {
84
+ valid: boolean
85
+ extractedValue?: string
86
+ error?: string
87
+ steps?: string[]
88
+ }
89
+
90
+ /**
91
+ * The cloud "replay": validate that a drafted recipe's selectors extract the
92
+ * expected value from a captured response, inside the TEE — `POST {apiUrl}
93
+ * /reclaim/validate-extraction`. Reuses the prover's own redaction engine, so
94
+ * a pass means the selectors won't diverge at proof time. No proof/secrets.
95
+ */
96
+ export async function popcornValidateExtraction(opts: {
97
+ apiUrl: string
98
+ responseBody: string
99
+ expectedValue: string
100
+ xPath?: string
101
+ jsonPath?: string
102
+ regex?: string
103
+ fetchImpl?: typeof fetch
104
+ }): Promise<ValidateExtractionResult> {
105
+ const doFetch = opts.fetchImpl ?? fetch
106
+ const base = opts.apiUrl.replace(/\/$/, '')
107
+ try {
108
+ const res = await doFetch(`${base}/reclaim/validate-extraction`, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({
112
+ responseBody: opts.responseBody,
113
+ expectedValue: opts.expectedValue,
114
+ ...(opts.xPath ? { xPath: opts.xPath } : {}),
115
+ ...(opts.jsonPath ? { jsonPath: opts.jsonPath } : {}),
116
+ ...(opts.regex ? { regex: opts.regex } : {}),
117
+ }),
118
+ })
119
+ if(!res.ok) {
120
+ const detail = await res.text().catch(() => '')
121
+ return { valid: false, error: `${res.status}: ${detail.slice(0, 300)}` }
122
+ }
123
+
124
+ const body = (await res.json()) as ValidateExtractionResult
125
+ return body
126
+ } catch(err) {
127
+ const error = err instanceof Error ? err.message : String(err)
128
+ return { valid: false, error }
129
+ }
130
+ }
131
+
132
+ /** Decode standard base64 (Go `base64.StdEncoding` from Popcorn) to bytes. */
133
+ function b64ToBytes(b64: string): Uint8Array {
134
+ return new Uint8Array(Buffer.from(b64, 'base64'))
135
+ }
136
+
137
+ /**
138
+ * Prove a single request through the Popcorn TEE. Returns the shared
139
+ * {@link ProofResult}; on any failure `verified` is false and `error` carries
140
+ * the reason (never throws for an expected proof failure).
141
+ */
142
+ export async function popcornProve(
143
+ opts: PopcornProveOptions,
144
+ ): Promise<ProofResult> {
145
+ const { apiUrl, params, secretParams, context } = opts
146
+ const doFetch = opts.fetchImpl ?? fetch
147
+ const base = apiUrl.replace(/\/$/, '')
148
+
149
+ const providerParamsJson = JSON.stringify({
150
+ name: 'http',
151
+ params,
152
+ secretParams,
153
+ ...(context ? { context: JSON.stringify(context) } : {}),
154
+ })
155
+ const fail = (error: string): ProofResult => ({
156
+ proof: undefined,
157
+ extractedValue: '',
158
+ extractedParameters: {},
159
+ verified: false,
160
+ error,
161
+ })
162
+
163
+ let resp: ReclaimProveResponse
164
+ let raw = ''
165
+ try {
166
+ const res = await doFetch(`${base}/reclaim/prove`, {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({ provider_params_json: providerParamsJson }),
170
+ })
171
+ // Read the body as text first: a proof failure/timeout is a 500 whose
172
+ // `message` we want to surface verbatim, and on an unexpected 200 shape we
173
+ // echo the raw body instead of a blind "no claim/signature".
174
+ raw = await res.text().catch(() => '')
175
+ if(!res.ok) {
176
+ return fail(`/reclaim/prove ${res.status}: ${raw.slice(0, 300)}`)
177
+ }
178
+
179
+ resp = JSON.parse(raw) as ReclaimProveResponse
180
+ } catch(err) {
181
+ return fail(err instanceof Error ? err.message : String(err))
182
+ }
183
+
184
+ const claim = resp.claim
185
+ const sig = resp.signature
186
+ if(!claim || !sig?.attestor_address || !sig?.claim_signature) {
187
+ return fail(
188
+ 'popcorn /reclaim/prove returned no claim/signature: '
189
+ + raw.slice(0, 300),
190
+ )
191
+ }
192
+
193
+ // claim.context is the JSON string carrying { extractedParameters, ... } —
194
+ // the same place the in-process path reads them from.
195
+ let extractedParameters: Record<string, string> = {}
196
+ if(typeof claim.context === 'string') {
197
+ try {
198
+ const ctx = JSON.parse(claim.context) as {
199
+ extractedParameters?: Record<string, string>
200
+ }
201
+ extractedParameters = ctx.extractedParameters ?? {}
202
+ } catch{
203
+ // leave empty if context isn't valid JSON
204
+ }
205
+ }
206
+
207
+ const attestorClaim: AttestorClaim = {
208
+ claim: {
209
+ provider: claim.provider ?? '',
210
+ parameters: claim.parameters ?? '',
211
+ owner: claim.owner ?? '',
212
+ timestampS: claim.timestamp_s ?? 0,
213
+ context: claim.context ?? '',
214
+ identifier: claim.identifier ?? '',
215
+ epoch: claim.epoch ?? 0,
216
+ },
217
+ attestorAddress: sig.attestor_address,
218
+ // Consumer-side verification expects hex; Popcorn signs in base64.
219
+ claimSignature: '0x' + bytesToHex(b64ToBytes(sig.claim_signature)),
220
+ }
221
+
222
+ return {
223
+ proof: resp,
224
+ verified: true,
225
+ extractedParameters,
226
+ extractedValue: Object.values(extractedParameters)[0] ?? '',
227
+ ...(claim.identifier ? { identifier: claim.identifier } : {}),
228
+ ...(claim.owner ? { owner: claim.owner } : {}),
229
+ attestorClaim,
230
+ }
231
+ }
@@ -183,9 +183,10 @@ export function elementAnchor(
183
183
  /** The tightest element that DIRECTLY holds a target. Prefers a COMPLETE-value
184
184
  * occurrence — an attribute whose value IS the target, or a text child that IS
185
185
  * the target — over one where the target is merely embedded in a longer string
186
- * (e.g. a userid inside a `1234567+user@…` noreply address). The complete
187
- * field is the semantic source and yields a portable anchor; the embedded one
188
- * is incidental. Smallest byte span wins within the chosen tier. */
186
+ * (for example, a userid inside a `1234567+user@…` noreply address). The
187
+ * complete field is the semantic source and yields a portable anchor; the
188
+ * embedded one is incidental. Smallest byte span wins within the chosen
189
+ * tier. */
189
190
  function findElement(root: Node, targets: string[]): Element | undefined {
190
191
  const complete: Element[] = []
191
192
  const embedded: Element[] = []
@@ -250,8 +251,8 @@ function* candidateXPaths(el: Element): Generator<Candidate> {
250
251
 
251
252
  // A uniquely-identified DIRECT child pins the parent without a positional
252
253
  // path (`//span[span[@id="labels"]]`). Self-verification discards it when the
253
- // child id/attr isn't unique — e.g. GitHub reuses `id="labels"` per email row
254
- // — so it never silently matches the wrong node.
254
+ // child id/attr isn't unique — for example, GitHub reuses `id="labels"` per
255
+ // email row — so it never silently matches the wrong node.
255
256
  for(const child of el.children) {
256
257
  if(!isTag(child)) {
257
258
  continue
@@ -295,8 +296,9 @@ function distinctiveClass(cls: string | undefined): string | undefined {
295
296
  .sort((x, y) => y.length - x.length)[0]
296
297
  }
297
298
 
298
- /** Positional path from `anc` (exclusive) down to `el` (inclusive), e.g.
299
- * `/ul[1]/li[3]/a[1]`; empty string when `anc` is not an ancestor of `el`. */
299
+ /** Positional path from `anc` (exclusive) down to `el` (inclusive), for
300
+ * example `/ul[1]/li[3]/a[1]`. Empty string when `anc` is not an ancestor
301
+ * of `el`. */
300
302
  function relativeSteps(anc: Element, el: Element): string {
301
303
  const parts: string[] = []
302
304
  let node: Node | null = el
@@ -392,8 +394,8 @@ function absolutePath(el: Element): string {
392
394
 
393
395
  /** True when `expression` selects EXACTLY `el` (one match) against `doc`. The
394
396
  * attestor applies the redaction regex to EVERY xPath match and throws if any
395
- * fails, so a non-unique selector (e.g. a class shared by sibling rows) is
396
- * unusable — it must resolve to the single target element. */
397
+ * fails, so a non-unique selector (for example, a class shared by sibling
398
+ * rows) is unusable — it must resolve to the single target element. */
397
399
  function matchesUniquely(doc: unknown, expression: string, el: Node): boolean {
398
400
  try {
399
401
  const engine = xpath as unknown as {
@@ -57,7 +57,8 @@ export interface DraftTarget {
57
57
 
58
58
  /** The realized extraction for one target — its value paired with the
59
59
  * redaction that reveals it (absent when matched literally). Lets a consumer
60
- * validate each value against its own selector (e.g. the cloud replay). */
60
+ * validate each value against its own selector (for example, the cloud
61
+ * replay). */
61
62
  export interface Extraction {
62
63
  name: string
63
64
  value: string
@@ -110,12 +111,14 @@ function sanitizeRequestHeaders(
110
111
  * match or redaction — either a neighbouring target's value or a target's own
111
112
  * value left un-templated. The matcher builds each target's window
112
113
  * independently and drops the per-user WORDS it can see, but it cannot know a
113
- * neighbouring value is itself user-specific (e.g. a name sharing a text node
114
- * with an id — a `<b>{{id}}</b>` sitting beside a `<td>{{name}}</td>`).
115
- * Cross-checking every value against every match here catches that: a portable
116
- * provider contains NO captured value as a literal — each appears only as its
117
- * `{{param}}`. Throwing discards the draft so the agent re-captures or
118
- * re-proposes rather than shipping a provider pinned to this one user. */
114
+ * neighbouring value is itself user-specific (for example, a name sharing a
115
+ * text node with an id — a `<b>{{id}}</b>` sitting beside a
116
+ * `<td>{{name}}</td>`). Cross-checking every value against every match here
117
+ * catches
118
+ * that: a portable provider contains NO captured value as a literal — each
119
+ * appears only as its `{{param}}`. Throwing discards the draft so the agent
120
+ * re-captures or re-proposes rather than shipping a provider pinned to this
121
+ * one user. */
119
122
  export function assertPortableMatches(
120
123
  matches: ResponseMatch[],
121
124
  redactions: ResponseRedaction[],
@@ -294,7 +297,7 @@ function resolveParamName(targetName: string, shorten: boolean): string {
294
297
  + 'shortenNames:false — it becomes a regex capture-group name AND '
295
298
  + 'a paramValues key, so it must start with a letter/underscore and '
296
299
  + 'contain only letters, digits, and underscores (we recommend '
297
- + 'lower_snake_case, e.g. "employer_name"). Rename it, or drop '
300
+ + 'lower_snake_case, for example, "employer_name"). Rename it, or drop '
298
301
  + '`shortenNames: false` to auto-derive a safe one.',
299
302
  )
300
303
  }
@@ -340,8 +343,8 @@ export function draftProvider(
340
343
  + `param name "${paramName}"`
341
344
  + (shortenNames
342
345
  ? ' (names are shortened to their last alphanumeric token) — '
343
- + 'give them more distinct names, e.g. "firstName"/"surname" '
344
- + 'instead of "first_name"/"last_name"'
346
+ + 'give them more distinct names, for example '
347
+ + '"firstName"/"surname" instead of "first_name"/"last_name"'
345
348
  : ' — give them distinct names, or drop `shortenNames: false` '
346
349
  + 'to auto-derive distinct ones'),
347
350
  )
@@ -1,28 +1,41 @@
1
1
  /**
2
- * Guidance for authoring page-injection scripts (`webSettings.jsUserScripts` on
3
- * the builder, `customInjection` on old-devtools), surfaced to the authoring
4
- * model from the publish tool description. The prose lives in
5
- * `skill/injection-api.md` so it can be edited as markdown without touching
6
- * code — the same pattern as `mcp/instructions.ts`. The path resolves the same
7
- * from `src/provider` (dev) and `lib/provider` (published): both are two levels
8
- * below the package root.
2
+ * Short field hints for the user-script inputs on both publish tools
3
+ * (`webSettings.jsUserScripts` on the builder, `customInjection` on
4
+ * old-devtools).
9
5
  *
10
- * The guide leads with plain browser JavaScript (navigation/interaction via
11
- * standard APIs; the client already intercepts page requests) and treats
12
- * `window.Reclaim` as a rarely-needed edge-case bridge, pointing at the
13
- * authoritative `src/support/env.d.ts` rather than reproducing its surface.
14
- * It also covers the HAWKEYE-style live-interception path (`injectionType` /
15
- * `interceptorType`, `disableRequestReplay` / `isDocumentRequestReplayEnabled`)
16
- * and `bodySniff`, reverse-engineered from the old-devtools Prisma schema,
17
- * controller code, and dashboard field hints. The replay flag IS settable via
18
- * the publish tools (`disableRequestReplay` on old-devtools — add-version or
19
- * the register follow-up; `interceptorOptions` on the builder), so this is
20
- * recognition + authoring guidance for the remaining internals.
6
+ * These used to inline the whole `skill/injection-api.md` guide (~22 KB) into
7
+ * the tool's input schema. That guide is needed by a small minority of
8
+ * providers, but an input-schema description is sent to the model on EVERY
9
+ * request — it was roughly half the old-devtools tool surface on its own. The
10
+ * prose now lives in `skill/user-script.md`, `skill/interception.md` and
11
+ * `skill/hash-validation.md`, served on demand as `how_it_works` topics; what
12
+ * stays here is the pointer plus the one warning that must be seen BEFORE the
13
+ * field is filled in, since acting on it wrongly is worse than not acting.
14
+ *
15
+ * Shared by both publish tools so the two can't drift apart.
21
16
  */
22
- import { readFileSync } from 'node:fs'
23
- import { fileURLToPath } from 'node:url'
24
17
 
25
- export const INJECTION_GUIDE = readFileSync(
26
- fileURLToPath(new URL('../../skill/injection-api.md', import.meta.url)),
27
- 'utf8',
28
- ).trim()
18
+ /** What the script is and where the real guidance lives. */
19
+ export const USER_SCRIPT_HINT =
20
+ 'Plain JavaScript injected into the provider page before every page load. '
21
+ + 'Use standard browser APIs (window.location, element.click(), forms) to '
22
+ + 'drive login/navigation so the target request fires — the client already '
23
+ + 'intercepts page requests, and window.Reclaim is only for rare edge cases, '
24
+ + 'not navigation. Full guide: how_it_works({ topic: "user-script" }).'
25
+
26
+ /** Prefixes the `allowedJsRequests` / `allowedInjectedRequestData` field. The
27
+ * trap it describes is silent and total — a mis-declared entry makes
28
+ * `verifyProof({ providerId })` reject EVERY real proof — so the warning
29
+ * itself stays in the schema even though its explanation moved to a topic. */
30
+ export const ALLOWED_REQUESTS_HINT =
31
+ '⚠️ Only populate this if you intend to gate '
32
+ + '`verifyProof(proof, { providerId })`: each entry feeds a client-side '
33
+ + 'hash-equality check and its `url` is hashed VERBATIM (no regex/template '
34
+ + 'resolution, even with urlType REGEX/TEMPLATE). If the real captured URL '
35
+ + 'carries ANY per-session part (nonce, CSRF token, cache-busting '
36
+ + 'timestamp), no declared spec can ever hash-match a real proof — '
37
+ + 'populating it then makes verifyProof reject every real proof, which is '
38
+ + 'worse than leaving it empty. Diff the same endpoint\'s captured URL '
39
+ + 'across two sessions first. Mechanics, the body-templating rules and the '
40
+ + 'dangerouslyDisableContentValidation fallback: '
41
+ + 'how_it_works({ topic: "hash-validation" }).'
@@ -6,8 +6,8 @@
6
6
  * index and may not behave correctly when there are fewer matches than
7
7
  * redactions. This is NOT enforced — the author keeps full control over what
8
8
  * they publish in either old-devtools or builder mode. The tools surface these
9
- * strings so the dev can make an informed decision (e.g. add a functional match
10
- * per redaction) rather than being blocked.
9
+ * strings so the dev can make an informed decision (for example, add a
10
+ * functional match per redaction) rather than being blocked.
11
11
  */
12
12
 
13
13
  /** One request's match/redaction counts, with a human label for the message. */
@@ -26,7 +26,7 @@ export function matchRedactionWarnings(requests: RequestCounts[]): string[] {
26
26
  warnings.push(
27
27
  `${req.label}: ${req.matchCount} response match(es) but ` +
28
28
  `${req.redactionCount} response redaction(s). Some verification ` +
29
- 'clients (e.g. the InApp SDK) may not work correctly when ' +
29
+ 'clients (for example, the InApp SDK) may not work correctly when ' +
30
30
  'responseMatches is shorter than responseRedactions — they pair the ' +
31
31
  'two by index. This is allowed; add a functional match per redaction ' +
32
32
  'if you hit verification issues.',
@@ -54,7 +54,7 @@ export function synthesizeMatcher(
54
54
  'OPRF hashing requires a target with a known-safe character class '
55
55
  + '(digits, slug, or email). '
56
56
  + `Got "${target}" which is free-form. Use a tighter target `
57
- + '(e.g. just the alphanumeric portion of the value) '
57
+ + '(for example, just the alphanumeric portion of the value) '
58
58
  + 'or call propose_provider without `hash`.',
59
59
  )
60
60
  }
@@ -196,10 +196,10 @@ function lastPathKey(jsonPath: string | undefined): string | undefined {
196
196
  * otherwise). Returns undefined when the value isn't preceded by a JSON key.
197
197
  *
198
198
  * When `key` is given, anchors the search on occurrences of the KEY (not the
199
- * value) — so a short/common value elsewhere in the body (e.g. the digit "5"
200
- * inside `"investment_amount":354.85`) can't be mistaken for this field. Only
201
- * falls back to a whole-body value search when there's no resolvable key
202
- * (e.g. `findJsonPath` returned an array-index leaf).
199
+ * value) — so a short/common value elsewhere in the body (for example, the
200
+ * digit "5" inside `"investment_amount":354.85`) can't be mistaken for this
201
+ * field. Only falls back to a whole-body value search when there's no
202
+ * resolvable key (for example, `findJsonPath` returned an array-index leaf).
203
203
  */
204
204
  function jsonKeyPrefix(
205
205
  text: string,
@@ -497,9 +497,9 @@ function portableWindow(
497
497
  /** Value is element text content. Left anchor: if only punctuation/whitespace
498
498
  * sits between the opening tag and the value, fold the stable opening tag in;
499
499
  * but if WORDS precede the value in the same text node they may be per-user
500
- * (e.g. `<li>Full Name ( {{id}} )` — the name), so drop through the last
501
- * alphanumeric and keep only the trailing punctuation as the anchor. Right:
502
- * include a closing tag, but stop at a sibling. */
500
+ * (for example, `<li>Full Name ( {{id}} )` — the name), so drop through the
501
+ * last alphanumeric and keep only the trailing punctuation as the anchor.
502
+ * Right: include a closing tag, but stop at a sibling. */
503
503
  function textWindow(
504
504
  markup: string,
505
505
  valueIdx: number,
@@ -671,9 +671,9 @@ function contentMatcher(
671
671
  * delimiter is kept unless it's whitespace (which the template also drops).
672
672
  */
673
673
  /** Indices where `cand` occurs in `body` as a WHOLE match of `valueClass` —
674
- * i.e. not a substring inside a longer run of the same class (the digit "5"
675
- * inside "354.85" is never a whole `\d+` match; "354" and "85" are). Only
676
- * meaningful for a structural (non-free-form) class. */
674
+ * that is, not a substring inside a longer run of the same class (the digit "5"
675
+ * inside "354.85" is never a whole `\d+` match; "354" and "85" are). Only
676
+ * meaningful for a structural (non-free-form) class. */
677
677
  function wholeTokenIndices(
678
678
  body: string,
679
679
  cand: string,
@@ -859,7 +859,8 @@ function inferValueClass(target: string): string | undefined {
859
859
  return '\\d+'
860
860
  }
861
861
 
862
- // Numbers with thousands separators / decimals, e.g. "1,908" or "12,345.6".
862
+ // Numbers with thousands separators / decimals, for example, "1,908" or
863
+ // "12,345.6".
863
864
  if(/^[0-9][0-9,.]*[0-9]$/.test(target)) {
864
865
  return '[0-9,.]+'
865
866
  }
@@ -126,9 +126,9 @@ export function secretUrlCharBudget(
126
126
  }
127
127
 
128
128
  /** A `paramValues`/context key is secret iff its name contains `SECRET`
129
- * (case-insensitive) — e.g. `SECRET_apiKey`, `context.SECRET_apiKey`. Same
130
- * naming-convention classifier reclaim-inapp-sdk and reclaim-portal use for
131
- * their `witnessParams`/`paramValues` maps (no separate boolean flag). */
129
+ * (case-insensitive) — for example, `SECRET_apiKey`, `context.SECRET_apiKey`.
130
+ * Same naming-convention classifier reclaim-inapp-sdk and reclaim-portal use
131
+ * for their `witnessParams`/`paramValues` maps (no separate boolean flag). */
132
132
  export function classifyParamName(name: string): boolean {
133
133
  return name.toUpperCase().includes('SECRET')
134
134
  }
@@ -41,14 +41,14 @@ export interface DraftRequest {
41
41
  * pagination/optionality flags of `RequestSelectionTemplate`. Reuses the
42
42
  * shared `DraftRequest` shape — no provider `name`/`paramValues` required. */
43
43
  export interface AllowedJsRequest extends DraftRequest {
44
- /** Allow the template to match many requests (e.g. pagination). Default
45
- * true. */
44
+ /** Allow the template to match many requests (for example, pagination).
45
+ * Default true. */
46
46
  multiple?: boolean
47
47
  /** Fail validation if no request matches this template. Default true. */
48
48
  required?: boolean
49
49
  /** Names of `${var}` placeholders in this template's `responseMatches`/
50
50
  * `responseRedactions` that get substituted at verify time from the
51
- * submitted proof's own witness params (e.g. an index list a
51
+ * submitted proof's own witness params (for example, an index list a
52
52
  * `Reclaim.requestClaim` call attached alongside its `{{var}}`
53
53
  * extractions). Omit (or leave empty) when the template has no such
54
54
  * placeholders — verification then matches it literally as-is. */
@@ -74,12 +74,13 @@ export interface CreateVersionOptions {
74
74
  * is explicitly passed (builder's schema has no `NONE` interceptorType;
75
75
  * omitting `inapp` entirely IS the "no interception" signal, so omitting
76
76
  * this must omit `inapp`, not default it to HAWKEYE). When it IS passed,
77
- * `isDocumentRequestReplayEnabled` is still ALWAYS force-disabled
78
- * regardless of what's passed here — see `providerToCreateVersionRequest`;
79
- * there is no author-facing way to enable it. */
77
+ * `isDocumentRequestReplayEnabled` defaults to `false` (disabled) unless
78
+ * explicitly overridden here — see `providerToCreateVersionRequest`. Only
79
+ * meaningful for `HAWKEYE`/`MSWJS`; inert for `CDP`. */
80
80
  interceptorOptions?: {
81
81
  interceptorType: 'HAWKEYE' | 'MSWJS' | 'CDP'
82
82
  interceptorSettings?: string
83
+ isDocumentRequestReplayEnabled?: boolean
83
84
  }
84
85
  }
85
86
 
@@ -249,14 +250,15 @@ export function providerToCreateVersionRequest(
249
250
  // omitting `interceptorOptions` must omit `inapp` too, or every
250
251
  // standard (non-interception) provider would incorrectly publish
251
252
  // as HAWKEYE. Within that block, `isDocumentRequestReplayEnabled`
252
- // is still force-disabled — see `CreateVersionOptions
253
- // .interceptorOptions`'s doc comment; there is no way to enable it.
253
+ // defaults to disabled but is author-overridable — see
254
+ // `CreateVersionOptions.interceptorOptions`'s doc comment.
254
255
  ...(interceptorOptions
255
256
  ? {
256
257
  inapp: {
257
258
  interceptorOptions: {
258
259
  interceptorType: interceptorOptions.interceptorType,
259
- isDocumentRequestReplayEnabled: false,
260
+ isDocumentRequestReplayEnabled:
261
+ interceptorOptions.isDocumentRequestReplayEnabled ?? false,
260
262
  ...(interceptorOptions.interceptorSettings
261
263
  ? {
262
264
  interceptorSettings:
@@ -70,11 +70,11 @@ export function errorAsToolText(err: unknown): string {
70
70
  /**
71
71
  * Adapt a tool to a host by WIDENING its input schema — add properties, mark
72
72
  * some required, optionally override the description — without touching the
73
- * handler. A host maps whichever shared tools it needs through this (e.g. the
74
- * MCP adds its `captureId` session handle + the credential-aware `run_proof`
75
- * contract). Args parse loosely, so the extra fields flow through to the
76
- * handler (its backend resolver reads them); the base tool stays untouched for
77
- * hosts that don't need them.
73
+ * handler. A host maps whichever shared tools it needs through this (for
74
+ * example, the MCP adds its `captureId` session handle + the credential-aware
75
+ * `run_proof` contract). Args parse loosely, so the extra fields flow through
76
+ * to the handler (its backend resolver reads them); the base tool stays
77
+ * untouched for hosts that don't need them.
78
78
  */
79
79
  export function extendTool(
80
80
  tool: RegisteredTool,