@reclaimprotocol/agent 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +5 -5
  2. package/lib/authoring/backend.d.ts +71 -0
  3. package/lib/authoring/backend.js +24 -0
  4. package/lib/authoring/find.d.ts +3 -0
  5. package/lib/authoring/find.js +54 -0
  6. package/lib/authoring/index.d.ts +4 -0
  7. package/lib/authoring/index.js +6 -0
  8. package/lib/authoring/propose.d.ts +3 -0
  9. package/lib/authoring/propose.js +109 -0
  10. package/lib/authoring/replay.d.ts +10 -0
  11. package/lib/authoring/replay.js +34 -0
  12. package/lib/authoring/tools.d.ts +16 -0
  13. package/lib/authoring/tools.js +193 -0
  14. package/lib/authoring/util.d.ts +27 -0
  15. package/lib/authoring/util.js +21 -0
  16. package/lib/cdp/capture-engine.js +1 -1
  17. package/lib/consts.d.ts +6 -1
  18. package/lib/consts.js +9 -2
  19. package/lib/mcp/api/operations.gen.js +46 -18
  20. package/lib/mcp/server.d.ts +2 -25
  21. package/lib/mcp/server.js +4 -17
  22. package/lib/mcp/start.js +4 -2
  23. package/lib/mcp/tools/agent/analyze.js +3 -3
  24. package/lib/mcp/tools/agent/build.d.ts +4 -4
  25. package/lib/mcp/tools/agent/build.js +13 -15
  26. package/lib/mcp/tools/agent/capture.d.ts +13 -1
  27. package/lib/mcp/tools/agent/capture.js +18 -85
  28. package/lib/mcp/tools/agent/eval.d.ts +5 -2
  29. package/lib/mcp/tools/agent/eval.js +11 -121
  30. package/lib/mcp/tools/agent/find.d.ts +10 -3
  31. package/lib/mcp/tools/agent/find.js +25 -31
  32. package/lib/mcp/tools/agent/proof.d.ts +13 -3
  33. package/lib/mcp/tools/agent/proof.js +81 -132
  34. package/lib/mcp/tools/agent/propose.d.ts +10 -9
  35. package/lib/mcp/tools/agent/propose.js +87 -64
  36. package/lib/mcp/tools/agent/publish.js +2 -2
  37. package/lib/mcp/tools/agent/replay.d.ts +10 -3
  38. package/lib/mcp/tools/agent/replay.js +29 -36
  39. package/lib/mcp/tools/agent/session.d.ts +1 -3
  40. package/lib/mcp/tools/agent/session.js +5 -5
  41. package/lib/mcp/tools/agent/shared.d.ts +8 -0
  42. package/lib/mcp/tools/agent/shared.js +125 -0
  43. package/lib/old/client.d.ts +35 -0
  44. package/lib/old/client.js +18 -0
  45. package/lib/old/login-cdp.d.ts +24 -0
  46. package/lib/old/login-cdp.js +28 -3
  47. package/lib/old/sdk-client.d.ts +39 -0
  48. package/lib/old/sdk-client.js +85 -0
  49. package/lib/old/to-register.d.ts +69 -0
  50. package/lib/old/to-register.js +52 -2
  51. package/lib/old/tools/build.d.ts +6 -1
  52. package/lib/old/tools/build.js +11 -1
  53. package/lib/old/tools/get-provider-info.d.ts +16 -0
  54. package/lib/old/tools/get-provider-info.js +73 -0
  55. package/lib/old/tools/get-version-config.d.ts +17 -0
  56. package/lib/old/tools/get-version-config.js +47 -0
  57. package/lib/old/tools/publish.js +189 -2
  58. package/lib/old/tools/update-provider-metadata.d.ts +16 -0
  59. package/lib/old/tools/update-provider-metadata.js +61 -0
  60. package/lib/proof/attestor.d.ts +6 -3
  61. package/lib/proof/attestor.js +10 -1
  62. package/lib/proof/debug-response-dump.d.ts +4 -0
  63. package/lib/proof/debug-response-dump.js +55 -0
  64. package/lib/proof/patch-domhandler.d.ts +1 -0
  65. package/lib/proof/patch-domhandler.js +59 -0
  66. package/lib/proof/proof-core.d.ts +1 -1
  67. package/lib/proof/proof-core.js +1 -1
  68. package/lib/proof/zk-files.js +7 -6
  69. package/lib/provider/dom-xpath.d.ts +17 -0
  70. package/lib/provider/dom-xpath.js +187 -0
  71. package/lib/provider/draft.d.ts +30 -2
  72. package/lib/provider/draft.js +117 -28
  73. package/lib/provider/index.d.ts +4 -0
  74. package/lib/provider/index.js +7 -0
  75. package/lib/provider/injection-doc.js +6 -0
  76. package/lib/provider/matcher.d.ts +17 -20
  77. package/lib/provider/matcher.js +394 -280
  78. package/lib/provider/schema.d.ts +7 -5
  79. package/lib/tools/registry.d.ts +50 -0
  80. package/lib/tools/registry.js +49 -0
  81. package/package.json +9 -1
  82. package/skill/SKILL.builder.md +55 -32
  83. package/skill/SKILL.old.md +20 -12
  84. package/skill/builder-mode.md +1 -1
  85. package/skill/injection-api.md +117 -0
  86. package/src/authoring/backend.ts +98 -0
  87. package/src/authoring/index.ts +20 -0
  88. package/src/authoring/tools.ts +273 -0
  89. package/src/authoring/util.ts +42 -0
  90. package/src/cdp/capture-engine.ts +1 -1
  91. package/src/consts.ts +15 -2
  92. package/src/mcp/api/operations.gen.ts +46 -18
  93. package/src/mcp/server.ts +9 -39
  94. package/src/mcp/start.ts +4 -1
  95. package/src/mcp/tools/agent/analyze.ts +3 -3
  96. package/src/mcp/tools/agent/build.ts +13 -21
  97. package/src/mcp/tools/agent/capture.ts +31 -115
  98. package/src/mcp/tools/agent/eval.ts +19 -155
  99. package/src/mcp/tools/agent/find.ts +33 -36
  100. package/src/mcp/tools/agent/proof.ts +87 -160
  101. package/src/mcp/tools/agent/propose.ts +93 -85
  102. package/src/mcp/tools/agent/publish.ts +2 -2
  103. package/src/mcp/tools/agent/replay.ts +32 -42
  104. package/src/mcp/tools/agent/session.ts +4 -5
  105. package/src/mcp/tools/agent/shared.ts +152 -0
  106. package/src/old/client.ts +56 -0
  107. package/src/old/login-cdp.ts +28 -3
  108. package/src/old/sdk-client.ts +105 -0
  109. package/src/old/to-register.ts +98 -6
  110. package/src/old/tools/build.ts +12 -0
  111. package/src/old/tools/get-provider-info.ts +91 -0
  112. package/src/old/tools/get-version-config.ts +56 -0
  113. package/src/old/tools/publish.ts +244 -1
  114. package/src/old/tools/update-provider-metadata.ts +72 -0
  115. package/src/proof/attestor.ts +16 -4
  116. package/src/proof/debug-response-dump.ts +70 -0
  117. package/src/proof/patch-domhandler.ts +68 -0
  118. package/src/proof/proof-core.ts +1 -1
  119. package/src/proof/zk-files.ts +7 -6
  120. package/src/provider/dom-xpath.ts +225 -0
  121. package/src/provider/draft.ts +168 -35
  122. package/src/provider/index.ts +8 -0
  123. package/src/provider/injection-doc.ts +6 -0
  124. package/src/provider/matcher.ts +477 -357
  125. package/src/provider/schema.ts +7 -5
  126. package/src/tools/registry.ts +92 -0
