@reclaimprotocol/agent 0.1.0-dev.1

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 (191) hide show
  1. package/README.md +287 -0
  2. package/lib/cdp/capture.d.ts +22 -0
  3. package/lib/cdp/capture.js +159 -0
  4. package/lib/cdp/client.d.ts +9 -0
  5. package/lib/cdp/client.js +11 -0
  6. package/lib/cdp/eviction.d.ts +16 -0
  7. package/lib/cdp/eviction.js +38 -0
  8. package/lib/cdp/launcher.d.ts +14 -0
  9. package/lib/cdp/launcher.js +102 -0
  10. package/lib/cdp/tabs.d.ts +6 -0
  11. package/lib/cdp/tabs.js +10 -0
  12. package/lib/cli/args.d.ts +14 -0
  13. package/lib/cli/args.js +19 -0
  14. package/lib/cli/commands.d.ts +1 -0
  15. package/lib/cli/commands.js +19 -0
  16. package/lib/cli.d.ts +1 -0
  17. package/lib/cli.js +7 -0
  18. package/lib/config/store.d.ts +45 -0
  19. package/lib/config/store.js +46 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +7 -0
  22. package/lib/logger.d.ts +2 -0
  23. package/lib/logger.js +7 -0
  24. package/lib/mcp/api/operations.gen.d.ts +152 -0
  25. package/lib/mcp/api/operations.gen.js +2300 -0
  26. package/lib/mcp/api/tools.d.ts +3 -0
  27. package/lib/mcp/api/tools.js +36 -0
  28. package/lib/mcp/remap_errors.d.ts +2 -0
  29. package/lib/mcp/remap_errors.js +27 -0
  30. package/lib/mcp/server.d.ts +38 -0
  31. package/lib/mcp/server.js +76 -0
  32. package/lib/mcp/start.d.ts +1 -0
  33. package/lib/mcp/start.js +63 -0
  34. package/lib/mcp/tools/agent/analyze.d.ts +3 -0
  35. package/lib/mcp/tools/agent/analyze.js +26 -0
  36. package/lib/mcp/tools/agent/attach.d.ts +11 -0
  37. package/lib/mcp/tools/agent/attach.js +72 -0
  38. package/lib/mcp/tools/agent/build.d.ts +33 -0
  39. package/lib/mcp/tools/agent/build.js +74 -0
  40. package/lib/mcp/tools/agent/capture.d.ts +10 -0
  41. package/lib/mcp/tools/agent/capture.js +142 -0
  42. package/lib/mcp/tools/agent/eval.d.ts +5 -0
  43. package/lib/mcp/tools/agent/eval.js +127 -0
  44. package/lib/mcp/tools/agent/find.d.ts +3 -0
  45. package/lib/mcp/tools/agent/find.js +60 -0
  46. package/lib/mcp/tools/agent/proof.d.ts +3 -0
  47. package/lib/mcp/tools/agent/proof.js +214 -0
  48. package/lib/mcp/tools/agent/propose.d.ts +9 -0
  49. package/lib/mcp/tools/agent/propose.js +75 -0
  50. package/lib/mcp/tools/agent/publish.d.ts +3 -0
  51. package/lib/mcp/tools/agent/publish.js +122 -0
  52. package/lib/mcp/tools/agent/replay.d.ts +3 -0
  53. package/lib/mcp/tools/agent/replay.js +46 -0
  54. package/lib/mcp/tools/agent/session.d.ts +10 -0
  55. package/lib/mcp/tools/agent/session.js +29 -0
  56. package/lib/mcp/tools/credentials/authenticate.d.ts +3 -0
  57. package/lib/mcp/tools/credentials/authenticate.js +108 -0
  58. package/lib/mcp/tools/credentials/build.d.ts +16 -0
  59. package/lib/mcp/tools/credentials/build.js +33 -0
  60. package/lib/mcp/tools/credentials/cache.d.ts +70 -0
  61. package/lib/mcp/tools/credentials/cache.js +114 -0
  62. package/lib/mcp/tools/credentials/env-file.d.ts +25 -0
  63. package/lib/mcp/tools/credentials/env-file.js +71 -0
  64. package/lib/mcp/tools/credentials/import.d.ts +2 -0
  65. package/lib/mcp/tools/credentials/import.js +52 -0
  66. package/lib/mcp/tools/credentials/issue.d.ts +2 -0
  67. package/lib/mcp/tools/credentials/issue.js +84 -0
  68. package/lib/mcp/tools/credentials/list-cached.d.ts +2 -0
  69. package/lib/mcp/tools/credentials/list-cached.js +55 -0
  70. package/lib/mcp/tools/credentials/register.d.ts +18 -0
  71. package/lib/mcp/tools/credentials/register.js +30 -0
  72. package/lib/mcp/tools/credentials/resolve-owner-key.d.ts +12 -0
  73. package/lib/mcp/tools/credentials/resolve-owner-key.js +68 -0
  74. package/lib/mcp/tools/credentials/session-cache.d.ts +14 -0
  75. package/lib/mcp/tools/credentials/session-cache.js +19 -0
  76. package/lib/mcp/tools/credentials/status.d.ts +3 -0
  77. package/lib/mcp/tools/credentials/status.js +62 -0
  78. package/lib/mcp-bin.d.ts +1 -0
  79. package/lib/mcp-bin.js +7 -0
  80. package/lib/old/client.d.ts +67 -0
  81. package/lib/old/client.js +146 -0
  82. package/lib/old/flag.d.ts +11 -0
  83. package/lib/old/flag.js +13 -0
  84. package/lib/old/identity.d.ts +22 -0
  85. package/lib/old/identity.js +47 -0
  86. package/lib/old/instructions.d.ts +7 -0
  87. package/lib/old/instructions.js +29 -0
  88. package/lib/old/login-cdp.d.ts +23 -0
  89. package/lib/old/login-cdp.js +125 -0
  90. package/lib/old/to-register.d.ts +47 -0
  91. package/lib/old/to-register.js +133 -0
  92. package/lib/old/tools/authenticate.d.ts +18 -0
  93. package/lib/old/tools/authenticate.js +127 -0
  94. package/lib/old/tools/build.d.ts +9 -0
  95. package/lib/old/tools/build.js +16 -0
  96. package/lib/old/tools/me-providers.d.ts +10 -0
  97. package/lib/old/tools/me-providers.js +32 -0
  98. package/lib/old/tools/publish.d.ts +16 -0
  99. package/lib/old/tools/publish.js +97 -0
  100. package/lib/paths.d.ts +11 -0
  101. package/lib/paths.js +32 -0
  102. package/lib/pkg.d.ts +3 -0
  103. package/lib/pkg.js +13 -0
  104. package/lib/proof/attestor.d.ts +39 -0
  105. package/lib/proof/attestor.js +207 -0
  106. package/lib/proof/replay.d.ts +8 -0
  107. package/lib/proof/replay.js +73 -0
  108. package/lib/provider/constraints.d.ts +2 -0
  109. package/lib/provider/constraints.js +122 -0
  110. package/lib/provider/draft.d.ts +6 -0
  111. package/lib/provider/draft.js +68 -0
  112. package/lib/provider/graphql.d.ts +2 -0
  113. package/lib/provider/graphql.js +35 -0
  114. package/lib/provider/matcher.d.ts +36 -0
  115. package/lib/provider/matcher.js +436 -0
  116. package/lib/provider/normalize.d.ts +4 -0
  117. package/lib/provider/normalize.js +51 -0
  118. package/lib/provider/schema.d.ts +76 -0
  119. package/lib/provider/schema.js +1 -0
  120. package/lib/provider/secrets.d.ts +5 -0
  121. package/lib/provider/secrets.js +22 -0
  122. package/lib/provider/to-version.d.ts +22 -0
  123. package/lib/provider/to-version.js +68 -0
  124. package/lib/types/openapi.d.ts +4 -0
  125. package/lib/types/openapi.js +1 -0
  126. package/package.json +88 -0
  127. package/scripts/download-zk-files.ts +42 -0
  128. package/scripts/prepend-shebang.ts +17 -0
  129. package/skill/SKILL.md +248 -0
  130. package/src/cdp/capture.ts +215 -0
  131. package/src/cdp/client.ts +20 -0
  132. package/src/cdp/eviction.ts +52 -0
  133. package/src/cdp/launcher.ts +133 -0
  134. package/src/cdp/tabs.ts +22 -0
  135. package/src/cli/args.ts +20 -0
  136. package/src/cli/commands.ts +27 -0
  137. package/src/cli.ts +7 -0
  138. package/src/config/store.ts +86 -0
  139. package/src/index.ts +12 -0
  140. package/src/logger.ts +8 -0
  141. package/src/mcp/api/operations.gen.ts +2397 -0
  142. package/src/mcp/api/tools.ts +45 -0
  143. package/src/mcp/remap_errors.ts +35 -0
  144. package/src/mcp/server.ts +119 -0
  145. package/src/mcp/start.ts +78 -0
  146. package/src/mcp/tools/agent/analyze.ts +34 -0
  147. package/src/mcp/tools/agent/attach.ts +95 -0
  148. package/src/mcp/tools/agent/build.ts +99 -0
  149. package/src/mcp/tools/agent/capture.ts +202 -0
  150. package/src/mcp/tools/agent/eval.ts +161 -0
  151. package/src/mcp/tools/agent/find.ts +73 -0
  152. package/src/mcp/tools/agent/proof.ts +272 -0
  153. package/src/mcp/tools/agent/propose.ts +111 -0
  154. package/src/mcp/tools/agent/publish.ts +155 -0
  155. package/src/mcp/tools/agent/replay.ts +58 -0
  156. package/src/mcp/tools/agent/session.ts +46 -0
  157. package/src/mcp/tools/credentials/authenticate.ts +135 -0
  158. package/src/mcp/tools/credentials/build.ts +37 -0
  159. package/src/mcp/tools/credentials/cache.ts +161 -0
  160. package/src/mcp/tools/credentials/env-file.ts +101 -0
  161. package/src/mcp/tools/credentials/import.ts +72 -0
  162. package/src/mcp/tools/credentials/issue.ts +107 -0
  163. package/src/mcp/tools/credentials/list-cached.ts +66 -0
  164. package/src/mcp/tools/credentials/register.ts +49 -0
  165. package/src/mcp/tools/credentials/resolve-owner-key.ts +78 -0
  166. package/src/mcp/tools/credentials/session-cache.ts +31 -0
  167. package/src/mcp/tools/credentials/status.ts +82 -0
  168. package/src/mcp-bin.ts +7 -0
  169. package/src/old/client.ts +236 -0
  170. package/src/old/flag.ts +13 -0
  171. package/src/old/identity.ts +56 -0
  172. package/src/old/instructions.ts +30 -0
  173. package/src/old/login-cdp.ts +153 -0
  174. package/src/old/to-register.ts +234 -0
  175. package/src/old/tools/authenticate.ts +152 -0
  176. package/src/old/tools/build.ts +21 -0
  177. package/src/old/tools/me-providers.ts +42 -0
  178. package/src/old/tools/publish.ts +135 -0
  179. package/src/paths.ts +37 -0
  180. package/src/pkg.ts +24 -0
  181. package/src/proof/attestor.ts +286 -0
  182. package/src/proof/replay.ts +102 -0
  183. package/src/provider/constraints.ts +150 -0
  184. package/src/provider/draft.ts +105 -0
  185. package/src/provider/graphql.ts +42 -0
  186. package/src/provider/matcher.ts +616 -0
  187. package/src/provider/normalize.ts +65 -0
  188. package/src/provider/schema.ts +92 -0
  189. package/src/provider/secrets.ts +29 -0
  190. package/src/provider/to-version.ts +90 -0
  191. package/src/types/openapi.ts +7 -0
