@reclaimprotocol/agent 0.1.36 → 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/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/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 +35 -5
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +48 -33
- package/skill/verification-client-integration.md +21 -6
- 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/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/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
package/lib/provider/draft.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { badRequest } from '@hapi/boom';
|
|
2
|
+
import assert from 'node:assert';
|
|
1
3
|
import { isOprfHashable, OPRF_MAX_BYTES, synthesizeMatcher } from "./matcher.js";
|
|
2
4
|
import { normalizeCandidates } from "./normalize.js";
|
|
5
|
+
import { DYNAMIC_GEO, } from "./schema.js";
|
|
3
6
|
import { secretUrlCharBudget, splitSecrets, stripTransportHeaders, } from "./secrets.js";
|
|
4
7
|
/** OPRF mode for the auto-hash floor — TEE-native (attestor rejects oprf-raw,
|
|
5
8
|
* isn't set up for gnark oprf). */
|
|
@@ -312,3 +315,135 @@ export function draftProvider(req, targets, name, hash, opts) {
|
|
|
312
315
|
}
|
|
313
316
|
return { provider, secretRefs, extractions };
|
|
314
317
|
}
|
|
318
|
+
const GEO_TEMPLATES = new Set([DYNAMIC_GEO, '{{DYNAMIC_GEO_SECRET}}']);
|
|
319
|
+
const MATCH_TYPES = new Set(['contains', 'regex']);
|
|
320
|
+
const OPRF_MODES = new Set(['oprf', 'oprf-raw', 'oprf-mpc']);
|
|
321
|
+
/**
|
|
322
|
+
* Apply an agent's edit to a drafted provider: validate the new matchers the
|
|
323
|
+
* way the attestor reads them (regexes compile with its `sgi` flags, every
|
|
324
|
+
* redaction has a selector, a hashed redaction has exactly one named group),
|
|
325
|
+
* then run the same portability gate as {@link draftProvider} so an edit can't
|
|
326
|
+
* bake a captured value in as a literal. Returns the updated provider, its
|
|
327
|
+
* extractions (re-paired with the redaction that names each param), and
|
|
328
|
+
* advisory warnings (for example, a `{{param}}` nothing resolves).
|
|
329
|
+
*/
|
|
330
|
+
export function editDraftProvider(provider, extractions, edit) {
|
|
331
|
+
const matches = edit.responseMatches ?? provider.responseMatches;
|
|
332
|
+
const redactions = edit.responseRedactions
|
|
333
|
+
?? provider.responseRedactions ?? [];
|
|
334
|
+
const paramValues = edit.paramValues ?? provider.paramValues ?? {};
|
|
335
|
+
const geoLocation = GEO_TEMPLATES.has(edit.geoLocation ?? '')
|
|
336
|
+
? edit.geoLocation
|
|
337
|
+
: edit.geoLocation?.toUpperCase();
|
|
338
|
+
assert(geoLocation === undefined || GEO_TEMPLATES.has(geoLocation)
|
|
339
|
+
|| /^[A-Z]{2}$/.test(geoLocation), badRequest('geoLocation must be a 2-letter ISO country code, {{DYNAMIC_GEO}}, '
|
|
340
|
+
+ 'or {{DYNAMIC_GEO_SECRET}}'));
|
|
341
|
+
assert(matches.length > 0, badRequest('responseMatches must have at least one entry'));
|
|
342
|
+
for (const [i, m] of matches.entries()) {
|
|
343
|
+
assert(MATCH_TYPES.has(m.type) && typeof m.value === 'string' && m.value, badRequest(`responseMatches[${i}] needs type "contains" or "regex" and a `
|
|
344
|
+
+ 'non-empty value'));
|
|
345
|
+
if (m.type === 'regex') {
|
|
346
|
+
assertRegex(m.value, `responseMatches[${i}].value`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const [i, r] of redactions.entries()) {
|
|
350
|
+
assert(r.xPath || r.jsonPath || r.regex, badRequest(`responseRedactions[${i}] needs an xPath, jsonPath, or regex`));
|
|
351
|
+
if (r.regex) {
|
|
352
|
+
assertRegex(r.regex, `responseRedactions[${i}].regex`);
|
|
353
|
+
}
|
|
354
|
+
assert(!r.hash || (OPRF_MODES.has(r.hash) && groupNames(r.regex).length === 1), badRequest(`responseRedactions[${i}] is hashed (${r.hash}), so it needs a hash `
|
|
355
|
+
+ 'of oprf, oprf-raw, or oprf-mpc and a regex with exactly one named '
|
|
356
|
+
+ 'capture group'));
|
|
357
|
+
}
|
|
358
|
+
// An edit can't lower privacy: a draft that redacts keeps redacting, and
|
|
359
|
+
// every group it hashed stays hashed with the same mode.
|
|
360
|
+
const original = provider.responseRedactions ?? [];
|
|
361
|
+
assert(!original.length || redactions.length, badRequest('responseRedactions can\'t be emptied: the attestor would see the '
|
|
362
|
+
+ 'whole response'));
|
|
363
|
+
for (const r of original.filter((o) => o.hash)) {
|
|
364
|
+
for (const name of groupNames(r.regex)) {
|
|
365
|
+
const kept = redactions.find((e) => groupNames(e.regex).includes(name));
|
|
366
|
+
assert(kept?.hash === r.hash, badRequest(`capture group ${name} is ${r.hash}-hashed in the draft; keep a `
|
|
367
|
+
+ 'redaction for it with the same hash'));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const groups = matcherGroups(matches, redactions);
|
|
371
|
+
// A group the draft never extracted has a value nothing below checks, and
|
|
372
|
+
// no extraction would track it: new data points need a fresh draft.
|
|
373
|
+
const known = new Set([
|
|
374
|
+
...extractions.map((e) => e.name),
|
|
375
|
+
...matcherGroups(provider.responseMatches, provider.responseRedactions ?? []),
|
|
376
|
+
]);
|
|
377
|
+
const unknown = [...groups].filter((name) => !known.has(name));
|
|
378
|
+
assert(!unknown.length, badRequest(`capture group(s) ${unknown.join(', ')} are not drafted extractions; `
|
|
379
|
+
+ 'propose a new draft to add a data point'));
|
|
380
|
+
// Always the captured values, so editing a paramValues entry can't hide
|
|
381
|
+
// one from the portability gate; edited values are checked as well.
|
|
382
|
+
const targets = [
|
|
383
|
+
...extractions.map(({ name, value }) => ({ name, value })),
|
|
384
|
+
...Object.entries(paramValues)
|
|
385
|
+
.filter(([name, value]) => !extractions
|
|
386
|
+
.some((e) => e.name === name && e.value === value))
|
|
387
|
+
.map(([name, value]) => ({ name, value })),
|
|
388
|
+
];
|
|
389
|
+
assertPortableMatches(matches, redactions, targets);
|
|
390
|
+
// Same OPRF gate as draftProvider, against the URL as the attestor signs it.
|
|
391
|
+
const hashed = redactions.flatMap(({ regex, hash }) => hash
|
|
392
|
+
? groupNames(regex).flatMap((name) => targets
|
|
393
|
+
.filter((t) => t.name === name)
|
|
394
|
+
.map((t) => ({ ...t, hash })))
|
|
395
|
+
: []);
|
|
396
|
+
const url = provider.url
|
|
397
|
+
.replace(/\{\{([^{}]+)\}\}/g, (t, name) => paramValues[name] ?? t);
|
|
398
|
+
assertNoOprfUrlConflict(url, hashed, hashed.map((t) => t.hash));
|
|
399
|
+
const { responseRedactions: _r, paramValues: _p, ...rest } = provider;
|
|
400
|
+
void _r;
|
|
401
|
+
void _p;
|
|
402
|
+
const next = {
|
|
403
|
+
...rest,
|
|
404
|
+
responseMatches: matches,
|
|
405
|
+
...(redactions.length ? { responseRedactions: redactions } : {}),
|
|
406
|
+
...(Object.keys(paramValues).length ? { paramValues } : {}),
|
|
407
|
+
...(geoLocation ? { geoLocation } : {}),
|
|
408
|
+
};
|
|
409
|
+
const warnings = matches
|
|
410
|
+
.flatMap((m) => [...m.value.matchAll(/\{\{([^{}]+)\}\}/g)].map((t) => t[1]))
|
|
411
|
+
.filter((name) => !(name in paramValues) && !groups.has(name))
|
|
412
|
+
.map((name) => `{{${name}}} has no paramValues entry or named capture `
|
|
413
|
+
+ 'group, so the attestor would look for it literally.');
|
|
414
|
+
const nextExtractions = extractions.map((e) => {
|
|
415
|
+
const redaction = redactions
|
|
416
|
+
.find((r) => groupNames(r.regex).includes(e.name));
|
|
417
|
+
// The captured value, never an edited one, so a later edit's gate still
|
|
418
|
+
// sees it.
|
|
419
|
+
return {
|
|
420
|
+
name: e.name,
|
|
421
|
+
value: e.value,
|
|
422
|
+
...(redaction ? { redaction } : {}),
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
return { provider: next, extractions: nextExtractions, warnings };
|
|
426
|
+
}
|
|
427
|
+
/** Named capture groups across regex matches and redactions. */
|
|
428
|
+
function matcherGroups(matches, redactions) {
|
|
429
|
+
return new Set([
|
|
430
|
+
...matches.flatMap((m) => m.type === 'regex' ? groupNames(m.value) : []),
|
|
431
|
+
...redactions.flatMap((r) => groupNames(r.regex)),
|
|
432
|
+
]);
|
|
433
|
+
}
|
|
434
|
+
/** Named capture groups in `regex` (none when absent). */
|
|
435
|
+
function groupNames(regex) {
|
|
436
|
+
return [...(regex ?? '').matchAll(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g)]
|
|
437
|
+
.map((m) => m[1]);
|
|
438
|
+
}
|
|
439
|
+
/** Throw a 400 unless `regex` compiles the way the attestor compiles it. */
|
|
440
|
+
function assertRegex(regex, where) {
|
|
441
|
+
let error;
|
|
442
|
+
try {
|
|
443
|
+
new RegExp(regex, 'sgi');
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
error = err instanceof Error ? err.message : String(err);
|
|
447
|
+
}
|
|
448
|
+
assert(!error, badRequest(`${where} is not a valid regex: ${error}`));
|
|
449
|
+
}
|
package/lib/provider/matcher.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { badRequest } from '@hapi/boom';
|
|
2
|
+
import assert from 'node:assert';
|
|
1
3
|
import { elementAnchor } from "./dom-xpath.js";
|
|
2
4
|
import { normalizeCandidates } from "./normalize.js";
|
|
3
5
|
/** Reclaim attestor caps OPRF-hashed values at 62 bytes (TOPRF marker
|
|
@@ -53,6 +55,12 @@ export function synthesizeMatcher(body, contentType, target, paramName, hash) {
|
|
|
53
55
|
return r;
|
|
54
56
|
}
|
|
55
57
|
}
|
|
58
|
+
// Inline JSON / JS-object text outside an HTML element (a JS bundle, a
|
|
59
|
+
// non-JSON text body): anchor on the key name right before the value.
|
|
60
|
+
const inline = inlineKeyMatcher(body, target, paramName, valueClass, hash);
|
|
61
|
+
if (inline) {
|
|
62
|
+
return inline;
|
|
63
|
+
}
|
|
56
64
|
// No JSON key and no stable xPath anchor: a content-anchored regex, matched
|
|
57
65
|
// by CONTENT so it reproduces regardless of DOM position at proof time (a
|
|
58
66
|
// positional `(//tag)[N]` resolves to the wrong node at proof — see 9fd7770).
|
|
@@ -308,7 +316,133 @@ function htmlMatcher(body, target, paramName, valueClass, hash) {
|
|
|
308
316
|
return keyValueResult(kp.prefix, kp.close, target, paramName, valueClass, { xPath: anchor.xPath, jsonPath }, hash);
|
|
309
317
|
}
|
|
310
318
|
}
|
|
311
|
-
|
|
319
|
+
// Inline JS-object / JSON text inside the element (a SvelteKit/Next/Nuxt
|
|
320
|
+
// hydration `<script>` that isn't valid JSON): anchor on the key name
|
|
321
|
+
// nearest the value (`currentTierDescription:"{{v}}"`), scoped by the xPath.
|
|
322
|
+
const inline = inlineKeyMatcher(revealSlice, target, paramName, valueClass, hash, anchor.xPath);
|
|
323
|
+
if (inline) {
|
|
324
|
+
return inline;
|
|
325
|
+
}
|
|
326
|
+
const result = elementResult(anchor.xPath, revealSlice, target, paramName, valueClass, hash);
|
|
327
|
+
const [left = '', right = ''] = result?.match.value
|
|
328
|
+
.split(`{{${paramName}}}`) ?? [];
|
|
329
|
+
const hits = result?.redaction?.regex
|
|
330
|
+
? countMatches(revealSlice, result.redaction.regex)
|
|
331
|
+
: 0;
|
|
332
|
+
assert(!result || !isGenericWindow(left, right) || hits <= 1, badRequest(`target value "${target}" sits in a <${elementTag(revealSlice)}> `
|
|
333
|
+
+ 'with no key name or markup next to it. The only literal context, '
|
|
334
|
+
+ `"${result?.match.value}", matches ${hits} values in that element, `
|
|
335
|
+
+ 'so it would not pin this one. Pick a request where the value has '
|
|
336
|
+
+ 'a key (for example, a JSON API response) or sits in its own '
|
|
337
|
+
+ 'element.'));
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
/** How many times `regex` matches in `text` (0 for an invalid regex). */
|
|
341
|
+
function countMatches(text, regex) {
|
|
342
|
+
try {
|
|
343
|
+
return [...text.matchAll(new RegExp(regex, 'g'))].length;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** Tag name of the element markup starting `markup` (`script`, `div`, …). */
|
|
350
|
+
function elementTag(markup) {
|
|
351
|
+
return /^<([A-Za-z][\w-]*)/.exec(markup)?.[1] ?? 'element';
|
|
352
|
+
}
|
|
353
|
+
/** A literal window with no letter or digit on either side (for example,
|
|
354
|
+
* `:"{{v}}",`): it pins a value only when the element holds nothing else. */
|
|
355
|
+
function isGenericWindow(left, right) {
|
|
356
|
+
return !/[A-Za-z0-9]/.test(left + right);
|
|
357
|
+
}
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// Inline key: `key:"value"` / `"key":"value"` in JS-object or JSON text
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
/** How far left of a value to look for its key. */
|
|
362
|
+
const MAX_KEY_LOOKBACK = 128;
|
|
363
|
+
/** The key directly before a value: JSON `"key":`, `'key':`, or a bare JS
|
|
364
|
+
* identifier `key:`, then the value's opening quote (if any) at the end. */
|
|
365
|
+
const KEY_BEFORE_VALUE = new RegExp('(?:"([A-Za-z_$][\\w$-]*)"|\'([A-Za-z_$][\\w$-]*)\''
|
|
366
|
+
+ '|(?<![\\w$"\'])([A-Za-z_$][\\w$]*))\\s*:\\s*(["\']?)$');
|
|
367
|
+
/**
|
|
368
|
+
* Matcher for a value written as `key:"value"` (JS object literal, as SvelteKit
|
|
369
|
+
* and similar frameworks inline into SSR pages) or `"key":"value"` inside text
|
|
370
|
+
* that isn't parseable JSON. The window is the key name plus the value's
|
|
371
|
+
* quotes, so the match pins this field and nothing user-specific:
|
|
372
|
+
* `currentTierDescription:"{{v}}"`.
|
|
373
|
+
*
|
|
374
|
+
* Rejects a key that appears more than once in `scope` with a different value:
|
|
375
|
+
* the regex would extract the first one, which may not be this field. When the
|
|
376
|
+
* value has a key but it's ambiguous, throws rather than falling back to a
|
|
377
|
+
* looser window. Returns undefined when no occurrence has a key before it.
|
|
378
|
+
*/
|
|
379
|
+
function inlineKeyMatcher(scope, target, paramName, valueClass, hash, xPath) {
|
|
380
|
+
const candidates = [target, ...normalizeCandidates(target)]
|
|
381
|
+
.filter((c) => c && !c.startsWith('"'));
|
|
382
|
+
let ambiguous;
|
|
383
|
+
for (const cand of new Set(candidates)) {
|
|
384
|
+
for (let idx = scope.indexOf(cand); idx >= 0; idx = scope.indexOf(cand, idx + 1)) {
|
|
385
|
+
const kv = keyBefore(scope, idx, cand);
|
|
386
|
+
if (!kv) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const others = valuesForKey(scope, kv.key)
|
|
390
|
+
.filter((v) => v !== cand);
|
|
391
|
+
if (others.length) {
|
|
392
|
+
ambiguous ?? (ambiguous = { key: kv.key, count: others.length + 1 });
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// A single-quoted free-form value must stop at its own quote.
|
|
396
|
+
const cls = kv.close === '\'' && valueClass === '[^"<>\\n]+'
|
|
397
|
+
? '[^\'<>\\n]+'
|
|
398
|
+
: valueClass;
|
|
399
|
+
return keyValueResult(kv.prefix, kv.close, cand, paramName, cls, xPath ? { xPath } : {}, hash);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
assert(!ambiguous, badRequest(`target value "${target}" sits under key "${ambiguous?.key}", but that `
|
|
403
|
+
+ `key occurs ${ambiguous?.count} times with different values, so a `
|
|
404
|
+
+ 'match on it would not pin this value. Pick a request where the key '
|
|
405
|
+
+ 'is unique (for example, a JSON API that returns only this object).'));
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
/** The key + opening-quote prefix directly before the value at `idx`, when the
|
|
409
|
+
* value is a complete quoted string (or a bare token) under that key. */
|
|
410
|
+
function keyBefore(scope, idx, value) {
|
|
411
|
+
const head = scope.slice(Math.max(0, idx - MAX_KEY_LOOKBACK), idx);
|
|
412
|
+
const m = KEY_BEFORE_VALUE.exec(head);
|
|
413
|
+
const key = m?.[1] ?? m?.[2] ?? m?.[3];
|
|
414
|
+
if (!m || !key || key.length < 2) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
// A bare key before a bare value (`score: 5`) is prose as often as code;
|
|
418
|
+
// require a quoted key or a quoted value.
|
|
419
|
+
const quote = m[4] ?? '';
|
|
420
|
+
const after = scope[idx + value.length] ?? '';
|
|
421
|
+
const complete = quote ? after === quote : /^$|[\s,;}\]]/.test(after);
|
|
422
|
+
if (!complete || (!quote && m[3])) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
return { key, prefix: m[0], close: quote };
|
|
426
|
+
}
|
|
427
|
+
/** Every value written under `key` in `scope` (JSON or JS-object syntax),
|
|
428
|
+
* raw — quoted strings without their quotes, bare tokens as-is. */
|
|
429
|
+
function valuesForKey(scope, key) {
|
|
430
|
+
const re = new RegExp(`(?<![\\w$])(["']?)${escapeRegex(key)}\\1\\s*:\\s*`, 'g');
|
|
431
|
+
const values = [];
|
|
432
|
+
for (const m of scope.matchAll(re)) {
|
|
433
|
+
const start = m.index + m[0].length;
|
|
434
|
+
const quote = scope[start];
|
|
435
|
+
if (quote === '"' || quote === '\'') {
|
|
436
|
+
let j = start + 1;
|
|
437
|
+
while (j < scope.length && scope[j] !== quote) {
|
|
438
|
+
j += scope[j] === '\\' ? 2 : 1;
|
|
439
|
+
}
|
|
440
|
+
values.push(scope.slice(start + 1, j));
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
values.push(/^[^\s,;}\]]*/.exec(scope.slice(start))?.[0] ?? '');
|
|
444
|
+
}
|
|
445
|
+
return values;
|
|
312
446
|
}
|
|
313
447
|
/**
|
|
314
448
|
* `contains` a PORTABLE window of the element around the value, with the value
|
|
@@ -547,14 +681,25 @@ function buildContentRegex(body, target, paramName, valueClass) {
|
|
|
547
681
|
const from = Math.max(0, start);
|
|
548
682
|
const before = escapeRegexFlexWs(body.slice(from, idx));
|
|
549
683
|
const re = `${before}(?<${paramName}>${valueClass})${right}`;
|
|
684
|
+
let found = false;
|
|
550
685
|
try {
|
|
551
|
-
|
|
552
|
-
return re;
|
|
553
|
-
}
|
|
686
|
+
found = new RegExp(re).exec(body)?.groups?.[paramName] === cand;
|
|
554
687
|
}
|
|
555
688
|
catch {
|
|
556
689
|
break;
|
|
557
690
|
}
|
|
691
|
+
if (found) {
|
|
692
|
+
const problem = contextProblem(body.slice(from, idx), right ? rawRight : '');
|
|
693
|
+
assert(!problem, badRequest(`target value "${target}" has no key name or element next to `
|
|
694
|
+
+ `it; the shortest window that pins it, "${body.slice(from, idx)}`
|
|
695
|
+
+ `{{${paramName}}}${right ? rawRight : ''}", ${problem}. That `
|
|
696
|
+
+ 'would pin the provider to this user or match the wrong value. '
|
|
697
|
+
+ 'This happens with positional arrays such as SvelteKit '
|
|
698
|
+
+ '`__data.json`. Use a request where the value sits under its '
|
|
699
|
+
+ 'own key (the HTML page or another JSON API), or stop and '
|
|
700
|
+
+ 'report.'));
|
|
701
|
+
return re;
|
|
702
|
+
}
|
|
558
703
|
if (from === 0) {
|
|
559
704
|
break;
|
|
560
705
|
}
|
|
@@ -562,6 +707,28 @@ function buildContentRegex(body, target, paramName, valueClass) {
|
|
|
562
707
|
}
|
|
563
708
|
return undefined;
|
|
564
709
|
}
|
|
710
|
+
/** A standalone number token (`33` in `:33},`) — a points balance, count, or
|
|
711
|
+
* id that differs per user; not a digit inside a word or class (`mb-2`). */
|
|
712
|
+
const VOLATILE_NUMBER = /(?<![\w.-])\d+(?:\.\d+)?(?![\w.])/;
|
|
713
|
+
/** The closing quote of a string followed by a separator — the end of a
|
|
714
|
+
* neighbouring VALUE (keys are followed by `:`), usually user-specific. */
|
|
715
|
+
const NEIGHBOUR_STRING = /"\s*[,\]}]/;
|
|
716
|
+
/** Why a content window around a value isn't portable, or undefined when it
|
|
717
|
+
* is: only punctuation (pins nothing), a number next to the value, or a
|
|
718
|
+
* neighbouring string value. */
|
|
719
|
+
function contextProblem(left, right) {
|
|
720
|
+
if (VOLATILE_NUMBER.test(left) || VOLATILE_NUMBER.test(right)) {
|
|
721
|
+
return 'contains a number next to the value, which is likely '
|
|
722
|
+
+ 'user-specific (for example, a points balance)';
|
|
723
|
+
}
|
|
724
|
+
if (NEIGHBOUR_STRING.test(left)) {
|
|
725
|
+
return 'contains a neighbouring value, which is likely user-specific';
|
|
726
|
+
}
|
|
727
|
+
if (!/[A-Za-z]/.test(left + right)) {
|
|
728
|
+
return 'is only punctuation and matches almost any value';
|
|
729
|
+
}
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
565
732
|
/**
|
|
566
733
|
* The literal `contains` template: surrounding structural context with the
|
|
567
734
|
* value replaced by `{{paramName}}`. Whitespace-adjacent context is dropped (a
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/agent",
|
|
3
3
|
"title": "Reclaim Agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.37",
|
|
5
5
|
"description": "Reclaim Protocol tools for AI coding agents. Reclaim lets your app verify data points about its users, such as a follower count, an account balance, or an order history. This package is the Reclaim MCP server: it gives your agent the tools to find, build, test, and fix data verification providers by capturing and replaying browser requests, drafting provider recipes, running zkTLS proofs, and publishing provider versions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"README.md"
|
|
45
45
|
],
|
|
46
46
|
"engines": {
|
|
47
|
-
"node": ">=22.
|
|
47
|
+
"node": ">=22.12.0"
|
|
48
48
|
},
|
|
49
49
|
"repository": {
|
|
50
50
|
"url": "https://github.com/reclaimprotocol/builder",
|
|
@@ -82,8 +82,9 @@
|
|
|
82
82
|
"@hapi/boom": "^9.1.4",
|
|
83
83
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
84
84
|
"@noble/hashes": "^2.2.0",
|
|
85
|
-
"@
|
|
86
|
-
"@reclaimprotocol/
|
|
85
|
+
"@puppeteer/browsers": "3.2.4",
|
|
86
|
+
"@reclaimprotocol/attestor-core": "^5.1.6",
|
|
87
|
+
"@reclaimprotocol/client": "0.1.37",
|
|
87
88
|
"@reclaimprotocol/tls": "^0.1.3",
|
|
88
89
|
"chrome-remote-interface": "^0.33.0",
|
|
89
90
|
"domhandler": "^6.0.0",
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"parse5-htmlparser2-tree-adapter": "^8.0.0",
|
|
93
94
|
"pino": "^9.14.0",
|
|
94
95
|
"xpath": "^0.0.34",
|
|
96
|
+
"yauzl": "3.4.0",
|
|
95
97
|
"zod": "^4.4.3"
|
|
96
98
|
},
|
|
97
99
|
"devDependencies": {
|
package/skill/SKILL.builder.md
CHANGED
|
@@ -15,6 +15,11 @@ Build a new org-private provider for the claim the developer described. Ask
|
|
|
15
15
|
one question if the site, account, or value is ambiguous. Copy this checklist
|
|
16
16
|
and check off each step. Read the linked topic before a step you don't know.
|
|
17
17
|
|
|
18
|
+
Steps 2 through 10 need the developer at the browser, so do them before other
|
|
19
|
+
work, such as app code. When you need the developer to answer or act, end
|
|
20
|
+
your turn with the question and every link they need. Don't use a background
|
|
21
|
+
or asynchronous question tool for a blocking question.
|
|
22
|
+
|
|
18
23
|
- [ ] 1. Name the data points: only the values the claim must prove, usually
|
|
19
24
|
one or two. No identity or profile fields.
|
|
20
25
|
- [ ] 2. `attach_browser`, then `list_tabs` → `start_capture(tabId)` ⇒
|
|
@@ -34,7 +39,7 @@ and check off each step. Read the linked topic before a step you don't know.
|
|
|
34
39
|
→ topic `params`
|
|
35
40
|
- [ ] 9. `replay_request(captureId, draftId)`, then again with
|
|
36
41
|
`withoutSecrets: true`. → topic `prove`
|
|
37
|
-
- [ ] 10. `run_proof(captureId, draftId
|
|
42
|
+
- [ ] 10. `run_proof(captureId, draftId)`.
|
|
38
43
|
→ topics `prove`, `credentials`
|
|
39
44
|
- [ ] 11. `create_provider` (or a new version of a provider your org owns) →
|
|
40
45
|
`create_provider_version_from_capture` (publishes the kept script as
|
|
@@ -303,7 +308,7 @@ with no secrets/cookies, as an anonymous visitor.
|
|
|
303
308
|
`viewer`/`currentUser` API) and re-check. If the value only ever appears on
|
|
304
309
|
public responses, tell the developer it can't be proven as an identity claim.
|
|
305
310
|
|
|
306
|
-
**3. Prove.** `run_proof(captureId, draftId
|
|
311
|
+
**3. Prove.** `run_proof(captureId, draftId)`. On
|
|
307
312
|
`verified: true` the full claim is written to
|
|
308
313
|
`~/.reclaim/claims/<timestamp>.json` and you get
|
|
309
314
|
`{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`.
|