@@ -0,0 +1,225 @@
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 { type Element, isTag, isText, type Node } 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.ts'
15
+
16
+ /** Identifying attributes stable enough to anchor an xPath on (value-bearing
17
+ * attrs like `content`/`href`/`value` are excluded). */
18
+ const IDENT_ATTRS =
19
+ ['name', 'property', 'itemprop', 'data-testid', 'rel', 'for']
20
+
21
+ export interface DomAnchor {
22
+ xPath: string
23
+ /** Byte range of the element in the source html (parse5 source locations),
24
+ * matching the slice the attestor reveals for this xPath. */
25
+ start: number
26
+ end: number
27
+ }
28
+
29
+ /**
30
+ * A verified xPath to the tightest element containing one of `targets`, or
31
+ * undefined if the value isn't present in the parsed tree. Prefers a short
32
+ * stable selector (unique id → identifying attr → class), falling back to an
33
+ * absolute child-indexed path (`/html[1]/body[1]/…/option[1]`). Every candidate
34
+ * is checked with the real `xpath` engine and only returned if it resolves,
35
+ * first-match, to that exact element.
36
+ */
37
+ export function elementAnchor(
38
+ html: string,
39
+ targets: string[],
40
+ ): DomAnchor | undefined {
41
+ let doc: unknown
42
+ try {
43
+ doc = parse(html, {
44
+ treeAdapter: htmlAdapter,
45
+ sourceCodeLocationInfo: true,
46
+ })
47
+ } catch{
48
+ return undefined
49
+ }
50
+
51
+ // `xpath` needs a named root node to start from (as attestor-core does).
52
+ ;(doc as { name: string }).name = 'root'
53
+
54
+ const el = findElement(doc as Node, targets)
55
+ if(!el) {
56
+ return undefined
57
+ }
58
+
59
+ const { startIndex, endIndex } = el
60
+ if(startIndex === null || endIndex === null) {
61
+ return undefined
62
+ }
63
+
64
+ for(const candidate of candidateXPaths(el)) {
65
+ if(matchesUniquely(doc, candidate, el)) {
66
+ // `[startIndex, endIndex)` is exactly the slice the attestor reveals for
67
+ // this xPath (its getNodeRange uses these same parse5 source locations).
68
+ return { xPath: candidate, start: startIndex, end: endIndex }
69
+ }
70
+ }
71
+
72
+ return undefined
73
+ }
74
+
75
+ /** The tightest element that DIRECTLY holds a target. Prefers a COMPLETE-value
76
+ * occurrence — an attribute whose value IS the target, or a text child that IS
77
+ * the target — over one where the target is merely embedded in a longer string
78
+ * (e.g. a userid inside a `1234567+user@…` noreply address). The complete
79
+ * field is the semantic source and yields a portable anchor; the embedded one
80
+ * is incidental. Smallest byte span wins within the chosen tier. */
81
+ function findElement(root: Node, targets: string[]): Element | undefined {
82
+ const complete: Element[] = []
83
+ const embedded: Element[] = []
84
+
85
+ const walk = (node: Node): void => {
86
+ if(isTag(node)) {
87
+ const el = node
88
+ const texts = el.children.filter(isText).map((c) => c.data)
89
+ const attrs = Object.values(el.attribs ?? {})
90
+ const isTarget = (v: string): boolean => targets.some((t) => v === t)
91
+ const hasTarget =
92
+ (v: string): boolean => targets.some((t) => v.includes(t))
93
+ if(texts.some((t) => isTarget(t.trim())) || attrs.some(isTarget)) {
94
+ complete.push(el)
95
+ } else if(texts.some(hasTarget) || attrs.some(hasTarget)) {
96
+ embedded.push(el)
97
+ }
98
+ }
99
+
100
+ if('children' in node && Array.isArray(node.children)) {
101
+ for(const child of node.children) {
102
+ walk(child)
103
+ }
104
+ }
105
+ }
106
+
107
+ walk(root)
108
+ return smallestElement(complete.length ? complete : embedded)
109
+ }
110
+
111
+ /** Byte span of an element (0 when source locations are missing). */
112
+ function elementSpan(e: Element): number {
113
+ return (e.endIndex ?? 0) - (e.startIndex ?? 0)
114
+ }
115
+
116
+ /** The element with the smallest byte span (the tightest / deepest match). */
117
+ function smallestElement(els: Element[]): Element | undefined {
118
+ return els
119
+ .filter((e) => e.startIndex !== null && e.endIndex !== null)
120
+ .sort((a, b) => elementSpan(a) - elementSpan(b))
121
+ .at(0)
122
+ }
123
+
124
+ /** Candidate xPaths for `el`, best (most stable / shortest) first. */
125
+ function* candidateXPaths(el: Element): Generator<string> {
126
+ const tag = el.name
127
+ const a = el.attribs ?? {}
128
+ if(a.id) {
129
+ yield `//${tag}[@id=${xpathLiteral(a.id)}]`
130
+ }
131
+
132
+ for(const name of IDENT_ATTRS) {
133
+ if(a[name]) {
134
+ yield `//${tag}[@${name}=${xpathLiteral(a[name])}]`
135
+ }
136
+ }
137
+
138
+ if(a.class) {
139
+ const distinctive = a.class
140
+ .split(/\s+/)
141
+ .filter(Boolean)
142
+ .sort((x, y) => y.length - x.length)[0]
143
+ if(distinctive) {
144
+ yield `//${tag}[contains(@class, ${xpathLiteral(distinctive)})]`
145
+ }
146
+ }
147
+
148
+ // A uniquely-identified DIRECT child pins the parent without a positional
149
+ // path (`//span[span[@id="labels"]]`). Self-verification (firstSelected)
150
+ // discards it when the child id/attr isn't unique — e.g. GitHub reuses
151
+ // `id="labels"` per email row — so it never silently matches the wrong node.
152
+ for(const child of el.children) {
153
+ if(!isTag(child)) {
154
+ continue
155
+ }
156
+
157
+ const ca = child.attribs ?? {}
158
+ if(ca.id) {
159
+ yield `//${tag}[${child.name}[@id=${xpathLiteral(ca.id)}]]`
160
+ }
161
+
162
+ for(const name of IDENT_ATTRS) {
163
+ if(ca[name]) {
164
+ yield `//${tag}[${child.name}[@${name}=${xpathLiteral(ca[name])}]]`
165
+ }
166
+ }
167
+ }
168
+
169
+ yield absolutePath(el)
170
+ }
171
+
172
+ /** Chrome "full XPath"-style absolute path: `/html[1]/body[1]/…/tag[n]`, where
173
+ * n is the element's 1-based position among same-tag siblings. */
174
+ function absolutePath(el: Element): string {
175
+ const parts: string[] = []
176
+ let node: Node | null = el
177
+ while(node && isTag(node)) {
178
+ const tag = node.name
179
+ let index = 1
180
+ for(let sib = node.prev; sib; sib = sib.prev) {
181
+ if(isTag(sib) && sib.name === tag) {
182
+ index++
183
+ }
184
+ }
185
+
186
+ parts.unshift(`/${tag}[${index}]`)
187
+ node = node.parent
188
+ }
189
+
190
+ return parts.join('')
191
+ }
192
+
193
+ /** True when `expression` selects EXACTLY `el` (one match) against `doc`. The
194
+ * attestor applies the redaction regex to EVERY xPath match and throws if any
195
+ * fails, so a non-unique selector (e.g. a class shared by sibling rows) is
196
+ * unusable — it must resolve to the single target element. */
197
+ function matchesUniquely(doc: unknown, expression: string, el: Node): boolean {
198
+ try {
199
+ const engine = xpath as unknown as {
200
+ parse(e: string): { select(o: unknown): Node[] }
201
+ }
202
+ const nodes = engine.parse(expression).select({
203
+ node: doc,
204
+ allowAnyNamespaceForNoPrefix: true,
205
+ })
206
+ return nodes.length === 1 && nodes[0] === el
207
+ } catch{
208
+ return false
209
+ }
210
+ }
211
+
212
+ /** Quote a string as an XPath 1.0 string literal (concat() when it holds both
213
+ * quote characters). */
214
+ function xpathLiteral(s: string): string {
215
+ if(!s.includes('"')) {
216
+ return `"${s}"`
217
+ }
218
+
219
+ if(!s.includes("'")) {
220
+ return `'${s}'`
221
+ }
222
+
223
+ const parts = s.split('"').map((p) => `"${p}"`)
224
+ return `concat(${parts.join(', \'"\', ')})`
225
+ }
@@ -3,35 +3,129 @@ import type {
3
3
  CapturedRequest,
4
4
  OprfMode,
5
5
  ReclaimProvider,
6
+ ResponseMatch,
7
+ ResponseRedaction,
6
8
  SecretParamRef,
7
9
  } from './schema.ts'
