@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
@@ -1,211 +1,378 @@
1
- <!-- common: header -->
2
-
1
+ <!-- topic: overview -->
2
+ # Authoring a Reclaim provider (Builder backend)
3
+
4
+ <!-- common: terminology -->
5
+
6
+ ## The loop, at a glance
7
+
8
+ Copy this checklist and check off each step as you finish it. Read the linked
9
+ topic before a step you aren't already fluent in.
10
+
11
+ - [ ] 1. `attach_browser` — pick a browser source with the developer.
12
+ → topic `browser`
13
+ - [ ] 2. `list_tabs` → `start_capture(tabId)` ⇒ **`captureId`**, your handle for
14
+ everything below. → topic `capture`
15
+ - [ ] 3. Drive the page: `navigate`, `wait_for_page`, `eval_in_page`,
16
+ `get_cookies`. Detect login, land on the page showing the value, trigger
17
+ the request. → topic `capture`
18
+ - [ ] 4. `find_requests_containing` → `analyze_request_constraints` → pick an
19
+ **authenticated** request. → topic `capture`
20
+ - [ ] 5. `propose_provider(captureId, requestId, …)` ⇒ **`draftId`** (recipe +
21
+ captured secrets, held server-side — secrets never enter the transcript).
22
+ → topic `draft`
23
+ - [ ] 6. Parameterize user-specific URL segments; mark secrets. → topic `params`
24
+ - [ ] 7. `replay_request(captureId, draftId)`, then again with
25
+ `withoutSecrets: true`. → topic `prove`
26
+ - [ ] 8. `run_proof(captureId, draftId, ownerAddress)`.
27
+ → topics `prove`, `credentials`
28
+ - [ ] 9. Resolve a `providerId` (**reuse before create**) →
29
+ `create_provider_version_from_capture` → `publish_provider_version` or
30
+ `submit_provider_version_for_review`. → topic `publish`
31
+
32
+ Multi-request provider? Run steps 5–8 once per request, then publish every
33
+ proven draft in one call. See topic `publish`.
34
+
35
+ <!-- topic: auth -->
3
36
  ## Getting authenticated
4
37
 
