@reclaimprotocol/agent 0.1.35 → 0.1.37

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 (106) hide show
  1. package/README.md +8 -8
  2. package/lib/authoring/backend.d.ts +12 -1
  3. package/lib/authoring/backend.js +3 -0
  4. package/lib/authoring/tools.js +33 -5
  5. package/lib/authoring/util.d.ts +43 -0
  6. package/lib/authoring/util.js +54 -0
  7. package/lib/cdp/chrome-for-testing.d.ts +14 -0
  8. package/lib/cdp/chrome-for-testing.js +43 -0
  9. package/lib/cdp/geo.d.ts +3 -0
  10. package/lib/cdp/geo.js +27 -15
  11. package/lib/cdp/launcher.d.ts +79 -1
  12. package/lib/cdp/launcher.js +244 -24
  13. package/lib/logger.js +3 -1
  14. package/lib/mcp/api/operations.gen.js +1 -1
  15. package/lib/mcp/instructions.js +3 -3
  16. package/lib/mcp/start.js +17 -2
  17. package/lib/mcp/tools/agent/attach.d.ts +6 -0
  18. package/lib/mcp/tools/agent/attach.js +58 -118
  19. package/lib/mcp/tools/agent/build.js +6 -0
  20. package/lib/mcp/tools/agent/capture.js +33 -10
  21. package/lib/mcp/tools/agent/check-attestor.js +15 -7
  22. package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
  23. package/lib/mcp/tools/agent/draft-tools.js +170 -0
  24. package/lib/mcp/tools/agent/pod-proof.js +5 -1
  25. package/lib/mcp/tools/agent/proof.d.ts +28 -0
  26. package/lib/mcp/tools/agent/proof.js +72 -0
  27. package/lib/mcp/tools/agent/shared.d.ts +8 -1
  28. package/lib/mcp/tools/agent/shared.js +98 -19
  29. package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
  30. package/lib/mcp/tools/agent/start-integration.js +10 -6
  31. package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
  32. package/lib/mcp/tools/authenticate/authenticate.js +212 -116
  33. package/lib/mcp/tools/authenticate/build.d.ts +2 -2
  34. package/lib/mcp/tools/authenticate/build.js +6 -3
  35. package/lib/mcp/tools/authenticate/builder-access.js +5 -2
  36. package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
  37. package/lib/mcp/tools/authenticate/eth-key.js +29 -5
  38. package/lib/mcp/tools/authenticate/index.d.ts +1 -1
  39. package/lib/mcp/tools/authenticate/index.js +1 -1
  40. package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
  41. package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
  42. package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
  43. package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
  44. package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
  45. package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
  46. package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
  47. package/lib/mcp/tools/authenticate/session-cache.js +95 -4
  48. package/lib/mcp/tools/mode/mode.js +6 -6
  49. package/lib/mcp/tools/verification/proxy-callback-events.d.ts +21 -3
  50. package/lib/mcp/tools/verification/proxy-callback-events.js +128 -45
  51. package/lib/old/login-cdp.js +2 -3
  52. package/lib/proof/attestor.js +2 -3
  53. package/lib/proof/check-attestor.d.ts +12 -2
  54. package/lib/proof/check-attestor.js +40 -10
  55. package/lib/proof/check-matchers.d.ts +36 -0
  56. package/lib/proof/check-matchers.js +161 -0
  57. package/lib/proof/zk-files.d.ts +50 -11
  58. package/lib/proof/zk-files.js +259 -63
  59. package/lib/provider/draft.d.ts +24 -1
  60. package/lib/provider/draft.js +135 -0
  61. package/lib/provider/matcher.js +171 -4
  62. package/package.json +6 -4
  63. package/skill/SKILL.builder.md +7 -2
  64. package/skill/SKILL.common.md +90 -70
  65. package/skill/SKILL.old.md +3 -3
  66. package/skill/builder-mode.md +44 -6
  67. package/skill/old-mode.md +18 -13
  68. package/skill/start-integration.md +51 -35
  69. package/skill/verification-client-integration.md +87 -15
  70. package/src/authoring/backend.ts +12 -1
  71. package/src/authoring/tools.ts +48 -7
  72. package/src/authoring/util.ts +64 -0
  73. package/src/cdp/chrome-for-testing.ts +66 -0
  74. package/src/cdp/geo.ts +36 -21
  75. package/src/cdp/launcher.ts +328 -24
  76. package/src/logger.ts +3 -1
  77. package/src/mcp/api/operations.gen.ts +1 -1
  78. package/src/mcp/instructions.ts +3 -3
  79. package/src/mcp/start.ts +20 -1
  80. package/src/mcp/tools/agent/attach.ts +65 -128
  81. package/src/mcp/tools/agent/build.ts +7 -0
  82. package/src/mcp/tools/agent/capture.ts +54 -11
  83. package/src/mcp/tools/agent/check-attestor.ts +15 -7
  84. package/src/mcp/tools/agent/draft-tools.ts +234 -0
  85. package/src/mcp/tools/agent/pod-proof.ts +5 -1
  86. package/src/mcp/tools/agent/proof.ts +110 -1
  87. package/src/mcp/tools/agent/shared.ts +130 -19
  88. package/src/mcp/tools/agent/start-integration.ts +10 -6
  89. package/src/mcp/tools/authenticate/authenticate.ts +276 -131
  90. package/src/mcp/tools/authenticate/build.ts +6 -3
  91. package/src/mcp/tools/authenticate/builder-access.ts +5 -2
  92. package/src/mcp/tools/authenticate/eth-key.ts +41 -7
  93. package/src/mcp/tools/authenticate/index.ts +1 -1
  94. package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
  95. package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
  96. package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
  97. package/src/mcp/tools/authenticate/session-cache.ts +125 -5
  98. package/src/mcp/tools/mode/mode.ts +6 -6
  99. package/src/mcp/tools/verification/proxy-callback-events.ts +151 -41
  100. package/src/old/login-cdp.ts +2 -3
  101. package/src/proof/attestor.ts +2 -3
  102. package/src/proof/check-attestor.ts +55 -12
  103. package/src/proof/check-matchers.ts +227 -0
  104. package/src/proof/zk-files.ts +357 -65
  105. package/src/provider/draft.ts +215 -7
  106. package/src/provider/matcher.ts +237 -4
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _Forwarder_instances, _Forwarder_client, _Forwarder_deps, _Forwarder_abort, _Forwarder_forwarded, _Forwarder_failed, _Forwarder_lastError, _Forwarder_recent, _Forwarder_unacked, _Forwarder_host, _Forwarder_running, _Forwarder_loop, _Forwarder_tick, _Forwarder_forward, _ForwarderRegistry_forwarders;
12
+ var _Forwarder_instances, _Forwarder_client, _Forwarder_deps, _Forwarder_abort, _Forwarder_forwarded, _Forwarder_failed, _Forwarder_lastError, _Forwarder_recent, _Forwarder_unacked, _Forwarder_held, _Forwarder_host, _Forwarder_running, _Forwarder_loop, _Forwarder_tick, _Forwarder_hold, _Forwarder_forward, _ForwarderRegistry_forwarders;
13
13
  import { badRequest } from '@hapi/boom';