8
10
  import { splitSecrets } from './secrets.ts'
9
11
 
12
+ /** One value to extract from the request: the captured `value`, a `name` (its
13
+ * param / capture-group), and an optional per-target OPRF `hash`. */
14
+ export interface DraftTarget {
15
+ value: string
16
+ name: string
17
+ hash?: OprfMode
18
+ }
19
+
20
+ /** The realized extraction for one target — its value paired with the
21
+ * redaction that reveals it (absent when matched literally). Lets a consumer
22
+ * validate each value against its own selector (e.g. the cloud replay). */
23
+ export interface Extraction {
24
+ name: string
25
+ value: string
26
+ redaction?: ResponseRedaction
27
+ }
28
+
10
29
  export interface DraftResult {
11
30
  provider: ReclaimProvider
12
31
  secretRefs: SecretParamRef[]
32
+ /** One entry per target, in `responseMatches` order. */
33
+ extractions: Extraction[]
13
34
  }
14
35
 
15
- /** Replace any existing `accept-encoding` (case-insensitive) with a
16
- * canonical `Accept-Encoding: identity`. Strips browser-captured
17
- * encodings like `gzip, deflate, br, zstd` that would otherwise make
18
- * the attestor match against compressed response bytes. */
19
- function withIdentityEncoding(
36
+ /** Request headers that must NOT survive into the provider params. Two kinds:
37
+ * - Conditional headers (`if-none-match`/`if-modified-since`/…): a captured
38
+ * cached ETag/date makes the origin answer 304/412 when the attestor
39
+ * re-issues, and the attestor treats non-2xx as failure. We always want the
40
+ * full fresh response.
41
+ * - Transfer headers the ATTESTOR sets itself (`connection: close`,
42
+ * `accept-encoding: identity`). Passing our own duplicates/collides with the
43
+ * request the attestor builds, so leave them out — the attestor already
44
+ * forces `identity` (uncompressed, so its byte-matching works). */
45
+ const STRIP_REQUEST_HEADERS = new Set([
46
+ 'if-none-match',
47
+ 'if-modified-since',
48
+ 'if-match',
49
+ 'if-unmodified-since',
50
+ 'if-range',
51
+ 'connection',
52
+ 'accept-encoding',
53
+ ])
54
+
55
+ /** Drop the headers the attestor manages or that would 304 at proof time
56
+ * (see {@link STRIP_REQUEST_HEADERS}); keep every other captured header so the
57
+ * attestor's replay looks like the browser's request. */
58
+ function sanitizeRequestHeaders(
20
59
  headers: Record<string, string>,
21
60
  ): Record<string, string> {
22
61
  const out: Record<string, string> = {}
23
62
  for(const [k, v] of Object.entries(headers)) {
24
- if(k.toLowerCase() === 'accept-encoding') {
63
+ if(STRIP_REQUEST_HEADERS.has(k.toLowerCase())) {
25
64
  continue
26
65
  }
27
66
 
28
67
  out[k] = v
29
68
  }
30
69
 
31
- out['Accept-Encoding'] = 'identity'
32
70
  return out
33
71
  }
34
72
 
73
+ /** Reject a draft that bakes a captured per-user value as a LITERAL into a
74
+ * match or redaction — either a neighbouring target's value or a target's own
75
+ * value left un-templated. The matcher builds each target's window
76
+ * independently and drops the per-user WORDS it can see, but it cannot know a
77
+ * neighbouring value is itself user-specific (e.g. a name sharing a text node
78
+ * with an id — a `<b>{{id}}</b>` sitting beside a `<td>{{name}}</td>`).
79
+ * Cross-checking every value against every match here catches that: a portable
80
+ * provider contains NO captured value as a literal — each appears only as its
81
+ * `{{param}}`. Throwing discards the draft so the agent re-captures or
82
+ * re-proposes rather than shipping a provider pinned to this one user. */
83
+ export function assertPortableMatches(
84
+ matches: ResponseMatch[],
85
+ redactions: ResponseRedaction[],
86
+ targets: DraftTarget[],
87
+ ): void {
88
+ // Guard only values long enough to be plausibly user-specific; shorter runs
89
+ // (single digits, 2-3 char tokens) collide with ordinary structural bytes.
90
+ const MIN_ALNUM = 4
91
+ const guarded = targets
92
+ .map((t) => t.value)
93
+ .filter((v) => v.replace(/[^A-Za-z0-9]/g, '').length >= MIN_ALNUM)
94
+ if(!guarded.length) {
95
+ return
96
+ }
97
+
98
+ const stripParams = (s: string): string => s.replace(/\{\{[^}]*\}\}/g, '')
99
+ const scan = (text: string, where: string, own: string | null): void => {
100
+ const bare = stripParams(text)
101
+ for(const v of guarded) {
102
+ if(bare.includes(v)) {
103
+ const kind = v === own
104
+ ? 'its own value un-templated'
105
+ : "a neighbouring target's per-user value"
106
+ throw new Error(
107
+ `drafted ${where} bakes ${kind} ("${v}") as a literal — the `
108
+ + 'provider would be pinned to this user. Re-capture or re-propose.',
109
+ )
110
+ }
111
+ }
112
+ }
113
+
114
+ for(const [i, m] of matches.entries()) {
115
+ scan(
116
+ m.value,
117
+ `responseMatch for "${targets[i]?.name ?? '?'}"`,
118
+ targets[i]?.value ?? null,
119
+ )
120
+ }
121
+
122
+ for(const r of redactions) {
123
+ if(r.regex) {
124
+ scan(r.regex, 'responseRedaction regex', null)
125
+ }
126
+ }
127
+ }
128
+
35
129
  /** Derive a `paramValues` key (and capture-group name) from the provider
36
130
  * name's last alphanumeric token. `"GitHub Username"` → `"username"`,
37
131
  * `"x-followers"` → `"followers"`. */
@@ -43,7 +137,7 @@ function deriveParamName(providerName: string): string {
43
137
 
44
138
  export function draftProvider(
45
139
  req: CapturedRequest,
46
- target: string,
140
+ targets: DraftTarget[],
47
141
  name: string,
48
142
  hash?: OprfMode,
49
143
  ): DraftResult {
@@ -51,31 +145,70 @@ export function draftProvider(
51
145
  throw new Error(`captured request ${req.requestId} has no response body`)
52
146
  }
53
147
 
54
- const paramName = deriveParamName(name)
55
- const synthesis = synthesizeMatcher(
56
- req.responseBody,
57
- req.contentType,
58
- target,
59
- paramName,
60
- hash,
61
- )
62
- if(!synthesis) {
63
- throw new Error(
64
- `target value "${target}" not found in` +
65
- ` response body for request ${req.requestId}`,
148
+ if(targets.length === 0) {
149
+ throw new Error('draftProvider needs at least one target')
150
+ }
151
+
152
+ // One request → N matches + N redactions. Synthesize each target against the
153
+ // SAME response body; a distinct param name per target keeps their capture
154
+ // groups + paramValues from colliding.
155
+ const matches: ResponseMatch[] = []
156
+ const redactions: ResponseRedaction[] = []
157
+ const extractions: Extraction[] = []
158
+ let paramValues: Record<string, string> | undefined
159
+ const seenParams = new Set<string>()
160
+ for(const t of targets) {
161
+ const paramName = deriveParamName(t.name)
162
+ if(seenParams.has(paramName)) {
163
+ throw new Error(
164
+ `two targets resolve to the same param "${paramName}"; `
165
+ + 'give them distinct names',
166
+ )
167
+ }
168
+
169
+ seenParams.add(paramName)
170
+ const synthesis = synthesizeMatcher(
171
+ req.responseBody,
172
+ req.contentType,
173
+ t.value,
174
+ paramName,
175
+ t.hash ?? hash,
66
176
  )
177
+ if(!synthesis) {
178
+ throw new Error(
179
+ `target value "${t.value}" not found in`
180
+ + ` response body for request ${req.requestId}`,
181
+ )
182
+ }
183
+
184
+ matches.push(synthesis.match)
185
+ if(synthesis.redaction) {
186
+ redactions.push(synthesis.redaction)
187
+ }
188
+
189
+ if(synthesis.paramValues) {
190
+ paramValues = { ...paramValues, ...synthesis.paramValues }
191
+ }
192
+
193
+ extractions.push({
194
+ name: paramName,
195
+ value: t.value,
196
+ ...(synthesis.redaction ? { redaction: synthesis.redaction } : {}),
197
+ })
67
198
  }
68
199
 
200
+ // Portability gate: never ship a draft that bakes a per-user value literally.
201
+ assertPortableMatches(matches, redactions, targets)
202
+
69
203
  const { secrets, publics } = splitSecrets(req.requestHeaders)
70
- // Force the server to respond uncompressed. Captured browser requests
71
- // usually carry `accept-encoding: gzip, deflate, br, zstd`; the attestor
72
- // matches against the raw response bytes (no transparent decompression
73
- // in the TLS replay path), so gzip/br/zstd payloads make every
74
- // xPath/jsonPath/regex redaction miss against unparseable bytes.
75
- // Replacing here also benefits `replay_request` — Node's `fetch` does
76
- // auto-decompress, but pinning the encoding keeps replay and proof
77
- // looking at the same body shape end-to-end.
78
- const headersForProvider = withIdentityEncoding(publics)
204
+ // Sanitize the public headers before they enter the provider. Two problems
205
+ // a captured browser request carries: (1) `accept-encoding: gzip, …`, which
206
+ // makes the attestor match against compressed bytes (it does no transparent
207
+ // decompression in the TLS replay path) — pin it to identity; (2) conditional
208
+ // headers (`if-none-match`/`if-modified-since`), which make the origin answer
209
+ // 304 at proof time and fail the proof. Sanitizing here also keeps
210
+ // `replay_request` and the proof looking at the same full-200 body shape.
211
+ const headersForProvider = sanitizeRequestHeaders(publics)
79
212
  const secretRefs: SecretParamRef[] = Object.keys(secrets).map((name) => ({
80
213
  name,
81
214
  source: name.toLowerCase() === 'cookie' ? 'cookie' : 'header',
@@ -87,19 +220,19 @@ export function draftProvider(
87
220
  url: req.url,
88
221
  method: req.method as ReclaimProvider['method'],
89
222
  headers: headersForProvider,
90
- responseMatches: [synthesis.match],
223
+ responseMatches: matches,
91
224
  }
92
- if(synthesis.redaction) {
93
- provider.responseRedactions = [synthesis.redaction]
225
+ if(redactions.length) {
226
+ provider.responseRedactions = redactions
94
227
  }
95
228
 
96
- if(synthesis.paramValues) {
97
- provider.paramValues = synthesis.paramValues
229
+ if(paramValues) {
230
+ provider.paramValues = paramValues
98
231
  }
99
232
 
100
233
  if(req.requestBody) {
101
234
  provider.body = req.requestBody
102
235
  }
103
236
 
104
- return { provider, secretRefs }
237
+ return { provider, secretRefs, extractions }
105
238
  }
@@ -1,6 +1,11 @@
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.ts'
7
+ export type { DraftResult } from './draft.ts'
8
+ export { detectConcerns } from './constraints.ts'
4
9
  export { extractGraphqlOp, looksLikeGraphqlEndpoint } from './graphql.ts'
5
10
  export type {
6
11
  CapturedRequest,
@@ -13,6 +18,9 @@ export type {
13
18
  SecretParamRef,
14
19
  } from './schema.ts'
15
20
  export { classifyHeader, splitSecrets } from './secrets.ts'
21
+ // Recipe → version `requests[]` (a `RequestSelection`) — the same mapping the
22
+ // MCP publish uses, so the cloud authoring publish stays byte-identical.
23
+ export { providerToRequestSelection } from './to-version.ts'
16
24
  // Re-exported attestor-core provider primitives (the registry, the param hash,
17
25
  // and the param type) so the builder imports them via the agent rather than
18
26
  // 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'