@@ -0,0 +1,68 @@
1
+ import { synthesizeMatcher } from "./matcher.js";
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) {
8
+ const out = {};
9
+ for (const [k, v] of Object.entries(headers)) {
10
+ if (k.toLowerCase() === 'accept-encoding') {
11
+ continue;
12
+ }
13
+ out[k] = v;
14
+ }
15
+ out['Accept-Encoding'] = 'identity';
16
+ return out;
17
+ }
18
+ /** Derive a `paramValues` key (and capture-group name) from the provider
19
+ * name's last alphanumeric token. `"GitHub Username"` → `"username"`,
20
+ * `"x-followers"` → `"followers"`. */
21
+ function deriveParamName(providerName) {
22
+ const tokens = providerName.split(/[^A-Za-z0-9]+/).filter(Boolean);
23
+ const last = tokens[tokens.length - 1] ?? 'value';
24
+ return last.toLowerCase();
25
+ }
26
+ export function draftProvider(req, target, name, hash) {
27
+ if (!req.responseBody) {
28
+ throw new Error(`captured request ${req.requestId} has no response body`);
29
+ }
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}`);
35
+ }
36
+ 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);
46
+ const secretRefs = Object.keys(secrets).map((name) => ({
47
+ name,
48
+ source: name.toLowerCase() === 'cookie' ? 'cookie' : 'header',
49
+ redacted: true,
50
+ }));
51
+ const provider = {
52
+ name,
53
+ url: req.url,
54
+ method: req.method,
55
+ headers: headersForProvider,
56
+ responseMatches: [synthesis.match],
57
+ };
58
+ if (synthesis.redaction) {
59
+ provider.responseRedactions = [synthesis.redaction];
60
+ }
61
+ if (synthesis.paramValues) {
62
+ provider.paramValues = synthesis.paramValues;
63
+ }
64
+ if (req.requestBody) {
65
+ provider.body = req.requestBody;
66
+ }
67
+ return { provider, secretRefs };
68
+ }
@@ -0,0 +1,2 @@
1
+ export declare function looksLikeGraphqlEndpoint(url: string): boolean;
2
+ export declare function extractGraphqlOp(body: string | undefined): string | undefined;
@@ -0,0 +1,35 @@
1
+ export function looksLikeGraphqlEndpoint(url) {
2
+ try {
3
+ const u = new URL(url);
4
+ return u.pathname.includes('/graphql');
5
+ }
6
+ catch {
7
+ return false;
8
+ }
9
+ }
10
+ export function extractGraphqlOp(body) {
11
+ if (!body) {
12
+ return undefined;
13
+ }
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(body);
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ if (typeof parsed !== 'object' || parsed === null) {
22
+ return undefined;
23
+ }
24
+ const obj = parsed;
25
+ if (typeof obj.operationName === 'string' && obj.operationName.length > 0) {
26
+ return obj.operationName;
27
+ }
28
+ if (typeof obj.query === 'string') {
29
+ const m = obj.query.match(/^\s*(?:query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/);
30
+ if (m) {
31
+ return m[1];
32
+ }
33
+ }
34
+ return undefined;
35
+ }
@@ -0,0 +1,36 @@
1
+ import type { OprfMode, ResponseMatch, ResponseRedaction } from './schema.ts';
2
+ /** Reclaim attestor caps OPRF-hashed values at 62 bytes (TOPRF marker
3
+ * encoding). Surfaces as a clean error here rather than a confusing
4
+ * attestor-side failure at proof time. */
5
+ export declare const OPRF_MAX_BYTES = 62;
6
+ export interface MatcherSynthesis {
7
+ match: ResponseMatch;
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. */
13
+ paramValues?: Record<string, string>;
14
+ }
15
+ /**
16
+ * Build the matcher pieces (responseMatches + responseRedactions +
17
+ * paramValues) for a target value seen in a captured response.
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
27
+ *
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.
30
+ *
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.
35
+ */
36
+ export declare function synthesizeMatcher(body: string, contentType: string, target: string, paramName: string, hash?: OprfMode): MatcherSynthesis | undefined;
@@ -0,0 +1,436 @@
1
+ import { normalizeCandidates } from "./normalize.js";
2
+ /**
3
+ * Minimum bytes per response redaction. The Reclaim zkTLS protocol uses
4
+ * AES-CTR; revealing fewer than one AES block (16 bytes) opens the slice
5
+ * to brute-force guessing. Matters only for the text/plain padded-regex
6
+ * fallback — xPath / jsonPath redactions reveal a whole TLS record, which
7
+ * is always larger.
8
+ */
9
+ const MIN_REVEAL_BYTES = 16;
10
+ /**
11
+ * Budget for climbing past the smallest enclosing element in search of an
12
+ * ancestor with an `id` or `class` anchor. Capped so we don't end up
13
+ * pointing at `<body>` or `<html>` (which would defeat redaction by
14
+ * revealing most of the page).
15
+ */
16
+ const MAX_ANCHOR_CLIMB_BYTES = 512;
17
+ /**
18
+ * Hard cap on how many bytes the redaction is allowed to reveal. When
19
+ * the picked xPath/jsonPath element exceeds this, the redaction is
20
+ * extended with a `regex` that narrows to the surrounding key/value
21
+ * pattern — so the attestor only ZKs ~50-100 bytes instead of multi-KB
22
+ * blobs. Without this, a perfectly anchorable element like
23
+ * `<script id="client-env">{...7KB of state...}</script>` would push
24
+ * the attestor through many MB of ZK work and OOM Node.
25
+ */
26
+ const MAX_REVEAL_BYTES = 512;
27
+ /** Reclaim attestor caps OPRF-hashed values at 62 bytes (TOPRF marker
28
+ * encoding). Surfaces as a clean error here rather than a confusing
29
+ * attestor-side failure at proof time. */
30
+ export const OPRF_MAX_BYTES = 62;
31
+ /**
32
+ * Build the matcher pieces (responseMatches + responseRedactions +
33
+ * paramValues) for a target value seen in a captured response.
34
+ *
35
+ * All outputs share the same shape:
36
+ * - responseMatches: a single `contains` whose value is the surrounding
37
+ * structural context with the target replaced by `{{paramName}}`
38
+ * - paramValues: `{ [paramName]: target }` — the prover's real value
39
+ * - responseRedactions: one entry that narrows the revealed slice
40
+ * (xPath / jsonPath / regex), and — when `hash` is requested — also
41
+ * carries a `regex` with a named capture group + a `hash` field for
42
+ * OPRF
43
+ *
44
+ * `hash` is opt-in. The skill prompts the dev. Drafting without `hash`
45
+ * is the default — the value travels in cleartext to the attestor.
46
+ *
47
+ * Throws when `hash` is set but the target has no safe character class
48
+ * (unicode, punctuation, spaces — anything that doesn't fit a digit /
49
+ * slug / email regex). Drafting OPRF for a value we can't capture
50
+ * reliably would yield a broken provider.
51
+ */
52
+ export function synthesizeMatcher(body, contentType, target, paramName, hash) {
53
+ if (!body.includes(target) &&
54
+ !normalizeCandidates(target).some((v) => body.includes(v))) {
55
+ return undefined;
56
+ }
57
+ if (hash && !inferValueClass(target)) {
58
+ throw new Error('OPRF hashing requires a target with a known-safe character class ' +
59
+ '(digits, slug, or email). ' +
60
+ `Got "${target}" which is free-form. Use a tighter target` +
61
+ '(e.g. just the alphanumeric portion of the value)' +
62
+ ' or call propose_provider without `hash`.');
63
+ }
64
+ const pick = pickAnchor(body, contentType, target);
65
+ // Two reasons to extend the redaction with a `regex` narrow:
66
+ // (a) hash is set — the regex carries the named capture group we OPRF
67
+ // (b) the picked element is too big to reveal whole — we narrow the
68
+ // slice to just the value's surrounding key/value pattern
69
+ const isOversized = pick?.elementBytes !== undefined && pick.elementBytes > MAX_REVEAL_BYTES;
70
+ const needsRegex = hash !== undefined || isOversized;
71
+ if (pick && needsRegex) {
72
+ // Free-form values can use a permissive class when only narrowing;
73
+ // `hash` already had the strict check above.
74
+ const valueClass = inferValueClass(target) ?? '[^"<>]+';
75
+ const captureRegex = buildCaptureRegex(body, target, paramName, valueClass);
76
+ const redaction = {
77
+ ...pick.redaction,
78
+ regex: captureRegex,
79
+ };
80
+ if (hash) {
81
+ redaction.hash = hash;
82
+ }
83
+ return makeResult(body, target, paramName, redaction);
84
+ }
85
+ if (pick) {
86
+ return makeResult(body, target, paramName, pick.redaction);
87
+ }
88
+ // No structural anchor — fall back to a regex-only redaction.
89
+ if (hash) {
90
+ const captureRegex = buildCaptureRegex(body, target, paramName, inferValueClass(target));
91
+ return makeResult(body, target, paramName, { regex: captureRegex, hash });
92
+ }
93
+ const padded = buildPaddedRegex(body, target);
94
+ if (padded) {
95
+ return makeResult(body, target, paramName, { regex: padded });
96
+ }
97
+ return undefined;
98
+ }
99
+ function makeResult(body, target, paramName, redaction) {
100
+ return {
101
+ match: {
102
+ type: 'contains',
103
+ value: buildContextualTemplate(body, target, paramName),
104
+ },
105
+ redaction,
106
+ paramValues: { [paramName]: target },
107
+ };
108
+ }
109
+ function pickAnchor(body, contentType, target) {
110
+ if (/json/i.test(contentType)) {
111
+ const jsonPath = findJsonPath(body, target);
112
+ if (jsonPath) {
113
+ return { redaction: { jsonPath } };
114
+ }
115
+ }
116
+ if (/html|xml/i.test(contentType) || looksLikeHtml(body)) {
117
+ const xPath = synthesizeXPath(body, target);
118
+ if (xPath) {
119
+ return {
120
+ redaction: { xPath: xPath.xPath },
121
+ elementBytes: xPath.elementBytes,
122
+ };
123
+ }
124
+ }
125
+ return undefined;
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // JSON helpers
129
+ // ---------------------------------------------------------------------------
130
+ function findJsonPath(body, target) {
131
+ let parsed;
132
+ try {
133
+ parsed = JSON.parse(body);
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ const candidates = normalizeCandidates(target);
139
+ return walk(parsed, '$', candidates);
140
+ }
141
+ function walk(node, path, candidates) {
142
+ if (node === null || node === undefined) {
143
+ return undefined;
144
+ }
145
+ if (typeof node === 'string' ||
146
+ typeof node === 'number' ||
147
+ typeof node === 'boolean') {
148
+ const str = String(node);
149
+ if (candidates.includes(str)) {
150
+ return path;
151
+ }
152
+ return undefined;
153
+ }
154
+ if (Array.isArray(node)) {
155
+ for (const [i, element] of node.entries()) {
156
+ const found = walk(element, `${path}[${i}]`, candidates);
157
+ if (found) {
158
+ return found;
159
+ }
160
+ }
161
+ return undefined;
162
+ }
163
+ if (typeof node === 'object') {
164
+ for (const [k, v] of Object.entries(node)) {
165
+ const childPath = /^[A-Za-z_][A-Za-z0-9_]*$/.test(k)
166
+ ? `${path}.${k}`
167
+ : `${path}["${k}"]`;
168
+ const found = walk(v, childPath, candidates);
169
+ if (found) {
170
+ return found;
171
+ }
172
+ }
173
+ }
174
+ return undefined;
175
+ }
176
+ function looksLikeHtml(body) {
177
+ return /<[a-zA-Z][a-zA-Z0-9-]*[\s>]/.test(body.slice(0, 2000));
178
+ }
179
+ /** Find the best xPath anchor for `target` in HTML-shaped `body`.
180
+ * Preference order:
181
+ * 1. id/class on the smallest enclosing element
182
+ * 2. id/class on the nearest ancestor within MAX_ANCHOR_CLIMB_BYTES
183
+ * 3. positional `(//tag)[N]` on the smallest enclosing element */
184
+ function synthesizeXPath(body, target) {
185
+ for (const variant of normalizeCandidates(target)) {
186
+ const idx = body.indexOf(variant);
187
+ if (idx < 0) {
188
+ continue;
189
+ }
190
+ let el = findEnclosingElement(body, idx, variant.length);
191
+ while (el && el.closeEnd - el.openStart < MIN_REVEAL_BYTES) {
192
+ const outer = findEnclosingElement(body, el.openStart, el.closeEnd - el.openStart);
193
+ if (!outer || outer.openStart >= el.openStart) {
194
+ break;
195
+ }
196
+ el = outer;
197
+ }
198
+ if (!el) {
199
+ continue;
200
+ }
201
+ const anchored = climbForAnchor(body, el);
202
+ const final = anchored ?? el;
203
+ return {
204
+ xPath: buildXPath(final, body),
205
+ elementBytes: final.closeEnd - final.openStart,
206
+ };
207
+ }
208
+ return undefined;
209
+ }
210
+ function climbForAnchor(body, el) {
211
+ if (el.id || el.classes.length > 0) {
212
+ return el;
213
+ }
214
+ let cur = el;
215
+ while (true) {
216
+ const parent = findEnclosingElement(body, cur.openStart, cur.closeEnd - cur.openStart);
217
+ if (!parent || parent.openStart >= cur.openStart) {
218
+ return undefined;
219
+ }
220
+ if (parent.closeEnd - parent.openStart > MAX_ANCHOR_CLIMB_BYTES) {
221
+ return undefined;
222
+ }
223
+ if (parent.id || parent.classes.length > 0) {
224
+ return parent;
225
+ }
226
+ cur = parent;
227
+ }
228
+ }
229
+ function findEnclosingElement(body, valueIdx, valueLen) {
230
+ let i = valueIdx - 1;
231
+ while (i >= 0) {
232
+ if (body[i] !== '>') {
233
+ i--;
234
+ continue;
235
+ }
236
+ const tagEnd = i;
237
+ const tagStart = body.lastIndexOf('<', tagEnd);
238
+ if (tagStart < 0) {
239
+ return undefined;
240
+ }
241
+ const openTag = body.slice(tagStart, tagEnd + 1);
242
+ if (openTag.startsWith('</') ||
243
+ openTag.startsWith('<!') ||
244
+ openTag.startsWith('<?') ||
245
+ openTag.trimEnd().endsWith('/>')) {
246
+ i = tagStart - 1;
247
+ continue;
248
+ }
249
+ const tagNameMatch = openTag.match(/^<([a-zA-Z][a-zA-Z0-9-]*)/);
250
+ if (!tagNameMatch) {
251
+ i = tagStart - 1;
252
+ continue;
253
+ }
254
+ const tagName = tagNameMatch[1].toLowerCase();
255
+ const closePattern = new RegExp(`</${tagName}\\s*>`, 'i');
256
+ const afterValue = body.slice(valueIdx + valueLen);
257
+ const closeMatch = afterValue.match(closePattern);
258
+ if (closeMatch?.index === undefined) {
259
+ i = tagStart - 1;
260
+ continue;
261
+ }
262
+ const closeEnd = valueIdx + valueLen + closeMatch.index + closeMatch[0].length;
263
+ const idMatch = openTag.match(/\bid\s*=\s*["']([^"']+)["']/);
264
+ const classMatch = openTag.match(/\bclass\s*=\s*["']([^"']+)["']/);
265
+ const classes = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
266
+ const el = {
267
+ tag: tagName,
268
+ openStart: tagStart,
269
+ closeEnd,
270
+ classes,
271
+ };
272
+ if (idMatch?.[1] !== undefined) {
273
+ el.id = idMatch[1];
274
+ }
275
+ return el;
276
+ }
277
+ return undefined;
278
+ }
279
+ function buildXPath(el, body) {
280
+ if (el.id) {
281
+ return `//${el.tag}[@id=${xpathLiteral(el.id)}]`;
282
+ }
283
+ if (el.classes.length > 0) {
284
+ const distinctive = [...el.classes].sort((a, b) => b.length - a.length)[0];
285
+ return `//${el.tag}[contains(@class, ${xpathLiteral(distinctive)})]`;
286
+ }
287
+ return `(//${el.tag})[${tagIndex(body, el)}]`;
288
+ }
289
+ function tagIndex(body, el) {
290
+ const pattern = new RegExp(`<${el.tag}\\b`, 'gi');
291
+ let n = 0;
292
+ let m;
293
+ while ((m = pattern.exec(body)) !== null) {
294
+ if (m.index > el.openStart) {
295
+ break;
296
+ }
297
+ n++;
298
+ }
299
+ return n;
300
+ }
301
+ /** Quote a string as an XPath 1.0 string literal. XPath 1.0 literals have no
302
+ * escape mechanism, so a value containing both quote characters must be
303
+ * assembled with concat(). Backslashes are not special and pass through. */
304
+ function xpathLiteral(s) {
305
+ if (!s.includes('"')) {
306
+ return `"${s}"`;
307
+ }
308
+ if (!s.includes("'")) {
309
+ return `'${s}'`;
310
+ }
311
+ const parts = s.split('"').map((p) => `"${p}"`);
312
+ return `concat(${parts.join(', \'"\', ')})`;
313
+ }
314
+ // ---------------------------------------------------------------------------
315
+ // Padded-regex fallback for non-JSON, non-HTML content
316
+ // ---------------------------------------------------------------------------
317
+ function buildPaddedRegex(body, target) {
318
+ for (const cand of normalizeCandidates(target)) {
319
+ const idx = body.indexOf(cand);
320
+ if (idx < 0) {
321
+ continue;
322
+ }
323
+ let start = idx;
324
+ let end = idx + cand.length;
325
+ const need = MIN_REVEAL_BYTES - cand.length;
326
+ if (need > 0) {
327
+ const leftPad = Math.floor(need / 2);
328
+ const rightPad = need - leftPad;
329
+ start = Math.max(0, idx - leftPad);
330
+ end = Math.min(body.length, idx + cand.length + rightPad);
331
+ if (start === 0) {
332
+ end = Math.min(body.length, idx + cand.length + need - (idx - 0));
333
+ }
334
+ if (end === body.length) {
335
+ start = Math.max(0, body.length - MIN_REVEAL_BYTES);
336
+ }
337
+ }
338
+ const isBoundary = (c) => {
339
+ return c === '<' || c === '>' || c === '"' || c === '}' || c === '{';
340
+ };
341
+ const STRETCH = 8;
342
+ for (let i = 0; i < STRETCH && start > 0 && !isBoundary(body[start]); i++) {
343
+ start--;
344
+ }
345
+ for (let i = 0; i < STRETCH && end < body.length && !isBoundary(body[end - 1]); i++) {
346
+ end++;
347
+ }
348
+ const window = body.slice(start, end);
349
+ if (window.length < MIN_REVEAL_BYTES) {
350
+ continue;
351
+ }
352
+ return escapeRegex(window);
353
+ }
354
+ return undefined;
355
+ }
356
+ function escapeRegex(s) {
357
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
358
+ }
359
+ // ---------------------------------------------------------------------------
360
+ // Capture group regex (only used when hash is requested)
361
+ // ---------------------------------------------------------------------------
362
+ function buildCaptureRegex(body, target, paramName, valueClass) {
363
+ const idx = body.indexOf(target);
364
+ if (idx < 0) {
365
+ return `(?<${paramName}>${valueClass})`;
366
+ }
367
+ const endIdx = idx + target.length;
368
+ const leftCtx = findLeftContext(body, idx);
369
+ const rightCtx = findRightContext(body, endIdx);
370
+ // eslint-disable-next-line @stylistic/max-len
371
+ return `${escapeRegex(leftCtx)}(?<${paramName}>${valueClass})${escapeRegex(rightCtx)}`;
372
+ }
373
+ // ---------------------------------------------------------------------------
374
+ // responseMatches template (literal `contains`, paramValues-substituted)
375
+ // ---------------------------------------------------------------------------
376
+ /** Build the literal substring template for `responseMatches.value` —
377
+ * the surrounding structural context with the target replaced by
378
+ * `{{paramName}}`. The attestor substitutes paramValues at proof time
379
+ * (or the OPRF nullifier, when the matching redaction has `hash`). */
380
+ function buildContextualTemplate(body, target, paramName) {
381
+ const idx = body.indexOf(target);
382
+ if (idx < 0) {
383
+ return `{{${paramName}}}`;
384
+ }
385
+ const endIdx = idx + target.length;
386
+ const leftCtx = findLeftContext(body, idx);
387
+ const rightCtx = findRightContext(body, endIdx);
388
+ return `${leftCtx}{{${paramName}}}${rightCtx}`;
389
+ }
390
+ /** Walk left up to 32 bytes, stopping at the first structural delimiter.
391
+ * `>` is included (closing of an HTML tag is useful context); `{`, `}`,
392
+ * `,`, `;`, `\n` are excluded (would over-specify the template). */
393
+ function findLeftContext(body, idx) {
394
+ const MAX_CONTEXT_BYTES = 32;
395
+ const stop = Math.max(0, idx - MAX_CONTEXT_BYTES);
396
+ for (let i = idx - 1; i >= stop; i--) {
397
+ const c = body[i];
398
+ if (c === '>') {
399
+ return body.slice(i, idx);
400
+ }
401
+ if (c === '{' || c === '}' || c === ',' || c === ';' || c === '\n') {
402
+ return body.slice(i + 1, idx);
403
+ }
404
+ }
405
+ return body.slice(stop, idx);
406
+ }
407
+ function findRightContext(body, endIdx) {
408
+ const MAX_CONTEXT_BYTES = 16;
409
+ const stop = Math.min(body.length, endIdx + MAX_CONTEXT_BYTES);
410
+ for (let i = endIdx; i < stop; i++) {
411
+ const c = body[i];
412
+ if (c === '<') {
413
+ return body.slice(endIdx, i + 1);
414
+ }
415
+ if (c === '{' || c === '}' || c === ',' || c === ';' || c === '\n') {
416
+ return body.slice(endIdx, i);
417
+ }
418
+ }
419
+ return body.slice(endIdx, stop);
420
+ }
421
+ /** Derive a regex character class from the target's shape. Returns
422
+ * `undefined` for free-form values — those can't be OPRF'd because we
423
+ * can't write a regex capture that reliably matches arbitrary unicode /
424
+ * punctuation. Caller should refuse the hash request. */
425
+ function inferValueClass(target) {
426
+ if (/^[0-9]+$/.test(target)) {
427
+ return '\\d+';
428
+ }
429
+ if (/^[A-Za-z0-9_-]+$/.test(target)) {
430
+ return '[A-Za-z0-9_-]+';
431
+ }
432
+ if (/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(target)) {
433
+ return '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}';
434
+ }
435
+ return undefined;
436
+ }
@@ -0,0 +1,4 @@
1
+ /** Return all string variants of `value` that might appear in a response
2
+ * body. Used by find_requests_containing to do server-side substring search
3
+ * without forcing the caller to guess the encoding. */
4
+ export declare function normalizeCandidates(value: string): string[];
@@ -0,0 +1,51 @@
1
+ /** Return all string variants of `value` that might appear in a response
2
+ * body. Used by find_requests_containing to do server-side substring search
3
+ * without forcing the caller to guess the encoding. */
4
+ export function normalizeCandidates(value) {
5
+ const out = new Set();
6
+ const trimmed = value.trim();
7
+ out.add(trimmed);
8
+ const asciiDigits = toAsciiDigits(trimmed);
9
+ if (asciiDigits !== trimmed) {
10
+ out.add(asciiDigits);
11
+ }
12
+ // Comma-grouped numbers: "1,247" -> "1247"
13
+ const dedup = asciiDigits.replace(/,/g, '');
14
+ if (dedup !== asciiDigits) {
15
+ out.add(dedup);
16
+ }
17
+ // k/M/B-suffixed display numbers: "1.2k" -> integer range
18
+ const suffixMatch = asciiDigits.match(/^(\d+(?:\.\d+)?)\s*([kKmMbB])$/);
19
+ if (suffixMatch) {
20
+ const base = parseFloat(suffixMatch[1]);
21
+ const mult = { k: 1e3, K: 1e3, m: 1e6, M: 1e6, b: 1e9, B: 1e9 }[suffixMatch[2]];
22
+ const center = base * mult;
23
+ // Display rounds: "1.2k" came from anything 1150..1249.
24
+ const window = mult / 10 ** (suffixMatch[1].split('.')[1]?.length ?? 0) / 2;
25
+ for (let n = Math.ceil(center - window); n < Math.ceil(center + window); n++) {
26
+ out.add(String(n));
27
+ }
28
+ }
29
+ // JSON-encoded + percent-encoded variants of every existing candidate.
30
+ for (const c of [...out]) {
31
+ out.add(JSON.stringify(c));
32
+ out.add(encodeURIComponent(c));
33
+ out.add(encodeURIComponent(JSON.stringify(c)));
34
+ }
35
+ return [...out];
36
+ }
37
+ function toAsciiDigits(s) {
38
+ return s.replace(/[\p{Nd}]/gu, (ch) => {
39
+ const code = ch.codePointAt(0);
40
+ // Find the zero of this digit's block: digit value = code - blockZero.
41
+ // Walk back at most 9 codepoints to find it.
42
+ for (let i = 0; i <= 9; i++) {
43
+ const candidate = String.fromCodePoint(code - i);
44
+ if (/^\p{Nd}$/u.test(candidate)) {
45
+ continue;
46
+ }
47
+ return String(i - 1);
48
+ }
49
+ return ch;
50
+ });
51
+ }