@reclaimprotocol/agent 0.1.4 → 0.1.6

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 (126) hide show
  1. package/README.md +5 -5
  2. package/lib/authoring/backend.d.ts +71 -0
  3. package/lib/authoring/backend.js +24 -0
  4. package/lib/authoring/find.d.ts +3 -0
  5. package/lib/authoring/find.js +54 -0
  6. package/lib/authoring/index.d.ts +4 -0
  7. package/lib/authoring/index.js +6 -0
  8. package/lib/authoring/propose.d.ts +3 -0
  9. package/lib/authoring/propose.js +109 -0
  10. package/lib/authoring/replay.d.ts +10 -0
  11. package/lib/authoring/replay.js +34 -0
  12. package/lib/authoring/tools.d.ts +16 -0
  13. package/lib/authoring/tools.js +193 -0
  14. package/lib/authoring/util.d.ts +27 -0
  15. package/lib/authoring/util.js +21 -0
  16. package/lib/cdp/capture-engine.js +1 -1
  17. package/lib/consts.d.ts +6 -1
  18. package/lib/consts.js +9 -2
  19. package/lib/mcp/api/operations.gen.js +46 -18
  20. package/lib/mcp/server.d.ts +2 -25
  21. package/lib/mcp/server.js +4 -17
  22. package/lib/mcp/start.js +4 -2
  23. package/lib/mcp/tools/agent/analyze.js +3 -3
  24. package/lib/mcp/tools/agent/build.d.ts +4 -4
  25. package/lib/mcp/tools/agent/build.js +13 -15
  26. package/lib/mcp/tools/agent/capture.d.ts +13 -1
  27. package/lib/mcp/tools/agent/capture.js +18 -85
  28. package/lib/mcp/tools/agent/eval.d.ts +5 -2
  29. package/lib/mcp/tools/agent/eval.js +11 -121
  30. package/lib/mcp/tools/agent/find.d.ts +10 -3
  31. package/lib/mcp/tools/agent/find.js +25 -31
  32. package/lib/mcp/tools/agent/proof.d.ts +13 -3
  33. package/lib/mcp/tools/agent/proof.js +81 -132
  34. package/lib/mcp/tools/agent/propose.d.ts +10 -9
  35. package/lib/mcp/tools/agent/propose.js +87 -64
  36. package/lib/mcp/tools/agent/publish.js +2 -2
  37. package/lib/mcp/tools/agent/replay.d.ts +10 -3
  38. package/lib/mcp/tools/agent/replay.js +29 -36
  39. package/lib/mcp/tools/agent/session.d.ts +1 -3
  40. package/lib/mcp/tools/agent/session.js +5 -5
  41. package/lib/mcp/tools/agent/shared.d.ts +8 -0
  42. package/lib/mcp/tools/agent/shared.js +125 -0
  43. package/lib/old/client.d.ts +35 -0
  44. package/lib/old/client.js +18 -0
  45. package/lib/old/login-cdp.d.ts +24 -0
  46. package/lib/old/login-cdp.js +28 -3
  47. package/lib/old/sdk-client.d.ts +39 -0
  48. package/lib/old/sdk-client.js +85 -0
  49. package/lib/old/to-register.d.ts +69 -0
  50. package/lib/old/to-register.js +52 -2
  51. package/lib/old/tools/build.d.ts +6 -1
  52. package/lib/old/tools/build.js +11 -1
  53. package/lib/old/tools/get-provider-info.d.ts +16 -0
  54. package/lib/old/tools/get-provider-info.js +73 -0
  55. package/lib/old/tools/get-version-config.d.ts +17 -0
  56. package/lib/old/tools/get-version-config.js +47 -0
  57. package/lib/old/tools/publish.js +189 -2
  58. package/lib/old/tools/update-provider-metadata.d.ts +16 -0
  59. package/lib/old/tools/update-provider-metadata.js +61 -0
  60. package/lib/proof/attestor.d.ts +6 -3
  61. package/lib/proof/attestor.js +10 -1
  62. package/lib/proof/debug-response-dump.d.ts +4 -0
  63. package/lib/proof/debug-response-dump.js +55 -0
  64. package/lib/proof/patch-domhandler.d.ts +1 -0
  65. package/lib/proof/patch-domhandler.js +59 -0
  66. package/lib/proof/proof-core.d.ts +1 -1
  67. package/lib/proof/proof-core.js +1 -1
  68. package/lib/proof/zk-files.js +7 -6
  69. package/lib/provider/dom-xpath.d.ts +17 -0
  70. package/lib/provider/dom-xpath.js +187 -0
  71. package/lib/provider/draft.d.ts +30 -2
  72. package/lib/provider/draft.js +117 -28
  73. package/lib/provider/index.d.ts +4 -0
  74. package/lib/provider/index.js +7 -0
  75. package/lib/provider/injection-doc.js +6 -0
  76. package/lib/provider/matcher.d.ts +17 -20
  77. package/lib/provider/matcher.js +394 -280
  78. package/lib/provider/schema.d.ts +7 -5
  79. package/lib/tools/registry.d.ts +50 -0
  80. package/lib/tools/registry.js +49 -0
  81. package/package.json +9 -1
  82. package/skill/SKILL.builder.md +55 -32
  83. package/skill/SKILL.old.md +20 -12
  84. package/skill/builder-mode.md +1 -1
  85. package/skill/injection-api.md +117 -0
  86. package/src/authoring/backend.ts +98 -0
  87. package/src/authoring/index.ts +20 -0
  88. package/src/authoring/tools.ts +273 -0
  89. package/src/authoring/util.ts +42 -0
  90. package/src/cdp/capture-engine.ts +1 -1
  91. package/src/consts.ts +15 -2
  92. package/src/mcp/api/operations.gen.ts +46 -18
  93. package/src/mcp/server.ts +9 -39
  94. package/src/mcp/start.ts +4 -1
  95. package/src/mcp/tools/agent/analyze.ts +3 -3
  96. package/src/mcp/tools/agent/build.ts +13 -21
  97. package/src/mcp/tools/agent/capture.ts +31 -115
  98. package/src/mcp/tools/agent/eval.ts +19 -155
  99. package/src/mcp/tools/agent/find.ts +33 -36
  100. package/src/mcp/tools/agent/proof.ts +87 -160
  101. package/src/mcp/tools/agent/propose.ts +93 -85
  102. package/src/mcp/tools/agent/publish.ts +2 -2
  103. package/src/mcp/tools/agent/replay.ts +32 -42
  104. package/src/mcp/tools/agent/session.ts +4 -5
  105. package/src/mcp/tools/agent/shared.ts +152 -0
  106. package/src/old/client.ts +56 -0
  107. package/src/old/login-cdp.ts +28 -3
  108. package/src/old/sdk-client.ts +105 -0
  109. package/src/old/to-register.ts +98 -6
  110. package/src/old/tools/build.ts +12 -0
  111. package/src/old/tools/get-provider-info.ts +91 -0
  112. package/src/old/tools/get-version-config.ts +56 -0
  113. package/src/old/tools/publish.ts +244 -1
  114. package/src/old/tools/update-provider-metadata.ts +72 -0
  115. package/src/proof/attestor.ts +16 -4
  116. package/src/proof/debug-response-dump.ts +70 -0
  117. package/src/proof/patch-domhandler.ts +68 -0
  118. package/src/proof/proof-core.ts +1 -1
  119. package/src/proof/zk-files.ts +7 -6
  120. package/src/provider/dom-xpath.ts +225 -0
  121. package/src/provider/draft.ts +168 -35
  122. package/src/provider/index.ts +8 -0
  123. package/src/provider/injection-doc.ts +6 -0
  124. package/src/provider/matcher.ts +477 -357
  125. package/src/provider/schema.ts +7 -5
  126. package/src/tools/registry.ts +92 -0
