@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
@@ -1,8 +1,46 @@
1
1
  import { Boom, isBoom } from '@hapi/boom';
2
2
  import { defineTool } from "../../mcp/server.js";
3
+ import { INJECTION_GUIDE } from "../../provider/injection-doc.js";
3
4
  import { countsFor, matchRedactionWarnings, } from "../../provider/match-redaction-warning.js";
4
- import { bumpPatchString, extractVersions, providerToConfigBody, providerToRegisterBody, } from "../to-register.js";
5
+ import { bumpPatchString, extractVersions, injectedRequestDataFrom, providerToConfigBody, providerToRegisterBody, } from "../to-register.js";
5
6
  const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture';
7
+ /** Fields with no partial-patch equivalent on this backend: a version always
8
+ * replaces the WHOLE `providerConfig`, so omitting one of these on
9
+ * add-version must carry forward the current value rather than publish it
10
+ * blank. `allowedInjectedRequestData` is handled alongside these (see
11
+ * `resolveInjectedRequests`) since it needs translation when freshly
12
+ * authored, unlike these — which pass through unchanged either way. */
13
+ const PRESERVABLE_KEYS = [
14
+ 'customInjection',
15
+ 'userAgent',
16
+ 'pageTitle',
17
+ 'stepsToFollow',
18
+ 'useIncognitoWebview',
19
+ 'extensionConfig',
20
+ ];
21
+ /** Pull `providerConfig` out of a `get_provider_version_config` /
22
+ * `getProviderConfig` response, tolerant of a missing/malformed shape. */
23
+ function extractProviderConfig(resp) {
24
+ if (resp && typeof resp === 'object') {
25
+ const cfg = resp.providerConfig;
26
+ if (cfg && typeof cfg === 'object') {
27
+ return cfg;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ /** Prefer the caller's explicit value; otherwise fall back to `current`'s
33
+ * value (if any) and record `fieldName` as carried forward. */
34
+ function resolveField(explicit, current, fieldName, carried) {
35
+ if (explicit !== undefined) {
36
+ return explicit;
37
+ }
38
+ if (current !== undefined) {
39
+ carried.push(fieldName);
40
+ return current;
41
+ }
42
+ return undefined;
43
+ }
6
44
  /**
7
45
  * The old backend lets you add a version only to a provider YOU own. A
8
46
  * `providerId` for a public provider (or anyone else's) comes back as
@@ -60,6 +98,23 @@ export function createVersionFromCaptureTool(client) {
60
98
  'must come from get_me_providers. To publish your own version of a ' +
61
99
  "PUBLIC provider you don't own, omit `providerId` to register your " +
62
100
  'own copy (you cannot edit someone else\'s provider here). \n\n' +
101
+ 'For data gated behind login/navigation, pass `customInjection` — a ' +
102
+ 'page-injection script (old-devtools\' name for the builder\'s ' +
103
+ 'jsUserScripts) that drives the page to where the request fires. \n\n' +
104
+ 'This backend has NO partial-patch endpoint — adding a version ' +
105
+ 'always replaces the WHOLE providerConfig. When `providerId` is ' +
106
+ 'set, any of `customInjection`, `userAgent`, `pageTitle`, ' +
107
+ '`allowedInjectedRequestData`, `stepsToFollow`, ' +
108
+ '`useIncognitoWebview`, `extensionConfig` that you OMIT is carried ' +
109
+ 'forward unchanged from the currently-published version (so ' +
110
+ 're-publishing requestData never wipes a working login script or ' +
111
+ 'other config someone set via the dashboard). Pass an explicit ' +
112
+ 'value (`""`, `[]`, `false`, ...) to intentionally clear one of ' +
113
+ 'them instead, or call get_provider_version_config first to see ' +
114
+ 'exactly what is live today before deciding what to change. ' +
115
+ '`allowedInjectedRequestData`/`useIncognitoWebview`/' +
116
+ '`extensionConfig` only take effect on add-version — the register ' +
117
+ 'endpoint (no `providerId`) ignores them. \n\n' +
63
118
  'Captured requests must be GET/POST. OPRF redactions are supported.',
64
119
  inputSchema: {
65
120
  type: 'object',
@@ -132,6 +187,71 @@ export function createVersionFromCaptureTool(client) {
132
187
  description: 'Change note for the new version (update only; must be ≥10 ' +
133
188
  'chars or a default is used).',
134
189
  },
190
+ customInjection: {
191
+ type: 'string',
192
+ description: 'Plain JavaScript injected into the provider page before every ' +
193
+ 'page load — old-devtools\' name for what the builder calls ' +
194
+ '`webSettings.jsUserScripts`. Use standard browser APIs ' +
195
+ '(window.location, element.click(), forms) to drive login/' +
196
+ 'navigation so the target request fires — window.Reclaim is ' +
197
+ 'only for rare edge cases, not navigation. The backend rejects ' +
198
+ 'a few deprecated injection APIs (e.g. window.payloadData, ' +
199
+ 'window.ReclaimInjected) with a 400. Guide:\n\n' +
200
+ INJECTION_GUIDE,
201
+ },
202
+ userAgent: {
203
+ type: 'object',
204
+ description: 'Per-platform user-agent override for the verification ' +
205
+ 'webview. Accepted on register too.',
206
+ properties: {
207
+ ios: { type: 'string' },
208
+ android: { type: 'string' },
209
+ },
210
+ },
211
+ pageTitle: {
212
+ type: 'string',
213
+ description: 'Page title metadata. Accepted on register too.',
214
+ },
215
+ allowedInjectedRequestData: {
216
+ type: 'array',
217
+ description: 'Extra requests the customInjection script is allowed to ' +
218
+ 'fire (e.g. pagination) — add-version ONLY, the register ' +
219
+ 'endpoint ignores this. Each item has the same shape as ' +
220
+ '`provider`, plus `multiple`/`required` (default true).',
221
+ items: {
222
+ type: 'object',
223
+ properties: {
224
+ url: { type: 'string' },
225
+ method: { type: 'string' },
226
+ body: { type: 'string' },
227
+ responseMatches: { type: 'array' },
228
+ responseRedactions: { type: 'array' },
229
+ multiple: {
230
+ type: 'boolean',
231
+ description: 'Allow matching many requests. Default true.',
232
+ },
233
+ required: {
234
+ type: 'boolean',
235
+ description: 'Fail validation if none match. Default true.',
236
+ },
237
+ },
238
+ required: ['url', 'method'],
239
+ },
240
+ },
241
+ stepsToFollow: {
242
+ type: 'string',
243
+ description: 'Free-text setup/manual-verification steps shown to the ' +
244
+ 'dev/verifier. Add-version ONLY.',
245
+ },
246
+ useIncognitoWebview: {
247
+ type: 'boolean',
248
+ description: 'Run the verification webview in incognito mode. ' +
249
+ 'Add-version ONLY.',
250
+ },
251
+ extensionConfig: {
252
+ description: 'Free-form JSON passed through to the client. Add-version ' +
253
+ 'ONLY.',
254
+ },
135
255
  },
136
256
  required: ['initialUrl'],
137
257
  },
@@ -158,6 +278,9 @@ export function createVersionFromCaptureTool(client) {
158
278
  if (args.providerType !== undefined) {
159
279
  opts.providerType = args.providerType;
160
280
  }
281
+ if (args.customInjection !== undefined) {
282
+ opts.customInjection = args.customInjection;
283
+ }
161
284
  // Non-blocking advisory per request — never alters or blocks the
162
285
  // publish; the author keeps full control. Attaches under `_notes`.
163
286
  const notes = matchRedactionWarnings(providers.map((p, i) => countsFor(`Request ${i + 1}`, p)));
@@ -167,8 +290,36 @@ export function createVersionFromCaptureTool(client) {
167
290
  }
168
291
  return result;
169
292
  };
170
- // New provider.
293
+ // New provider — no prior version to carry anything forward from.
294
+ // `allowedInjectedRequestData` / `useIncognitoWebview` /
295
+ // `extensionConfig` are accepted on the schema but the register
296
+ // controller never reads them from the body — warn rather than
297
+ // silently drop, so a dev doesn't think they took effect.
171
298
  if (!args.providerId) {
299
+ if (opts.customInjection?.trim()) {
300
+ notes.push('customInjection will run in the verification client before ' +
301
+ 'every page load (before navigating to initialUrl), matching ' +
302
+ 'the builder\'s jsUserScripts.');
303
+ }
304
+ const REGISTER_IGNORED_KEYS = [
305
+ 'allowedInjectedRequestData',
306
+ 'useIncognitoWebview',
307
+ 'extensionConfig',
308
+ ];
309
+ const ignoredOnRegister = REGISTER_IGNORED_KEYS
310
+ .filter((k) => args[k] !== undefined);
311
+ if (ignoredOnRegister.length) {
312
+ notes.push(`${ignoredOnRegister.join(', ')} ${ignoredOnRegister.length > 1 ? 'are' : 'is'} ignored on register (no providerId) — the old backend only ` +
313
+ 'reads them when adding a version. Publish once to create ' +
314
+ 'the provider, then call this again WITH the returned ' +
315
+ 'providerId to set them.');
316
+ }
317
+ if (args.userAgent !== undefined) {
318
+ opts.userAgent = args.userAgent;
319
+ }
320
+ if (args.pageTitle !== undefined) {
321
+ opts.pageTitle = args.pageTitle;
322
+ }
172
323
  const body = providerToRegisterBody(providers, opts);
173
324
  return withNotes(await client.registerProvider(body));
174
325
  }
@@ -178,6 +329,42 @@ export function createVersionFromCaptureTool(client) {
178
329
  // workaround instead of a bare permission error.
179
330
  const providerId = args.providerId;
180
331
  try {
332
+ // This backend has no partial-patch endpoint — a version always
333
+ // replaces the WHOLE providerConfig. Anything the caller didn't
334
+ // pass at all is carried forward from the currently-published
335
+ // version instead of going out blank. Pass an explicit empty
336
+ // value ("", [], false) to intentionally clear one.
337
+ const needsCurrent = PRESERVABLE_KEYS.some((k) => args[k] === undefined)
338
+ || args.allowedInjectedRequestData === undefined;
339
+ const current = needsCurrent
340
+ ? extractProviderConfig(await client.getProviderConfig(providerId))
341
+ : undefined;
342
+ const carried = [];
343
+ for (const key of PRESERVABLE_KEYS) {
344
+ const resolved = resolveField(args[key], current?.[key], key, carried);
345
+ if (resolved !== undefined) {
346
+ opts[key] = resolved;
347
+ }
348
+ }
349
+ const freshInjected = args.allowedInjectedRequestData !== undefined
350
+ ? injectedRequestDataFrom(args.allowedInjectedRequestData)
351
+ : undefined;
352
+ const resolvedInjected = resolveField(freshInjected, current?.allowedInjectedRequestData, 'allowedInjectedRequestData', carried);
353
+ if (resolvedInjected !== undefined) {
354
+ opts.allowedInjectedRequestData = resolvedInjected;
355
+ }
356
+ if (carried.length) {
357
+ notes.push('Not passed, carried forward unchanged from the current ' +
358
+ `published version: ${carried.join(', ')}. Pass an explicit ` +
359
+ 'value ("", [], false, ...) to override any of them, or ' +
360
+ 'call get_provider_version_config first to see what is ' +
361
+ 'live today.');
362
+ }
363
+ if (args.customInjection !== undefined && opts.customInjection?.trim()) {
364
+ notes.push('customInjection will run in the verification client before ' +
365
+ 'every page load (before navigating to initialUrl), matching ' +
366
+ 'the builder\'s jsUserScripts.');
367
+ }
181
368
  const versionsResp = await client.listVersions(providerId);
182
369
  const version = bumpPatchString(extractVersions(versionsResp));
183
370
  const versionInfo = args.notes && args.notes.trim().length >= 10
@@ -0,0 +1,16 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ import type { ReclaimOldClient } from '../client.ts';
3
+ /**
4
+ * Edit provider-level metadata on the old devtools backend
5
+ * (`POST /api/providers/:providerId`) — a genuine PARTIAL update on the
6
+ * PROVIDER document itself, distinct from both
7
+ * `create_provider_version_from_capture` paths: it does NOT touch
8
+ * `providerConfig`/versions at all, and only the fields you pass here
9
+ * change — everything else on the provider is left untouched (no need to
10
+ * re-supply `requestData`/`customInjection`/etc., and
11
+ * no new version is created).
12
+ *
13
+ * The backend 403s if you don't own the provider (and aren't an admin) —
14
+ * surfaced verbatim, same as any other auth failure from this client.
15
+ */
16
+ export declare function updateProviderMetadataTool(client: ReclaimOldClient): RegisteredTool;
@@ -0,0 +1,61 @@
1
+ import { defineTool } from "../../mcp/server.js";
2
+ /**
3
+ * Edit provider-level metadata on the old devtools backend
4
+ * (`POST /api/providers/:providerId`) — a genuine PARTIAL update on the
5
+ * PROVIDER document itself, distinct from both
6
+ * `create_provider_version_from_capture` paths: it does NOT touch
7
+ * `providerConfig`/versions at all, and only the fields you pass here
8
+ * change — everything else on the provider is left untouched (no need to
9
+ * re-supply `requestData`/`customInjection`/etc., and
10
+ * no new version is created).
11
+ *
12
+ * The backend 403s if you don't own the provider (and aren't an admin) —
13
+ * surfaced verbatim, same as any other auth failure from this client.
14
+ */
15
+ export function updateProviderMetadataTool(client) {
16
+ return defineTool({
17
+ name: 'update_provider_metadata',
18
+ description: 'Rename, redescribe, retag, flip providerType (PRIVATE/PUBLIC), or ' +
19
+ 'toggle isActive on a provider YOU own — WITHOUT creating a new ' +
20
+ 'version. Only the fields you pass change; everything else ' +
21
+ '(requestData, customInjection, versions, ...) is untouched. Pass ' +
22
+ 'at least one of the optional fields. Get a `providerId` from ' +
23
+ 'get_me_providers.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ providerId: {
28
+ type: 'string',
29
+ description: 'UUID of the provider to update.',
30
+ },
31
+ name: { type: 'string', description: 'New provider name.' },
32
+ description: {
33
+ type: 'string',
34
+ description: 'New provider description.',
35
+ },
36
+ providerType: {
37
+ type: 'string',
38
+ enum: ['PRIVATE', 'PUBLIC'],
39
+ description: 'Change PRIVATE/PUBLIC visibility.',
40
+ },
41
+ tags: {
42
+ type: 'array',
43
+ items: { type: 'string' },
44
+ description: 'Replace the provider\'s tag list.',
45
+ },
46
+ isActive: {
47
+ type: 'boolean',
48
+ description: 'Whether the provider is active.',
49
+ },
50
+ },
51
+ required: ['providerId'],
52
+ },
53
+ }, async ({ providerId, ...body }) => {
54
+ const hasAnyField = Object.values(body).some((v) => v !== undefined);
55
+ if (!hasAnyField) {
56
+ throw new Error('Pass at least one of: name, description, providerType, tags, ' +
57
+ 'isActive.');
58
+ }
59
+ return client.updateProviderMetadata(providerId, body);
60
+ });
61
+ }
@@ -1,3 +1,4 @@
1
+ import './patch-domhandler.ts';
1
2
  import type { ReclaimProvider } from '../provider/schema.ts';