14
14
  import assert from 'node:assert';
15
15
  import { setTimeout as delay } from 'node:timers/promises';
@@ -21,15 +21,25 @@ const POLL_MS = 2000;
21
21
  const MAX_BACKOFF_MS = 30000;
22
22
  const FORWARD_TIMEOUT_MS = 10000;
23
23
  const BATCH = 50;
24
+ /**
25
+ * Pages read per poll. A page is `BATCH` events, so a poll can look past up to
26
+ * 500 pending events. That is enough to reach other sessions behind several
27
+ * held ones, and still bounds the work of one poll.
28
+ */
29
+ const MAX_PAGES = 10;
24
30
  const RECENT = 10;
25
31
  /** Loopback addresses, in the order tried. Dev servers often bind only one. */
26
32
  const HOSTS = ['127.0.0.1', '[::1]'];
27
33
  /**
28
34
  * Polls the org's pending pull events and POSTs each envelope to a local
29
35
  * route, like `stripe listen --forward-to`. 2xx responses are acknowledged;
30
- * anything else (redirects included) stays pending and is retried with
31
- * exponential backoff. After a failure the batch stops, so events reach the
32
- * app in order.
36
+ * anything else (redirects included) stays pending.
37
+ *
38
+ * Order holds per session. When an event fails, its session is held: its later
39
+ * events are not sent or acknowledged, and the failed event is retried with
40
+ * exponential backoff (2 s, 4 s, ... 30 s). Other sessions keep flowing at the
41
+ * normal poll rate. Held sessions appear in `status().blocked`, and clear when
42
+ * the app accepts the event.
33
43
  */