@@ -19,11 +19,13 @@ export type ResponseMatch = HttpParams['responseMatches'][number];
19
19
  */
20
20
  export type ResponseRedaction = NonNullable<HttpParams['responseRedactions']>[number];
21
21
  /**
22
- * OPRF mode for hashed redactions. `oprf-raw` is what the dev-loop defaults
23
- * to when privacy is requested — the only variant that runs without external
24
- * deps (gnark for `oprf`, a TEE for `oprf-mpc`). It is the weakest of the
25
- * three (the attestor participates in hashing) and is meant to be swapped
26
- * out before production.
22
+ * OPRF mode for hashed redactions:
23
+ * - `oprf-mpc` — TEE-based MPC OPRF. The default for the cloud/hosted flow: a
24
+ * TEE attestor accepts (and expects) this and REJECTS `oprf-raw`.
25
+ * - `oprf` — client-side ZK OPRF via gnark (needs the gnark deps).
26
+ * - `oprf-raw` — server-side OPRF, no external deps; the local dev-loop
27
+ * default (Windows / non-TEE). Weakest of the three (the attestor
28
+ * participates in hashing) and not accepted by a TEE attestor.
27
29
  */
28
30
  export type OprfMode = NonNullable<ResponseRedaction['hash']>;
29
31
  /** A single captured request + response, as we store it server-side. */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The ONE tool primitive for the agent. A tool is authored once with
