@reclaimprotocol/agent 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/lib/authoring/backend.d.ts +0 -24
  2. package/lib/authoring/find.d.ts +3 -0
  3. package/lib/authoring/find.js +54 -0
  4. package/lib/authoring/index.d.ts +3 -3
  5. package/lib/authoring/index.js +4 -3
  6. package/lib/authoring/propose.d.ts +3 -0
  7. package/lib/authoring/propose.js +109 -0
  8. package/lib/authoring/replay.d.ts +10 -0
  9. package/lib/authoring/replay.js +34 -0
  10. package/lib/authoring/tools.d.ts +13 -6
  11. package/lib/authoring/tools.js +108 -293
  12. package/lib/authoring/util.d.ts +27 -0
  13. package/lib/authoring/util.js +21 -0
  14. package/lib/consts.d.ts +6 -1
  15. package/lib/consts.js +9 -2
  16. package/lib/mcp/server.d.ts +2 -41
  17. package/lib/mcp/server.js +6 -34
  18. package/lib/mcp/start.js +4 -2
  19. package/lib/mcp/tools/agent/analyze.js +3 -3
  20. package/lib/mcp/tools/agent/build.d.ts +4 -4
  21. package/lib/mcp/tools/agent/build.js +13 -15
  22. package/lib/mcp/tools/agent/capture.d.ts +13 -1
  23. package/lib/mcp/tools/agent/capture.js +18 -85
  24. package/lib/mcp/tools/agent/eval.d.ts +5 -2
  25. package/lib/mcp/tools/agent/eval.js +11 -121
  26. package/lib/mcp/tools/agent/find.d.ts +10 -3
  27. package/lib/mcp/tools/agent/find.js +25 -31
  28. package/lib/mcp/tools/agent/proof.d.ts +13 -3
  29. package/lib/mcp/tools/agent/proof.js +81 -132
  30. package/lib/mcp/tools/agent/propose.d.ts +10 -9
  31. package/lib/mcp/tools/agent/propose.js +87 -64
  32. package/lib/mcp/tools/agent/replay.d.ts +10 -3
  33. package/lib/mcp/tools/agent/replay.js +29 -36
  34. package/lib/mcp/tools/agent/session.d.ts +1 -3
  35. package/lib/mcp/tools/agent/session.js +5 -5
  36. package/lib/mcp/tools/agent/shared.d.ts +5 -3
  37. package/lib/mcp/tools/agent/shared.js +29 -19
  38. package/lib/old/client.d.ts +35 -0
  39. package/lib/old/client.js +18 -0
  40. package/lib/old/login-cdp.d.ts +24 -5
  41. package/lib/old/login-cdp.js +29 -15
  42. package/lib/old/sdk-client.d.ts +39 -0
  43. package/lib/old/sdk-client.js +85 -0
  44. package/lib/old/to-register.d.ts +69 -0
  45. package/lib/old/to-register.js +52 -2
  46. package/lib/old/tools/authenticate.js +3 -7
  47. package/lib/old/tools/build.d.ts +6 -1
  48. package/lib/old/tools/build.js +11 -1
  49. package/lib/old/tools/get-provider-info.d.ts +16 -0
  50. package/lib/old/tools/get-provider-info.js +73 -0
  51. package/lib/old/tools/get-version-config.d.ts +17 -0
  52. package/lib/old/tools/get-version-config.js +47 -0
  53. package/lib/old/tools/publish.js +189 -2
  54. package/lib/old/tools/update-provider-metadata.d.ts +16 -0
  55. package/lib/old/tools/update-provider-metadata.js +61 -0
  56. package/lib/proof/attestor.d.ts +6 -3
  57. package/lib/proof/attestor.js +10 -1
  58. package/lib/proof/debug-response-dump.d.ts +4 -0
  59. package/lib/proof/debug-response-dump.js +55 -0
  60. package/lib/proof/patch-domhandler.d.ts +1 -0
  61. package/lib/proof/patch-domhandler.js +59 -0
  62. package/lib/proof/zk-files.js +7 -6
  63. package/lib/provider/dom-xpath.d.ts +17 -0
  64. package/lib/provider/dom-xpath.js +187 -0
  65. package/lib/provider/draft.d.ts +30 -2
  66. package/lib/provider/draft.js +117 -28
  67. package/lib/provider/index.d.ts +4 -0
  68. package/lib/provider/index.js +7 -0
  69. package/lib/provider/injection-doc.js +6 -0
  70. package/lib/provider/matcher.d.ts +17 -20
  71. package/lib/provider/matcher.js +394 -280
  72. package/lib/provider/schema.d.ts +7 -5
  73. package/lib/tools/registry.d.ts +14 -0
  74. package/lib/tools/registry.js +32 -0
  75. package/package.json +9 -1
  76. package/skill/SKILL.builder.md +42 -23
  77. package/skill/SKILL.old.md +6 -2
  78. package/skill/builder-mode.md +1 -1
  79. package/skill/injection-api.md +117 -0
  80. package/src/authoring/backend.ts +98 -0
  81. package/src/authoring/index.ts +20 -0
  82. package/src/authoring/tools.ts +273 -0
  83. package/src/authoring/util.ts +42 -0
  84. package/src/consts.ts +15 -2
  85. package/src/mcp/server.ts +11 -78
  86. package/src/mcp/start.ts +4 -1
  87. package/src/mcp/tools/agent/analyze.ts +3 -3
  88. package/src/mcp/tools/agent/build.ts +13 -21
  89. package/src/mcp/tools/agent/capture.ts +31 -115
  90. package/src/mcp/tools/agent/eval.ts +19 -155
  91. package/src/mcp/tools/agent/find.ts +33 -36
  92. package/src/mcp/tools/agent/proof.ts +87 -160
  93. package/src/mcp/tools/agent/propose.ts +93 -85
  94. package/src/mcp/tools/agent/replay.ts +32 -42
  95. package/src/mcp/tools/agent/session.ts +4 -5
  96. package/src/mcp/tools/agent/shared.ts +152 -0
  97. package/src/old/client.ts +56 -0
  98. package/src/old/login-cdp.ts +29 -23
  99. package/src/old/sdk-client.ts +105 -0
  100. package/src/old/to-register.ts +98 -6
  101. package/src/old/tools/authenticate.ts +3 -11
  102. package/src/old/tools/build.ts +12 -0
  103. package/src/old/tools/get-provider-info.ts +91 -0
  104. package/src/old/tools/get-version-config.ts +56 -0
  105. package/src/old/tools/publish.ts +244 -1
  106. package/src/old/tools/update-provider-metadata.ts +72 -0
  107. package/src/proof/attestor.ts +16 -4
  108. package/src/proof/debug-response-dump.ts +70 -0
  109. package/src/proof/patch-domhandler.ts +68 -0
  110. package/src/proof/zk-files.ts +7 -6
  111. package/src/provider/dom-xpath.ts +225 -0
  112. package/src/provider/draft.ts +168 -35
  113. package/src/provider/index.ts +8 -0
  114. package/src/provider/injection-doc.ts +6 -0
  115. package/src/provider/matcher.ts +477 -357
  116. package/src/provider/schema.ts +7 -5
  117. package/src/tools/registry.ts +92 -0