34
44
  class Forwarder {
35
45
  constructor(client, listener, target, deps) {
@@ -44,7 +54,11 @@ class Forwarder {
44
54
  /** Delivered to the app but not yet acknowledged; never POSTed twice. */
45
55
  );
46
56
  /** Delivered to the app but not yet acknowledged; never POSTed twice. */
47
- _Forwarder_unacked.set(this, new Set());
57
+ _Forwarder_unacked.set(this, new Set()
58
+ /** Held sessions by session id, with the time of their next attempt. */
59
+ );
60
+ /** Held sessions by session id, with the time of their next attempt. */
61
+ _Forwarder_held.set(this, new Map());
48
62
  _Forwarder_host.set(this, HOSTS[0]);
49
63
  _Forwarder_running.set(this, void 0);
50
64
  __classPrivateFieldSet(this, _Forwarder_client, client, "f");
@@ -62,6 +76,7 @@ class Forwarder {
62
76
  failed: __classPrivateFieldGet(this, _Forwarder_failed, "f"),
63
77
  ...(__classPrivateFieldGet(this, _Forwarder_lastError, "f") ? { lastError: __classPrivateFieldGet(this, _Forwarder_lastError, "f") } : {}),
64
78
  recent: [...__classPrivateFieldGet(this, _Forwarder_recent, "f")],
79
+ blocked: [...__classPrivateFieldGet(this, _Forwarder_held, "f").values()].map((h) => h.blocked),
65
80
  };
66
81
  }
67
82
  async stop() {
@@ -69,16 +84,16 @@ class Forwarder {
69
84
  await __classPrivateFieldGet(this, _Forwarder_running, "f");
70
85
  }
71
86
  }
72
- _Forwarder_client = new WeakMap(), _Forwarder_deps = new WeakMap(), _Forwarder_abort = new WeakMap(), _Forwarder_forwarded = new WeakMap(), _Forwarder_failed = new WeakMap(), _Forwarder_lastError = new WeakMap(), _Forwarder_recent = new WeakMap(), _Forwarder_unacked = new WeakMap(), _Forwarder_host = new WeakMap(), _Forwarder_running = new WeakMap(), _Forwarder_instances = new WeakSet(), _Forwarder_loop = async function _Forwarder_loop() {
87
+ _Forwarder_client = new WeakMap(), _Forwarder_deps = new WeakMap(), _Forwarder_abort = new WeakMap(), _Forwarder_forwarded = new WeakMap(), _Forwarder_failed = new WeakMap(), _Forwarder_lastError = new WeakMap(), _Forwarder_recent = new WeakMap(), _Forwarder_unacked = new WeakMap(), _Forwarder_held = new WeakMap(), _Forwarder_host = new WeakMap(), _Forwarder_running = new WeakMap(), _Forwarder_instances = new WeakSet(), _Forwarder_loop = async function _Forwarder_loop() {
73
88
  const sleep = __classPrivateFieldGet(this, _Forwarder_deps, "f").sleep || sleepFor;
74
89
  const signal = __classPrivateFieldGet(this, _Forwarder_abort, "f").signal;
75
90
  let backoff = 0;
76
91
  while (!signal.aborted) {
77
92
  let wait = POLL_MS;
78
93
  try {
79
- const { ok, count } = await __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_tick).call(this);
80
- backoff = ok ? 0 : nextBackoff(backoff);
81
- wait = ok ? (count >= BATCH ? 0 : POLL_MS) : backoff;
94
+ const more = await __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_tick).call(this);
95
+ backoff = 0;
96
+ wait = more ? 0 : POLL_MS;
82
97
  }
83
98
  catch (err) {
84
99
  __classPrivateFieldSet(this, _Forwarder_lastError, err instanceof Error ? err.message : String(err), "f");
@@ -90,37 +105,96 @@ _Forwarder_client = new WeakMap(), _Forwarder_deps = new WeakMap(), _Forwarder_a
90
105
  await sleep(wait, signal);
91
106
  }
92
107
  }
93
- }, _Forwarder_tick = async function _Forwarder_tick() {
94
- const { data } = await __classPrivateFieldGet(this, _Forwarder_client, "f").call('ListCallbackEvents', {
95
- query: { pending: true, limit: BATCH },
96
- });
97
- const ack = [];
98
- let ok = true;
99
- for (const item of data.items) {
100
- if (__classPrivateFieldGet(this, _Forwarder_abort, "f").signal.aborted) {
101
- break;
102
- }
103
- if (__classPrivateFieldGet(this, _Forwarder_unacked, "f").has(item.id) || await __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_forward).call(this, item)) {
104
- ack.push(item.id);
105
- }
106
- else {
107
- // Stop the batch so later events don't overtake the failed one.
108
- ok = false;
109
- break;
108
+ }, _Forwarder_tick =
109
+ /**
110
+ * Forwards one poll's worth of pending events. Returns whether to poll again
111
+ * at once: the page limit was hit with events still unread, and progress was
112
+ * made (so held sessions alone never cause a busy loop).
113
+ */
114
+ async function _Forwarder_tick() {
115
+ const now = (__classPrivateFieldGet(this, _Forwarder_deps, "f").now || Date.now)();
116
+ // Sessions whose later events are skipped in this poll.
117
+ const skip = new Set();
118
+ const seen = new Set();
119
+ let after;
120
+ let page = 0;
121
+ let full = false;
122
+ let progress = false;
123
+ do {
124
+ const { data } = await __classPrivateFieldGet(this, _Forwarder_client, "f").call('ListCallbackEvents', {
125
+ query: { pending: true, limit: BATCH, ...(after ? { after } : {}) },
126
+ });
127
+ const ack = [];
128
+ for (const item of data.items) {
129
+ if (__classPrivateFieldGet(this, _Forwarder_abort, "f").signal.aborted) {
130
+ break;
131
+ }
132
+ seen.add(item.sessionId);
133
+ const held = __classPrivateFieldGet(this, _Forwarder_held, "f").get(item.sessionId);
134
+ if (skip.has(item.sessionId)
135
+ || (held && held.retryAt > now && !__classPrivateFieldGet(this, _Forwarder_unacked, "f").has(item.id))) {
136
+ skip.add(item.sessionId);
137
+ continue;
138
+ }
139
+ if (__classPrivateFieldGet(this, _Forwarder_unacked, "f").has(item.id)) {
140
+ ack.push(item.id);
141
+ continue;
142
+ }
143
+ const outcome = await __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_forward).call(this, item);
144
+ if (outcome.ok) {
145
+ __classPrivateFieldGet(this, _Forwarder_held, "f").delete(item.sessionId);
146
+ ack.push(item.id);
147
+ }
148
+ else {
149
+ // Hold the session so its later events don't overtake this one.
150
+ __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_hold).call(this, item, outcome, now);
151
+ skip.add(item.sessionId);
152
+ }
110
153
  }
111
- }
112
- if (ack.length) {
113
- for (const id of ack) {
114
- __classPrivateFieldGet(this, _Forwarder_unacked, "f").add(id);
154
+ if (ack.length) {
155
+ progress = true;
156
+ for (const id of ack) {
157
+ __classPrivateFieldGet(this, _Forwarder_unacked, "f").add(id);
158
+ }
159
+ await __classPrivateFieldGet(this, _Forwarder_client, "f").call('AcknowledgeCallbackEvents', {
160
+ body: { ids: ack },
161
+ });
162
+ for (const id of ack) {
163
+ __classPrivateFieldGet(this, _Forwarder_unacked, "f").delete(id);
164
+ }
115
165
  }
116
- await __classPrivateFieldGet(this, _Forwarder_client, "f").call('AcknowledgeCallbackEvents', {
117
- body: { ids: ack },
118
- });
119
- for (const id of ack) {
120
- __classPrivateFieldGet(this, _Forwarder_unacked, "f").delete(id);
166
+ // `next` skips what this page returned, including held events.
167
+ after = data.next;
168
+ full = data.items.length >= BATCH;
169
+ page += 1;
170
+ } while (full && page < MAX_PAGES && !__classPrivateFieldGet(this, _Forwarder_abort, "f").signal.aborted);
171
+ if (!full) {
172
+ // The listing was read to its end: forget sessions with nothing pending.
173
+ for (const sessionId of __classPrivateFieldGet(this, _Forwarder_held, "f").keys()) {
174
+ if (!seen.has(sessionId)) {
175
+ __classPrivateFieldGet(this, _Forwarder_held, "f").delete(sessionId);
176
+ }
121
177
  }
122
178
  }
123
- return { ok, count: data.items.length };
179
+ return full && progress;
180
+ }, _Forwarder_hold = function _Forwarder_hold(item, outcome, now) {
181
+ const prev = __classPrivateFieldGet(this, _Forwarder_held, "f").get(item.sessionId);
182
+ const delay = nextBackoff(prev?.delay || 0);
183
+ __classPrivateFieldGet(this, _Forwarder_held, "f").set(item.sessionId, {
184
+ blocked: {
185
+ sessionId: item.sessionId,
186
+ event: item.event,
187
+ eventId: item.id,
188
+ ...(outcome.httpStatus === undefined
189
+ ? {}
190
+ : { httpStatus: outcome.httpStatus }),
191
+ attempts: (prev?.blocked.attempts || 0) + 1,
192
+ lastError: outcome.error,
193
+ since: prev?.blocked.since || new Date(now).toISOString(),
194
+ },
195
+ retryAt: now + delay,
196
+ delay,
197
+ });
124
198
  }, _Forwarder_forward = async function _Forwarder_forward(item) {
125
199
  const doFetch = __classPrivateFieldGet(this, _Forwarder_deps, "f").forwardFetch || globalThis.fetch;
126
200
  const record = (status, httpStatus) => {
@@ -150,7 +224,7 @@ _Forwarder_client = new WeakMap(), _Forwarder_deps = new WeakMap(), _Forwarder_a
150
224
  if (res.ok) {
151
225
  __classPrivateFieldSet(this, _Forwarder_forwarded, __classPrivateFieldGet(this, _Forwarder_forwarded, "f") + 1, "f");
152
226
  record('delivered', res.status);
153
- return true;
227
+ return { ok: true };
154
228
  }
155
229
  __classPrivateFieldSet(this, _Forwarder_failed, __classPrivateFieldGet(this, _Forwarder_failed, "f") + 1, "f");
156
230
  const url = targetUrl(this.target, host);
@@ -159,7 +233,7 @@ _Forwarder_client = new WeakMap(), _Forwarder_deps = new WeakMap(), _Forwarder_a
159
233
  + 'followed; call again with the redirect target\'s `path`.'
160
234
  : `${url} returned HTTP ${res.status}`, "f");
161
235
  record('failed', res.status);
162
- return false;
236
+ return { ok: false, httpStatus: res.status, error: __classPrivateFieldGet(this, _Forwarder_lastError, "f") };
163
237
  }
164
238
  catch (err) {
165
239
  __classPrivateFieldSet(this, _Forwarder_lastError, err instanceof Error ? err.message : String(err), "f");
@@ -168,7 +242,7 @@ _Forwarder_client = new WeakMap(), _Forwarder_deps = new WeakMap(), _Forwarder_a
168
242
  // No loopback address accepted the connection.
169
243
  __classPrivateFieldSet(this, _Forwarder_failed, __classPrivateFieldGet(this, _Forwarder_failed, "f") + 1, "f");
170
244
  record('failed');
171
- return false;
245
+ return { ok: false, error: __classPrivateFieldGet(this, _Forwarder_lastError, "f") || 'Connection failed' };
172
246
  };
173
247
  /** The forwarders of one MCP process, one per org key. */
174
248
  export class ForwarderRegistry {
@@ -207,17 +281,26 @@ export function proxyCallbackEventsTool(deps = {}) {
207
281
  + 'to a local route, like `stripe listen --forward-to`. Call it '
208
282
  + 'when the app\'s callback route runs locally. It POSTs each '
209
283
  + 'envelope as JSON to `http://127.0.0.1:<port><path>` (loopback '
210
- + 'only; also tries `[::1]`) in order, acknowledges 2xx '
211
- + 'responses, and retries anything else, redirects included, '
212
- + 'with backoff. Creates the listener on first use, so it is not '
284
+ + 'only; also tries `[::1]`) and acknowledges 2xx responses. '
285
+ + 'Events of one session arrive in order. When the app rejects '
286
+ + 'an event, including with a redirect, that session is held and '
287
+ + 'retried with backoff (2 s up to 30 s) while other sessions '
288
+ + 'keep flowing. Creates the listener on first use, so it is not '
213
289
  + 'read-only. One forwarder runs per org: call again to read '
214
290
  + 'status, or with another `port` or `path` to retarget. '
215
291
  + '`stop: true` stops forwarding only. Forwarding lasts only '
216
292
  + 'while this MCP server runs; production needs '
217
293
  + '`create_org_callback_subscription`. Returns `{ forwarding, '
218
- + 'target, listener, forwarded, failed, lastError?, recent }`; '
219
- + '`listener.expiresAt` is the lease end, and `recent` lists the '
220
- + 'last events with `status` and `httpStatus`.',
294
+ + 'target, listener, forwarded, failed, lastError?, recent, '
295
+ + 'blocked }`. `listener.expiresAt` is the lease end, and '
296
+ + '`recent` lists the last events with `status` and '
297
+ + '`httpStatus`. `blocked` lists each held session as '
298
+ + '`{ sessionId, event, eventId, httpStatus?, attempts, '
299
+ + 'lastError?, since }`, and is `[]` when none is held. When '
300
+ + '`blocked` is not empty, check the app\'s response and logs '
301
+ + 'for that session. The app must return 2xx for a result that '
302
+ + 'failed verification, and 5xx only for a temporary error. '
303
+ + 'Fix the app, and the session resumes on the next retry.',
221
304
  inputSchema: {
222
305
  type: 'object',
223
306
  properties: {
@@ -1,5 +1,5 @@
1
1
  import { connectCdp } from "../cdp/client.js";
2
- import { defaultProfileDir, launchChrome, waitForCdpReady, } from "../cdp/launcher.js";
2
+ import { defaultProfileDir, launchLocalBrowser, waitForCdpReady, } from "../cdp/launcher.js";
3
3
  import { ConfigStore } from "../config/store.js";
4
4
  import { DEFAULT_LOGIN_URL, oldLoginUrl } from "../consts.js";
5
5
  /**
@@ -127,8 +127,7 @@ export async function captureFirebaseTokenViaCdp(opts) {
127
127
  `with --remote-debugging-port=${port} AND a non-default ` +
128
128
  '--user-data-dir, or use the dedicated local login mode.');
129
129
  }
130
- launchChrome({ port, profileDir: defaultProfileDir() });
131
- await waitForCdpReady(port, 10000);
130
+ await launchLocalBrowser({ port, profileDir: defaultProfileDir() });
132
131
  launchedByUs = true;
133
132
  }
134
133
  const handles = await connectCdp(port);
@@ -108,9 +108,8 @@ export async function runAttestorProof(provider, secrets, opts = {}) {
108
108
  extractedParameters: {},
109
109
  verified: false,
110
110
  error: 'No eth proof-owner key available (no RECLAIM_PRIVATE_KEY in env, ' +
111
- '.env, or RECLAIM_PRIVATE_KEY_FILE). Call resolve_owner_key, then ' +
112
- 'issue_credentials (generate) or import_credentials (provide one), ' +
113
- 'and retry run_proof.',
111
+ '.env, or RECLAIM_PRIVATE_KEY_FILE). Call resolve_owner_key, ' +
112
+ 'which creates a local test key, and retry run_proof.',
114
113
  errorKind: 'missing-owner-key',
115
114
  };
116
115
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { createClaimOnAttestor } from '@reclaimprotocol/attestor-core';
7
7
  import './patch-domhandler.ts';
8
+ import { type ZkFilesState } from './zk-files.ts';
8
9
  /**
9
10
  * Default time budget for one check. A live run takes about 8 s; a longer
10
11
  * budget risks the MCP client's own tool-call timeout.
@@ -37,11 +38,16 @@ export declare const COIN_GECKO_PARAMS: {
37
38
  additionalClientOptions: {};
38
39
  };
39
40
  export type AttestorCheckErrorKind =
40
- /** Socket, connection, DNS, TLS, or timeout failure. */
41
+ /** Socket, connection, DNS, TLS, or timeout failure, with the proof files
42
+ * ready: the attestor itself didn't answer. */
41
43
  'attestor-unreachable'
42
44
  /** The attestor answered and rejected or failed the claim. */
43
45
  | 'attestor-error'
44
- /** Local setup (ZK files) failed before the attestor was contacted. */
46
+ /** The proof files are still downloading; the attestor wasn't contacted. */
47
+ | 'proof-files-downloading'
48
+ /** The proof file download failed; the attestor wasn't contacted. */
49
+ | 'proof-files-failed'
50
+ /** Other local setup failed before the attestor was contacted. */
45
51
  | 'setup-failed';
46
52
  export interface AttestorCheckResult {
47
53
  ok: boolean;
@@ -50,6 +56,8 @@ export interface AttestorCheckResult {
50
56
  extractedParameters?: Record<string, string>;
51
57
  error?: string;
52
58
  errorKind?: AttestorCheckErrorKind;
59
+ /** The proof file download state when the check finished. */
60
+ proofFiles?: ZkFilesState;
53
61
  }
54
62
  /** Creates the claim on the attestor (`createClaimOnAttestor` by default). */
55
63
  export type CreateClaim = (args: Parameters<typeof createClaimOnAttestor>[0]) => Promise<unknown>;
@@ -60,6 +68,8 @@ export interface CheckAttestorOptions {
60
68
  createClaim?: CreateClaim;
61
69
  /** Replaces the ZK file download in tests. */
62
70
  prepare?: () => Promise<void>;
71
+ /** Reports the ZK file download state (`zkFilesState` by default). */
72
+ proofFilesState?: () => ZkFilesState;
63
73
  }
64
74
  /** True when `message` looks like a network failure rather than a rejection. */
65
75
  export declare function isNetworkError(message: string | undefined): boolean;
@@ -7,7 +7,7 @@ import { createClaimOnAttestor } from '@reclaimprotocol/attestor-core';
7
7
  import { generateKeypair } from '@reclaimprotocol/client';
8
8
  import "./patch-domhandler.js";
9
9
  import { claimRejection, ensureAttestorCrypto, parseClaim, resolveAttestorUrl, } from "./proof-core.js";
10
- import { ensureZkFiles } from "./zk-files.js";
10
+ import { ensureZkFiles, zkFilesState } from "./zk-files.js";
11
11
  /**
12
12
  * Default time budget for one check. A live run takes about 8 s; a longer
13
13
  * budget risks the MCP client's own tool-call timeout.
@@ -87,9 +87,15 @@ export async function checkAttestor(opts = {}) {
87
87
  ensureAttestorCrypto();
88
88
  await ensureZkFiles();
89
89
  });
90
+ const proofFilesState = opts.proofFilesState ?? zkFilesState;
90
91
  const startedAt = Date.now();
91
92
  const finish = (outcome) => {
92
- return { ...outcome, attestorUrl, durationMs: Date.now() - startedAt };
93
+ return {
94
+ ...outcome,
95
+ attestorUrl,
96
+ durationMs: Date.now() - startedAt,
97
+ proofFiles: proofFilesState(),
98
+ };
93
99
  };
94
100
  let timer;
95
101
  const timeout = new Promise((_, reject) => {
@@ -102,14 +108,7 @@ export async function checkAttestor(opts = {}) {
102
108
  await Promise.race([prepare(), timeout]);
103
109
  }
104
110
  catch (err) {
105
- const error = messageOf(err);
106
- return finish({
107
- ok: false,
108
- error,
109
- errorKind: error.startsWith(CHECK_TIMEOUT_MESSAGE)
110
- ? 'attestor-unreachable'
111
- : 'setup-failed',
112
- });
111
+ return finish({ ok: false, ...setupFailure(messageOf(err)) });
113
112
  }
114
113
  const claim = await Promise.race([
115
114
  createClaim({
@@ -150,6 +149,37 @@ export async function checkAttestor(opts = {}) {
150
149
  finally {
151
150
  clearTimeout(timer);
152
151
  }
152
+ /** Classify a failure before the attestor was contacted by the proof
153
+ * file download state, so a slow or failed download never reads as an
154
+ * unreachable attestor. */
155
+ function setupFailure(error) {
156
+ const files = proofFilesState();
157
+ if (files.status === 'downloading') {
158
+ const done = files.bytesDownloaded !== undefined
159
+ ? `, ${files.bytesDownloaded}`
160
+ + (files.totalBytes !== undefined ? `/${files.totalBytes}` : '')
161
+ + ' bytes'
162
+ : '';
163
+ return {
164
+ error: `proof files are still downloading (${files.method}, `
165
+ + `${Math.round(files.elapsedMs / 1000)}s${done}); the attestor `
166
+ + 'was not contacted. Run check_attestor again shortly.',
167
+ errorKind: 'proof-files-downloading',
168
+ };
169
+ }
170
+ if (files.status === 'failed') {
171
+ return {
172
+ error: `proof file download failed: ${files.reason}`,
173
+ errorKind: 'proof-files-failed',
174
+ };
175
+ }
176
+ return {
177
+ error,
178
+ errorKind: error.startsWith(CHECK_TIMEOUT_MESSAGE)
179
+ ? 'attestor-unreachable'
180
+ : 'setup-failed',
181
+ };
182
+ }
153
183
  }
154
184
  function messageOf(err) {
155
185
  return err instanceof Error ? err.message : String(err);
@@ -0,0 +1,36 @@
1
+ import './patch-domhandler.ts';
2
+ import type { CapturedRequest, ReclaimProvider } from '../provider/schema.ts';
3
+ export interface RevealedSlice {
4
+ /** Byte offsets in the response body. */
5
+ from: number;
6
+ to: number;
7
+ /** Revealed text, truncated to {@link MAX_SLICE_CHARS}. */
8
+ text: string;
9
+ /** Set when the slice is an OPRF-hashed value (plaintext here). */
10
+ hashed?: true;
11
+ }
12
+ export interface MatcherCheckResult {
13
+ /** True when the attestor would accept this response. */
14
+ ok: boolean;
15
+ /** Where it failed: building the request, selecting what to reveal, or
16
+ * checking `responseMatches` on the revealed bytes. */
17
+ failedAt?: 'request' | 'redaction' | 'match';
18
+ /** Attestor-core's error, truncated. */
19
+ reason?: string;
20
+ /** Params the attestor would extract (regex groups + `paramValues` the
21
+ * matches use). */
22
+ extractedParameters: Record<string, string>;
23
+ /** The response body slices revealed to the attestor. */
24
+ revealed: RevealedSlice[];
25
+ revealedSliceCount: number;
26
+ revealedBytes: number;
27
+ bodyBytes: number;
28
+ /** Set when an OPRF-hashed redaction was checked in plaintext. */
29
+ note?: string;
30
+ }
31
+ /**
32
+ * Check `provider`'s matchers against `captured`'s response body, offline.
33
+ * Uses the captured status and content type, so a non-2xx capture fails the
34
+ * same way it would at proof time.
35
+ */
36
+ export declare function checkMatchersOffline(provider: ReclaimProvider, captured: Pick<CapturedRequest, 'status' | 'contentType' | 'responseBody'>): MatcherCheckResult;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Offline matcher check: run a recipe's `responseRedactions` +
3
+ * `responseMatches` against a captured response with attestor-core's own HTTP
4
+ * provider, without a network call or a TLS session.
5
+ *
6
+ * It replays the two halves of a real proof on a synthetic transcript:
7
+ * 1. prover side: `providers.http.createRequest` builds the request and
8
+ * `getResponseRedactions` picks the response slices to reveal (xPath /
9
+ * jsonPath / regex, exactly as at proof time);
10
+ * 2. attestor side: `providers.http.assertValidProviderReceipt` checks the
11
+ * redacted transcript against `responseMatches` and extracts params.
12
+ *
13
+ * Hashed (OPRF) slices stay in plaintext here: OPRF needs the attestor, so the
14
+ * check treats the value as its own nullifier. Everything else is the same
15
+ * engine the attestor runs.
16
+ */
17
+ import { CURRENT_ATTESTOR_VERSION, PROVIDER_CTX, providers, redactSlices, } from '@reclaimprotocol/attestor-core';
18
+ // Side-effect: make external domhandler nodes DOM-like so attestor-core's
19
+ // bundled `xpath` accepts them (see patch-domhandler.ts).
20
+ import "./patch-domhandler.js";
21
+ import { LOGGER } from "../logger.js";
22
+ import { buildHttpParams } from "./proof-core.js";
23
+ /** Longest revealed slice returned; the full slice is still checked. */
24
+ const MAX_SLICE_CHARS = 400;
25
+ /** Most revealed slices returned. */
26
+ const MAX_SLICES = 10;
27
+ /** Longest error message returned (attestor errors can embed the base64 of
28
+ * a whole element). */
29
+ const MAX_REASON_CHARS = 600;
30
+ /** Placeholder secret header: the request builder needs one secret, and the
31
+ * check never sends the request. */
32
+ const OFFLINE_SECRET = { 'X-Reclaim-Offline-Check': '1' };
33
+ /**
34
+ * Check `provider`'s matchers against `captured`'s response body, offline.
35
+ * Uses the captured status and content type, so a non-2xx capture fails the
36
+ * same way it would at proof time.
37
+ */
38
+ export function checkMatchersOffline(provider, captured) {
39
+ const http = providers.http;
40
+ const params = buildHttpParams(provider);
41
+ const logger = silentLogger();
42
+ const body = Buffer.from(captured.responseBody ?? '', 'utf8');
43
+ const head = Buffer.from(`HTTP/1.1 ${captured.status} ${captured.status < 300 ? 'OK' : 'ERR'}\r\n`
44
+ + `Content-Type: ${captured.contentType || 'text/plain'}\r\n`
45
+ + `Content-Length: ${body.length}\r\n\r\n`);
46
+ const response = new Uint8Array(Buffer.concat([head, body]));
47
+ const empty = {
48
+ extractedParameters: {},
49
+ revealed: [],
50
+ revealedSliceCount: 0,
51
+ revealedBytes: 0,
52
+ bodyBytes: body.length,
53
+ };
54
+ let request;
55
+ try {
56
+ const req = http.createRequest({ headers: OFFLINE_SECRET }, structuredClone(params), logger);
57
+ const data = typeof req.data === 'string'
58
+ ? new Uint8Array(Buffer.from(req.data))
59
+ : req.data;
60
+ request = redactSlices(data, req.redactions);
61
+ }
62
+ catch (err) {
63
+ return { ...empty, ok: false, failedAt: 'request', reason: reasonOf(err) };
64
+ }
65
+ let slices;
66
+ try {
67
+ slices = http.getResponseRedactions?.({
68
+ response, params: structuredClone(params), logger, ctx: PROVIDER_CTX,
69
+ }) ?? [];
70
+ }
71
+ catch (err) {
72
+ return { ...empty, ok: false, failedAt: 'redaction', reason: reasonOf(err) };
73
+ }
74
+ const hidden = slices.filter((s) => !s.hash);
75
+ const hashed = slices.filter((s) => s.hash);
76
+ const view = revealedSlices(head.length, response.length, hidden, hashed);
77
+ const redacted = redactSlices(response, hidden);
78
+ const seen = {
79
+ revealed: view.slice(0, MAX_SLICES).map((s) => ({
80
+ from: s.from,
81
+ to: s.to,
82
+ text: truncate(Buffer.from(response.subarray(head.length + s.from, head.length + s.to))
83
+ .toString('utf8'), MAX_SLICE_CHARS),
84
+ ...(s.hashed ? { hashed: true } : {}),
85
+ })),
86
+ revealedSliceCount: view.length,
87
+ revealedBytes: view.reduce((n, s) => n + s.to - s.from, 0),
88
+ bodyBytes: body.length,
89
+ ...(hashed.length
90
+ ? {
91
+ note: 'OPRF-hashed values were checked in plaintext; at proof time '
92
+ + 'the attestor sees their nullifier instead.',
93
+ }
94
+ : {}),
95
+ };
96
+ try {
97
+ const out = http.assertValidProviderReceipt({
98
+ clientVersion: CURRENT_ATTESTOR_VERSION,
99
+ receipt: [
100
+ { sender: 'client', message: request },
101
+ { sender: 'server', message: redacted },
102
+ ],
103
+ params: structuredClone(params),
104
+ logger,
105
+ ctx: PROVIDER_CTX,
106
+ });
107
+ // The http provider validates synchronously; a thenable here would mean
108
+ // a provider we don't run offline.
109
+ const extracted = out instanceof Promise ? undefined : out;
110
+ return {
111
+ ok: true,
112
+ extractedParameters: extracted?.extractedParameters ?? {},
113
+ ...seen,
114
+ };
115
+ }
116
+ catch (err) {
117
+ return {
118
+ ok: false,
119
+ failedAt: 'match',
120
+ reason: reasonOf(err),
121
+ extractedParameters: {},
122
+ ...seen,
123
+ };
124
+ }
125
+ }
126
+ /** Body-relative revealed ranges: the complement of `hidden` within the body,
127
+ * merged with the hashed slices (revealed as their nullifier at proof time).
128
+ */
129
+ function revealedSlices(bodyStart, total, hidden, hashed) {
130
+ const out = [];
131
+ let cursor = bodyStart;
132
+ const sorted = [...hidden].sort((a, b) => a.fromIndex - b.fromIndex);
133
+ for (const s of sorted) {
134
+ if (s.fromIndex > cursor) {
135
+ out.push({ from: cursor - bodyStart, to: s.fromIndex - bodyStart });
136
+ }
137
+ cursor = Math.max(cursor, s.toIndex);
138
+ }
139
+ if (cursor < total) {
140
+ out.push({ from: cursor - bodyStart, to: total - bodyStart });
141
+ }
142
+ for (const h of hashed) {
143
+ out.push({
144
+ from: h.fromIndex - bodyStart, to: h.toIndex - bodyStart, hashed: true,
145
+ });
146
+ }
147
+ return out
148
+ .filter((s) => s.to > s.from && s.from >= 0)
149
+ .sort((a, b) => a.from - b.from);
150
+ }
151
+ function reasonOf(err) {
152
+ return truncate(err instanceof Error ? err.message : String(err), MAX_REASON_CHARS);
153
+ }
154
+ function truncate(s, max) {
155
+ return s.length > max ? `${s.slice(0, max)}… (${s.length} chars)` : s;
156
+ }
157
+ /** A logger for attestor-core calls that records nothing: the check reports
158
+ * its outcome in the result, and the provider logs whole responses. */
159
+ function silentLogger() {
160
+ return LOGGER.child({ scope: 'check-matchers' }, { level: 'silent' });
161
+ }