3
+ * {@link defineTool} — `{ definition, handler }`, where the handler receives
4
+ * args already parsed + validated against the definition's JSON Schema (via
5
+ * zod), so the schema shown to the model and the validation applied to inbound
6
+ * args cannot drift.
7
+ *
8
+ * This lives outside `mcp/` on purpose: it depends only on zod (NOT the MCP
9
+ * SDK), so non-MCP consumers — the cloud authoring loop — can author and run
10
+ * these tools without pulling in `@modelcontextprotocol/sdk`. The MCP-specific
11
+ * glue (`registerToolList` onto an `McpServer`) stays in `mcp/server.ts`, which
12
+ * re-exports this primitive for the MCP-side tool files.
13
+ */
14
+ import type { JSONSchema } from 'zod/v4/core';
15
+ export interface ToolDefinition {
16
+ name: string;
17
+ description: string;
18
+ inputSchema: JSONSchema.ObjectSchema;
19
+ }
20
+ export interface RegisteredTool {
21
+ definition: ToolDefinition;
22
+ /**
23
+ * @param args Tool arguments (already zod-validated against the schema).
24
+ * @returns A JSON-stringifiable value to return to the LLM.
25
+ * @throws Any error; the host (MCP `registerToolList` / the cloud loop)
26
+ * catches it and surfaces it as an error tool-result.
27
+ */
28
+ handler: (args: unknown) => Promise<unknown>;
29
+ }
30
+ /**
31
+ * Define a tool with a single inputSchema (JSON Schema) as the source of truth.
32
+ * The handler receives args already parsed and type-narrowed via a Zod schema
33
+ * derived from that same JSON Schema — so the description sent to the client
34
+ * and the validation applied to inbound args cannot drift.
35
+ */
36
+ export declare function defineTool<TArgs>(definition: ToolDefinition, handler: (args: TArgs) => Promise<unknown>): RegisteredTool;
37
+ /**
38
+ * Adapt a tool to a host by WIDENING its input schema — add properties, mark
39
+ * some required, optionally override the description — without touching the
40
+ * handler. A host maps whichever shared tools it needs through this (e.g. the
41
+ * MCP adds its `captureId` session handle + the credential-aware `run_proof`
42
+ * contract). Args parse loosely, so the extra fields flow through to the
43
+ * handler (its backend resolver reads them); the base tool stays untouched for
44
+ * hosts that don't need them.
45
+ */
46
+ export declare function extendTool(tool: RegisteredTool, extra: {
47
+ description?: string;
48
+ properties?: Record<string, unknown>;
49
+ required?: string[];
50
+ }): RegisteredTool;
@@ -0,0 +1,49 @@
1
+ import { toZod } from "../mcp/api/to-zod.js";
2
+ /**
3
+ * Define a tool with a single inputSchema (JSON Schema) as the source of truth.
4
+ * The handler receives args already parsed and type-narrowed via a Zod schema
5
+ * derived from that same JSON Schema — so the description sent to the client
6
+ * and the validation applied to inbound args cannot drift.
7
+ */
8
+ export function defineTool(definition, handler) {
9
+ const schema = toZod(definition.inputSchema);
10
+ return {
11
+ definition,
12
+ handler: async (raw) => {
13
+ const args = schema.parse(raw);
14
+ return await handler(args);
15
+ },
16
+ };
17
+ }
18
+ /**
19
+ * Adapt a tool to a host by WIDENING its input schema — add properties, mark
20
+ * some required, optionally override the description — without touching the
21
+ * handler. A host maps whichever shared tools it needs through this (e.g. the
22
+ * MCP adds its `captureId` session handle + the credential-aware `run_proof`
23
+ * contract). Args parse loosely, so the extra fields flow through to the
24
+ * handler (its backend resolver reads them); the base tool stays untouched for
25
+ * hosts that don't need them.
26
+ */
27
+ export function extendTool(tool, extra) {
28
+ const base = tool.definition;
29
+ const required = [
30
+ ...(base.inputSchema.required ?? []),
31
+ ...(extra.required ?? []),
32
+ ];
33
+ const inputSchema = {
34
+ ...base.inputSchema,
35
+ properties: {
36
+ ...base.inputSchema.properties,
37
+ ...extra.properties,
38
+ },
39
+ ...(required.length ? { required } : {}),
40
+ };
41
+ return {
42
+ definition: {
43
+ name: base.name,
44
+ description: extra.description ?? base.description,
45
+ inputSchema,
46
+ },
47
+ handler: tool.handler,
48
+ };
49
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "description": "Reclaim Protocol agent — SDK, CLI, and MCP server in one package. SDK + CLI talk to the builder backend; MCP server adds Chrome CDP capture, provider synthesis, replay, and zkTLS proof.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -11,6 +11,10 @@
11
11
  "types": "./lib/index.d.ts",
12
12
  "import": "./lib/index.js"
13
13
  },
14
+ "./authoring": {
15
+ "types": "./lib/authoring/index.d.ts",
16
+ "import": "./lib/authoring/index.js"
17
+ },
14
18
  "./provider": {
15
19
  "types": "./lib/provider/index.d.ts",
16
20
  "import": "./lib/provider/index.js"
@@ -83,7 +87,11 @@
83
87
  "@reclaimprotocol/client": "*",
84
88
  "@reclaimprotocol/tls": "^0.1.3",
85
89
  "chrome-remote-interface": "^0.33.0",
90
+ "domhandler": "^6.0.0",
91
+ "parse5": "^8.0.0",
92
+ "parse5-htmlparser2-tree-adapter": "^8.0.0",
86
93
  "pino": "^9.14.0",
94
+ "xpath": "^0.0.34",
87
95
  "zod": "^4.4.3"
88
96
  },