@@ -1,6 +1,7 @@
1
1
  // Lazy download of the @reclaimprotocol/zk-symmetric-crypto circuit resources
2
- // that the snarkjs proof engine needs. These are ~280MB and are NOT fetched in
3
- // a `postinstall` (that blocked the MCP server's first launch past the connect
2
+ // the proof engines need (the downloader fetches every engine — stwo, snarkjs,
3
+ // gnark — as one bundle). These are ~280MB and are NOT fetched in a
4
+ // `postinstall` (that blocked the MCP server's first launch past the connect
4
5
  // timeout — see the install notes). Instead we fetch them on the first proof,
5
6
  // once, so the server starts immediately and only `run_proof` pays the cost.
6
7
  import { spawn } from 'node:child_process';
@@ -38,12 +39,12 @@ async function download() {
38
39
  return;
39
40
  }
40
41
  // mainPath is <pkg>/lib/index.js → resources live at <pkg>/resources, the
41
- // downloader at <pkg>/lib/scripts/download-files.js. We only use the snarkjs
42
- // engine (createClaimOnAttestor zkEngine: 'snarkjs'), so its presence is the
43
- // signal that the circuits are ready.
42
+ // downloader at <pkg>/lib/scripts/download-files.js. Proofs default to the
43
+ // stwo (WASM) engine, so its resource dir being present is the signal that
44
+ // the circuits are ready (the downloader fetches all engines together).
44
45
  const libDir = dirname(mainPath);
45
46
  const pkgDir = dirname(libDir);
