@reclaimprotocol/agent 0.1.13 → 0.1.15

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.
@@ -189,15 +189,86 @@ export function createVersionFromCaptureTool(client) {
189
189
  description: 'Extra requests the customInjection script is allowed to ' +
190
190
  'fire (e.g. pagination) — add-version ONLY, the register ' +
191
191
  'endpoint ignores this. Each item has the same shape as ' +
192
- '`provider`, plus `multiple`/`required` (default true).',
192
+ '`provider`, plus `multiple`/`required` (default true). ' +
193
+ 'IMPORTANT whenever a claim\'s request HAS a body: set `body` ' +
194
+ 'here (see its own description below) — if only a small part ' +
195
+ 'of the real body varies per user/run (e.g. one id field, not ' +
196
+ 'the whole payload), template just that part with ' +
197
+ '`{{paramName}}` and keep the rest as fixed literal text; see ' +
198
+ 'the "bodySniff" section of the customInjection field\'s guide ' +
199
+ 'for the exact mechanics and a worked example.',
193
200
  items: {
194
201
  type: 'object',
195
202
  properties: {
196
203
  url: { type: 'string' },
197
204
  method: { type: 'string' },
198
- body: { type: 'string' },
199
- responseMatches: { type: 'array' },
200
- responseRedactions: { type: 'array' },
205
+ body: {
206
+ type: 'string',
207
+ description: 'Becomes `bodySniff: { enabled: true, template: <this ' +
208
+ 'string> }` — set this whenever the request has a ' +
209
+ 'body at all (skip only for a genuinely bodyless ' +
210
+ 'request, typically GET). If the real body is FIXED ' +
211
+ '(identical for every user/run), put that literal body ' +
212
+ 'text here verbatim. If only PART of it varies (e.g. ' +
213
+ '`{"request":1,"type":"user_info","id":"12345"}` where ' +
214
+ 'only `id` differs per user), template ONLY that part: ' +
215
+ '`{"request":1,"type":"user_info",' +
216
+ '"id":"{{REQ_BODY_USER_ID}}"}` — keep everything else ' +
217
+ 'as fixed literal text rather than templating the whole ' +
218
+ 'body. Confirmed directly against attestor-core\'s ' +
219
+ '`hashProviderParams`: the hash embeds this string ' +
220
+ 'LITERALLY, unresolved braces and all, never the real ' +
221
+ 'substituted body, so a claim built with the SAME ' +
222
+ 'literal template (plus a witness/extracted param per ' +
223
+ 'placeholder, supplying the real value for ' +
224
+ 'verification) hashes identically regardless of what ' +
225
+ 'those real values are. This is the OPPOSITE of ' +
226
+ '`templateParams`\' `${var}` syntax below, which ' +
227
+ 'genuinely IS substituted before hashing — do not ' +
228
+ 'confuse `{{param}}` (body, never resolved) with ' +
229
+ '`${var}` (responseMatches/responseRedactions, resolved ' +
230
+ 'at verify time). Before publishing, hand-verify that ' +
231
+ 'substituting real sample values into this template ' +
232
+ 'reproduces the real captured request body ' +
233
+ 'byte-for-byte. Fallback if per-field templating keeps ' +
234
+ 'mismatching (more fields vary than expected, unstable ' +
235
+ 'key order, etc.) and you can\'t pin down every ' +
236
+ 'variable field: template the WHOLE body as one ' +
237
+ 'placeholder instead, `{{REQ_BODY_GRD}}` — the `GRD` ' +
238
+ 'suffix makes it a greedy match (captures everything) ' +
239
+ 'rather than the default non-greedy match, which is ' +
240
+ 'needed when nothing fixed follows the placeholder. ' +
241
+ 'This pins down nothing about body structure, so prefer ' +
242
+ 'minimal per-field placeholders whenever you can ' +
243
+ 'actually identify which fields vary.',
244
+ },
245
+ responseMatches: {
246
+ type: 'array',
247
+ description: 'Each item: `{ value: string, type?: "contains" | ' +
248
+ '"regex", invert?: boolean, isOptional?: boolean }`. ' +
249
+ '`value` must literally appear (or match, if `regex`) ' +
250
+ 'somewhere in the response body for the claim to be ' +
251
+ 'accepted; it commonly embeds one or more ' +
252
+ '`{{paramName}}` extraction placeholders (e.g. ' +
253
+ '`"name":"{{name_0}}"`) whose real value is redacted ' +
254
+ 'out and supplied separately as a witness/extracted ' +
255
+ 'param. Use `${var}` (not `{{var}}`) inside this string ' +
256
+ 'only if you also declare that name in this entry\'s ' +
257
+ 'own `templateParams` below — that\'s a DIFFERENT, ' +
258
+ 'verify-time-substituted placeholder, resolved from the ' +
259
+ 'submitted proof before hashing (unlike a body ' +
260
+ '`{{param}}`, which never resolves — see `body` above).',
261
+ },
262
+ responseRedactions: {
263
+ type: 'array',
264
+ description: 'Each item: `{ jsonPath?: string, xPath?: string, ' +
265
+ 'regex?: string, hash?: string }`. Identifies WHICH ' +
266
+ 'part of the response the matching `{{paramName}}` in ' +
267
+ '`responseMatches` actually redacts/extracts — e.g. ' +
268
+ '`jsonPath: "$.data.items[0].name"` for a JSON response. ' +
269
+ 'Order/count should line up with `responseMatches`\' ' +
270
+ 'placeholders one-to-one for a typical claim.',
271
+ },
201
272
  multiple: {
202
273
  type: 'boolean',
203
274
  description: 'Allow matching many requests. Default true.',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.13",
4
+ "version": "0.1.15",
5
5
  "description": "Reclaim Protocol's tools for agents — Reclaim Protocol lets your app verify **data points** about its users — a follower count, an account balance, an order history. This package gives your AI coding agent the tools to **find, build, test and fix the _data verification providers_** (the recipes that verify a data point) by inspecting and replaying captured requests, checking sessions, evaluate proofs, publishing updates, and much more.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -86,7 +86,7 @@
86
86
  "@modelcontextprotocol/sdk": "^1.29.0",
87
87
  "@noble/hashes": "^2.2.0",
88
88
  "@reclaimprotocol/attestor-core": "^5.0.8",
89
- "@reclaimprotocol/client": "*",
89
+ "@reclaimprotocol/client": "0.1.15",
90
90
  "@reclaimprotocol/tls": "^0.1.3",
91
91
  "chrome-remote-interface": "^0.33.0",
92
92
  "domhandler": "^6.0.0",
@@ -133,13 +133,104 @@ field isn't touched by `create_provider_version_from_capture` at all).
133
133
  ## `bodySniff` — unrelated to the above
134
134
 
135
135
  A per-request field (sits next to `url`/`method` on each `requestData` entry,
136
- not a version-level setting): `{ enabled: boolean, template: string }`. Per
137
- the InApp SDK's own type comment, it's "the template used for matching and
138
- extracting params from `requestBody`" — the REQUEST-body counterpart of
139
- `responseMatches`/`responseRedactions`, which only ever look at the response.
140
- It applies regardless of `injectionType`/`interceptorType`. Both publish
141
- tools in this package already set it for you from `provider.body` — you
142
- should not need to touch it directly.
136
+ not a version-level setting): `{ enabled: boolean, template: string }`.
137
+
138
+ **`bodySniff.enabled` should be `true` whenever the request HAS a body at
139
+ all** — this isn't a special case reserved for dynamic bodies. Only skip it
140
+ (leave `enabled: false` / template empty) when the request genuinely sends
141
+ no body (typically a GET). Both publish tools in this package already set
142
+ `enabled`/`template` for you from `provider.body` for a normal
143
+ capture-and-replay provider — you don't need to touch it directly there.
144
+ Read the rest of this section if you're authoring a claim via live
145
+ interception (`window.Reclaim.requestClaim`) whose body isn't simply the
146
+ one literal string you captured once — most commonly because some SMALL
147
+ part of it is dynamic per user/run (e.g. a user id, not necessarily the
148
+ whole payload).
149
+
150
+ **What actually gets hashed.** Confirmed directly from attestor-core
151
+ (`hashProviderParams` in `@reclaimprotocol/attestor-core`): the claim/spec
152
+ hash is `keccak256(canonicalStringify({ url, method, body, responseMatches,
153
+ responseRedactions }))`, where `body` is whatever plain string sits in
154
+ `params.body` — attestor-core does **no** template resolution, substitution,
155
+ or `{{}}`-stripping on it; it hashes exactly the string it's given, verbatim.
156
+ `bodySniff` as a concept (the `{enabled, template}` wrapper) doesn't exist
157
+ inside attestor-core at all — it's translated to a plain `body: string`
158
+ one layer up (old-devtools' `providerUtils.ts`: `body: bodySniff.enabled ?
159
+ bodySniff.template : ''`), and THAT literal string — including any
160
+ unresolved `{{paramName}}` text still in it — is what reaches
161
+ `hashProviderParams`.
162
+
163
+ **The consequence — and why this differs from `templateParams`.** A live
164
+ claim's own `body` field (submitted via `requestClaim`) can ALSO be set to
165
+ a literal template string containing `{{paramName}}`, rather than the real
166
+ substituted content, with the real value supplied separately as a witness
167
+ param. Since attestor-core hashes `body` as an opaque string with no
168
+ resolution, a claim whose `body` is the literal template `T` and a spec
169
+ whose `body`/`bodySniff.template` is that SAME literal template `T` hash
170
+ identically — regardless of what real value the `{{paramName}}` hole stood
171
+ for. This is the OPPOSITE of `templateParams`/`${var}` (used in
172
+ `responseMatches`/`responseRedactions`), which — per the JS SDK's
173
+ `generateSpecsFromRequestSpecTemplate` — genuinely gets substituted with a
174
+ real value at verify time before hashing. Don't conflate the two: `${var}`
175
+ in a match/redaction template IS resolved before hashing; `{{var}}` in a
176
+ body template is NEVER resolved before hashing, on either side.
177
+
178
+ **Practical use**: check exactly which part of the body actually changes
179
+ across users/runs — usually it's one small field (an id, a token, a short
180
+ list), not the whole payload. Template ONLY that minimal changing part;
181
+ leave everything else as fixed literal text. For a body like
182
+ `{"request":1,"type":"user_info","id":"12345"}` where only `id` varies, the
183
+ template is `{"request":1,"type":"user_info","id":"{{REQ_BODY_USER_ID}}"}`
184
+ — NOT e.g. `{{whole_body}}` wrapping the entire object, which would throw
185
+ away the ability to structurally pin down everything else about the
186
+ request. Then:
187
+ 1. In the claim you build for `requestClaim`, set `bodySniff: { enabled:
188
+ true, template: '{"request":1,"type":"user_info","id":"{{REQ_BODY_USER_ID}}"}' }`
189
+ and add a matching witness/extracted param
190
+ `REQ_BODY_USER_ID: theRealUserId` so the real value is still
191
+ independently verifiable, just not part of the hash.
192
+ 2. In the corresponding `allowedInjectedRequestData` entry, set `body` to
193
+ that EXACT SAME literal template string (byte-for-byte, same
194
+ `{{REQ_BODY_USER_ID}}` placeholder, same everything else) — NOT a
195
+ `${var}` substitution, since `body` doesn't go through that substitution
196
+ step.
197
+ 3. Before publishing, sanity-check by hand that
198
+ `template.replace('{{REQ_BODY_USER_ID}}', 'a real sample id')`
199
+ reproduces the real captured/expected request body byte-for-byte — if it
200
+ doesn't (extra whitespace, different key order, a field that turned out
201
+ to ALSO vary but wasn't templated, etc.), the hashes won't match either,
202
+ since the SPEC side uses the literal template as-is and the real claim's
203
+ body must independently be built to match that same shape when the
204
+ placeholder(s) are filled in. If MULTIPLE fields vary independently,
205
+ template each one with its own `{{placeholder}}` and its own witness
206
+ param — you're not limited to one hole per template.
207
+
208
+ **Fallback — whole-body placeholder.** If precise per-field templating
209
+ keeps causing hash mismatches (e.g. more fields turn out to vary than
210
+ expected, key order isn't stable, whitespace differs run-to-run) and you
211
+ can't pin down every variable field individually, fall back to templating
212
+ the ENTIRE body as one placeholder instead: `{{REQ_BODY_GRD}}`. The `GRD`
213
+ suffix matters — per the response-matching template engine
214
+ (`convertTemplateToRegex`), a placeholder name ending in `GRD` is treated as
215
+ a GREEDY match (`(.*)`, matching everything including embedded delimiters)
216
+ rather than the default non-greedy `(.*?)` (which stops at the first
217
+ occurrence of whatever comes after it in the template) — for a placeholder
218
+ covering the entire remaining body with nothing fixed after it, greedy is
219
+ what you want. This is strictly a fallback: it means NOTHING about the
220
+ body's structure is pinned down by the hash anymore (any body at all would
221
+ hash the same), so prefer the minimal-placeholder approach above whenever
222
+ you can actually identify which fields vary.
223
+
224
+ Caveat: I confirmed `hashProviderParams` treats `body` as an opaque string
225
+ with no `{{}}` resolution directly from the attestor-core bundle shipped in
226
+ this repo's `node_modules`. I did NOT find, in this same bundle, the code
227
+ path that actually invokes `hashProviderParams` against a live submitted
228
+ claim to gate proof acceptance — that caller lives elsewhere (the mobile/
229
+ InApp SDK client and/or a server-side verifier not checked into this repo).
230
+ So this is strong but not 100% end-to-end-traced confirmation; treat it as
231
+ "very likely correct, empirically consistent with real proof/verifyProof
232
+ behavior observed in practice" rather than "certain from first-hand
233
+ tracing of the whole call chain."
143
234
 
144
235
  ## Recognizing this on a provider you didn't author
145
236
 
@@ -240,15 +240,89 @@ export function createVersionFromCaptureTool(
240
240
  'Extra requests the customInjection script is allowed to ' +
241
241
  'fire (e.g. pagination) — add-version ONLY, the register ' +
242
242
  'endpoint ignores this. Each item has the same shape as ' +
243
- '`provider`, plus `multiple`/`required` (default true).',
243
+ '`provider`, plus `multiple`/`required` (default true). ' +
244
+ 'IMPORTANT whenever a claim\'s request HAS a body: set `body` ' +
245
+ 'here (see its own description below) — if only a small part ' +
246
+ 'of the real body varies per user/run (e.g. one id field, not ' +
247
+ 'the whole payload), template just that part with ' +
248
+ '`{{paramName}}` and keep the rest as fixed literal text; see ' +
249
+ 'the "bodySniff" section of the customInjection field\'s guide ' +
250
+ 'for the exact mechanics and a worked example.',
244
251
  items: {
245
252
  type: 'object',
246
253
  properties: {
247
254
  url: { type: 'string' },
248
255
  method: { type: 'string' },
249
- body: { type: 'string' },
250
- responseMatches: { type: 'array' },
251
- responseRedactions: { type: 'array' },
256
+ body: {
257
+ type: 'string',
258
+ description:
259
+ 'Becomes `bodySniff: { enabled: true, template: <this ' +
260
+ 'string> }` — set this whenever the request has a ' +
261
+ 'body at all (skip only for a genuinely bodyless ' +
262
+ 'request, typically GET). If the real body is FIXED ' +
263
+ '(identical for every user/run), put that literal body ' +
264
+ 'text here verbatim. If only PART of it varies (e.g. ' +
265
+ '`{"request":1,"type":"user_info","id":"12345"}` where ' +
266
+ 'only `id` differs per user), template ONLY that part: ' +
267
+ '`{"request":1,"type":"user_info",' +
268
+ '"id":"{{REQ_BODY_USER_ID}}"}` — keep everything else ' +
269
+ 'as fixed literal text rather than templating the whole ' +
270
+ 'body. Confirmed directly against attestor-core\'s ' +
271
+ '`hashProviderParams`: the hash embeds this string ' +
272
+ 'LITERALLY, unresolved braces and all, never the real ' +
273
+ 'substituted body, so a claim built with the SAME ' +
274
+ 'literal template (plus a witness/extracted param per ' +
275
+ 'placeholder, supplying the real value for ' +
276
+ 'verification) hashes identically regardless of what ' +
277
+ 'those real values are. This is the OPPOSITE of ' +
278
+ '`templateParams`\' `${var}` syntax below, which ' +
279
+ 'genuinely IS substituted before hashing — do not ' +
280
+ 'confuse `{{param}}` (body, never resolved) with ' +
281
+ '`${var}` (responseMatches/responseRedactions, resolved ' +
282
+ 'at verify time). Before publishing, hand-verify that ' +
283
+ 'substituting real sample values into this template ' +
284
+ 'reproduces the real captured request body ' +
285
+ 'byte-for-byte. Fallback if per-field templating keeps ' +
286
+ 'mismatching (more fields vary than expected, unstable ' +
287
+ 'key order, etc.) and you can\'t pin down every ' +
288
+ 'variable field: template the WHOLE body as one ' +
289
+ 'placeholder instead, `{{REQ_BODY_GRD}}` — the `GRD` ' +
290
+ 'suffix makes it a greedy match (captures everything) ' +
291
+ 'rather than the default non-greedy match, which is ' +
292
+ 'needed when nothing fixed follows the placeholder. ' +
293
+ 'This pins down nothing about body structure, so prefer ' +
294
+ 'minimal per-field placeholders whenever you can ' +
295
+ 'actually identify which fields vary.',
296
+ },
297
+ responseMatches: {
298
+ type: 'array',
299
+ description:
300
+ 'Each item: `{ value: string, type?: "contains" | ' +
301
+ '"regex", invert?: boolean, isOptional?: boolean }`. ' +
302
+ '`value` must literally appear (or match, if `regex`) ' +
303
+ 'somewhere in the response body for the claim to be ' +
304
+ 'accepted; it commonly embeds one or more ' +
305
+ '`{{paramName}}` extraction placeholders (e.g. ' +
306
+ '`"name":"{{name_0}}"`) whose real value is redacted ' +
307
+ 'out and supplied separately as a witness/extracted ' +
308
+ 'param. Use `${var}` (not `{{var}}`) inside this string ' +
309
+ 'only if you also declare that name in this entry\'s ' +
310
+ 'own `templateParams` below — that\'s a DIFFERENT, ' +
311
+ 'verify-time-substituted placeholder, resolved from the ' +
312
+ 'submitted proof before hashing (unlike a body ' +
313
+ '`{{param}}`, which never resolves — see `body` above).',
314
+ },
315
+ responseRedactions: {
316
+ type: 'array',
317
+ description:
318
+ 'Each item: `{ jsonPath?: string, xPath?: string, ' +
319
+ 'regex?: string, hash?: string }`. Identifies WHICH ' +
320
+ 'part of the response the matching `{{paramName}}` in ' +
321
+ '`responseMatches` actually redacts/extracts — e.g. ' +
322
+ '`jsonPath: "$.data.items[0].name"` for a JSON response. ' +
323
+ 'Order/count should line up with `responseMatches`\' ' +
324
+ 'placeholders one-to-one for a typical claim.',
325
+ },
252
326
  multiple: {
253
327
  type: 'boolean',
254
328
  description: 'Allow matching many requests. Default true.',