89
97
  "devDependencies": {
@@ -1,10 +1,14 @@
1
- # Authoring a Reclaim Provider / Verification
2
-
3
- > **Terminology note:** "provider" and "verification" refer to the same
4
- > thing. The API and tool parameters use **provider** / **providerId**
5
- > (the technical term). Conversations with developers prefer
6
- > **verification** (the user-facing term). Either is acceptable — never
7
- > correct the developer for using one or the other.
1
+ # Authoring a Reclaim Provider
2
+
3
+ > **Terminology note:** A **provider** is the recipe you author here — the
4
+ > instructions for extracting a data point from a website (the
5
+ > `responseMatches` / `responseRedactions` spec, identified by a
6
+ > `providerId`). Always call it a **provider**; never call a provider a
7
+ > "verification." A **verification** (or **verification session**) is the
8
+ > separate runtime act in which an end-user proves a data point *using* a
9
+ > provider — reserve that word for the session/result, never for the recipe.
10
+ > If a developer calls the recipe a "verification," gently use "provider"
11
+ > yourself rather than correcting them.
8
12
 
9
13
  You drive the browser through the capture → draft → replay → prove loop using the `reclaim-agent` MCP server. **Your default stance is driver, not orchestrator.** Only pause to ask the dev for things that genuinely require a human: typing credentials, completing 2FA, solving a captcha, confirming an ambiguous value. Navigation, reloads, DOM inspection, request capture — all yours.
10
14
 
@@ -28,9 +32,9 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
28
32
 
29
33
  Re-call with the chosen `attachMode`.
30
34
 
31
- 2. **Pick a tab and open the site yourself.** Call `list_tabs`. If there's already a tab on the right origin, use it. Otherwise pick any tab (the first one is usually fine) and call `navigate(tabId, url)` to send it to the site you've inferred from the dev's request (e.g. "GitHub username" → `https://github.com`). Don't ask the dev to navigate.
35
+ 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.
32
36
 
33
- 3. **Check login state, wait if needed.** Use `eval_in_page(tabId, ...)` to evaluate a site-specific check.
37
+ 3. **Check login state, wait if needed.** Use `eval_in_page(captureId, ...)` to evaluate a site-specific check.
34
38
 
35
39
  **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.
36
40
 
@@ -41,13 +45,13 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
41
45
 
42
46
  If the result is truthy, skip to step 4.
43
47
 
44
- 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(tabId, "<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(tabId, 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.
48
+ 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.
45
49
 
46
50
  **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.
47
51
 
48
- 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(tabId, url)`. Don't ask the dev to navigate.
52
+ 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.
49
53
 
50
- 5. **Start capture, then trigger the request.** Call `start_capture(tabId)` first. Then issue a reload so the relevant API call fires while capture is live: `eval_in_page(tabId, "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.
54
+ 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.
51
55
 
52
56
  6. **Identify the value from the DOM.** `eval_in_page` to read the displayed value (e.g. the logged-in username from `meta[name="user-login"]`'s `content` attribute). If the value is unambiguous, proceed without asking. Only confirm with the dev when there are multiple plausible interpretations (e.g. the page shows both a display name "John Doe" and a handle "@jdoe" and you need to pick which to prove).
53
57
 
@@ -79,11 +83,24 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
79
83
 
80
84
  **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.
81
85
 
82
- 9. **Draft.** Call `propose_provider(captureId, requestId, target, name, hash?)`. Pass `hash` only if the dev opted in. Inspect the returned provider:
86
+ 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.
87
+
88
+ **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):
89
+
90
+ ```
91
+ propose_provider(captureId, requestId, name, targets: [
92
+ { value: "Scratch-net", name: "username" },
93
+ { value: "Alex", name: "name" },
94
+ ])
95
+ ```
96
+
97
+ 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:
83
98
  - Is the right header marked secret? (cookies, Authorization, X-\* tokens)
84
99
  - Is `responseMatches[0].value` a `contains` with a `{{name}}` template (matching the captured value's structural context)?
85
100
  - Is `paramValues[name]` set to the original captured value?
86
- - Does `responseRedactions[0]` carry an `xPath` or `jsonPath` anchor? (And if `hash` was requested, also a `regex` with a named capture and a `hash: oprf-…` field?)
101
+ - 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.)
102
+
103
+ **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.
87
104
 
88
105
  **Canonical drafted shape** (no hash):
89
106
 
@@ -92,7 +109,12 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
92
109
  "responseMatches": [
93
110
  { "type": "contains", "value": "\"login\":\"{{username}}\"" }
94
111
  ],
95
- "responseRedactions": [{ "xPath": "//script[@id=\"client-env\"]" }],
112
+ "responseRedactions": [
113
+ {
114
+ "xPath": "//script[@id=\"client-env\"]",
115
+ "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\""
116
+ }
117
+ ],
96
118
  "paramValues": { "username": "Scratch-net" }
97
119
  }
98
120
  ```
@@ -143,20 +165,20 @@ replace with `{{context.<name>}}`. E.g. `https://github.com/Scratch-net/repos`
143
165
 
144
166
  `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.
145
167
 
146
- 11. **Replay.** Call `replay_request(provider, secretParamsRef)`. Three outcomes:
168
+ 11. **Replay.** Call `replay_request(captureId, draftId)`. Three outcomes:
147
169
 
148
170
  - `matchesOriginal: true` → proceed.
149
171
  - `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.
150
172
  - `matchesOriginal: false` with stable diff (timestamps etc.) → tighten the matcher and retry.
151
173
 
152
- **After replay succeeds, run an authentication-bound check.** Call `replay_request(provider)` _without_ the `secretParamsRef` (omit it). This sends the request stripped of all secret headers — the same shape an unauthenticated visitor would send.
174
+ **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).
153
175
 
154
- - If `matchesOriginal: false` (e.g. 401/403, or the value differs) → the endpoint is auth-bound. Good. The proof will actually mean "the holder of these secrets saw this value."
155
- - If `matchesOriginal: true` with the same value → the endpoint is fully public. Tell the dev verbatim: "I verified this endpoint returns the same value with no authentication — the proof won't prove your identity, only that this URL returned the value. Anyone could generate the same proof. Want to keep digging for an auth-bound endpoint, or proceed anyway?" Wait for their decision before continuing.
176
+ - 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."
177
+ - 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.
156
178
 
157
- 12. **Prove.** Call `run_proof(provider, secretParamsRef)`. 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.
179
+ 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.
158
180
 
159
- **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 `secretParamsRef` from `propose_provider` is still valid.
181
+ **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.
160
182
 
161
183
  **On any `verified: false` response, the result includes (inline):**
162
184
  - `error` — the SDK's thrown error message verbatim
@@ -175,15 +197,16 @@ replace with `{{context.<name>}}`. E.g. `https://github.com/Scratch-net/repos`
175
197
 
176
198
  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.
177
199
 
178
- 1. **Create the verification if it doesn't exist:** `create_verification(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`.
200
+ 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`.
179
201
  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.
202
+ - **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.
180
203
  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.:
181
204
 
182
205
  ```json
183
206
  { "requiredContext": { "type": "object", "required": ["username"], "properties": { "username": { "type": "string", "description": "GitHub username to verify" } } } }
184
207
  ```
185
208
  4. **Go live:** PRIVATE → `publish_provider_version`; PUBLIC → `submit_provider_version_for_review(providerId, version)` and tell the dev it's awaiting admin review.
186
- 5. **Tell the dev how consumers call it:** sessions for this verification 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.
209
+ 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.
187
210
 
188
211
  When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
189
212
 
@@ -196,10 +219,10 @@ When `analyze_request_constraints` returns concerns, repeat each `message` to th
196
219
  - Never echo a raw `Authorization`, `Cookie`, or `X-*-Token` value back to the dev or write one to any file. The MCP guarantees these don't appear in tool responses — keep that guarantee.
197
220
  - Never call `run_proof` before `replay_request` succeeds. Attestor calls are expensive.
198
221
  - Never pretend a proof succeeded when `verified: false` came back. Show the error and iterate.
199
- - **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 (optionally + regex+hash when `hash` is passed) for the redaction. The named-capture regex in the redaction is the synthesizer's job, not yours.
222
+ - **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.
200
223
  - Never write a literal value into `responseMatches.value` — the synthesizer always emits `{{name}}` templates with paramValues. A literal value bakes the original user into the provider.
201
224
  - Never pass `hash` to `propose_provider` for a free-form (unicode / spaces / punctuation) target — the synthesizer will error, and the right move is either to use a tighter `target` or accept cleartext (no `hash`).
202
- - Never accept "the synthesizer's xPath looks too narrow, let me write a regex" — the xPath is fine, the attestor will reveal the whole matched element which is naturally ≥16 bytes.
225
+ - 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.
203
226
 
204
227
  ## Common first-time issues — surface these proactively
205
228
 
@@ -216,21 +239,21 @@ If `attach_chrome` errors, times out, or `waitForCdpReady` reports the port isn'
216
239
  **Credential for signing the proof.** `run_proof` requires an `ownerAddress` — the eth proof-owner key whose private half signs the claim. It does **no** resolution itself; get the address from `resolve_owner_key` first. The private key is resolved locally (`RECLAIM_PRIVATE_KEY` env → a `RECLAIM_PRIVATE_KEY=0x…` line in the project `.env` → a file at `RECLAIM_PRIVATE_KEY_FILE`) and never enters the conversation.
217
240
 
218
241
  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.
219
- - `{ ready: true, address }` → call `run_proof({ provider, ownerAddress: address, … })`. No prompts.
242
+ - `{ ready: true, address }` → call `run_proof(captureId, draftId, address)`. No prompts.
220
243
  - `{ ready: false }` → no key exists yet. Ask the dev once how to set one up:
221
244
  - **Provide their own key** → `import_credentials({ privateKey: "0x…" })` (also accepts a `keyFile` path).
222
245
  - **Create a new one** → `issue_credentials({})`. (Fine default for test proofs — owner identity doesn't matter.)
223
246
  - Either way the key is written to the project `.env` (mode 0600; `.env` is added to `.gitignore`) — never to `~/.reclaim`.
224
247
  - Then call `resolve_owner_key` again and `run_proof` with the returned address.
225
248
  2. All fully local — no network, no server registration. The key is read live from `.env` on each `run_proof`, so no restart is needed after issuing/importing.
226
- - **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 `secretParamsRef`). The `.env` flow above avoids that — prefer it.
249
+ - **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.
227
250
  - **Multiple MCP config files exist on Windows.** `~/.claude.json` takes precedence over `~/.claude/mcp.json`. When advising config edits, point them at the right file or tell them to use `claude mcp list` / `claude mcp add --scope user` rather than editing JSON by hand.
228
251
 
229
- If `replay_request` or `run_proof` errors with `unknown secretParamsRef`:
252
+ If `replay_request` or `run_proof` errors with `unknown draftId` (or `no capture`):
230
253
 
231
- - **The ref is bound to the MCP server process.** Anything that restarts the server (Claude Code restart, `reset_session`, machine reboot) invalidates every previously-issued ref. The fix is always: re-run `start_capture` → interact → `propose_provider` to mint a fresh ref. Don't ask the dev to "re-enter the value" — walk them back through capture.
232
- - **Exception:** if the drafted provider has empty `secretRefs: []` (no secret headers were captured), you can call `replay_request` and `run_proof` without `secretParamsRef` at all — both tools accept it as optional in that case.
254
+ - **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.
255
+ - **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.
233
256
 
234
- ## Out of scope (v0)
257
+ ## Recognizing a HAWKEYE / live-interception provider
235
258
 
236
- If the dev needs a provider for a flow that requires multiple sequential requests (CSRF preflight, refresh-token chains), tell them: "multi-request providers are v1. For now, we can produce a single-request provider for the target value — let's see if one exists in this capture."
259
+ 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.
@@ -1,10 +1,14 @@
1
- # Authoring a Reclaim Provider / Verification
2
-
3
- > **Terminology note:** "provider" and "verification" refer to the same
4
- > thing. The API and tool parameters use **provider** / **providerId**
5
- > (the technical term). Conversations with developers prefer
6
- > **verification** (the user-facing term). Either is acceptable — never
7
- > correct the developer for using one or the other.
1
+ # Authoring a Reclaim Provider
2
+
3
+ > **Terminology note:** A **provider** is the recipe you author here — the
4
+ > instructions for extracting a data point from a website (the
5
+ > `responseMatches` / `responseRedactions` spec, identified by a
6
+ > `providerId`). Always call it a **provider**; never call a provider a
7
+ > "verification." A **verification** (or **verification session**) is the
8
+ > separate runtime act in which an end-user proves a data point *using* a
9
+ > provider — reserve that word for the session/result, never for the recipe.
10
+ > If a developer calls the recipe a "verification," gently use "provider"
11
+ > yourself rather than correcting them.
8
12
 
9
13
  You drive the browser through the capture → draft → replay → prove loop using the `reclaim-agent` MCP server. **Your default stance is driver, not orchestrator.** Only pause to ask the dev for things that genuinely require a human: typing credentials, completing 2FA, solving a captcha, confirming an ambiguous value. Navigation, reloads, DOM inspection, request capture — all yours.
10
14
 
@@ -164,9 +168,13 @@ Call `reclaim_authenticate({})` with no arguments. It opens the Reclaim dashboar
164
168
  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.
165
169
 
166
170
  Call `create_provider_version_from_capture`:
167
- - **New verification:** omit `providerId`. It registers a new provider (version 1.0.0). Pass `provider`, `initialUrl`, and optionally `description`, `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
168
- - **New version of an existing verification:** pass its `providerId` — the patch is auto-bumped above the highest existing version. Pass `notes` (≥10 chars) as the change note.
169
- - `get_me_providers({ searchQuery? })` lists the verifications you own — use it to find a `providerId` or to confirm the result. It's the only provider-listing tool in this mode.
171
+ - **New provider:** omit `providerId`. It registers a new provider (version 1.0.0). Pass `provider`, `initialUrl`, and optionally `description`, `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
172
+ - **New version of an existing provider:** pass its `providerId` — the patch is auto-bumped above the highest existing version. Pass `notes` (≥10 chars) as the change note.
173
+ - **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 publish all of them together in ONE call by passing `providers: [draftA, draftB, ...]` instead of the single-draft `provider`. Every entry becomes its own parallel `requestData` entry on the SAME version, and all are required at verification time.
174
+ - `get_me_providers({ searchQuery? })` lists the providers you own — use it to find a `providerId` or to confirm the result. It's the only provider-listing tool in this mode.
175
+ - **Before adding a version to an EXISTING provider, call `get_provider_version_config(providerId)` first.** This backend has no partial-patch endpoint — a version replaces the WHOLE `providerConfig` — so seeing what's currently live (`customInjection`, `userAgent`, `pageTitle`, `stepsToFollow`, etc.) tells you what you're about to change vs. carry forward before you commit to it, rather than finding out after. (`create_provider_version_from_capture` carries forward anything you omit automatically, but knowing what that IS before publishing beats being surprised after.)
176
+ - **To rename, redescribe, retag, flip `providerType`, or toggle `isActive` on a provider you own — WITHOUT creating a new version** — use `update_provider_metadata(providerId, {...})` instead. It's a genuine partial update on the provider itself; don't route a pure metadata edit through `create_provider_version_from_capture`.
177
+ - **To look up any published provider (not just ones you own), or to see exactly what the live verification client would fetch** — use `get_provider_info(providerId, versionNumber?)`. It reads the public production SDK backend directly (no auth needed), separate from everything else in this mode.
170
178
  - There's no separate "create-then-version" step and no org / public-review workflow here.
171
179
  - Consumer-supplied params (step 10) keep the `{{context_<name>}}` form in this mode; there is no `requiredContext` declaration on the legacy backend.
172
180
 
@@ -216,6 +224,6 @@ If `replay_request` or `run_proof` errors with `unknown secretParamsRef`:
216
224
  - **The ref is bound to the MCP server process.** Anything that restarts the server (Claude Code restart, `reset_session`, machine reboot) invalidates every previously-issued ref. The fix is always: re-run `start_capture` → interact → `propose_provider` to mint a fresh ref. Don't ask the dev to "re-enter the value" — walk them back through capture.
217
225
  - **Exception:** if the drafted provider has empty `secretRefs: []` (no secret headers were captured), you can call `replay_request` and `run_proof` without `secretParamsRef` at all — both tools accept it as optional in that case.
218
226
 
219
- ## Out of scope (v0)
227
+ ## Recognizing a HAWKEYE / live-interception provider
220
228
 
221
- If the dev needs a provider for a flow that requires multiple sequential requests (CSRF preflight, refresh-token chains), tell them: "multi-request providers are v1. For now, we can produce a single-request provider for the target value — let's see if one exists in this capture."
229
+ Everything above builds the STANDARD flow: capture → draft → replay (the backend independently re-sends the request) → prove. There is a second, separate flow — `injectionType: HAWKEYE` (or `MSWJS`/`XHOOK`/`CDP`) plus a `customInjection` script that calls `window.reclaimInterceptor.addResponseMiddleware(...)` to observe the page's REAL responses as they happen, instead of the backend replaying anything. You will not author this flow with the current tools — `create_provider_version_from_capture` only builds the standard one. But you WILL see the fields when reading an existing provider (`get_provider_version_config`, `get_provider_info`): don't assume `injectionType` present-but-not-`NONE` means something is broken — the old backend's own schema DEFAULTS `injectionType` to `HAWKEYE` for every provider, standard-flow ones included; it's inert with no interceptor script installed. `disableRequestReplay` is a NARROWER, separate toggle — it does not gate interception in general (that fires regardless, for every `injectionType` including `CDP`); it only controls whether the current page's own document/navigation request specifically gets pulled through a non-CDP interceptor, and exists as a site-compatibility escape hatch, not an architecture choice. See `injection-api.md` (embedded in the `customInjection` field description) for the full, source-verified breakdown of `injectionType`/`disableRequestReplay`/`bodySniff` before guessing at any of these three.
@@ -2,7 +2,7 @@ This server targets the Reclaim BUILDER backend. It exposes three tool layers: g
2
2
 
3
3
  **Authentication:** try `get_me` first. On 401, `authenticate({})` opens a device-pairing session — give the user the `attachUrl`, then poll `authenticate({ code })` until LINKED. Tokens are cached ~2 days.
4
4
 
5
- **Provider-authoring flow:** `attach_chrome` → `start_capture` → drive the page (`navigate`, `wait_for_page`) → inspect (`list_requests`, `get_request`, `find_requests_containing`) → `propose_provider` → `replay_request` → `run_proof` → resolve a `providerId` (**Reuse before create**, below) → `create_provider_version_from_capture` (lands on the draft branch) → `publish_provider_version` (PRIVATE) or `submit_provider_version_for_review` (PUBLIC).
5
+ **Provider-authoring flow:** `attach_chrome` → `start_capture(tabId)` ⇒ a **`captureId`** (the session handle every browser/inspect tool takes as its first arg) → drive the page (`navigate`, `wait_for_page`, `eval_in_page`, `get_cookies`) → inspect (`list_requests`, `get_request`, `find_requests_containing`) → `propose_provider` ⇒ a **`draftId`** (the recipe + captured secrets held server-side; secrets never enter the transcript) → `replay_request(captureId, draftId)` → `run_proof(captureId, draftId, ownerAddress)` → resolve a `providerId` (**Reuse before create**, below) → `create_provider_version_from_capture` (lands on the draft branch) → `publish_provider_version` (PRIVATE) or `submit_provider_version_for_review` (PUBLIC).
6
6
 
7
7
  **Reuse before create (avoid duplicates):** `create_provider_version_from_capture` requires a `providerId`. Before minting a new one with `create_provider`, search your existing providers with `providers` (by domain / data point) — a site you've already built will be there. If one matches, pass its `providerId` so the capture lands as a NEW VERSION on that provider. Only `create_provider` (then use its returned id) when none exists. Re-running the flow against `create_provider` each time is what stacks duplicate providers in "My providers".
8
8
 
@@ -40,3 +40,120 @@ than guessing.
40
40
  > provider's `initialUrl`), and old-devtools runs `customInjection` the same
41
41
  > way. (`window.Reclaim` itself is still only forwarded/available in the cases
42
42
  > noted above.)
43
+
44
+ ## Interception mode: `injectionType` / `interceptorType`
45
+
46
+ Separate from the script's CONTENT is HOW the client captures network traffic
47
+ for it to consume — a per-version field alongside `customInjection`/
48
+ `jsUserScripts`, not something you write into the script itself. Whichever
49
+ mechanism is selected, the injected script consumes what it caught through
50
+ the SAME API: `window.reclaimInterceptor.addResponseMiddleware(middleware,
51
+ middlewareName?)`. That consumption API fires for any intercepted request
52
+ under EVERY mechanism below, including `CDP` — it is not a HAWKEYE-specific
53
+ thing.
54
+
55
+ - **old-devtools** field: `injectionType`, one of `NONE | MSWJS | XHOOK | CDP |
56
+ HAWKEYE`. Verified from the backend's Prisma schema: **the column default is
57
+ `HAWKEYE`**, not `NONE` — every provider `create_provider_version_from_capture`
58
+ registers gets `injectionType: HAWKEYE` implicitly, since neither this tool
59
+ nor the old backend's register/config controllers ever set it explicitly.
60
+ This is harmless for the normal capture→replay→prove flow: with no
61
+ interceptor script installed, there's nothing calling
62
+ `addResponseMiddleware`, so the standard flow behaves identically
63
+ regardless of this field's value.
64
+ - **builder** field: `webSettings.inapp.interceptorOptions.interceptorType`,
65
+ one of `HAWKEYE | MSWJS | CDP` (no `NONE`/`XHOOK` in this schema).
66
+ `create_provider_version_from_capture` (builder mode) never sets `inapp` at
67
+ all, so this is a no-op for the standard flow there too.
68
+
69
+ **The mechanisms, and why you'd pick one over plain replay:**
70
+ - **`CDP`** — interception at the browser/DevTools-protocol level, below the
71
+ page's own JS. Works uniformly for every request the page makes, INCLUDING
72
+ its own document/navigation load — no caveats, "works best."
73
+ - **`HAWKEYE` / `MSWJS` / `XHOOK`** — interception done IN the page's JS
74
+ context (different underlying libraries; same idea). These see the page's
75
+ ordinary sub-resource/API requests fine, but the page's OWN document
76
+ request is a special case — see `disableRequestReplay` below.
77
+ - Reach for ANY of the above (over the plain replay flow, `injectionType:
78
+ NONE` / no `inapp`) only when the data genuinely can't be reproduced by an
79
+ independent replay — e.g. it depends on one-time server-side state (a
80
+ nonce, a websocket push) that asking again wouldn't give you back. For
81
+ everything else, the standard flow is simpler and is what `propose_provider`
82
+ / `run_proof` are built for.
83
+
84
+ ## `disableRequestReplay` / `isDocumentRequestReplayEnabled`
85
+
86
+ **This does NOT gate `addResponseMiddleware` in general** — that fires for
87
+ any intercepted request regardless of this field. What it actually controls:
88
+ whether the CURRENT PAGE'S OWN document/navigation request (the request that
89
+ loads the page's HTML — not a sub-resource or API call) is sent for real
90
+ over the network at all, for the non-CDP mechanisms above. And it only
91
+ matters if the request you're proving *is* that document request — e.g. the
92
+ target value is embedded straight in the page's own HTML rather than
93
+ fetched via a separate API call.
94
+
95
+ - `false` (old-devtools' schema default) — the document request IS sent for
96
+ real (for `HAWKEYE`/`MSWJS`/`XHOOK`), so `addResponseMiddleware` sees it
97
+ like any other intercepted request. This is what you need if you're
98
+ proving data straight out of the current page's own HTML.
99
+ - `true` — the document request is constructed/synthesized locally instead
100
+ of actually being sent, so the interceptor never observes it (every OTHER
101
+ request the page makes is still intercepted normally). This exists as a
102
+ **compatibility escape hatch**: some websites misbehave when their own
103
+ top-level navigation request gets pulled through a JS-level interceptor —
104
+ flip this to `true` for those sites. It's not a privacy or architecture
105
+ choice, it's a per-site workaround.
106
+ - **`CDP` is exempt from all of this** — it intercepts at the browser level,
107
+ so the document request is never a special case for it.
108
+ `disableRequestReplay`/`isDocumentRequestReplayEnabled` is simply inert
109
+ when `injectionType`/`interceptorType` is `CDP`.
110
+
111
+ Field names, per backend:
112
+ - **old-devtools**: `disableRequestReplay` (boolean, schema default `false`).
113
+ The dashboard's own field hint (verbatim): *"When enabled (default), the
114
+ main document HTTP request is actually sent to get the response. When
115
+ disabled, the expected request and response are constructed locally
116
+ without making the network call."* Note the double negative: the
117
+ dashboard's checkbox is labeled "Enable Document Request Replay" and is
118
+ CHECKED when `disableRequestReplay` is `false` — the checkbox and the field
119
+ it writes are inverted from each other.
120
+ - **builder**: `webSettings.inapp.interceptorOptions.isDocumentRequestReplayEnabled`
121
+ (boolean, positive naming this time — `true` means the document request IS
122
+ sent). Per its openapi description, the schema's own stated default is
123
+ `false`. Don't assume the two backends' defaults line up just because the
124
+ concept is the same — old effectively defaults to "sent", the builder
125
+ schema's stated default is "not sent"; verify against whichever backend
126
+ you're publishing to rather than carrying an assumption across.
127
+
128
+ If you're not authoring a non-CDP interceptor script that specifically needs
129
+ to observe the current page's own document request, leave this alone (the
130
+ default already matches the standard flow on old-devtools; on builder, this
131
+ field isn't touched by `create_provider_version_from_capture` at all).
132
+
133
+ ## `bodySniff` — unrelated to the above
134
+
135
+ A per-request field (sits next to `url`/`method` on each `requestData` entry,
136
+ not a version-level setting): `{ enabled: boolean, template: string }`. Per
137
+ the InApp SDK's own type comment, it's "the template used for matching and
138
+ extracting params from `requestBody`" — the REQUEST-body counterpart of
139
+ `responseMatches`/`responseRedactions`, which only ever look at the response.
140
+ It applies regardless of `injectionType`/`interceptorType`. Both publish
141
+ tools in this package already set it for you from `provider.body` — you
142
+ should not need to touch it directly.
143
+
144
+ ## Recognizing this on a provider you didn't author
145
+
146
+ `get_provider_version_config` (old-devtools) and `get_provider_info` (public
147
+ SDK backend) both return the raw `providerConfig`. If you see
148
+ `injectionType`/`interceptorType` set to anything other than the inert
149
+ default AND a `customInjection`/`jsUserScripts` that calls
150
+ `reclaimInterceptor.addResponseMiddleware` (or references `MSWJS`/`XHook`),
151
+ that provider is on the live-interception path, not the standard replay
152
+ flow — read the script before assuming you can safely carry it forward or
153
+ edit around it. If, on top of that, the request being proven IS the login/
154
+ landing page itself (its `url` matches `loginUrl`, i.e. it's the document
155
+ request, not a separate API call) and `injectionType`/`interceptorType`
156
+ isn't `CDP`, also check `disableRequestReplay`/
157
+ `isDocumentRequestReplayEnabled` — `true` there means that request was
158
+ deliberately routed around live interception for site-compatibility
159
+ reasons, not an oversight.