@reclaimprotocol/agent 0.1.10 → 0.1.12
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/lib/authoring/tools.js +67 -0
- package/lib/mcp/tools/agent/find.js +41 -16
- package/lib/mcp/tools/agent/propose.js +4 -4
- package/lib/old/client.d.ts +31 -5
- package/lib/old/client.js +33 -10
- package/lib/old/to-register.d.ts +2 -0
- package/lib/old/to-register.js +9 -5
- package/lib/old/tools/build.js +12 -2
- package/lib/old/tools/check-app-status.d.ts +8 -0
- package/lib/old/tools/check-app-status.js +27 -0
- package/lib/old/tools/get-provider-info.d.ts +10 -7
- package/lib/old/tools/get-provider-info.js +21 -15
- package/lib/old/tools/issue-app-credentials.d.ts +11 -0
- package/lib/old/tools/issue-app-credentials.js +35 -0
- package/lib/old/tools/js-sdk-integration-guide.d.ts +2 -0
- package/lib/old/tools/js-sdk-integration-guide.js +141 -0
- package/lib/old/tools/link-app.d.ts +10 -0
- package/lib/old/tools/link-app.js +72 -0
- package/lib/old/tools/publish.js +89 -126
- package/lib/old/tools/scaffold-demo-server.d.ts +2 -0
- package/lib/old/tools/scaffold-demo-server.js +242 -0
- package/lib/old/tools/verify-sdk-setup.d.ts +9 -0
- package/lib/old/tools/verify-sdk-setup.js +128 -0
- package/lib/provider/dom-xpath.js +178 -25
- package/lib/provider/draft.d.ts +6 -0
- package/lib/provider/draft.js +54 -5
- package/lib/provider/matcher.d.ts +4 -0
- package/lib/provider/matcher.js +6 -0
- package/lib/provider/to-version.d.ts +13 -0
- package/package.json +2 -1
- package/src/authoring/tools.ts +76 -0
- package/src/mcp/tools/agent/find.ts +44 -16
- package/src/mcp/tools/agent/propose.ts +4 -4
- package/src/old/client.ts +60 -18
- package/src/old/to-register.ts +11 -5
- package/src/old/tools/build.ts +12 -2
- package/src/old/tools/check-app-status.ts +33 -0
- package/src/old/tools/get-provider-info.ts +21 -15
- package/src/old/tools/issue-app-credentials.ts +42 -0
- package/src/old/tools/js-sdk-integration-guide.ts +147 -0
- package/src/old/tools/link-app.ts +88 -0
- package/src/old/tools/publish.ts +98 -158
- package/src/old/tools/scaffold-demo-server.ts +265 -0
- package/src/old/tools/verify-sdk-setup.ts +158 -0
- package/src/provider/dom-xpath.ts +227 -27
- package/src/provider/draft.ts +59 -5
- package/src/provider/matcher.ts +7 -0
- package/src/provider/to-version.ts +13 -0
- package/src/old/tools/get-version-config.ts +0 -56
|
@@ -18,6 +18,104 @@ import '../proof/patch-domhandler.ts'
|
|
|
18
18
|
const IDENT_ATTRS =
|
|
19
19
|
['name', 'property', 'itemprop', 'data-testid', 'rel', 'for']
|
|
20
20
|
|
|
21
|
+
/** What an xPath candidate anchors on — used to weight its base stability. */
|
|
22
|
+
type AnchorKind = 'ident-attr' | 'id' | 'class' | 'positional'
|
|
23
|
+
|
|
24
|
+
/** One verified xPath option for the target element, tagged with what it
|
|
25
|
+
* anchors on so it can be scored for FUTURE stability (see
|
|
26
|
+
* {@link anchorStability}) rather than just "first that resolves". */
|
|
27
|
+
interface Candidate {
|
|
28
|
+
xPath: string
|
|
29
|
+
kind: AnchorKind
|
|
30
|
+
/** The identifier the anchor keys on (an id / attr / class value); undefined
|
|
31
|
+
* for a purely positional path. */
|
|
32
|
+
token?: string
|
|
33
|
+
/** Ancestor distance from the value element (0 = the element itself or a
|
|
34
|
+
* direct-child anchor); larger = higher up. Tie-break nudge only. */
|
|
35
|
+
depth: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Base weight by anchor kind: purpose-built test hooks and ids beat classes,
|
|
39
|
+
* which beat a positional path. Modulated by {@link tokenQuality}. */
|
|
40
|
+
const TYPE_WEIGHT: Record<AnchorKind, number> = {
|
|
41
|
+
'ident-attr': 0.95,
|
|
42
|
+
id: 0.9,
|
|
43
|
+
class: 0.6,
|
|
44
|
+
positional: 0.3,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const UUID_RE =
|
|
48
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* How likely an identifier token is to survive a future render, by its SHAPE —
|
|
52
|
+
* framework-agnostic, so it catches generated ids/classes from any tool (this
|
|
53
|
+
* year's or next) without naming them. Returns `[0.02, 1]`.
|
|
54
|
+
*
|
|
55
|
+
* Volatile shapes (machine-generated): a `:` (React `useId` `:r1:`, Radix), a
|
|
56
|
+
* UUID, digit-heavy or trailing-digit tokens (`ember123`, `tab-2`), hex/hash
|
|
57
|
+
* runs (`css-1a2b3c`), or opaque wordless strings. Durable shapes (human
|
|
58
|
+
* authored): real word segments, kebab/snake of words (`settings-header`,
|
|
59
|
+
* `primary_email_select`).
|
|
60
|
+
*/
|
|
61
|
+
function tokenQuality(token: string): number {
|
|
62
|
+
const t = token.trim()
|
|
63
|
+
if(!t) {
|
|
64
|
+
return 0.3
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Kill signals — these tokens are (almost) always per-render generated.
|
|
68
|
+
if(t.includes(':') || UUID_RE.test(t)) {
|
|
69
|
+
return 0.05
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let q = 1
|
|
73
|
+
const len = t.length
|
|
74
|
+
const digits = (t.match(/\d/g) ?? []).length
|
|
75
|
+
const digitRatio = digits / len
|
|
76
|
+
// A trailing digit run after letters is the classic generated-id tail.
|
|
77
|
+
if(/[A-Za-z][-_]?\d+$/.test(t)) {
|
|
78
|
+
q *= 0.3
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if(digitRatio > 0.3) {
|
|
82
|
+
q *= 0.4
|
|
83
|
+
} else if(digitRatio > 0.15) {
|
|
84
|
+
q *= 0.7
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A pure hex/short-hash run (`1a2b3c`, `deadbeef`) — no semantic content.
|
|
88
|
+
if(/^[0-9a-f]{6,}$/i.test(t)) {
|
|
89
|
+
q *= 0.15
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A real word — an alpha run ≥3 that ITSELF contains a vowel — is the
|
|
93
|
+
// strongest durability signal; its absence means the token is opaque /
|
|
94
|
+
// hash-like (`css-1a2b3c`, `sc-bdfBwQ`). Checking per-segment matters: a
|
|
95
|
+
// stray vowel elsewhere in a hash must not count as "has a word".
|
|
96
|
+
const hasWord = (t.match(/[A-Za-z]+/g) ?? [])
|
|
97
|
+
.some((run) => run.length >= 3 && /[aeiou]/i.test(run))
|
|
98
|
+
if(!hasWord) {
|
|
99
|
+
q *= 0.3
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if(len > 40) {
|
|
103
|
+
q *= 0.6
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Math.max(0.02, Math.min(1, q))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Stability score for a candidate: base weight for its kind × the shape
|
|
110
|
+
* quality of its identifier, minus a small depth nudge so a nearer/shorter
|
|
111
|
+
* anchor wins an otherwise-tie. Positional paths score on kind alone. */
|
|
112
|
+
function anchorStability(c: Candidate): number {
|
|
113
|
+
const base = TYPE_WEIGHT[c.kind]
|
|
114
|
+
const quality = c.kind === 'positional' ? 1 : tokenQuality(c.token ?? '')
|
|
115
|
+
const depthNudge = 1 - Math.min(c.depth, 8) * 0.01
|
|
116
|
+
return base * quality * depthNudge
|
|
117
|
+
}
|
|
118
|
+
|
|
21
119
|
export interface DomAnchor {
|
|
22
120
|
xPath: string
|
|
23
121
|
/** Byte range of the element in the source html (parse5 source locations),
|
|
@@ -61,15 +159,25 @@ export function elementAnchor(
|
|
|
61
159
|
return undefined
|
|
62
160
|
}
|
|
63
161
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
162
|
+
// Score EVERY candidate that provably resolves (first-match, uniquely) to the
|
|
163
|
+
// target, and take the most durable — not merely the first that resolves.
|
|
164
|
+
// Because each survivor already matches uniquely NOW, the choice only trades
|
|
165
|
+
// on FUTURE stability; it can never pick a wrong/non-resolving node.
|
|
166
|
+
const verified = [...candidateXPaths(el)]
|
|
167
|
+
.map((c, i) => ({ c, i }))
|
|
168
|
+
.filter(({ c }) => matchesUniquely(doc, c.xPath, el))
|
|
169
|
+
if(!verified.length) {
|
|
170
|
+
return undefined
|
|
70
171
|
}
|
|
71
172
|
|
|
72
|
-
|
|
173
|
+
verified.sort((a, b) => {
|
|
174
|
+
return anchorStability(b.c) - anchorStability(a.c) ||
|
|
175
|
+
a.c.depth - b.c.depth ||
|
|
176
|
+
a.i - b.i
|
|
177
|
+
})
|
|
178
|
+
// `[startIndex, endIndex)` is exactly the slice the attestor reveals for this
|
|
179
|
+
// xPath (its getNodeRange uses these same parse5 source locations).
|
|
180
|
+
return { xPath: verified[0].c.xPath, start: startIndex, end: endIndex }
|
|
73
181
|
}
|
|
74
182
|
|
|
75
183
|
/** The tightest element that DIRECTLY holds a target. Prefers a COMPLETE-value
|
|
@@ -121,34 +229,29 @@ function smallestElement(els: Element[]): Element | undefined {
|
|
|
121
229
|
.at(0)
|
|
122
230
|
}
|
|
123
231
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
232
|
+
/** Every candidate xPath for `el`, each tagged with what it anchors on. The
|
|
233
|
+
* caller verifies + scores them ({@link anchorStability}) rather than trusting
|
|
234
|
+
* emission order, so the DURABILITY winner is chosen, not the first that
|
|
235
|
+
* resolves. Order here is only the final tie-break. */
|
|
236
|
+
function* candidateXPaths(el: Element): Generator<Candidate> {
|
|
126
237
|
const tag = el.name
|
|
127
238
|
const a = el.attribs ?? {}
|
|
128
239
|
if(a.id) {
|
|
129
|
-
yield `//${tag}[@id=${xpathLiteral(a.id)}]
|
|
240
|
+
yield { xPath: `//${tag}[@id=${xpathLiteral(a.id)}]`, kind: 'id',
|
|
241
|
+
token: a.id, depth: 0 }
|
|
130
242
|
}
|
|
131
243
|
|
|
132
244
|
for(const name of IDENT_ATTRS) {
|
|
133
245
|
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)})]`
|
|
246
|
+
yield { xPath: `//${tag}[@${name}=${xpathLiteral(a[name])}]`,
|
|
247
|
+
kind: 'ident-attr', token: a[name], depth: 0 }
|
|
145
248
|
}
|
|
146
249
|
}
|
|
147
250
|
|
|
148
251
|
// A uniquely-identified DIRECT child pins the parent without a positional
|
|
149
|
-
// path (`//span[span[@id="labels"]]`). Self-verification
|
|
150
|
-
//
|
|
151
|
-
//
|
|
252
|
+
// path (`//span[span[@id="labels"]]`). Self-verification discards it when the
|
|
253
|
+
// child id/attr isn't unique — e.g. GitHub reuses `id="labels"` per email row
|
|
254
|
+
// — so it never silently matches the wrong node.
|
|
152
255
|
for(const child of el.children) {
|
|
153
256
|
if(!isTag(child)) {
|
|
154
257
|
continue
|
|
@@ -156,17 +259,114 @@ function* candidateXPaths(el: Element): Generator<string> {
|
|
|
156
259
|
|
|
157
260
|
const ca = child.attribs ?? {}
|
|
158
261
|
if(ca.id) {
|
|
159
|
-
yield `//${tag}[${child.name}[@id=${xpathLiteral(ca.id)}]]
|
|
262
|
+
yield { xPath: `//${tag}[${child.name}[@id=${xpathLiteral(ca.id)}]]`,
|
|
263
|
+
kind: 'id', token: ca.id, depth: 0 }
|
|
160
264
|
}
|
|
161
265
|
|
|
162
266
|
for(const name of IDENT_ATTRS) {
|
|
163
267
|
if(ca[name]) {
|
|
164
|
-
yield
|
|
268
|
+
yield {
|
|
269
|
+
xPath: `//${tag}[${child.name}[@${name}=${xpathLiteral(ca[name])}]]`,
|
|
270
|
+
kind: 'ident-attr', token: ca[name], depth: 0,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The value element's own distinctive class.
|
|
277
|
+
const distinctive = distinctiveClass(a.class)
|
|
278
|
+
if(distinctive) {
|
|
279
|
+
yield { xPath: `//${tag}[contains(@class, ${xpathLiteral(distinctive)})]`,
|
|
280
|
+
kind: 'class', token: distinctive, depth: 0 }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Ancestor-anchored (id/attr, then class), descending positionally to `el`.
|
|
284
|
+
yield* ancestorAnchoredCandidates(el)
|
|
285
|
+
|
|
286
|
+
yield { xPath: absolutePath(el), kind: 'positional', depth: 99 }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The longest whitespace-delimited token of a class attribute (the most
|
|
290
|
+
* distinctive one), or undefined when there's no class. */
|
|
291
|
+
function distinctiveClass(cls: string | undefined): string | undefined {
|
|
292
|
+
return (cls ?? '')
|
|
293
|
+
.split(/\s+/)
|
|
294
|
+
.filter(Boolean)
|
|
295
|
+
.sort((x, y) => y.length - x.length)[0]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Positional path from `anc` (exclusive) down to `el` (inclusive), e.g.
|
|
299
|
+
* `/ul[1]/li[3]/a[1]`; empty string when `anc` is not an ancestor of `el`. */
|
|
300
|
+
function relativeSteps(anc: Element, el: Element): string {
|
|
301
|
+
const parts: string[] = []
|
|
302
|
+
let node: Node | null = el
|
|
303
|
+
while(node && isTag(node) && node !== anc) {
|
|
304
|
+
const tag = node.name
|
|
305
|
+
let index = 1
|
|
306
|
+
for(let sib = node.prev; sib; sib = sib.prev) {
|
|
307
|
+
if(isTag(sib) && sib.name === tag) {
|
|
308
|
+
index++
|
|
165
309
|
}
|
|
166
310
|
}
|
|
311
|
+
|
|
312
|
+
parts.unshift(`/${tag}[${index}]`)
|
|
313
|
+
node = node.parent
|
|
167
314
|
}
|
|
168
315
|
|
|
169
|
-
|
|
316
|
+
return node === anc ? parts.join('') : ''
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Ancestors of `el` (nearest first) with their depth, skipping
|
|
320
|
+
* `html`/`body`/`head` (≈ the absolute path) and non-ancestors. Capped depth
|
|
321
|
+
* keeps paths short. */
|
|
322
|
+
function* anchorAncestors(el: Element): Generator<{
|
|
323
|
+
tag: string
|
|
324
|
+
attribs: Record<string, string>
|
|
325
|
+
rel: string
|
|
326
|
+
depth: number
|
|
327
|
+
}> {
|
|
328
|
+
let depth = 1
|
|
329
|
+
for(
|
|
330
|
+
let anc: Node | null = el.parent;
|
|
331
|
+
anc && isTag(anc) && depth <= 10;
|
|
332
|
+
anc = anc.parent, depth++
|
|
333
|
+
) {
|
|
334
|
+
const tag = anc.name
|
|
335
|
+
if(tag === 'html' || tag === 'body' || tag === 'head') {
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const rel = relativeSteps(anc, el)
|
|
340
|
+
if(rel) {
|
|
341
|
+
yield { tag, attribs: anc.attribs ?? {}, rel, depth }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Ancestor-anchored candidates, descending positionally to `el`
|
|
347
|
+
* (`//div[@id="nav"]/ul[1]/li[1]/a[1]`): an id / identifying-attr or a
|
|
348
|
+
* distinctive-class anchor per ancestor, tagged with its kind + depth so the
|
|
349
|
+
* scorer weighs a stable ancestor id above the value's own utility class. */
|
|
350
|
+
function* ancestorAnchoredCandidates(el: Element): Generator<Candidate> {
|
|
351
|
+
for(const { tag, attribs, rel, depth } of anchorAncestors(el)) {
|
|
352
|
+
if(attribs.id) {
|
|
353
|
+
yield { xPath: `//${tag}[@id=${xpathLiteral(attribs.id)}]${rel}`,
|
|
354
|
+
kind: 'id', token: attribs.id, depth }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for(const name of IDENT_ATTRS) {
|
|
358
|
+
if(attribs[name]) {
|
|
359
|
+
const xp = `//${tag}[@${name}=${xpathLiteral(attribs[name])}]${rel}`
|
|
360
|
+
yield { xPath: xp, kind: 'ident-attr', token: attribs[name], depth }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const dc = distinctiveClass(attribs.class)
|
|
365
|
+
if(dc) {
|
|
366
|
+
yield { xPath: `//${tag}[contains(@class, ${xpathLiteral(dc)})]${rel}`,
|
|
367
|
+
kind: 'class', token: dc, depth }
|
|
368
|
+
}
|
|
369
|
+
}
|
|
170
370
|
}
|
|
171
371
|
|
|
172
372
|
/** Chrome "full XPath"-style absolute path: `/html[1]/body[1]/…/tag[n]`, where
|
package/src/provider/draft.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { synthesizeMatcher } from './matcher.ts'
|
|
1
|
+
import { isOprfHashable, OPRF_MAX_BYTES, synthesizeMatcher } from './matcher.ts'
|
|
2
2
|
import type {
|
|
3
3
|
CapturedRequest,
|
|
4
4
|
OprfMode,
|
|
@@ -9,6 +9,39 @@ import type {
|
|
|
9
9
|
} from './schema.ts'
|
|
10
10
|
import { splitSecrets } from './secrets.ts'
|
|
11
11
|
|
|
12
|
+
/** OPRF mode for the auto-hash floor — TEE-native (attestor rejects oprf-raw,
|
|
13
|
+
* isn't set up for gnark oprf). */
|
|
14
|
+
const DEFAULT_SENSITIVE_HASH: OprfMode = 'oprf-mpc'
|
|
15
|
+
|
|
16
|
+
// The deterministic ANTI-LEAK FLOOR: values whose SHAPE is unambiguously
|
|
17
|
+
// sensitive PII, force-hashed no matter the field name or the model's judgment.
|
|
18
|
+
// Deliberately only FIXED-FORMAT identifiers — OPRF is byte-exact, so a
|
|
19
|
+
// self-mutable value (a display name re-cased) would be grindable and pointless
|
|
20
|
+
// to hash; and a bare number is ambiguous (a private account id vs a PUBLIC
|
|
21
|
+
// user id / follower count that's meant to be proven in the clear). Those
|
|
22
|
+
// contextual calls are the model's, via an explicit per-target `hash`. This
|
|
23
|
+
// floor is universal (not domain-specific) and high-precision.
|
|
24
|
+
const EMAIL_VALUE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
|
|
25
|
+
const SSN_VALUE = /^\d{3}-\d{2}-\d{4}$/
|
|
26
|
+
const IBAN_VALUE = /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/
|
|
27
|
+
|
|
28
|
+
function isCanonicalPii(value: string): boolean {
|
|
29
|
+
return EMAIL_VALUE.test(value)
|
|
30
|
+
|| SSN_VALUE.test(value) || IBAN_VALUE.test(value)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** OPRF mode to apply when the caller passed none: the anti-leak floor —
|
|
34
|
+
* force-hash a value whose SHAPE is unambiguous PII, but ONLY when it's
|
|
35
|
+
* OPRF-hashable AND within the 62-byte MPC-circuit cap (UTF-8). Undefined =
|
|
36
|
+
* leave plaintext (a longer value can't be OPRF'd; contextual PII + the proven
|
|
37
|
+
* identity are the model's/plaintext call). */
|
|
38
|
+
export function defaultSensitiveHash(value: string): OprfMode | undefined {
|
|
39
|
+
const hashable = isCanonicalPii(value)
|
|
40
|
+
&& isOprfHashable(value)
|
|
41
|
+
&& Buffer.byteLength(value, 'utf8') <= OPRF_MAX_BYTES
|
|
42
|
+
return hashable ? DEFAULT_SENSITIVE_HASH : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
12
45
|
/** One value to extract from the request: the captured `value`, a `name` (its
|
|
13
46
|
* param / capture-group), and an optional per-target OPRF `hash`. */
|
|
14
47
|
export interface DraftTarget {
|
|
@@ -33,7 +66,7 @@ export interface DraftResult {
|
|
|
33
66
|
extractions: Extraction[]
|
|
34
67
|
}
|
|
35
68
|
|
|
36
|
-
/** Request headers that must NOT survive into the provider params.
|
|
69
|
+
/** Request headers that must NOT survive into the provider params. Three kinds:
|
|
37
70
|
* - Conditional headers (`if-none-match`/`if-modified-since`/…): a captured
|
|
38
71
|
* cached ETag/date makes the origin answer 304/412 when the attestor
|
|
39
72
|
* re-issues, and the attestor treats non-2xx as failure. We always want the
|
|
@@ -41,7 +74,14 @@ export interface DraftResult {
|
|
|
41
74
|
* - Transfer headers the ATTESTOR sets itself (`connection: close`,
|
|
42
75
|
* `accept-encoding: identity`). Passing our own duplicates/collides with the
|
|
43
76
|
* request the attestor builds, so leave them out — the attestor already
|
|
44
|
-
* forces `identity` (uncompressed, so its byte-matching works).
|
|
77
|
+
* forces `identity` (uncompressed, so its byte-matching works).
|
|
78
|
+
* - Host / framing headers the TRANSPORT owns (`host`, `content-length`,
|
|
79
|
+
* `transfer-encoding`) and HTTP/2 pseudo-headers (`:authority`/`:method`/…,
|
|
80
|
+
* which CDP surfaces on h2 captures). The attestor derives `Host` from the
|
|
81
|
+
* URL and sets its own; a baked `Host` produces a DUPLICATE
|
|
82
|
+
* (`found: h,h`) that fails the receipt check / 400s the origin. A stale
|
|
83
|
+
* `content-length` breaks a re-serialized body. Pseudo-headers aren't valid
|
|
84
|
+
* real headers at all. */
|
|
45
85
|
const STRIP_REQUEST_HEADERS = new Set([
|
|
46
86
|
'if-none-match',
|
|
47
87
|
'if-modified-since',
|
|
@@ -50,8 +90,17 @@ const STRIP_REQUEST_HEADERS = new Set([
|
|
|
50
90
|
'if-range',
|
|
51
91
|
'connection',
|
|
52
92
|
'accept-encoding',
|
|
93
|
+
'host',
|
|
94
|
+
'content-length',
|
|
95
|
+
'transfer-encoding',
|
|
53
96
|
])
|
|
54
97
|
|
|
98
|
+
/** True for an HTTP/2 pseudo-header (`:authority`, `:method`, `:path`,
|
|
99
|
+
* `:scheme`) — never a valid request header the attestor can replay. */
|
|
100
|
+
function isPseudoHeader(name: string): boolean {
|
|
101
|
+
return name.startsWith(':')
|
|
102
|
+
}
|
|
103
|
+
|
|
55
104
|
/** Drop the headers the attestor manages or that would 304 at proof time
|
|
56
105
|
* (see {@link STRIP_REQUEST_HEADERS}); keep every other captured header so the
|
|
57
106
|
* attestor's replay looks like the browser's request. */
|
|
@@ -60,7 +109,7 @@ function sanitizeRequestHeaders(
|
|
|
60
109
|
): Record<string, string> {
|
|
61
110
|
const out: Record<string, string> = {}
|
|
62
111
|
for(const [k, v] of Object.entries(headers)) {
|
|
63
|
-
if(STRIP_REQUEST_HEADERS.has(k.toLowerCase())) {
|
|
112
|
+
if(STRIP_REQUEST_HEADERS.has(k.toLowerCase()) || isPseudoHeader(k)) {
|
|
64
113
|
continue
|
|
65
114
|
}
|
|
66
115
|
|
|
@@ -167,12 +216,17 @@ export function draftProvider(
|
|
|
167
216
|
}
|
|
168
217
|
|
|
169
218
|
seenParams.add(paramName)
|
|
219
|
+
// Precedence: explicit per-target hash → explicit request-wide hash →
|
|
220
|
+
// the anti-leak floor (force-OPRF values whose SHAPE is unambiguous PII).
|
|
221
|
+
// The model makes the contextual calls (which named fields are sensitive)
|
|
222
|
+
// via an explicit per-target hash; this floor only guarantees the obvious.
|
|
223
|
+
const effectiveHash = t.hash ?? hash ?? defaultSensitiveHash(t.value)
|
|
170
224
|
const synthesis = synthesizeMatcher(
|
|
171
225
|
req.responseBody,
|
|
172
226
|
req.contentType,
|
|
173
227
|
t.value,
|
|
174
228
|
paramName,
|
|
175
|
-
|
|
229
|
+
effectiveHash,
|
|
176
230
|
)
|
|
177
231
|
if(!synthesis) {
|
|
178
232
|
throw new Error(
|
package/src/provider/matcher.ts
CHANGED
|
@@ -734,3 +734,10 @@ function inferValueClass(target: string): string | undefined {
|
|
|
734
734
|
|
|
735
735
|
return undefined
|
|
736
736
|
}
|
|
737
|
+
|
|
738
|
+
/** True when `target` has a character class safe to OPRF-hash. Free-form values
|
|
739
|
+
* (names with spaces, arbitrary unicode/punctuation) return false — the caller
|
|
740
|
+
* must NOT default them to a hash (it would throw in synthesizeMatcher). */
|
|
741
|
+
export function isOprfHashable(target: string): boolean {
|
|
742
|
+
return inferValueClass(target) !== undefined
|
|
743
|
+
}
|
|
@@ -37,6 +37,19 @@ export interface AllowedJsRequest extends DraftRequest {
|
|
|
37
37
|
multiple?: boolean
|
|
38
38
|
/** Fail validation if no request matches this template. Default true. */
|
|
39
39
|
required?: boolean
|
|
40
|
+
/** Names of `${var}` placeholders in this template's `responseMatches`/
|
|
41
|
+
* `responseRedactions` that get substituted at verify time from the
|
|
42
|
+
* submitted proof's own witness params (e.g. an index list a
|
|
43
|
+
* `Reclaim.requestClaim` call attached alongside its `{{var}}`
|
|
44
|
+
* extractions). Omit (or leave empty) when the template has no such
|
|
45
|
+
* placeholders — verification then matches it literally as-is. */
|
|
46
|
+
templateParams?: string[]
|
|
47
|
+
/** How multiple values for `templateParams` expand into request specs.
|
|
48
|
+
* `'separate'` (default): one independent spec per value, each expected
|
|
49
|
+
* to match its own proof. `'merge'`: all values folded into ONE spec
|
|
50
|
+
* with one `responseMatches`/`responseRedactions` entry per value,
|
|
51
|
+
* matching a single proof that bundles them all into one claim. */
|
|
52
|
+
templateParamsMode?: 'separate' | 'merge'
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/** Optional extras for a create-version body beyond the request list. */
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
2
|
-
import type { ReclaimOldClient } from '../client.ts'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Read the full `providerConfig` (loginUrl, customInjection, requestData,
|
|
6
|
-
* geoLocation, ...) of one provider version on the old devtools backend —
|
|
7
|
-
* the latest version when `version` is omitted.
|
|
8
|
-
*
|
|
9
|
-
* Exists so a dev/agent can see what's CURRENTLY published before adding a
|
|
10
|
-
* new version with `create_provider_version_from_capture`. That tool always
|
|
11
|
-
* takes a FULL replacement `providerConfig` (no partial-patch endpoint on
|
|
12
|
-
* this backend), so a version added without knowing the current
|
|
13
|
-
* `customInjection` would silently publish it blank — wiping a working
|
|
14
|
-
* login script instead of preserving it. Read it here first, then pass it
|
|
15
|
-
* straight through (or omit `customInjection` on publish to have it carried
|
|
16
|
-
* forward automatically).
|
|
17
|
-
*/
|
|
18
|
-
export function getProviderVersionConfigTool(
|
|
19
|
-
client: ReclaimOldClient,
|
|
20
|
-
): RegisteredTool {
|
|
21
|
-
return defineTool<{ providerId: string, version?: string }>(
|
|
22
|
-
{
|
|
23
|
-
name: 'get_provider_version_config',
|
|
24
|
-
description:
|
|
25
|
-
'Fetch the full providerConfig (loginUrl, customInjection, ' +
|
|
26
|
-
'requestData, geoLocation, ...) of one version on the old devtools ' +
|
|
27
|
-
'backend — the latest version when `version` is omitted. Readable ' +
|
|
28
|
-
'for any non-archived provider, not just ones you own. Use this ' +
|
|
29
|
-
'BEFORE calling create_provider_version_from_capture with a ' +
|
|
30
|
-
'`providerId` to see the currently-published customInjection/' +
|
|
31
|
-
'requestData so you know what you would be changing — that tool ' +
|
|
32
|
-
'replaces the whole providerConfig, so a field you never see and ' +
|
|
33
|
-
"never pass comes out blank in the new version, even if it's set " +
|
|
34
|
-
'today.',
|
|
35
|
-
inputSchema: {
|
|
36
|
-
type: 'object',
|
|
37
|
-
properties: {
|
|
38
|
-
providerId: {
|
|
39
|
-
type: 'string',
|
|
40
|
-
description: 'UUID of the provider (from get_me_providers).',
|
|
41
|
-
},
|
|
42
|
-
version: {
|
|
43
|
-
type: 'string',
|
|
44
|
-
description:
|
|
45
|
-
'Specific version to fetch, e.g. "1.0.2". Omit for the ' +
|
|
46
|
-
"provider's current latest version.",
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
required: ['providerId'],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
async({ providerId, version }) => {
|
|
53
|
-
return client.getProviderConfig(providerId, version)
|
|
54
|
-
},
|
|
55
|
-
)
|
|
56
|
-
}
|