@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.
- package/README.md +8 -8
- package/lib/authoring/backend.d.ts +12 -1
- package/lib/authoring/backend.js +3 -0
- package/lib/authoring/tools.js +33 -5
- package/lib/authoring/util.d.ts +43 -0
- package/lib/authoring/util.js +54 -0
- package/lib/cdp/chrome-for-testing.d.ts +14 -0
- package/lib/cdp/chrome-for-testing.js +43 -0
- package/lib/cdp/geo.d.ts +3 -0
- package/lib/cdp/geo.js +27 -15
- package/lib/cdp/launcher.d.ts +79 -1
- package/lib/cdp/launcher.js +244 -24
- package/lib/logger.js +3 -1
- package/lib/mcp/api/operations.gen.js +1 -1
- package/lib/mcp/instructions.js +3 -3
- package/lib/mcp/start.js +17 -2
- package/lib/mcp/tools/agent/attach.d.ts +6 -0
- package/lib/mcp/tools/agent/attach.js +58 -118
- package/lib/mcp/tools/agent/build.js +6 -0
- package/lib/mcp/tools/agent/capture.js +33 -10
- package/lib/mcp/tools/agent/check-attestor.js +15 -7
- package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
- package/lib/mcp/tools/agent/draft-tools.js +170 -0
- package/lib/mcp/tools/agent/pod-proof.js +5 -1
- package/lib/mcp/tools/agent/proof.d.ts +28 -0
- package/lib/mcp/tools/agent/proof.js +72 -0
- package/lib/mcp/tools/agent/shared.d.ts +8 -1
- package/lib/mcp/tools/agent/shared.js +98 -19
- package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
- package/lib/mcp/tools/agent/start-integration.js +10 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
- package/lib/mcp/tools/authenticate/authenticate.js +212 -116
- package/lib/mcp/tools/authenticate/build.d.ts +2 -2
- package/lib/mcp/tools/authenticate/build.js +6 -3
- package/lib/mcp/tools/authenticate/builder-access.js +5 -2
- package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
- package/lib/mcp/tools/authenticate/eth-key.js +29 -5
- package/lib/mcp/tools/authenticate/index.d.ts +1 -1
- package/lib/mcp/tools/authenticate/index.js +1 -1
- package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
- package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
- package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
- package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
- package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
- package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
- package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
- package/lib/mcp/tools/authenticate/session-cache.js +95 -4
- package/lib/mcp/tools/mode/mode.js +6 -6
- package/lib/mcp/tools/verification/proxy-callback-events.d.ts +21 -3
- package/lib/mcp/tools/verification/proxy-callback-events.js +128 -45
- package/lib/old/login-cdp.js +2 -3
- package/lib/proof/attestor.js +2 -3
- package/lib/proof/check-attestor.d.ts +12 -2
- package/lib/proof/check-attestor.js +40 -10
- package/lib/proof/check-matchers.d.ts +36 -0
- package/lib/proof/check-matchers.js +161 -0
- package/lib/proof/zk-files.d.ts +50 -11
- package/lib/proof/zk-files.js +259 -63
- package/lib/provider/draft.d.ts +24 -1
- package/lib/provider/draft.js +135 -0
- package/lib/provider/matcher.js +171 -4
- package/package.json +6 -4
- package/skill/SKILL.builder.md +7 -2
- package/skill/SKILL.common.md +90 -70
- package/skill/SKILL.old.md +3 -3
- package/skill/builder-mode.md +44 -6
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +51 -35
- package/skill/verification-client-integration.md +87 -15
- package/src/authoring/backend.ts +12 -1
- package/src/authoring/tools.ts +48 -7
- package/src/authoring/util.ts +64 -0
- package/src/cdp/chrome-for-testing.ts +66 -0
- package/src/cdp/geo.ts +36 -21
- package/src/cdp/launcher.ts +328 -24
- package/src/logger.ts +3 -1
- package/src/mcp/api/operations.gen.ts +1 -1
- package/src/mcp/instructions.ts +3 -3
- package/src/mcp/start.ts +20 -1
- package/src/mcp/tools/agent/attach.ts +65 -128
- package/src/mcp/tools/agent/build.ts +7 -0
- package/src/mcp/tools/agent/capture.ts +54 -11
- package/src/mcp/tools/agent/check-attestor.ts +15 -7
- package/src/mcp/tools/agent/draft-tools.ts +234 -0
- package/src/mcp/tools/agent/pod-proof.ts +5 -1
- package/src/mcp/tools/agent/proof.ts +110 -1
- package/src/mcp/tools/agent/shared.ts +130 -19
- package/src/mcp/tools/agent/start-integration.ts +10 -6
- package/src/mcp/tools/authenticate/authenticate.ts +276 -131
- package/src/mcp/tools/authenticate/build.ts +6 -3
- package/src/mcp/tools/authenticate/builder-access.ts +5 -2
- package/src/mcp/tools/authenticate/eth-key.ts +41 -7
- package/src/mcp/tools/authenticate/index.ts +1 -1
- package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
- package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
- package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
- package/src/mcp/tools/authenticate/session-cache.ts +125 -5
- package/src/mcp/tools/mode/mode.ts +6 -6
- package/src/mcp/tools/verification/proxy-callback-events.ts +151 -41
- package/src/old/login-cdp.ts +2 -3
- package/src/proof/attestor.ts +2 -3
- package/src/proof/check-attestor.ts +55 -12
- package/src/proof/check-matchers.ts +227 -0
- package/src/proof/zk-files.ts +357 -65
- package/src/provider/draft.ts +215 -7
- 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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
80
|
-
backoff =
|
|
81
|
-
wait =
|
|
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 =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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]`)
|
|
211
|
-
+ '
|
|
212
|
-
+ '
|
|
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
|
|
220
|
-
+ 'last events with `status` and
|
|
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: {
|
package/lib/old/login-cdp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { connectCdp } from "../cdp/client.js";
|
|
2
|
-
import { defaultProfileDir,
|
|
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
|
-
|
|
131
|
-
await waitForCdpReady(port, 10000);
|
|
130
|
+
await launchLocalBrowser({ port, profileDir: defaultProfileDir() });
|
|
132
131
|
launchedByUs = true;
|
|
133
132
|
}
|
|
134
133
|
const handles = await connectCdp(port);
|
package/lib/proof/attestor.js
CHANGED
|
@@ -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,
|
|
112
|
-
'
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|