2
3
  export interface AttestorLogEntry {
3
4
  level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
@@ -26,9 +27,11 @@ export interface RunAttestorProofOptions {
26
27
  /** Sign with this key directly (e.g. the app/VC's ephemeral per-session key)
27
28
  * instead of resolving from env/.env/file. */
28
29
  ownerPrivateKey?: string;
29
- /** ZK engine for `createClaimOnAttestor`. Defaults to `'gnark'`; the app's
30
- * server-side verification runner uses `'snarkjs'`. */
31
- zkEngine?: 'gnark' | 'snarkjs';
30
+ /** ZK engine for `createClaimOnAttestor`. Defaults to `'stwo'` — a pure-WASM
31
+ * prover that runs everywhere (no native shared library, so it works on
32
+ * Windows where `'gnark'` fails to load its `.dll`). Server-side paths that
33
+ * want gnark's speed pin it explicitly (e.g. the verify runner). */
34
+ zkEngine?: 'gnark' | 'snarkjs' | 'stwo';
32
35
  /** Extract the structured {@link AttestorClaim} (claim + attestor signature)
33
36
  * onto the result for nested-proof construction. */
34
37
  includeAttestorClaim?: boolean;
@@ -7,8 +7,13 @@
7
7
  // reveals), never on responseMatches.
8
8
  import { bytesToHex } from '@noble/hashes/utils.js';
9
9
  import { createClaimOnAttestor, } from '@reclaimprotocol/attestor-core';
10
+ // Side-effect: make external domhandler nodes DOM-like so attestor-core's
11
+ // bundled `xpath` accepts them (see patch-domhandler.ts). Must load before any
12
+ // proof runs its response-redaction extraction.
13
+ import "./patch-domhandler.js";
10
14
  import { agentTraceEnabled } from "../consts.js";
11
15
  import { resolveEthKey } from "../mcp/tools/authenticate/eth-key.js";
16
+ import { installResponseDump } from "./debug-response-dump.js";
12
17
  import { buildHttpParams, ensureAttestorCrypto, parseClaim, resolveAttestorUrl, } from "./proof-core.js";
13
18
  import { ensureZkFiles } from "./zk-files.js";
14
19
  /** Trim a captured log stream to the last N entries — for shrinking the
@@ -140,6 +145,10 @@ export async function runAttestorProof(provider, secrets, opts = {}) {
140
145
  const traceEnabled = agentTraceEnabled();
141
146
  const capture = traceEnabled ? makeCaptureLogger() : undefined;
142
147
  const attestorLogs = capture?.entries;
148
+ // Debug (RECLAIM_AGENT_DUMP_RESPONSE): dump the exact response the
149
+ // attestor validates against, so "Response does not contain X" is
150
+ // diagnosable against the attestor's real server-side fetch.
151
+ installResponseDump();
143
152
  let claim;
144
153
  try {
145
154
  claim = await createClaimOnAttestor({
@@ -150,7 +159,7 @@ export async function runAttestorProof(provider, secrets, opts = {}) {
150
159
  },
151
160
  ownerPrivateKey,
152
161
  client: { url: resolveAttestorUrl(opts.attestorUrl) },
153
- zkEngine: opts.zkEngine ?? 'gnark',
162
+ zkEngine: opts.zkEngine ?? 'stwo',
154
163
  ...(capture !== undefined && { logger: capture.logger }),
155
164
  });
156
165
  }
@@ -0,0 +1,4 @@
1
+ /** Wrap attestor-core's http `getResponseRedactions` to dump the response it
2
+ * runs against. Idempotent; no-op unless RECLAIM_AGENT_DUMP_RESPONSE is
3
+ * set. */
4
+ export declare function installResponseDump(): void;
@@ -0,0 +1,55 @@
1
+ // Debug helper (gated by RECLAIM_AGENT_DUMP_RESPONSE): capture the EXACT
2
+ // response bytes the attestor validates a claim against, plus a per-match
3
+ // presence check. A "Response does not contain X" failure means the attestor's
4
+ // server-side re-fetch differs from what the browser (replay) saw — e.g. GitHub
5
+ // embeds nav/userMenu data for browser requests but not for the attestor's raw
6
+ // fetch. Seeing the attestor's actual response is the only way to tell what it
7
+ // really returned.
8
+ //
9
+ // On each getResponseRedactions call it writes the response to
10
+ // `attestor-last-response.html` in the process CWD and logs the byte count +,
11
+ // for every responseMatch (paramValues substituted), whether it's present.
12
+ import { providers } from '@reclaimprotocol/attestor-core';
13
+ import { writeFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ let installed = false;
16
+ /** Wrap attestor-core's http `getResponseRedactions` to dump the response it
17
+ * runs against. Idempotent; no-op unless RECLAIM_AGENT_DUMP_RESPONSE is
18
+ * set. */
19
+ export function installResponseDump() {
20
+ if (installed || !process.env['RECLAIM_AGENT_DUMP_RESPONSE']) {
21
+ return;
22
+ }
23
+ const http = providers['http'];
24
+ const original = http.getResponseRedactions;
25
+ if (typeof original !== 'function') {
26
+ return;
27
+ }
28
+ installed = true;
29
+ http.getResponseRedactions = function (arg) {
30
+ try {
31
+ const bytes = Buffer.from(arg.response);
32
+ const text = bytes.toString('utf8');
33
+ const paramValues = arg.params.paramValues ?? {};
34
+ const matches = (arg.params.responseMatches ?? []).map((m) => {
35
+ let value = m.value ?? '';
36
+ for (const [key, val] of Object.entries(paramValues)) {
37
+ value = value.split(`{{${key}}}`).join(val);
38
+ }
39
+ return { value, present: text.includes(value) };
40
+ });
41
+ const path = join(process.cwd(), 'attestor-last-response.html');
42
+ writeFileSync(path, text);
43
+ process.stderr.write(`${JSON.stringify({
44
+ tag: 'attestor-response-dump',
45
+ bytes: bytes.length,
46
+ path,
47
+ matches,
48
+ })}\n`);
49
+ }
50
+ catch {
51
+ // diagnostic only — never break proving
52
+ }
53
+ return original.call(this, arg);
54
+ };
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ // Side-effect module: make `domhandler` nodes DOM-like for the `xpath` engine.
2
+ //
3
+ // WHY THIS EXISTS
4
+ // `@reclaimprotocol/attestor-core`'s published `lib` bundles its own inlined
5
+ // copy of `domhandler` and applies a DOM-compat patch (nodeName / localName +
6
+ // a NamedNodeMap `item()` shim) to THAT copy. But at proof time the HTML tree
7
+ // is built by the *external* `parse5-htmlparser2-tree-adapter`, which uses the
8
+ // separately-installed `domhandler` — a different, UNPATCHED copy. The `xpath`
9
+ // engine then fails `isNodeLike()` on every node (it needs a string `nodeName`)
10
+ // and throws "Context node does not appear to be a valid DOM node", so ALL
11
+ // xPath / xPath+jsonPath response redactions fail during `runAttestorProof`'s
12
+ // receipt validation. (Regex-only redactions escape it — they never touch the
13
+ // parse5/xpath path.) Running attestor-core from source works because there is
14
+ // then a single shared `domhandler`; the bug is only in the bundled `lib`.
15
+ //
16
+ // Rather than fork attestor-core, apply the identical patch to the external
17
+ // `domhandler` the adapter actually uses, so its tree nodes are DOM-like. This
18
+ // mirrors attestor-core's own `patch-parse5-tree`. Idempotent and
19
+ // version-agnostic. Import it before any proving path runs.
20
+ //
21
+ // Remove once attestor-core externalizes `domhandler` in its build (one shared
22
+ // copy → its own patch suffices).
23
+ import { Element, Node } from 'domhandler';
24
+ const PATCH_FLAG = Symbol.for('reclaim.domhandler.domCompatPatched');
25
+ const proto = Node.prototype;
26
+ if (!proto[PATCH_FLAG]) {
27
+ // `xpath`'s isNodeLike() requires a string `nodeName`; domhandler nodes carry
28
+ // the tag under `.name`.
29
+ Object.defineProperty(Node.prototype, 'nodeName', {
30
+ configurable: true,
31
+ get() {
32
+ return this.name;
33
+ },
34
+ });
35
+ Object.defineProperty(Node.prototype, 'localName', {
36
+ configurable: true,
37
+ get() {
38
+ return this.name;
39
+ },
40
+ });
41
+ // Attribute-predicate steps (`[@id=…]`, `contains(@class,…)`) walk the
42
+ // NamedNodeMap via `.item(i)`; give each returned attribute node the DOM
43
+ // shape `xpath` expects (nodeType 2 + localName).
44
+ const attrsGetter = Object.getOwnPropertyDescriptor(Element.prototype, 'attributes')?.get;
45
+ if (attrsGetter) {
46
+ Object.defineProperty(Element.prototype, 'attributes', {
47
+ configurable: true,
48
+ get() {
49
+ const attrs = attrsGetter.call(this);
50
+ attrs.item = (i) => {
51
+ const attr = attrs[i];
52
+ return { ...attr, nodeType: 2, localName: attr.name };
53
+ };
54
+ return attrs;
55
+ },
56
+ });
57
+ }
58
+ proto[PATCH_FLAG] = true;
59
+ }
@@ -18,7 +18,7 @@ export declare function ensureAttestorCrypto(): void;
18
18
  * default. Resolved per-call so an env change or per-request override takes
19
19
  * effect without restarting. */
20
20
  export declare function resolveAttestorUrl(override?: string): string;
21
- /** Map a verification's provider recipe straight to the attestor's HTTP
21
+ /** Map a provider's recipe straight to the attestor's HTTP
22
22
  * params — never synthesized from captured traffic. */
23
23
  export declare function buildHttpParams(provider: ReclaimProvider): ProviderParams<'http'>;
24
24
  export interface ParsedClaim {
@@ -21,7 +21,7 @@ export function ensureAttestorCrypto() {
21
21
  export function resolveAttestorUrl(override) {
22
22
  return (override ?? attestorUrlOverride() ?? FALLBACK_ATTESTOR_URL);
23
23
  }
24
- /** Map a verification's provider recipe straight to the attestor's HTTP
24
+ /** Map a provider's recipe straight to the attestor's HTTP
25
25
  * params — never synthesized from captured traffic. */
26
26
  export function buildHttpParams(provider) {
27
27
  // A blank egress is treated as unset: omit it so the local proof uses the
@@ -1,6 +1,7 @@
1
1
  // Lazy download of the @reclaimprotocol/zk-symmetric-crypto circuit resources
2
- // that the snarkjs proof engine needs. These are ~280MB and are NOT fetched in
3
- // a `postinstall` (that blocked the MCP server's first launch past the connect
2
+ // the proof engines need (the downloader fetches every engine — stwo, snarkjs,
3
+ // gnark — as one bundle). These are ~280MB and are NOT fetched in a
4
+ // `postinstall` (that blocked the MCP server's first launch past the connect
4
5
  // timeout — see the install notes). Instead we fetch them on the first proof,
5
6
  // once, so the server starts immediately and only `run_proof` pays the cost.
6
7
  import { spawn } from 'node:child_process';
@@ -38,12 +39,12 @@ async function download() {
38
39
  return;
39
40
  }
40
41
  // mainPath is <pkg>/lib/index.js → resources live at <pkg>/resources, the
41
- // downloader at <pkg>/lib/scripts/download-files.js. We only use the snarkjs
42
- // engine (createClaimOnAttestor zkEngine: 'snarkjs'), so its presence is the
43
- // signal that the circuits are ready.
42
+ // downloader at <pkg>/lib/scripts/download-files.js. Proofs default to the
43
+ // stwo (WASM) engine, so its resource dir being present is the signal that
44
+ // the circuits are ready (the downloader fetches all engines together).
44
45
  const libDir = dirname(mainPath);
45
46
  const pkgDir = dirname(libDir);
46
- if (existsSync(join(pkgDir, 'resources', 'snarkjs'))) {
47
+ if (existsSync(join(pkgDir, 'resources', 'stwo'))) {
47
48
  return;
48
49
  }
49
50
  const downloadScript = join(libDir, 'scripts', 'download-files.js');
@@ -0,0 +1,17 @@
1
+ import '../proof/patch-domhandler.ts';
2
+ export interface DomAnchor {
3
+ xPath: string;
4
+ /** Byte range of the element in the source html (parse5 source locations),
5
+ * matching the slice the attestor reveals for this xPath. */
6
+ start: number;
7
+ end: number;
8
+ }
9
+ /**
10
+ * A verified xPath to the tightest element containing one of `targets`, or
11
+ * undefined if the value isn't present in the parsed tree. Prefers a short
12
+ * stable selector (unique id → identifying attr → class), falling back to an
13
+ * absolute child-indexed path (`/html[1]/body[1]/…/option[1]`). Every candidate
14
+ * is checked with the real `xpath` engine and only returned if it resolves,
15
+ * first-match, to that exact element.
16
+ */
17
+ export declare function elementAnchor(html: string, targets: string[]): DomAnchor | undefined;