46
- if (existsSync(join(pkgDir, 'resources', 'snarkjs'))) {
47
+ if (existsSync(join(pkgDir, 'resources', 'stwo'))) {
47
48
  return;
48
49
  }
49
50
  const downloadScript = join(libDir, 'scripts', 'download-files.js');
@@ -0,0 +1,17 @@
1
+ import '../proof/patch-domhandler.ts';
2
+ export interface DomAnchor {
3
+ xPath: string;
4
+ /** Byte range of the element in the source html (parse5 source locations),
5
+ * matching the slice the attestor reveals for this xPath. */
6
+ start: number;
7
+ end: number;
8
+ }
9
+ /**
10
+ * A verified xPath to the tightest element containing one of `targets`, or
11
+ * undefined if the value isn't present in the parsed tree. Prefers a short
12
+ * stable selector (unique id → identifying attr → class), falling back to an
13
+ * absolute child-indexed path (`/html[1]/body[1]/…/option[1]`). Every candidate
14
+ * is checked with the real `xpath` engine and only returned if it resolves,
15
+ * first-match, to that exact element.
16
+ */
17
+ export declare function elementAnchor(html: string, targets: string[]): DomAnchor | undefined;
@@ -0,0 +1,187 @@
1
+ // Generate a verified xPath to the element holding a target value, using the
2
+ // SAME parser the attestor uses (parse5 + parse5-htmlparser2-tree-adapter) and
3
+ // self-verifying each candidate with the same `xpath` engine. Because the path
4
+ // is built from — and checked against — the attestor's own parse of the raw
5
+ // response body, it provably resolves to that element at proof time, on any
6
+ // site (no per-endpoint logic, no fragile byte-offset counting).
7
+ //
8
+ // The domhandler patch makes the tree's nodes DOM-like for `xpath` (see
9
+ // patch-domhandler.ts — mirrors attestor-core's patch-parse5-tree).
10
+ import { isTag, isText } from 'domhandler';
11
+ import { parse } from 'parse5';
12
+ import { adapter as htmlAdapter } from 'parse5-htmlparser2-tree-adapter';
13
+ import xpath from 'xpath';
14
+ import "../proof/patch-domhandler.js";
15
+ /** Identifying attributes stable enough to anchor an xPath on (value-bearing
16
+ * attrs like `content`/`href`/`value` are excluded). */
17
+ const IDENT_ATTRS = ['name', 'property', 'itemprop', 'data-testid', 'rel', 'for'];
18
+ /**
19
+ * A verified xPath to the tightest element containing one of `targets`, or
20
+ * undefined if the value isn't present in the parsed tree. Prefers a short
21
+ * stable selector (unique id → identifying attr → class), falling back to an
22
+ * absolute child-indexed path (`/html[1]/body[1]/…/option[1]`). Every candidate
23
+ * is checked with the real `xpath` engine and only returned if it resolves,
24
+ * first-match, to that exact element.
25
+ */
26
+ export function elementAnchor(html, targets) {
27
+ let doc;
28
+ try {
29
+ doc = parse(html, {
30
+ treeAdapter: htmlAdapter,
31
+ sourceCodeLocationInfo: true,
32
+ });
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ // `xpath` needs a named root node to start from (as attestor-core does).
38
+ ;
39
+ doc.name = 'root';
40
+ const el = findElement(doc, targets);
41
+ if (!el) {
42
+ return undefined;
43
+ }
44
+ const { startIndex, endIndex } = el;
45
+ if (startIndex === null || endIndex === null) {
46
+ return undefined;
47
+ }
48
+ for (const candidate of candidateXPaths(el)) {
49
+ if (matchesUniquely(doc, candidate, el)) {
50
+ // `[startIndex, endIndex)` is exactly the slice the attestor reveals for
51
+ // this xPath (its getNodeRange uses these same parse5 source locations).
52
+ return { xPath: candidate, start: startIndex, end: endIndex };
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ /** The tightest element that DIRECTLY holds a target. Prefers a COMPLETE-value
58
+ * occurrence — an attribute whose value IS the target, or a text child that IS
59
+ * the target — over one where the target is merely embedded in a longer string
60
+ * (e.g. a userid inside a `1234567+user@…` noreply address). The complete
61
+ * field is the semantic source and yields a portable anchor; the embedded one
62
+ * is incidental. Smallest byte span wins within the chosen tier. */
63
+ function findElement(root, targets) {
64
+ const complete = [];
65
+ const embedded = [];
66
+ const walk = (node) => {
67
+ if (isTag(node)) {
68
+ const el = node;
69
+ const texts = el.children.filter(isText).map((c) => c.data);
70
+ const attrs = Object.values(el.attribs ?? {});
71
+ const isTarget = (v) => targets.some((t) => v === t);
72
+ const hasTarget = (v) => targets.some((t) => v.includes(t));
73
+ if (texts.some((t) => isTarget(t.trim())) || attrs.some(isTarget)) {
74
+ complete.push(el);
75
+ }
76
+ else if (texts.some(hasTarget) || attrs.some(hasTarget)) {
77
+ embedded.push(el);
78
+ }
79
+ }
80
+ if ('children' in node && Array.isArray(node.children)) {
81
+ for (const child of node.children) {
82
+ walk(child);
83
+ }
84
+ }
85
+ };
86
+ walk(root);
87
+ return smallestElement(complete.length ? complete : embedded);
88
+ }
89
+ /** Byte span of an element (0 when source locations are missing). */
90
+ function elementSpan(e) {
91
+ return (e.endIndex ?? 0) - (e.startIndex ?? 0);
92
+ }
93
+ /** The element with the smallest byte span (the tightest / deepest match). */
94
+ function smallestElement(els) {
95
+ return els
96
+ .filter((e) => e.startIndex !== null && e.endIndex !== null)
97
+ .sort((a, b) => elementSpan(a) - elementSpan(b))
98
+ .at(0);
99
+ }
100
+ /** Candidate xPaths for `el`, best (most stable / shortest) first. */
101
+ function* candidateXPaths(el) {
102
+ const tag = el.name;
103
+ const a = el.attribs ?? {};
104
+ if (a.id) {
105
+ yield `//${tag}[@id=${xpathLiteral(a.id)}]`;
106
+ }
107
+ for (const name of IDENT_ATTRS) {
108
+ if (a[name]) {
109
+ yield `//${tag}[@${name}=${xpathLiteral(a[name])}]`;
110
+ }
111
+ }
112
+ if (a.class) {
113
+ const distinctive = a.class
114
+ .split(/\s+/)
115
+ .filter(Boolean)
116
+ .sort((x, y) => y.length - x.length)[0];
117
+ if (distinctive) {
118
+ yield `//${tag}[contains(@class, ${xpathLiteral(distinctive)})]`;
119
+ }
120
+ }
121
+ // A uniquely-identified DIRECT child pins the parent without a positional
122
+ // path (`//span[span[@id="labels"]]`). Self-verification (firstSelected)
123
+ // discards it when the child id/attr isn't unique — e.g. GitHub reuses
124
+ // `id="labels"` per email row — so it never silently matches the wrong node.
125
+ for (const child of el.children) {
126
+ if (!isTag(child)) {
127
+ continue;
128
+ }
129
+ const ca = child.attribs ?? {};
130
+ if (ca.id) {
131
+ yield `//${tag}[${child.name}[@id=${xpathLiteral(ca.id)}]]`;
132
+ }
133
+ for (const name of IDENT_ATTRS) {
134
+ if (ca[name]) {
135
+ yield `//${tag}[${child.name}[@${name}=${xpathLiteral(ca[name])}]]`;
136
+ }
137
+ }
138
+ }
139
+ yield absolutePath(el);
140
+ }
141
+ /** Chrome "full XPath"-style absolute path: `/html[1]/body[1]/…/tag[n]`, where
142
+ * n is the element's 1-based position among same-tag siblings. */
143
+ function absolutePath(el) {
144
+ const parts = [];
145
+ let node = el;
146
+ while (node && isTag(node)) {
147
+ const tag = node.name;
148
+ let index = 1;
149
+ for (let sib = node.prev; sib; sib = sib.prev) {
150
+ if (isTag(sib) && sib.name === tag) {
151
+ index++;
152
+ }
153
+ }
154
+ parts.unshift(`/${tag}[${index}]`);
155
+ node = node.parent;
156
+ }
157
+ return parts.join('');
158
+ }
159
+ /** True when `expression` selects EXACTLY `el` (one match) against `doc`. The
160
+ * attestor applies the redaction regex to EVERY xPath match and throws if any
161
+ * fails, so a non-unique selector (e.g. a class shared by sibling rows) is
162
+ * unusable — it must resolve to the single target element. */
163
+ function matchesUniquely(doc, expression, el) {
164
+ try {
165
+ const engine = xpath;
166
+ const nodes = engine.parse(expression).select({
167
+ node: doc,
168
+ allowAnyNamespaceForNoPrefix: true,
169
+ });
170
+ return nodes.length === 1 && nodes[0] === el;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ /** Quote a string as an XPath 1.0 string literal (concat() when it holds both
177
+ * quote characters). */
178
+ function xpathLiteral(s) {
179
+ if (!s.includes('"')) {
180
+ return `"${s}"`;
181
+ }
182
+ if (!s.includes("'")) {
183
+ return `'${s}'`;
184
+ }
185
+ const parts = s.split('"').map((p) => `"${p}"`);
186
+ return `concat(${parts.join(', \'"\', ')})`;
187
+ }
@@ -1,6 +1,34 @@
1
- import type { CapturedRequest, OprfMode, ReclaimProvider, SecretParamRef } from './schema.ts';
1
+ import type { CapturedRequest, OprfMode, ReclaimProvider, ResponseMatch, ResponseRedaction, SecretParamRef } from './schema.ts';
2
+ /** One value to extract from the request: the captured `value`, a `name` (its
3
+ * param / capture-group), and an optional per-target OPRF `hash`. */
4
+ export interface DraftTarget {
5
+ value: string;
6
+ name: string;
7
+ hash?: OprfMode;
8
+ }
9
+ /** The realized extraction for one target — its value paired with the
10
+ * redaction that reveals it (absent when matched literally). Lets a consumer
11
+ * validate each value against its own selector (e.g. the cloud replay). */
12
+ export interface Extraction {
13
+ name: string;
14
+ value: string;
15
+ redaction?: ResponseRedaction;
16
+ }
2
17
  export interface DraftResult {
3
18
  provider: ReclaimProvider;
4
19
  secretRefs: SecretParamRef[];
20
+ /** One entry per target, in `responseMatches` order. */
21
+ extractions: Extraction[];
5
22
  }
6
- export declare function draftProvider(req: CapturedRequest, target: string, name: string, hash?: OprfMode): DraftResult;
23
+ /** Reject a draft that bakes a captured per-user value as a LITERAL into a
24
+ * match or redaction — either a neighbouring target's value or a target's own
25
+ * value left un-templated. The matcher builds each target's window
26
+ * independently and drops the per-user WORDS it can see, but it cannot know a
27
+ * neighbouring value is itself user-specific (e.g. a name sharing a text node
28
+ * with an id — a `<b>{{id}}</b>` sitting beside a `<td>{{name}}</td>`).
29
+ * Cross-checking every value against every match here catches that: a portable
30
+ * provider contains NO captured value as a literal — each appears only as its
31
+ * `{{param}}`. Throwing discards the draft so the agent re-captures or
32
+ * re-proposes rather than shipping a provider pinned to this one user. */
33
+ export declare function assertPortableMatches(matches: ResponseMatch[], redactions: ResponseRedaction[], targets: DraftTarget[]): void;
34
+ export declare function draftProvider(req: CapturedRequest, targets: DraftTarget[], name: string, hash?: OprfMode): DraftResult;
@@ -1,20 +1,78 @@
1
1
  import { synthesizeMatcher } from "./matcher.js";
2
2
  import { splitSecrets } from "./secrets.js";
3
- /** Replace any existing `accept-encoding` (case-insensitive) with a
4
- * canonical `Accept-Encoding: identity`. Strips browser-captured
5
- * encodings like `gzip, deflate, br, zstd` that would otherwise make
6
- * the attestor match against compressed response bytes. */
7
- function withIdentityEncoding(headers) {
3
+ /** Request headers that must NOT survive into the provider params. Two kinds:
4
+ * - Conditional headers (`if-none-match`/`if-modified-since`/…): a captured
5
+ * cached ETag/date makes the origin answer 304/412 when the attestor
6
+ * re-issues, and the attestor treats non-2xx as failure. We always want the
7
+ * full fresh response.
8
+ * - Transfer headers the ATTESTOR sets itself (`connection: close`,
9
+ * `accept-encoding: identity`). Passing our own duplicates/collides with the
10
+ * request the attestor builds, so leave them out — the attestor already
11
+ * forces `identity` (uncompressed, so its byte-matching works). */
12
+ const STRIP_REQUEST_HEADERS = new Set([
13
+ 'if-none-match',
14
+ 'if-modified-since',
15
+ 'if-match',
16
+ 'if-unmodified-since',
17
+ 'if-range',
18
+ 'connection',
19
+ 'accept-encoding',
20
+ ]);
21
+ /** Drop the headers the attestor manages or that would 304 at proof time
22
+ * (see {@link STRIP_REQUEST_HEADERS}); keep every other captured header so the
23
+ * attestor's replay looks like the browser's request. */
24
+ function sanitizeRequestHeaders(headers) {
8
25
  const out = {};
9
26
  for (const [k, v] of Object.entries(headers)) {
10
- if (k.toLowerCase() === 'accept-encoding') {
27
+ if (STRIP_REQUEST_HEADERS.has(k.toLowerCase())) {
11
28
  continue;
12
29
  }
13
30
  out[k] = v;
14
31
  }
15
- out['Accept-Encoding'] = 'identity';
16
32
  return out;
17
33
  }
34
+ /** Reject a draft that bakes a captured per-user value as a LITERAL into a
35
+ * match or redaction — either a neighbouring target's value or a target's own
36
+ * value left un-templated. The matcher builds each target's window
37
+ * independently and drops the per-user WORDS it can see, but it cannot know a
38
+ * neighbouring value is itself user-specific (e.g. a name sharing a text node
39
+ * with an id — a `<b>{{id}}</b>` sitting beside a `<td>{{name}}</td>`).
40
+ * Cross-checking every value against every match here catches that: a portable
41
+ * provider contains NO captured value as a literal — each appears only as its
42
+ * `{{param}}`. Throwing discards the draft so the agent re-captures or
43
+ * re-proposes rather than shipping a provider pinned to this one user. */
44
+ export function assertPortableMatches(matches, redactions, targets) {
45
+ // Guard only values long enough to be plausibly user-specific; shorter runs
46
+ // (single digits, 2-3 char tokens) collide with ordinary structural bytes.
47
+ const MIN_ALNUM = 4;
48
+ const guarded = targets
49
+ .map((t) => t.value)
50
+ .filter((v) => v.replace(/[^A-Za-z0-9]/g, '').length >= MIN_ALNUM);
51
+ if (!guarded.length) {
52
+ return;
53
+ }
54
+ const stripParams = (s) => s.replace(/\{\{[^}]*\}\}/g, '');
55
+ const scan = (text, where, own) => {
56
+ const bare = stripParams(text);
57
+ for (const v of guarded) {
58
+ if (bare.includes(v)) {
59
+ const kind = v === own
60
+ ? 'its own value un-templated'
61
+ : "a neighbouring target's per-user value";
62
+ throw new Error(`drafted ${where} bakes ${kind} ("${v}") as a literal — the `
63
+ + 'provider would be pinned to this user. Re-capture or re-propose.');
64
+ }
65
+ }
66
+ };
67
+ for (const [i, m] of matches.entries()) {
68
+ scan(m.value, `responseMatch for "${targets[i]?.name ?? '?'}"`, targets[i]?.value ?? null);
69
+ }
70
+ for (const r of redactions) {
71
+ if (r.regex) {
72
+ scan(r.regex, 'responseRedaction regex', null);
73
+ }
74
+ }
75
+ }
18
76
  /** Derive a `paramValues` key (and capture-group name) from the provider
19
77
  * name's last alphanumeric token. `"GitHub Username"` → `"username"`,
20
78
  * `"x-followers"` → `"followers"`. */
@@ -23,26 +81,57 @@ function deriveParamName(providerName) {
23
81
  const last = tokens[tokens.length - 1] ?? 'value';
24
82
  return last.toLowerCase();
25
83
  }
26
- export function draftProvider(req, target, name, hash) {
84
+ export function draftProvider(req, targets, name, hash) {
27
85
  if (!req.responseBody) {
28
86
  throw new Error(`captured request ${req.requestId} has no response body`);
29
87
  }
30
- const paramName = deriveParamName(name);
31
- const synthesis = synthesizeMatcher(req.responseBody, req.contentType, target, paramName, hash);
32
- if (!synthesis) {
33
- throw new Error(`target value "${target}" not found in` +
34
- ` response body for request ${req.requestId}`);
88
+ if (targets.length === 0) {
89
+ throw new Error('draftProvider needs at least one target');
90
+ }
91
+ // One request → N matches + N redactions. Synthesize each target against the
92
+ // SAME response body; a distinct param name per target keeps their capture
93
+ // groups + paramValues from colliding.
94
+ const matches = [];
95
+ const redactions = [];
96
+ const extractions = [];
97
+ let paramValues;
98
+ const seenParams = new Set();
99
+ for (const t of targets) {
100
+ const paramName = deriveParamName(t.name);
101
+ if (seenParams.has(paramName)) {
102
+ throw new Error(`two targets resolve to the same param "${paramName}"; `
103
+ + 'give them distinct names');
104
+ }
105
+ seenParams.add(paramName);
106
+ const synthesis = synthesizeMatcher(req.responseBody, req.contentType, t.value, paramName, t.hash ?? hash);
107
+ if (!synthesis) {
108
+ throw new Error(`target value "${t.value}" not found in`
109
+ + ` response body for request ${req.requestId}`);
110
+ }
111
+ matches.push(synthesis.match);
112
+ if (synthesis.redaction) {
113
+ redactions.push(synthesis.redaction);
114
+ }
115
+ if (synthesis.paramValues) {
116
+ paramValues = { ...paramValues, ...synthesis.paramValues };
117
+ }
118
+ extractions.push({
119
+ name: paramName,
120
+ value: t.value,
121
+ ...(synthesis.redaction ? { redaction: synthesis.redaction } : {}),
122
+ });
35
123
  }
124
+ // Portability gate: never ship a draft that bakes a per-user value literally.
125
+ assertPortableMatches(matches, redactions, targets);
36
126
  const { secrets, publics } = splitSecrets(req.requestHeaders);
37
- // Force the server to respond uncompressed. Captured browser requests
38
- // usually carry `accept-encoding: gzip, deflate, br, zstd`; the attestor
39
- // matches against the raw response bytes (no transparent decompression
40
- // in the TLS replay path), so gzip/br/zstd payloads make every
41
- // xPath/jsonPath/regex redaction miss against unparseable bytes.
42
- // Replacing here also benefits `replay_request` — Node's `fetch` does
43
- // auto-decompress, but pinning the encoding keeps replay and proof
44
- // looking at the same body shape end-to-end.
45
- const headersForProvider = withIdentityEncoding(publics);
127
+ // Sanitize the public headers before they enter the provider. Two problems
128
+ // a captured browser request carries: (1) `accept-encoding: gzip, …`, which
129
+ // makes the attestor match against compressed bytes (it does no transparent
130
+ // decompression in the TLS replay path) — pin it to identity; (2) conditional
131
+ // headers (`if-none-match`/`if-modified-since`), which make the origin answer
132
+ // 304 at proof time and fail the proof. Sanitizing here also keeps
133
+ // `replay_request` and the proof looking at the same full-200 body shape.
134
+ const headersForProvider = sanitizeRequestHeaders(publics);
46
135
  const secretRefs = Object.keys(secrets).map((name) => ({
47
136
  name,
48
137
  source: name.toLowerCase() === 'cookie' ? 'cookie' : 'header',
@@ -53,16 +142,16 @@ export function draftProvider(req, target, name, hash) {
53
142
  url: req.url,
54
143
  method: req.method,
55
144
  headers: headersForProvider,
56
- responseMatches: [synthesis.match],
145
+ responseMatches: matches,
57
146
  };
58
- if (synthesis.redaction) {
59
- provider.responseRedactions = [synthesis.redaction];
147
+ if (redactions.length) {
148
+ provider.responseRedactions = redactions;
60
149
  }
61
- if (synthesis.paramValues) {
62
- provider.paramValues = synthesis.paramValues;
150
+ if (paramValues) {
151
+ provider.paramValues = paramValues;
63
152
  }
64
153
  if (req.requestBody) {
65
154
  provider.body = req.requestBody;
66
155
  }
67
- return { provider, secretRefs };
156
+ return { provider, secretRefs, extractions };
68
157
  }
@@ -1,5 +1,9 @@
1
+ export { draftProvider } from './draft.ts';
2
+ export type { DraftResult } from './draft.ts';
3
+ export { detectConcerns } from './constraints.ts';
1
4
  export { extractGraphqlOp, looksLikeGraphqlEndpoint } from './graphql.ts';
2
5
  export type { CapturedRequest, HttpMethod, OprfMode, ReclaimProvider, RequestConcern, ResponseMatch, ResponseRedaction, SecretParamRef, } from './schema.ts';
3
6
  export { classifyHeader, splitSecrets } from './secrets.ts';
7
+ export { providerToRequestSelection } from './to-version.ts';
4
8
  export { hashProviderParams, providers } from '@reclaimprotocol/attestor-core';
5
9
  export type { ProviderParams } from '@reclaimprotocol/attestor-core';
@@ -1,8 +1,15 @@
1
1
  // Provider primitives shared with the app's capture pipeline:
2
2
  // the attestor-derived recipe types, secret classification, and the GraphQL
3
3
  // op parser. Exposed as `@reclaimprotocol/agent/provider`.
4
+ // Provider synthesis from a captured request (the `propose_provider` core):
5
+ // drafts the canonical responseMatches/redactions + paramValues + secret refs.
6
+ export { draftProvider } from "./draft.js";
7
+ export { detectConcerns } from "./constraints.js";
4
8
  export { extractGraphqlOp, looksLikeGraphqlEndpoint } from "./graphql.js";
5
9
  export { classifyHeader, splitSecrets } from "./secrets.js";
10
+ // Recipe → version `requests[]` (a `RequestSelection`) — the same mapping the
11
+ // MCP publish uses, so the cloud authoring publish stays byte-identical.
12
+ export { providerToRequestSelection } from "./to-version.js";
6
13
  // Re-exported attestor-core provider primitives (the registry, the param hash,
7
14
  // and the param type) so the builder imports them via the agent rather than
8
15
  // depending on @reclaimprotocol/attestor-core directly.
@@ -11,6 +11,12 @@
11
11
  * standard APIs; the client already intercepts page requests) and treats
12
12
  * `window.Reclaim` as a rarely-needed edge-case bridge, pointing at the
13
13
  * authoritative `src/support/env.d.ts` rather than reproducing its surface.
14
+ * It also covers the HAWKEYE-style live-interception path (`injectionType` /
15
+ * `interceptorType`, `disableRequestReplay` / `isDocumentRequestReplayEnabled`)
16
+ * and `bodySniff`, reverse-engineered from the old-devtools Prisma schema,
17
+ * controller code, and dashboard field hints — neither publish tool exposes
18
+ * these as inputs (both only build the standard replay flow), so this is
19
+ * read-the-field-and-recognize-it documentation, not authoring guidance.
14
20
  */
15
21
  import { readFileSync } from 'node:fs';
16
22
  import { fileURLToPath } from 'node:url';
@@ -6,31 +6,28 @@ export declare const OPRF_MAX_BYTES = 62;
6
6
  export interface MatcherSynthesis {
7
7
  match: ResponseMatch;
8
8
  redaction?: ResponseRedaction;
9
- /** Always set: the `{{paramName}}` template in `match.value` resolves
10
- * to this at proof time. The original captured value is carried here
11
- * so the prover can replay/proof against their actual data without
12
- * baking it into the provider. */
9
+ /** The captured value, keyed by `paramName`. NOT baked into the shipped
10
+ * recipe — the caller uses it for the author's own proof; each verifying
11
+ * user's value is re-extracted live (see verify/run.ts `extractBareParams`).
12
+ */
13
13
  paramValues?: Record<string, string>;
14
14
  }
15
15
  /**
16
- * Build the matcher pieces (responseMatches + responseRedactions +
17
- * paramValues) for a target value seen in a captured response.
16
+ * Build the matcher pieces (one responseMatch + one responseRedaction) for a
17
+ * target value seen in a captured response. The design, in one line:
18
18
  *
19
- * All outputs share the same shape:
20
- * - responseMatches: a single `contains` whose value is the surrounding
21
- * structural context with the target replaced by `{{paramName}}`
22
- * - paramValues: `{ [paramName]: target }` — the prover's real value
23
- * - responseRedactions: one entry that narrows the revealed slice
24
- * (xPath / jsonPath / regex), and — when `hash` is requested — also
25
- * carries a `regex` with a named capture group + a `hash` field for
26
- * OPRF
19
+ * the REDACTION selects with xPath/jsonPath and names the value with a
20
+ * capture group; the MATCH is a `contains` of the surrounding structure with
21
+ * the value replaced by `{{paramName}}`.
27
22
  *
28
- * `hash` is opt-in. The skill prompts the dev. Drafting without `hash`
29
- * is the default — the value travels in cleartext to the attestor.
23
+ * The match is what the attestor enforces (against the prover's revealed
24
+ * bytes), so it must anchor the value in context the prover can't relocate — a
25
+ * JSON key (`"login":"{{v}}"`) or the value's HTML element (`<span …>{{v}}`).
26
+ * The redaction's regex only extracts/narrows; xPath/jsonPath do the selecting.
30
27
  *
31
- * Throws when `hash` is set but the target has no safe character class
32
- * (unicode, punctuation, spaces — anything that doesn't fit a digit /
33
- * slug / email regex). Drafting OPRF for a value we can't capture
34
- * reliably would yield a broken provider.
28
+ * `hash` (OPRF) is opt-in — enable it only when the request asks to protect
29
+ * PII. It rides the `contains {{v}}` path: the redaction carries `hash`, match
30
+ * keeps `{{v}}`, and the attestor substitutes the nullifier from paramValues at
31
+ * proof time. Throws when `hash` is set on a free-form value (no safe class).
35
32
  */
36
33
  export declare function synthesizeMatcher(body: string, contentType: string, target: string, paramName: string, hash?: OprfMode): MatcherSynthesis | undefined;