5
- **Check first:** try `get_me`. If it returns the user's profile, you're authenticated — proceed to the loop.
6
-
7
- **On 401:** call `authenticate({})` to start a device-pairing session:
8
-
9
- 1. It returns immediately with a `code` and `attachUrl` — it does NOT poll.
10
- 2. Tell the user: _"Open `<attachUrl>`, sign in, and click 'Authorize this device'."_
11
- 3. After they confirm, call `authenticate({ code: "<code>" })`: `LINKED` → proceed; `PENDING` → wait and call again; `EXPIRED` → start a new session.
12
-
13
- The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (`RECLAIM_API_TOKEN` in the server env overrides it).
14
-
15
- ## The loop
16
-
17
- <!-- common: step-attach -->
18
-
19
- 2. **Pick a tab and start a capture.** Call `list_tabs` and pick a tab (if one is already on the right origin use it; otherwise the first is usually fine). Then call `start_capture(tabId)` — it returns a **`captureId`** that is your session handle for the rest of the flow: `navigate`, `eval_in_page`, `wait_for_page`, `get_cookies`, `list_requests`, `find_requests_containing`, `propose_provider` all take it as their first argument. Capturing from the start is harmless (it just also records the login navigation). Then `navigate(captureId, url)` to the site you inferred from the dev's request (e.g. "GitHub username" → `https://github.com`). Don't ask the dev to navigate.
20
-
21
- 3. **Check login state, wait if needed.** Use `eval_in_page(captureId, ...)` to evaluate a site-specific check.
22
-
23
- **Combine a positive signal (logged-in-only element present) with a negative signal (login button absent), AND them together.** Single signals are too prone to false positives — most notably, `<meta>` tags often exist on every page regardless of login state; only their `.content` attribute is populated. The check `!!document.querySelector('meta[name="user-login"]')` returns true even when logged out — DO NOT use existence-only checks on meta tags or other persistent-but-empty elements.
24
-
25
- Examples (always verify against the live page if these break — sites change selectors):
26
- - GitHub: `(document.querySelector('meta[name="user-login"]')?.content?.length || 0) > 0 && !document.querySelector('a.HeaderMenu-link[href="/login"]')`
27
- - X / Twitter: `!!document.querySelector('[data-testid="AppTabBar_Profile_Link"]') && !document.querySelector('[data-testid="loginButton"]')`
28
- - Generic shape: `<element-present-only-when-logged-in> && !<login-button-present-only-when-logged-out>`
29
-
30
- If the result is truthy, skip to step 4.
31
-
32
- If falsy: **navigate to the site's login URL yourself first**, so the dev lands directly on the login form instead of having to find it. Common patterns: `https://<host>/login`, `https://<host>/signin`, `https://<host>/i/flow/login` (Twitter/X). Pick the one you know works for the site. Call `navigate(captureId, "<login-url>")`. Then tell the dev once: "I've opened the login page on `<site>` — please log in (with 2FA if needed). I'll detect when you're done." Then call `wait_for_page(captureId, expression="<same composite expression>", timeoutMs=180000)`. The MCP polls every second and returns when the expression goes truthy. **Do not** instruct the dev to "tell me when you're done" — just wait via the tool.
33
-
34
- **Self-check before trusting the result:** if your detector says "already logged in" on a freshly-launched Chrome profile (cookies-clean), the detector is wrong. Brand-new profile means brand-new cookie jar — there's no way to be already authenticated. If you see that contradiction, your check is matching something that's present regardless of login (a meta tag, a generic class). Rewrite the expression and re-evaluate.
35
-
36
- 4. **Navigate to the page that displays the value.** Use what you know about the site to construct the URL. If you need the dev's username, eval it (e.g. on GitHub: `document.querySelector('meta[name="user-login"]')?.content`). Then `navigate(captureId, url)`. Don't ask the dev to navigate.
37
-
38
- 5. **Trigger the request.** Capture has been live since step 2, so just make the relevant API call fire: issue a reload with `eval_in_page(captureId, "location.reload()")`. For some pages you may need to click a specific element or call a SPA route — use `eval_in_page` to do that programmatically rather than asking the dev to click.
39
-
40
- <!-- common: step-identify-value -->
41
-
42
- <!-- common: step-find-request -->
43
-
44
- 8. **Decide whether to OPRF the value** (privacy from the attestor). Before calling `propose_provider`, decide if the captured value should be hashed end-to-end so the attestor never sees it in cleartext. Four options:
45
- - **No hash** (default). Value travels via `paramValues` + `{{name}}` template; attestor sees the cleartext substring. Simplest, fewest moving parts. Right call for non-sensitive values (public IPs from `ifconfig.me`, public usernames the dev is happy to expose, public repo names).
46
- - **`oprf-raw`**. Server-side OPRF. **The only variant that runs on Windows / without external deps.** Use this when the dev wants any privacy and you're in a normal dev loop. Privacy is the weakest of the three (attestor still participates in hashing) — surface that honestly.
47
- - **`oprf`**. Client-side ZK OPRF via gnark. Strongest privacy. Requires gnark install (Linux/macOS in practice). Don't suggest this on Windows.
48
- - **`oprf-mpc`**. TEE-based MPC OPRF. Requires a TEE attestor deployment. Niche; only suggest if the dev brings it up.
49
-
50
- **Default skip-the-question heuristic.** If the value is obviously non-sensitive (public IP, public username, public repo name, public follower/star counts on sites where the captured endpoint is auth-bound), default to **no hash** and proceed without asking. If the value is or could be sensitive (email, real name, private ID, anything the dev wouldn't post publicly), surface the choice verbatim:
51
-
52
- > "Do you want this value kept private from the attestor via OPRF?
53
- >
54
- > - **No** (default) — value visible to attestor
55
- > - **`oprf-raw`** — works on Windows / without extra deps; weakest of the OPRF variants
56
- > - **`oprf`** — gnark/ZK; strongest privacy but requires Linux/macOS + gnark install
57
- > - **`oprf-mpc`** — TEE-based; requires a TEE attestor"
58
-
59
- **Hard constraint:** OPRF-hashed values are capped at **62 bytes**. If the value is longer and the dev picks any hash, surface that and propose either a tighter `target` (e.g. just the email local-part) or no hash. `propose_provider` will return a clean error if you try anyway.
60
-
61
- **Hard constraint: OPRF and the request URL are mutually exclusive for the same value.** The entire point of `hash` is that the attestor never sees the value in cleartext — but any value templated into the request URL (`{{name}}`, `{{context.<name>}}`, or left literal) is always signed into the attestor's context in the clear, regardless of hashing. So if the target's value ALSO appears anywhere in the request URL, hashing it is not just pointless, it doesn't verify. Check this before requesting `hash` for a target: if that value shows up in the captured URL too, either drop `hash` for that target or capture a different request whose URL doesn't carry the value. `propose_provider` throws a clean error if you try anyway — same as the byte cap above.
62
-
63
- 9. **Draft.** Call `propose_provider`. It returns a **`draftId`** (the handle you pass to `replay_request` and `run_proof`), the secret-free `provider`, and `secretRefs`. The recipe **and the captured secret values** are held server-side under that `draftId` — the secret values never enter the transcript, so you carry the `draftId`, not the provider JSON, into replay/prove.
64
-
65
- **Extracting MORE THAN ONE value?** Prefer a **single request whose response contains ALL of them**, and draft them together — the provider gets one `responseMatches` + `responseRedactions` entry per value. First locate that request: run `find_requests_containing` for each value and pick a `requestId` that appears for all. Then pass a `targets` array — one entry per value, each with its `value` (exactly as it appears) and a distinct `name` (its parameter):
66
-
67
- ```
68
- propose_provider(captureId, requestId, name, targets: [
69
- { value: "Scratch-net", name: "username" },
70
- { value: "Alex", name: "name" },
71
- ])
72
- ```
73
-
74
- For a single value, the shorthand `propose_provider(captureId, requestId, target, name, hash?)` still works (one target). Pass `hash` only if the dev opted in (per-target `hash` also works inside `targets`). Inspect the returned provider:
75
- - Is the right header marked secret? (cookies, Authorization, X-\* tokens)
76
- - Is `responseMatches[0].value` a `contains` with a `{{name}}` template (matching the captured value's structural context)?
77
- - Is `paramValues[name]` set to the original captured value?
78
- - Does each `responseRedactions` entry carry an `xPath` or `jsonPath` anchor **plus** a named-capture `regex` that isolates just the value? The synthesizer adds the regex for BOTH cases — an HTML element reveal is the element markup and a jsonPath reveal is the surrounding `"key":"value"`, so neither is the bare value without the regex. (And if `hash` was requested, the regex also carries a `hash: oprf-…` field.)
38
+ **Check first:** call `get_me`. A profile back means you're authenticated —
39
+ proceed.
40
+
41
+ **On 401:** `authenticate({})` starts a device-pairing session. It returns
42
+ immediately with a `code` and `attachUrl` — it does NOT poll.
43
+
44
+ 1. Tell the user: _"Open `<attachUrl>`, sign in, and click 'Authorize this
45
+ device'."_
46
+ 2. After they confirm, call `authenticate({ code: "<code>" })` —
47
+ `LINKED` → proceed; `PENDING` → wait and call again; `EXPIRED` → start over.
48
+
49
+ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs
50
+ (`RECLAIM_API_TOKEN` in the server env overrides it).
51
+
52
+ <!-- topic: browser -->
53
+ <!-- common: browser -->
54
+
55
+ <!-- topic: capture -->
56
+ ## Capture
57
+
58
+ 1. **Attach** — topic `browser`.
59
+
60
+ 2. **Pick a tab and start the capture.** `list_tabs`, then
61
+ `start_capture(tabId)` ⇒ a **`captureId`**: the session handle every browser
62
+ and inspect tool takes as its first argument (`navigate`, `eval_in_page`,
63
+ `wait_for_page`, `get_cookies`, `list_requests`, `find_requests_containing`,
64
+ `propose_provider`). If a tab is already on the right origin use it,
65
+ otherwise the first is usually fine. Capturing from the start is harmless —
66
+ it also records the login navigation. Then `navigate(captureId, url)` to
67
+ the site you inferred from the developer's request ("GitHub username" →
68
+ `https://github.com`). Don't ask the developer to navigate.
69
+
70
+ 3. <!-- common: login-detect -->
71
+
72
+ 4. **Navigate to the page that displays the value.** Construct the URL from what
73
+ you know about the site; eval for anything you need (for example, GitHub's
74
+ `meta[name="user-login"]`'s `content`), then `navigate(captureId, url)`.
75
+
76
+ 5. **Trigger the request.** Capture has been live since step 2, so make the
77
+ call fire: `eval_in_page(captureId, "location.reload()")`, or click an
78
+ element / hit a SPA route programmatically. Don't ask the developer to click.
79
+
80
+ 6. <!-- common: identify-value -->
81
+
82
+ 7. <!-- common: find-request -->
83
+
84
+ <!-- topic: draft -->
85
+ ## Draft the provider
86
+
87
+ <!-- common: oprf -->
88
+
89
+ **Call `propose_provider`.** It returns a **`draftId`** — the handle for
90
+ `replay_request` and `run_proof` — plus the secret-free `provider` and
91
+ `secretRefs`. The recipe **and the captured secret values** live server-side
92
+ under that `draftId`; the secrets never enter the transcript, so you carry the
93
+ `draftId`, not the provider JSON, into replay/prove.
94
+
95
+ **Extracting MORE THAN ONE value?** Prefer a **single request whose response
96
+ contains all of them** and draft them together — one `responseMatches` +
97
+ `responseRedactions` pair per value. Find that request by running
98
+ `find_requests_containing` for each value and picking a `requestId` common to
99
+ all. Then pass a `targets` array, each entry with its `value` (exactly as it
100
+ appears) and a distinct `name`:
101
+
102
+ ```
103
+ propose_provider(captureId, requestId, name, targets: [
104
+ { value: "Scratch-net", name: "username" },
105
+ { value: "Alex", name: "name" },
106
+ ])
107
+ ```
108
+
109
+ The shorthand `propose_provider(captureId, requestId, target, name, hash?)`
110
+ still works for one target. Per-target `hash` works inside `targets` too.
111
+
112
+ **Inspect what comes back:**
113
+
114
+ - Is the right header marked secret? (cookies, `Authorization`, `X-*` tokens)
115
+ - Is `responseMatches[0].value` a `contains` with a `{{name}}` template matching
116
+ the captured value's structural context?
117
+ - Is `paramValues[name]` the original captured value?
118
+ - Does each `responseRedactions` entry carry an `xPath`/`jsonPath` anchor
119
+ **plus** a named-capture `regex` isolating the value? The synthesizer adds the
120
+ regex in both cases — an HTML reveal is the element markup and a jsonPath
121
+ reveal is the surrounding `"key":"value"`, so neither is the bare value
122
+ without it. (With `hash`, the regex also carries `hash: oprf-…`.)
123
+
124
+ **Provenance — match the structure, never the bare value.** The `contains`
125
+ template and the redaction regex are always anchored on the value's surrounding
126
+ context — the JSON key (`"login":"{{username}}"`) or the enclosing element
127
+ (`<div class="handle">{{username}}</div>`) — never `{{username}}` alone. That
128
+ ties the proof to *where the value legitimately appears*; a bare-value match
129
+ would let a user surface the same string from any field they control. The
130
+ synthesizer does this for you.
131
+
132
+ **Canonical drafted shape** (no hash):
133
+
134
+ ```json
135
+ {
136
+ "responseMatches": [
137
+ { "type": "contains", "value": "\"login\":\"{{username}}\"" }
138
+ ],
139
+ "responseRedactions": [
140
+ {
141
+ "xPath": "//script[@id=\"client-env\"]",
142
+ "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\""
143
+ }
144
+ ],
145
+ "paramValues": { "username": "Scratch-net" }
146
+ }
147
+ ```
148
+
149
+ With `hash: 'oprf-raw'`, the redaction gains `"hash": "oprf-raw"`. Nothing else
150
+ changes.
79
151
 
80
- **Provenance — match the structure, never the bare value.** The `contains` template and the redaction `regex` are always anchored on the value's surrounding context — the JSON key (`"login":"{{username}}"`) or the enclosing element (`<div class="handle">{{username}}</div>`), never just `{{username}}` or `>{{username}}<`. This ties the proof to *where the value legitimately appears*; a bare-value match would let a user surface the same string from any field they control. The synthesizer does this for you.
81
-
82
- **Canonical drafted shape** (no hash):
83
-
84
- ```json
85
- {
86
- "responseMatches": [
87
- { "type": "contains", "value": "\"login\":\"{{username}}\"" }
88
- ],
89
- "responseRedactions": [
90
- {
91
- "xPath": "//script[@id=\"client-env\"]",
92
- "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\""
93
- }
94
- ],
95
- "paramValues": { "username": "Scratch-net" }
96
- }
97
- ```
152
+ <!-- common: rewrite-shape-block -->
98
153
 
99
- **Canonical drafted shape** (with `hash: 'oprf-raw'`):
154
+ <!-- topic: params -->
155
+ ## Parameters: extracted, consumer-supplied, and secret
100
156
 
101
- ```json
102
- {
103
- "responseMatches": [
104
- { "type": "contains", "value": "\"login\":\"{{username}}\"" }
105
- ],
106
- "responseRedactions": [
107
- {
108
- "xPath": "//script[@id=\"client-env\"]",
109
- "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\"",
110
- "hash": "oprf-raw"
111
- }
112
- ],
113
- "paramValues": { "username": "Scratch-net" }
114
- }
115
- ```
157
+ **Parameterize user-specific URL segments** so the provider isn't pinned to one
158
+ user. The matcher already puts the *captured* value in `paramValues`. If the URL
159
+ *also* carries user-identifying segments, those are **consumer-supplied context
160
+ params**: at verification time their values come from the consumer's session
161
+ `context`, not from extraction.
116
162
 
117
- <!-- common: rewrite-shape-block -->
163
+ Replace them with `{{context.<name>}}`, for example
164
+ `https://github.com/Scratch-net/repos` →
165
+ `https://github.com/{{context.username}}/repos`, with `paramValues: { …,
166
+ "context.username": "Scratch-net" }` for replay/proof (the key must match the
167
+ placeholder exactly, dot included).
118
168
 
119
- 10. **Parameterize user-specific URL segments** so the provider isn't user-specific at the URL level. The matcher already populates `paramValues` with the _captured value_ (e.g. `{username: "Scratch-net"}` for a username target). If the URL ALSO contains user-identifying segments, those are **consumer-supplied context params** — at verification time their values come from the Consumer's session `context`, not from extraction. They use a namespaced placeholder, and the namespace differs by mode:
120
-
121
- replace with `{{context.<name>}}`. E.g. `https://github.com/Scratch-net/repos` → `https://github.com/{{context.username}}/repos`, with `paramValues: { ..., "context.username": "Scratch-net" }` for replay/proof (the paramValues key must match the placeholder name exactly, dot included). Declare these in the version's `requiredContext` at publish time (step 13).
122
- - Query parameters with user-specific values → same treatment.
169
+ - Query params with user-specific values → same treatment.
123
170
  - Generic segments (`/api/v3`, `/users`, `/repos`) stay literal.
124
- - If the URL has no user-identifying segments (e.g. `/settings/profile`), leave it untouched — the matcher already handled the value via paramValues.
171
+ - No user-identifying segments (for example, `/settings/profile`) → leave it
172
+ alone.
125
173
 
126
- **Marking a value secret:** name ANY param with `SECRET` anywhere in the key (case-insensitive) — a bare `{{SECRET_x}}` (e.g. a value extracted earlier in a multi-request flow) just as much as a consumer-supplied `{{context.SECRET_apiKey}}` — and it's routed to `secretParams.paramValues` (`classifyParamName`/`splitSecretParamValues`), same naming convention `reclaim-inapp-sdk`/`reclaim-portal` use for their `paramValues`. The `context.` prefix is a BUILDER-only namespace (consumer-supplied vs extracted-from-traffic); the attestor itself just looks up `{{whatever}}` in `paramValues`/`secretParams.paramValues` and doesn't care about the prefix. Headers are secret by the OPPOSITE rule — an ALLOWLIST, not a name check: any header not in a small fixed public list (`user-agent`, `accept`, `accept-language`, `accept-encoding`, `sec-fetch-*`, `origin`, `x-requested-with`, `sec-ch-ua*`) is secret by default, including `Cookie`/`Authorization` and anything custom (`classifyHeader`/`splitSecrets`).
174
+ Declare them in the version's `requiredContext` at publish time (topic
175
+ `publish`).
127
176
 
128
- A secret value CAN be used in the URL or `geoLocation` — write-redaction genuinely hides it there, but only up to a small character budget, and that budget is a TOTAL summed across every secret value used in that URL, not a per-value allowance (`secretUrlCharBudget`): 2 characters under `key-update` (the default), 12 under `zk`. Over budget, the value is effectively still visible to the attestor no matter how it's marked. Since the actual value doesn't exist until a verification session supplies one, this can only be checked once that value is known — at verification time, not at publish time (`assertSecretParamValuesFitUrlBudget` in `verify/run.ts`); a captured secret HEADER whose value happens to also sit in the URL gets the same budget check at DRAFT time instead, since its value is already known then (`assertNoSecretUrlConflict`). Using longer than limit secrets in url actually causes the claim creation from attestor to fail.
177
+ <!-- common: secrets -->
129
178
 
130
- The value of any parameter marked secret is not included in the final proof, omitted from it.
179
+ The `context.` prefix is a **Builder-only namespace** (consumer-supplied vs
180
+ extracted-from-traffic). The attestor itself only looks up `{{whatever}}` in
181
+ `paramValues`/`secretParams.paramValues` and doesn't care about the prefix.
131
182
 
132
183
  <!-- common: no-rename-param -->
133
184
 
134
- **How the verification client resolves these at runtime**:
135
-
136
- - Requests are matched by **exact origin+path** (query ignored). A `{{context.<name>}}` segment matches **only the value the consumer sent** in the session context (validated by `requiredContext`); a bare `{{name}}` segment matches any one path segment and its value is **auto-extracted from the live traffic**.
137
- - Interpolation and `contains` matching are **plain text** — they work for any captured content (JSON, XML, HTML, form-encoded bodies, …).
138
- - Extract selectors apply **client-side in spec order xPath → jsonPath → regex**, each to the prior selection — so only the FIRST selector constrains the full response (an `xPath`→`jsonPath` chain reads JSON embedded in an HTML page; a jsonPath-first extract needs the whole response to be JSON). Only the selected portions reach the attestor.
139
- - A matched response that can't satisfy the recipe is **skipped**, not failed: the first selector can't operate on it (e.g. jsonPath-first against the HTML navigation of a URL whose JSON XHR was captured), or a required `contains` match is missing. The VC keeps polling for a satisfying request until timeout.
140
- - So: choose `{{context.<name>}}` when the consumer knows the value up front and the proof should be **pinned to it**; keep it bare when the value should come from whatever the logged-in user's traffic shows.
141
-
142
- `propose_provider` returns `verificationWarnings` for the calls only an author can make (currently: a captured value baked literally into the URL, needing the `{{context.<name>}}`-vs-bare-vs-re-capture decision). They are advisory, never blocking — **surface each warning to the dev verbatim** and resolve it before publishing.
143
-
144
- 11. **Replay.** Call `replay_request(captureId, draftId)`. Three outcomes:
185
+ ### How the verification client resolves these at runtime
186
+
187
+ - Requests match by **exact origin+path** (query ignored). A
188
+ `{{context.<name>}}` segment matches **only the value the consumer sent**
189
+ (validated against `requiredContext`); a bare `{{name}}` segment matches any
190
+ one path segment and its value is **auto-extracted from live traffic**.
191
+ - Interpolation and `contains` matching are **plain text** — JSON, XML, HTML,
192
+ form-encoded, anything.
193
+ - Extract selectors apply **client-side in spec order xPath → jsonPath →
194
+ regex**, each to the prior selection. Only the FIRST selector constrains the
195
+ full response (an `xPath`→`jsonPath` chain reads JSON embedded in HTML; a
196
+ jsonPath-first extract needs the whole response to be JSON). Only the selected
197
+ portions reach the attestor.
198
+ - A matched response that can't satisfy the recipe is **skipped, not failed**
199
+ (the first selector can't operate on it, or a required `contains` is missing).
200
+ The client keeps polling until timeout.
201
+ - So: `{{context.<name>}}` when the consumer knows the value up front and the
202
+ proof should be **pinned** to it; bare when the value should come from
203
+ whatever the logged-in user's traffic shows.
204
+
205
+ `propose_provider` returns `verificationWarnings` for calls only an author can
206
+ make (currently: a captured value baked literally into the URL, needing the
207
+ `{{context.<name>}}`-vs-bare-vs-recapture decision). Advisory, never blocking —
208
+ **surface each one verbatim** and resolve it before publishing.
209
+
210
+ <!-- topic: prove -->
211
+ ## Replay, then prove
212
+
213
+ **1. Replay.** `replay_request(captureId, draftId)`:
145
214
 
146
215
  - `matchesOriginal: true` → proceed.
147
- - `matchesOriginal: false` with `hints` mentioning auth/csrf → re-trigger the action yourself: navigate or reload via the tools, then loop to step 5. Don't ask the dev to redo it manually.
148
- - `matchesOriginal: false` with stable diff (timestamps etc.) → tighten the matcher and retry.
149
-
150
- **After replay succeeds, run the authentication-bound check — this is mandatory.** Call `replay_request(captureId, draftId, withoutSecrets: true)`. This re-issues the request with NO secrets/cookies (an anonymous visitor).
151
-
152
- - If it FAILS to extract (401/403/redirect, or the value is gone) → the endpoint is auth-bound. Good — the proof genuinely means "the holder of these secrets saw this value."
153
- - If it STILL extracts the value → the endpoint is **PUBLIC**: anyone gets the same value with no login, so the proof binds to nobody. **Do NOT prove it.** Find a request whose response *depends on the session* (an API keyed to the logged-in user, e.g. `/settings/*` or a `viewer`/`currentUser` API), and re-check. If a value only ever appears on public responses, tell the dev it can't be proven as an identity claim rather than proving a public one.
154
-
155
- 12. **Prove.** Call `run_proof(captureId, draftId, ownerAddress)`. On `verified: true`, the full claim has been written to `~/.reclaim/claims/<timestamp>.json` and the tool returns `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`. The `claim` field is a display-ready version of the proof — its huge `request` payload has been stripped and all byte arrays converted to base64, so it renders cleanly. **Show it to the dev as a fenced JSON code block at the end** along with the provider JSON. That's the final result. On `verified: false`, diagnose using the returned error and iterate.
156
-
157
- **Missing signing credential (`errorKind: "missing-owner-key"`)** — the dev hasn't set up an eth proof-owner key yet. Tell them what's happening, then drive the bootstrap yourself: call `resolve_owner_key`; if it returns no key, ask once _"shall I mint a fresh eth key via `issue_credentials` (or `import_credentials` if you already hold one)? It's written to the project `.env` (mode 0600, gitignored) for this and future sessions."_; on yes, call `issue_credentials` (or `import_credentials`) and retry the same `run_proof` call. **No restart needed** — the key is read live from `.env` on each `run_proof`, and the existing `draftId` from `propose_provider` is still valid.
216
+ - `false` with `hints` mentioning auth/csrf → re-trigger the action yourself
217
+ (navigate or reload with the tools) and redraft. Don't ask the developer to
218
+ redo it.
219
+ - `false` with a stable diff (timestamps, and so on) → tighten the matcher
220
+ and retry.
221
+
222
+ **2. The authentication-bound check — mandatory.**
223
+ `replay_request(captureId, draftId, withoutSecrets: true)` re-issues the request
224
+ with no secrets/cookies, as an anonymous visitor.
225
+
226
+ - FAILS to extract (401/403/redirect, or the value is gone) → the endpoint is
227
+ auth-bound. Good: the proof genuinely means "the holder of these secrets saw
228
+ this value."
229
+ - STILL extracts the value → the endpoint is **PUBLIC**; the proof binds to
230
+ nobody. **Do NOT prove it.** Find a request whose response depends on the
231
+ session (an API keyed to the logged-in user — `/settings/*`, a
232
+ `viewer`/`currentUser` API) and re-check. If the value only ever appears on
233
+ public responses, tell the developer it can't be proven as an identity claim.
234
+
235
+ **3. Prove.** `run_proof(captureId, draftId, ownerAddress)`. On
236
+ `verified: true` the full claim is written to
237
+ `~/.reclaim/claims/<timestamp>.json` and you get
238
+ `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`.
239
+ `claim` is display-ready (huge `request` payload stripped, byte arrays base64'd)
240
+ — **show it to the developer as a fenced JSON block at the end**, with the
241
+ provider JSON. That's the final result.
242
+
243
+ **`errorKind: "missing-owner-key"`** → no eth proof-owner key yet. Tell the
244
+ developer what's happening and drive the bootstrap yourself — see topic
245
+ `credentials`. No restart needed, and the existing `draftId` stays valid.
158
246
 
159
247
  <!-- common: prove-tail -->
160
248
 
161
- 13. **Publish the proved provider.** `provider` is the exact JSON from step 9 with the step-10 URL parameterisation applied, and `initialUrl` is the page where the user logs in / the value is displayed (e.g. `https://github.com/Syed`) — **not** the API endpoint URL inside the provider.
162
-
163
- 1. **Create the provider if it doesn't exist:** `create_provider(orgId, domain, slug, title, description)` — find `orgId` via `list_orgs`. `domain` e.g. `github.com`; `slug` e.g. `github-username`; `title` e.g. "GitHub Username". Starts PRIVATE with an empty seed version. Skip if the dev already has a `providerId`.
164
- 2. **Persist the version:** `create_provider_version_from_capture(providerId, provider, initialUrl, notes)` — `notes` e.g. `"Initial draft from browser capture"`. The version lands on the `draft` branch.
165
- - **Multi-request provider** (e.g. a landing-page call plus a details call, or a CSRF-preflight/refresh-token chain): run steps 5–12 ONCE PER REQUEST — a separate `propose_provider` → `replay_request` → `run_proof` cycle for each — then pass `providers: [draftA, draftB, ...]` instead of the single-draft `provider` in this same call. Every entry becomes its own request in the version, and all are required at verification time.
166
- 3. **`requiredContext` is derived for you.** The publish tool scans the recipe's url/body for `{{context.<name>}}` placeholders (step 10) and emits a JSON Schema requiring each one — the Builder then validates every session-create's `context` against it (a clear 400 instead of a late attestor failure). Extra context fields are always allowed. The derived properties are plain strings; to refine types or add human-readable descriptions, call `update_provider_version` with a `requiredContext` override while the version is on `draft`, e.g.:
167
-
168
- ```json
169
- { "requiredContext": { "type": "object", "required": ["username"], "properties": { "username": { "type": "string", "description": "GitHub username to verify" } } } }
170
- ```
171
- 4. **Go live:** PRIVATE → `publish_provider_version`; PUBLIC → `submit_provider_version_for_review(providerId, version)` and tell the dev it's awaiting admin review.
172
- 5. **Tell the dev how consumers call it:** sessions for this provider must include the context params, e.g. `context: { "username": "..." }` in `POST /verifications/sessions`. Quote the `requiredContext` returned on the created version — a session missing those fields gets a 400, and verification matches the supplied values exactly.
173
-
174
- When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
175
-
176
- - **If a browser is still attached**, `create_provider_version_from_capture`'s response carries a `_notes` reminder about it. Surface that to the dev and ask whether they want to keep the session open for further edits — only call `dispose_browser` once they confirm they're done. Never close it unprompted; a `builder`-mode session is quota-accounted, but the dev may still be mid-workflow. Closing remote browsers
177
- soon is also important to avoid wasting resources. So as best
178
- practise, close remote browser session when not needed anymore.
179
-
180
- <!-- common: concerns-section -->
181
-
249
+ <!-- topic: publish -->
250
+ ## Publish
251
+
252
+ `provider` is the JSON from the draft step with the `params` URL
253
+ parameterization applied. `initialUrl` is the page where the user logs in / the
254
+ value is displayed (for example, `https://github.com/Syed`) — **not** the API
255
+ endpoint URL inside the provider.
256
+
257
+ **1. Reuse before create — this is how duplicates happen.**
258
+ `create_provider_version_from_capture` requires a `providerId`. Before minting a
259
+ new one, search your existing providers with `providers` (by domain / data
260
+ point) — a site you've already built is there. Match → pass its
261
+ `providerId` and the capture lands as a NEW VERSION on it. Only call
262
+ `create_provider(orgId, domain, slug, title, description)` when none exists
263
+ (`orgId` from `list_orgs`; `domain` such as `github.com`, `slug` such as
264
+ `github-username`, `title` such as "GitHub Username"). It starts PRIVATE with an
265
+ empty seed version. Routing every run through `create_provider` is what stacks
266
+ duplicates in "My providers".
267
+
268
+ **2. Persist the version.**
269
+ `create_provider_version_from_capture(providerId, provider, initialUrl, notes)`
270
+ — `notes` such as `"Initial draft from browser capture"`. It lands on the
271
+ `draft` branch.
272
+
273
+ - **Multi-request provider** (landing call + details call, a
274
+ CSRF-preflight/refresh-token chain, …): run capture→draft→replay→prove ONCE
275
+ PER REQUEST, then pass `providers: [draftA, draftB, …]` instead of the single
276
+ `provider`. Each becomes its own request on the version and **all** are
277
+ required at verification time.
278
+ - **Login/navigation-gated data:** supply `jsUserScripts` (plain page JS that
279
+ drives login/waits/navigation so the target request fires), or
280
+ `allowedJsRequests` (extra requests the script may fire, for example,
281
+ pagination). → topic `user-script`. Before populating `allowedJsRequests` for
282
+ a `verifyProof({ providerId })` use case, read topic `hash-validation` — a
283
+ session-unique URL there makes verifyProof reject every real proof.
284
+
285
+ **3. `requiredContext` is derived for you.** The publish tool scans the recipe's
286
+ url/body for `{{context.<name>}}` placeholders and emits a JSON Schema requiring
287
+ each — the Builder then validates every session-create's `context` against it (a
288
+ clear 400 instead of a late attestor failure). Extra context fields are always
289
+ allowed; it's optional when the recipe has no `{{context.*}}` placeholders.
290
+ Derived properties are plain strings; refine types or add descriptions with
291
+ `update_provider_version` while the version is still on `draft`:
292
+
293
+ ```json
294
+ { "requiredContext": { "type": "object", "required": ["username"], "properties": { "username": { "type": "string", "description": "GitHub username to verify" } } } }
295
+ ```
296
+
297
+ **4. Go live.** PRIVATE → `publish_provider_version`. PUBLIC →
298
+ `submit_provider_version_for_review(providerId, version)`, then tell the
299
+ developer it's awaiting admin review.
300
+
301
+ **5. Tell the developer how consumers call it.** Sessions must include the
302
+ context params — for example, `context: { "username": "…" }` in
303
+ `create_verification_session`. Quote the `requiredContext` from the created
304
+ version verbatim — a session missing those fields gets a 400, and verification
305
+ matches the supplied values exactly.
306
+
307
+ **6. The browser is still open.** The response's `_notes` says so. Surface it
308
+ and ask whether the developer wants to keep the session for further edits — call
309
+ `dispose_browser` only once they confirm. Never close it unprompted; a
310
+ `builder`-mode session is quota-accounted, so don't leave it idle either.
311
+
312
+ <!-- topic: discover -->
313
+ ## Discover and resolve providers for the developer's app
314
+
315
+ When a developer wants to add verification but doesn't know which providers to
316
+ use, run the same flow the Discover UI does:
317
+
318
+ 1. Ask which website their users prove data from.
319
+ 2. `discover_data_points({ domain })` → what's verifiable there. Confirm the
320
+ data points with the developer.
321
+ 3. `discover_resolve({ domain, dataPoints })` → the smallest provider set
322
+ (`providers`, greedy combine), the `requiredContext` inputs to collect, and
323
+ any `uncovered` points (author those with the flow above, then re-resolve).
324
+ 4. `create_verification_session({ providers: [{ providerId }, …], context,
325
+ verificationClientUrl })` with EVERY resolved provider id and the collected
326
+ context.
327
+
328
+ `merge.mergeable` only signals the set *could* collapse into one provider — you
329
+ still pass them all; the Builder runs them in one session.
330
+
331
+ **Verification sessions.** Consumers authenticate with a per-org token
332
+ (`rorg_…`, issued in the dashboard via `POST /orgs/{orgId}/token`) and create
333
+ sessions via `POST /verifications/sessions`. Results are ECIES-encrypted
334
+ (secp256k1 + AES-256-GCM) to the org's eth public key when `canEncryptResult` is
335
+ enabled (set via `PUT /orgs/{orgId}/keypair`) and delivered to the org's
336
+ callback subscriptions; plaintext otherwise. Troubleshoot with
337
+ `get_verification_session`, `list_verification_events`,
338
+ `list_org_verification_sessions`.
339
+
340
+ <!-- topic: never-do -->
182
341
  ## Things to never do
183
342
 
184
343
  <!-- common: never-do-common -->
185
- - **Never hand-craft `responseMatches` or `responseRedactions`.** If you find yourself typing a regex by hand, OR writing `(?<name>...)`-style patterns by hand, OR copying the captured value literally into the match — stop. The synthesizer in `propose_provider` already produces the canonical shape: `contains` template + paramValues for the match, xPath/jsonPath for the redaction — plus a value-isolating named-capture `regex` (for every xPath/jsonPath anchor — the reveal is a byte range, never the bare value). The named-capture regex in the redaction is the synthesizer's job, not yours.
186
- - Never accept "the synthesizer's xPath looks too narrow, let me write a regex" — the synthesizer already pairs the xPath with a structure-anchored named-capture regex (for HTML) that both reveals the element and isolates the value. If extraction still looks wrong, re-run `propose_provider` with a better request/target — don't hand-write patterns.
187
- - Mark ANY param secret (not just consumer-supplied `context.*` ones) by naming it with `SECRET` (e.g. `{{SECRET_x}}` or `{{context.SECRET_apiKey}}`). It CAN be used in the URL, geoLocation, request body, response match, response redactions — write-redaction hides it there. Any value of a parameter marked secret is not included in the proof. There's also a limitation for secret marked variables secrets in url: only up to a small TOTAL character budget across every secret value in a URL (2 chars under `key-update`, the default; 12 under `zk`). That's checked at verification time against the consumer's real value, not at publish time (see step 10). Headers are secret by allowlist, not by name — anything not on a small fixed public list is secret by default.
188
-
189
- <!-- common: first-time-issues-intro -->
190
-
191
- <!-- common: credential-intro -->
192
-
193
- 1. **Call `resolve_owner_key`.** It checks `RECLAIM_PRIVATE_KEY` (MCP env) → the project `.env` → `RECLAIM_PRIVATE_KEY_FILE`, and returns only the derived address.
194
- - `{ ready: true, address }` → call `run_proof(captureId, draftId, address)`. No prompts.
195
- - `{ ready: false }` → no key exists yet. Ask the dev once how to set one up:
196
- - **Provide their own key** → `import_credentials({ privateKey: "0x…" })` (also accepts a `keyFile` path).
197
- - **Create a new one** → `issue_credentials({})`. (Fine default for test proofs — owner identity doesn't matter.)
198
- - Either way the key is written to the project `.env` (mode 0600; `.env` is added to `.gitignore`) — never to `~/.reclaim`.
199
- - Then call `resolve_owner_key` again and `run_proof` with the returned address.
200
- <!-- common: credential-step2 -->
201
- - **Advanced:** setting `RECLAIM_PRIVATE_KEY` directly in the MCP server's env also works, but env vars are snapshotted at Claude Code startup, so that route needs a full restart (which invalidates every `draftId`). The `.env` flow above avoids that — prefer it.
202
- <!-- common: mcp-config-bullet -->
203
-
204
- If `replay_request` or `run_proof` errors with `unknown draftId` (or `no capture`):
205
-
206
- - **The draft is bound to its capture, held in the MCP server process.** Anything that restarts the server (Claude Code restart, `reset_session`, machine reboot) drops every capture and its drafts. The fix is always: re-run `start_capture` → interact → `propose_provider` to mint a fresh `draftId`. Don't ask the dev to "re-enter the value" — walk them back through capture.
207
- - **Auth-bound check:** to replay/prove the request as an unauthenticated visitor (no secret headers), pass `withoutSecrets: true` to `replay_request` — the draft still carries the captured secrets, so you never re-supply them.
208
-
209
- ## Recognizing a HAWKEYE / live-interception provider
210
-
211
- Everything above builds the STANDARD flow: capture → draft → replay → prove. There is a second flow — `webSettings.inapp.interceptorOptions.interceptorType: HAWKEYE` (or `MSWJS`/`CDP`) plus a `jsUserScripts` script that calls `window.reclaimInterceptor.addResponseMiddleware(...)` to observe the page's REAL responses as they happen, instead of an independent replay. `create_provider_version_from_capture` never sets `inapp` at all, so you will not author this flow with the current tools — but you may see these fields on a version someone else published outside this flow. `isDocumentRequestReplayEnabled` is a NARROWER, separate toggle from the interception itself — it does not gate `addResponseMiddleware` in general (that fires for every `interceptorType` including `CDP`); it only controls whether the current page's own document/navigation request specifically gets pulled through a non-CDP interceptor, as a site-compatibility escape hatch. See `injection-api.md` (embedded in the `jsUserScripts` field description) for the source-verified breakdown before guessing at what they mean.
344
+ - Never request `hash` for a target whose value also appears in the request URL
345
+ (topic `draft`), and never `hash` a free-form target — `propose_provider`
346
+ throws either way; retrying won't help.
347
+
348
+ <!-- topic: credentials -->
349
+ <!-- common: credentials -->
350
+
351
+ <!-- topic: troubleshooting -->
352
+ ## Troubleshooting
353
+
354
+ **`unknown draftId` / `no capture`.** The draft is bound to its capture, held in
355
+ the MCP server process. Anything that restarts the server (Claude Code restart,
356
+ `reset_session`, reboot) drops every capture and its drafts. The fix is always
357
+ `start_capture` → interact → `propose_provider` for a fresh `draftId`. Don't ask
358
+ the developer to "re-enter the value" — walk them back through capture.
359
+
360
+ **Replaying as an anonymous visitor.** Pass `withoutSecrets: true` to
361
+ `replay_request`; the draft still holds the captured secrets, so you never
362
+ re-supply them.
363
+
364
+ **Site won't load, won't let the user sign in, or breaks right after
365
+ sign-in.** Check WHICH client the session targets first: on `builder` (the
366
+ default) and `portals` interception happens over CDP, so it is never the
367
+ cause — look for a captcha instead. On `inapp-sdk`, `verifier-app`, or the
368
+ browser extension, suspect interception before the recipe, and bisect: FIRST
369
+ turn
370
+ document replay off and keep the interceptor (`disableRequestReplay: true`, or
371
+ `isDocumentRequestReplayEnabled: false` on builder) — often that alone is what
372
+ breaks the page, and it's the smallest change. Only if that doesn't help, turn
373
+ interception off entirely (`injectionType: NONE`, or omit
374
+ `interceptorOptions`). Full checklist and remedies: topic `interception`.
375
+
376
+ **Proof failures / attestor errors** → topic `prove`.
377
+ **Browser won't launch or attach** → topic `browser`.
378
+ **Missing signing key** → topic `credentials`.