@reclaimprotocol/agent 0.1.17 → 0.1.20

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 (171) hide show
  1. package/README.md +6 -2
  2. package/lib/authoring/backend.d.ts +4 -0
  3. package/lib/authoring/tools.js +4 -1
  4. package/lib/cdp/capture.d.ts +2 -2
  5. package/lib/cdp/capture.js +25 -21
  6. package/lib/cdp/client.d.ts +39 -1
  7. package/lib/cdp/client.js +125 -1
  8. package/lib/consts.d.ts +7 -0
  9. package/lib/consts.js +15 -0
  10. package/lib/mcp/api/operations.gen.js +33 -9
  11. package/lib/mcp/instructions.js +27 -3
  12. package/lib/mcp/start.js +12 -8
  13. package/lib/mcp/tools/agent/attach.d.ts +18 -4
  14. package/lib/mcp/tools/agent/attach.js +142 -52
  15. package/lib/mcp/tools/agent/build.d.ts +1 -1
  16. package/lib/mcp/tools/agent/build.js +5 -3
  17. package/lib/mcp/tools/agent/capture.d.ts +1 -0
  18. package/lib/mcp/tools/agent/capture.js +8 -1
  19. package/lib/mcp/tools/agent/eval.js +2 -3
  20. package/lib/mcp/tools/agent/how-it-works.d.ts +1 -1
  21. package/lib/mcp/tools/agent/how-it-works.js +1 -1
  22. package/lib/mcp/tools/agent/publish.js +55 -1
  23. package/lib/mcp/tools/agent/session.d.ts +2 -2
  24. package/lib/mcp/tools/agent/session.js +21 -5
  25. package/lib/mcp/tools/agent/shared.js +4 -6
  26. package/lib/mcp/tools/authenticate/authenticate.d.ts +1 -1
  27. package/lib/mcp/tools/authenticate/authenticate.js +10 -4
  28. package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -0
  29. package/lib/mcp/tools/authenticate/builder-access.js +31 -0
  30. package/lib/mcp/tools/authenticate/index.d.ts +1 -0
  31. package/lib/mcp/tools/authenticate/index.js +1 -0
  32. package/lib/mcp/tools/mode/mode.d.ts +3 -2
  33. package/lib/mcp/tools/mode/mode.js +24 -4
  34. package/lib/old/login-cdp.js +11 -3
  35. package/lib/old/to-register.d.ts +16 -8
  36. package/lib/old/to-register.js +29 -21
  37. package/lib/old/tools/publish.js +109 -9
  38. package/lib/provider/draft.d.ts +20 -0
  39. package/lib/provider/draft.js +75 -51
  40. package/lib/provider/index.d.ts +1 -1
  41. package/lib/provider/index.js +1 -1
  42. package/lib/provider/injection-doc.js +4 -3
  43. package/lib/provider/schema.d.ts +26 -1
  44. package/lib/provider/secrets.d.ts +37 -0
  45. package/lib/provider/secrets.js +147 -6
  46. package/lib/provider/to-version.d.ts +26 -2
  47. package/lib/provider/to-version.js +29 -5
  48. package/package.json +2 -2
  49. package/skill/SKILL.builder.md +43 -95
  50. package/skill/SKILL.common.md +102 -0
  51. package/skill/SKILL.old.md +20 -74
  52. package/skill/builder-mode.md +4 -2
  53. package/skill/injection-api.md +95 -10
  54. package/skill/old-mode.md +5 -3
  55. package/src/authoring/backend.ts +4 -0
  56. package/src/authoring/tools.ts +4 -1
  57. package/src/cdp/capture.ts +24 -24
  58. package/src/cdp/client.ts +197 -2
  59. package/src/consts.ts +20 -0
  60. package/src/mcp/api/operations.gen.ts +33 -9
  61. package/src/mcp/instructions.ts +36 -3
  62. package/src/mcp/start.ts +13 -8
  63. package/src/mcp/tools/agent/attach.ts +198 -57
  64. package/src/mcp/tools/agent/build.ts +9 -2
  65. package/src/mcp/tools/agent/capture.ts +10 -1
  66. package/src/mcp/tools/agent/eval.ts +2 -3
  67. package/src/mcp/tools/agent/how-it-works.ts +1 -1
  68. package/src/mcp/tools/agent/publish.ts +64 -1
  69. package/src/mcp/tools/agent/session.ts +29 -8
  70. package/src/mcp/tools/agent/shared.ts +5 -10
  71. package/src/mcp/tools/authenticate/authenticate.ts +14 -4
  72. package/src/mcp/tools/authenticate/builder-access.ts +42 -0
  73. package/src/mcp/tools/authenticate/index.ts +4 -0
  74. package/src/mcp/tools/mode/mode.ts +28 -4
  75. package/src/old/login-cdp.ts +13 -3
  76. package/src/old/to-register.ts +50 -27
  77. package/src/old/tools/publish.ts +131 -9
  78. package/src/provider/draft.ts +98 -56
  79. package/src/provider/index.ts +9 -1
  80. package/src/provider/injection-doc.ts +4 -3
  81. package/src/provider/schema.ts +27 -1
  82. package/src/provider/secrets.ts +171 -6
  83. package/src/provider/to-version.ts +53 -7
  84. package/lib/authoring/find.d.ts +0 -3
  85. package/lib/authoring/find.js +0 -54
  86. package/lib/authoring/propose.d.ts +0 -3
  87. package/lib/authoring/propose.js +0 -109
  88. package/lib/authoring/replay.d.ts +0 -10
  89. package/lib/authoring/replay.js +0 -34
  90. package/lib/cli/args.d.ts +0 -14
  91. package/lib/cli/args.js +0 -19
  92. package/lib/cli/commands.d.ts +0 -1
  93. package/lib/cli/commands.js +0 -19
  94. package/lib/cli.d.ts +0 -1
  95. package/lib/cli.js +0 -7
  96. package/lib/mcp/credentials/authenticate.d.ts +0 -3
  97. package/lib/mcp/credentials/authenticate.js +0 -108
  98. package/lib/mcp/credentials/build.d.ts +0 -5
  99. package/lib/mcp/credentials/build.js +0 -18
  100. package/lib/mcp/credentials/cache.d.ts +0 -70
  101. package/lib/mcp/credentials/cache.js +0 -131
  102. package/lib/mcp/credentials/env-file.d.ts +0 -25
  103. package/lib/mcp/credentials/env-file.js +0 -71
  104. package/lib/mcp/credentials/import.d.ts +0 -2
  105. package/lib/mcp/credentials/import.js +0 -52
  106. package/lib/mcp/credentials/issue.d.ts +0 -2
  107. package/lib/mcp/credentials/issue.js +0 -84
  108. package/lib/mcp/credentials/keypair.d.ts +0 -17
  109. package/lib/mcp/credentials/keypair.js +0 -48
  110. package/lib/mcp/credentials/link.d.ts +0 -3
  111. package/lib/mcp/credentials/link.js +0 -107
  112. package/lib/mcp/credentials/list-cached.d.ts +0 -2
  113. package/lib/mcp/credentials/list-cached.js +0 -55
  114. package/lib/mcp/credentials/owner-key.d.ts +0 -23
  115. package/lib/mcp/credentials/owner-key.js +0 -58
  116. package/lib/mcp/credentials/paths.d.ts +0 -12
  117. package/lib/mcp/credentials/paths.js +0 -35
  118. package/lib/mcp/credentials/poll.d.ts +0 -27
  119. package/lib/mcp/credentials/poll.js +0 -61
  120. package/lib/mcp/credentials/register.d.ts +0 -18
  121. package/lib/mcp/credentials/register.js +0 -30
  122. package/lib/mcp/credentials/resolve-owner-key.d.ts +0 -12
  123. package/lib/mcp/credentials/resolve-owner-key.js +0 -68
  124. package/lib/mcp/credentials/session-cache.d.ts +0 -14
  125. package/lib/mcp/credentials/session-cache.js +0 -42
  126. package/lib/mcp/credentials/status.d.ts +0 -3
  127. package/lib/mcp/credentials/status.js +0 -76
  128. package/lib/mcp/register.d.ts +0 -38
  129. package/lib/mcp/register.js +0 -80
  130. package/lib/mcp/tools/analyze.d.ts +0 -3
  131. package/lib/mcp/tools/analyze.js +0 -26
  132. package/lib/mcp/tools/attach.d.ts +0 -11
  133. package/lib/mcp/tools/attach.js +0 -72
  134. package/lib/mcp/tools/capture.d.ts +0 -10
  135. package/lib/mcp/tools/capture.js +0 -142
  136. package/lib/mcp/tools/eval.d.ts +0 -5
  137. package/lib/mcp/tools/eval.js +0 -127
  138. package/lib/mcp/tools/find.d.ts +0 -3
  139. package/lib/mcp/tools/find.js +0 -60
  140. package/lib/mcp/tools/owner-key.d.ts +0 -2
  141. package/lib/mcp/tools/owner-key.js +0 -63
  142. package/lib/mcp/tools/proof.d.ts +0 -3
  143. package/lib/mcp/tools/proof.js +0 -214
  144. package/lib/mcp/tools/propose.d.ts +0 -9
  145. package/lib/mcp/tools/propose.js +0 -75
  146. package/lib/mcp/tools/publish.d.ts +0 -3
  147. package/lib/mcp/tools/publish.js +0 -59
  148. package/lib/mcp/tools/replay.d.ts +0 -3
  149. package/lib/mcp/tools/replay.js +0 -46
  150. package/lib/mcp/tools/session.d.ts +0 -10
  151. package/lib/mcp/tools/session.js +0 -29
  152. package/lib/mcp/tools/verification/diagnose.d.ts +0 -11
  153. package/lib/mcp/tools/verification/diagnose.js +0 -132
  154. package/lib/mcp/verifications/build.d.ts +0 -5
  155. package/lib/mcp/verifications/build.js +0 -14
  156. package/lib/mcp/verifications/create.d.ts +0 -3
  157. package/lib/mcp/verifications/create.js +0 -57
  158. package/lib/mcp/verifications/find.d.ts +0 -3
  159. package/lib/mcp/verifications/find.js +0 -37
  160. package/lib/mcp/verifications/get.d.ts +0 -3
  161. package/lib/mcp/verifications/get.js +0 -26
  162. package/lib/mcp/verifications/update.d.ts +0 -3
  163. package/lib/mcp/verifications/update.js +0 -78
  164. package/lib/old/tools/get-version-config.d.ts +0 -17
  165. package/lib/old/tools/get-version-config.js +0 -47
  166. package/lib/routes.gen.d.ts +0 -271
  167. package/lib/routes.gen.js +0 -73
  168. package/lib/types/openapi.d.ts +0 -4
  169. package/lib/types/openapi.gen.d.ts +0 -3967
  170. package/lib/types/openapi.gen.js +0 -5
  171. package/lib/types/openapi.js +0 -1
@@ -113,15 +113,14 @@ export async function captureFirebaseTokenViaCdp(opts) {
113
113
  const { targetId } = await Target.createTarget({ url: loginUrl });
114
114
  try {
115
115
  const session = await handles.attachToTab(targetId);
116
- const { Runtime } = session;
117
116
  const deadline = Date.now() + timeoutMs;
118
117
  while (Date.now() < deadline) {
119
- const r = await Runtime.evaluate({
118
+ const result = await session.send('Runtime.evaluate', {
120
119
  expression: EXTRACT_TOKEN_JS,
121
120
  awaitPromise: true,
122
121
  returnByValue: true,
123
122
  });
124
- const value = r.result?.value;
123
+ const value = runtimeResultValue(result);
125
124
  if (typeof value === 'string' && value.length > 0) {
126
125
  return { token: value, closedBrowser: launchedByUs };
127
126
  }
@@ -146,3 +145,12 @@ export async function captureFirebaseTokenViaCdp(opts) {
146
145
  await handles.close().catch(() => { });
147
146
  }
148
147
  }
148
+ function runtimeResultValue(value) {
149
+ if (typeof value !== 'object' || !value || !('result' in value)) {
150
+ return undefined;
151
+ }
152
+ const result = value.result;
153
+ return typeof result === 'object' && result && 'value' in result
154
+ ? result.value
155
+ : undefined;
156
+ }
@@ -3,15 +3,15 @@ import type { AllowedJsRequest } from '../provider/to-version.ts';
3
3
  import type { RegisterProviderBody, UpdateProviderConfigBody } from './client.ts';
4
4
  /**
5
5
  * Translate a drafted `ReclaimProvider` (from `propose_provider`) into the
6
- * OLD devtools backend shapes. Deliberately a SEPARATE module from
6
+ * devtools backend shapes. Deliberately a SEPARATE module from
7
7
  * `provider/to-version.ts` (the builder translator) — the two backends have
8
8
  * structurally different bodies:
9
9
  *
10
10
  * - builder NESTS each redaction into its match's `extract`;
11
- * - old devtools keeps `responseMatches` and `responseRedactions` as
11
+ * - devtools keeps `responseMatches` and `responseRedactions` as
12
12
  * PARALLEL arrays on each `requestData` entry.
13
13
  *
14
- * Old-devtools limit we enforce here (a clear error beats a confusing
14
+ * Devtools limit we enforce here (a clear error beats a confusing
15
15
  * server-side 400): only `GET` / `POST` methods. OPRF redactions are passed
16
16
  * through unchanged — the backend's `hash` field accepts them.
17
17
  */
@@ -23,7 +23,7 @@ export interface OldProviderOpts {
23
23
  /** Defaults to `true` when omitted. Register only (Provider-level flag). */
24
24
  useProxy?: boolean;
25
25
  providerType?: 'PRIVATE' | 'PUBLIC';
26
- /** Page-injection script — old-devtools' name for the builder's
26
+ /** Page-injection script — the devtools name for the builder's
27
27
  * `webSettings.jsUserScripts`. Top-level on register, nested under
28
28
  * `providerConfig` on config (add-version). The backend rejects a few
29
29
  * deprecated injection APIs (e.g. `window.payloadData`) with a 400. */
@@ -50,6 +50,14 @@ export interface OldProviderOpts {
50
50
  /** Config (add-version) ONLY — the register endpoint never reads this
51
51
  * field from the request body. Free-form JSON. */
52
52
  extensionConfig?: unknown;
53
+ /** Provider-level `disableRequestReplay` (HAWKEYE document replay skip).
54
+ * Config (add-version) ONLY: the register endpoint hardcodes it to false
55
+ * — the publish tool follows a register up with an add-version call to
56
+ * apply it for new providers (see `old/tools/publish.ts`). NOTE the
57
+ * builder backend has NO equivalent; its inverse is
58
+ * `webSettings.clientOptions.inapp.interceptorOptions
59
+ * .isDocumentRequestReplayEnabled`. */
60
+ disableRequestReplay?: boolean;
53
61
  }
54
62
  interface OldResponseMatch {
55
63
  value: string;
@@ -68,11 +76,11 @@ interface OldResponseRedaction {
68
76
  }
69
77
  interface OldRequestData {
70
78
  url: string;
71
- urlType: 'CONSTANT' | 'TEMPLATE';
79
+ urlType: 'REGEX' | 'CONSTANT' | 'TEMPLATE';
72
80
  method: 'GET' | 'POST';
73
81
  responseMatches: OldResponseMatch[];
74
82
  responseRedactions: OldResponseRedaction[];
75
- credentials: 'include';
83
+ credentials: 'omit' | 'same-origin' | 'include';
76
84
  bodySniff?: {
77
85
  enabled: boolean;
78
86
  template: string;
@@ -88,7 +96,7 @@ export interface OldInjectedRequestData extends OldRequestData {
88
96
  }
89
97
  /** Translate fresh `allowedInjectedRequestData` drafts (the author's input
90
98
  * shape, `AllowedJsRequest[]` — same shape the builder translator uses for
91
- * its own `allowedJsRequests`) into the old backend's shape. A
99
+ * its own `allowedJsRequests`) into the devtools backend's shape. A
92
100
  * carried-forward value read back from an existing version is ALREADY in
93
101
  * this shape and should be passed straight through instead — don't run it
94
102
  * through this a second time. */
@@ -101,7 +109,7 @@ export declare function providerToRegisterBody(providers: ReclaimProvider[], opt
101
109
  * an EXISTING provider. `version` must not already exist; `versionInfo` is
102
110
  * required and must be ≥10 chars (enforced by the backend). */
103
111
  export declare function providerToConfigBody(providers: ReclaimProvider[], opts: OldProviderOpts, version: string, versionInfo: string): UpdateProviderConfigBody;
104
- /** Minimal version triplet as the old backend stores it. */
112
+ /** Minimal version triplet as the devtools backend stores it. */
105
113
  export interface SemverTriplet {
106
114
  major: number;
107
115
  minor: number;
@@ -1,5 +1,6 @@
1
+ import assert from 'node:assert';
1
2
  /** Default web settings, mirroring the builder openapi defaults
2
- * (`ProviderWebSettings.geoLocation`, `PortalClientOptions.useProxy`). The old
3
+ * (`ProviderWebSettings.geoLocation`, `PortalClientOptions.useProxy`). The
3
4
  * backend accepts both: `geoLocation` lives on the providerConfig, `useProxy`
4
5
  * is a Provider-level flag (register only). */
5
6
  const DEFAULT_GEO_LOCATION = '{{DYNAMIC_GEO}}';
@@ -10,29 +11,29 @@ const DEFAULT_USE_PROXY = true;
10
11
  * entries — a `ReclaimProvider` satisfies it structurally either way. */
11
12
  function requestDataFromProvider(provider) {
12
13
  const method = String(provider.method).toUpperCase();
13
- if (method !== 'GET' && method !== 'POST') {
14
- throw new Error('Old devtools supports only GET/POST requests; got ' +
15
- `"${provider.method}". Re-capture against a GET/POST endpoint.`);
16
- }
17
- // The old backend has no dotted interpolation: consumer-supplied params
14
+ assert(method === 'GET' || method === 'POST', new Error('Devtools supports only GET/POST requests; got ' +
15
+ `"${provider.method}". Re-capture against a GET/POST endpoint.`));
16
+ // The devtools backend has no dotted interpolation: consumer-supplied params
18
17
  // must use the `{{context_<name>}}` form here, not the builder's
19
18
  // `{{context.<name>}}`. Catch the mix-up at publish with a clear error
20
19
  // instead of a template that silently never resolves.
21
20
  const dotted = `${provider.url}\n${provider.body ?? ''}`
22
21
  .match(/\{\{context\.[A-Za-z_][A-Za-z0-9_]*\}\}/);
23
- if (dotted) {
24
- throw new Error(`Old devtools does not support ${dotted[0]} — rename ` +
25
- 'consumer-supplied params to the {{context_<name>}} form ' +
26
- 'for this backend.');
27
- }
22
+ assert(!dotted, new Error(`Devtools does not support ${dotted?.[0]} — rename ` +
23
+ 'consumer-supplied params to the {{context_<name>}} form ' +
24
+ 'for this backend.'));
28
25
  const responseMatches = (provider.responseMatches ?? [])
29
26
  .map((m, i) => ({
30
27
  value: m.value,
31
28
  type: m.type ?? 'contains',
32
- isOptional: false,
29
+ // The backend honors isOptional on BOTH register and add-version
30
+ // (responseMatches pass through verbatim) — forward the author's
31
+ // flag, default false. (`invert` has no old-backend equivalent and
32
+ // stays dropped.)
33
+ isOptional: m.isOptional ?? false,
33
34
  order: i,
34
35
  }));
35
- // Old devtools wants `responseMatches` and `responseRedactions` as PARALLEL
36
+ // Devtools wants `responseMatches` and `responseRedactions` as PARALLEL
36
37
  // arrays — same shape the builder uses, so pass them through directly. We do
37
38
  // NOT enforce any match/redaction count relationship here (and never pad —
38
39
  // empty placeholder matches break proof generation): the author keeps full
@@ -58,33 +59,35 @@ function requestDataFromProvider(provider) {
58
59
  });
59
60
  const requestData = {
60
61
  url: provider.url,
61
- // `{{param}}` placeholders → TEMPLATE so the backend templates them;
62
- // otherwise a fixed CONSTANT url. (REGEX is never drafted.)
63
- urlType: provider.url.includes('{{') ? 'TEMPLATE' : 'CONSTANT',
62
+ // Explicit override wins; otherwise `{{param}}` placeholders → TEMPLATE
63
+ // so the backend templates them, else a fixed CONSTANT url.
64
+ urlType: provider.urlType
65
+ ?? (provider.url.includes('{{') ? 'TEMPLATE' : 'CONSTANT'),
64
66
  method: method,
65
67
  responseMatches,
66
68
  responseRedactions,
67
69
  // The verification client re-supplies cookies/auth at runtime; secrets
68
70
  // are never persisted (propose_provider already stripped them).
69
- credentials: 'include',
71
+ credentials: provider.credentials ?? 'include',
70
72
  };
71
73
  if (provider.body !== undefined) {
72
74
  requestData.bodySniff = { enabled: true, template: provider.body };
73
75
  }
74
- // NOTE: `responseVariables` is intentionally omitted — the old backend
76
+ // NOTE: `responseVariables` is intentionally omitted — the devtools backend
75
77
  // auto-extracts it from `{{var}}` placeholders in responseMatches. Sending
76
78
  // it ourselves would risk drifting from the server's own extraction.
77
79
  return requestData;
78
80
  }
79
81
  /** One `requestData` entry per drafted provider. A multi-request provider
80
- * becomes parallel entries that the old backend runs together at verification
81
- * (the same way the builder version carries multiple `requests`). */
82
+ * becomes parallel entries that the devtools backend runs together at
83
+ * verification (the same way the builder version carries multiple
84
+ * `requests`). */
82
85
  function requestDataFrom(providers) {
83
86
  return providers.map(requestDataFromProvider);
84
87
  }
85
88
  /** Translate fresh `allowedInjectedRequestData` drafts (the author's input
86
89
  * shape, `AllowedJsRequest[]` — same shape the builder translator uses for
87
- * its own `allowedJsRequests`) into the old backend's shape. A
90
+ * its own `allowedJsRequests`) into the devtools backend's shape. A
88
91
  * carried-forward value read back from an existing version is ALREADY in
89
92
  * this shape and should be passed straight through instead — don't run it
90
93
  * through this a second time. */
@@ -165,6 +168,11 @@ export function providerToConfigBody(providers, opts, version, versionInfo) {
165
168
  if (opts.extensionConfig !== undefined) {
166
169
  providerConfig.extensionConfig = opts.extensionConfig;
167
170
  }
171
+ // The config (add-version) controller persists providerConfig verbatim,
172
+ // so this lands as-is; the register controller ignores it entirely.
173
+ if (opts.disableRequestReplay !== undefined) {
174
+ providerConfig.disableRequestReplay = opts.disableRequestReplay;
175
+ }
168
176
  return { providerConfig, version, versionInfo };
169
177
  }
170
178
  /**
@@ -4,6 +4,12 @@ import { INJECTION_GUIDE } from "../../provider/injection-doc.js";
4
4
  import { countsFor, matchRedactionWarnings, } from "../../provider/match-redaction-warning.js";
5
5
  import { bumpPatchString, extractVersions, injectedRequestDataFrom, providerToConfigBody, providerToRegisterBody, } from "../to-register.js";
6
6
  const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture';
7
+ /** Pull the new provider's id out of a register response, tolerant of the
8
+ * envelope (`{providerId}`, `{provider:{providerId}}`, `{id}`). */
9
+ function extractProviderId(resp) {
10
+ const obj = resp;
11
+ return obj?.providerId ?? obj?.provider?.providerId ?? obj?.id;
12
+ }
7
13
  /**
8
14
  * The old backend lets you add a version only to a provider YOU own. A
9
15
  * `providerId` for a public provider (or anyone else's) comes back as
@@ -44,7 +50,7 @@ function rethrowOwnershipError(err, providerId) {
44
50
  export function createVersionFromCaptureTool(client) {
45
51
  return defineTool({
46
52
  name: 'create_provider_version_from_capture',
47
- description: 'Publish drafted ReclaimProvider(s) to the OLD devtools backend. ' +
53
+ description: 'Publish drafted ReclaimProvider(s) to the devtools backend. ' +
48
54
  'Pass the `provider` object from propose_provider VERBATIM and the ' +
49
55
  '`initialUrl` the user landed on (the login/landing page, NOT the ' +
50
56
  'API url). \n\n' +
@@ -62,7 +68,7 @@ export function createVersionFromCaptureTool(client) {
62
68
  "PUBLIC provider you don't own, omit `providerId` to register your " +
63
69
  'own copy (you cannot edit someone else\'s provider here). \n\n' +
64
70
  'For data gated behind login/navigation, pass `customInjection` — a ' +
65
- 'page-injection script (old-devtools\' name for the builder\'s ' +
71
+ 'page-injection script (the devtools name for the builder\'s ' +
66
72
  'jsUserScripts) that drives the page to where the request fires. \n\n' +
67
73
  'BY DESIGN, this backend only ever CREATES A NEW VERSION (POST) — ' +
68
74
  'there is no partial-patch/edit endpoint, and this tool does NOT ' +
@@ -79,7 +85,14 @@ export function createVersionFromCaptureTool(client) {
79
85
  'values plus your change) back into this call. ' +
80
86
  '`allowedInjectedRequestData`/`useIncognitoWebview`/' +
81
87
  '`extensionConfig` only take effect on add-version — the register ' +
82
- 'endpoint (no `providerId`) ignores them. \n\n' +
88
+ 'endpoint (no `providerId`) ignores them. `disableRequestReplay` ' +
89
+ 'is also add-version-only, but when it is true the tool follows ' +
90
+ 'a register up with an add-version call automatically so it takes ' +
91
+ 'effect on new providers too. Per-request fields you may set on ' +
92
+ '`provider`/`providers`: `credentials` (omit/same-origin/include, ' +
93
+ 'default include), `urlType` (REGEX/CONSTANT/TEMPLATE, default ' +
94
+ 'derived from {{placeholders}}), and `isOptional` on each ' +
95
+ 'responseMatch (default false). \n\n' +
83
96
  'Captured requests must be GET/POST. OPRF redactions are supported.',
84
97
  inputSchema: {
85
98
  type: 'object',
@@ -94,6 +107,19 @@ export function createVersionFromCaptureTool(client) {
94
107
  method: { type: 'string' },
95
108
  headers: { type: 'object' },
96
109
  body: { type: 'string' },
110
+ credentials: {
111
+ type: 'string',
112
+ enum: ['omit', 'same-origin', 'include'],
113
+ description: 'How the verification client supplies cookies/auth ' +
114
+ 'when replaying this request. Default `include`.',
115
+ },
116
+ urlType: {
117
+ type: 'string',
118
+ enum: ['REGEX', 'CONSTANT', 'TEMPLATE'],
119
+ description: 'How the backend treats the URL. Default derived: ' +
120
+ 'TEMPLATE when the URL has {{placeholders}}, else ' +
121
+ 'CONSTANT.',
122
+ },
97
123
  responseMatches: { type: 'array' },
98
124
  responseRedactions: { type: 'array' },
99
125
  paramValues: { type: 'object' },
@@ -122,6 +148,14 @@ export function createVersionFromCaptureTool(client) {
122
148
  method: { type: 'string' },
123
149
  headers: { type: 'object' },
124
150
  body: { type: 'string' },
151
+ credentials: {
152
+ type: 'string',
153
+ enum: ['omit', 'same-origin', 'include'],
154
+ },
155
+ urlType: {
156
+ type: 'string',
157
+ enum: ['REGEX', 'CONSTANT', 'TEMPLATE'],
158
+ },
125
159
  responseMatches: { type: 'array' },
126
160
  responseRedactions: { type: 'array' },
127
161
  paramValues: { type: 'object' },
@@ -196,12 +230,41 @@ export function createVersionFromCaptureTool(client) {
196
230
  'the whole payload), template just that part with ' +
197
231
  '`{{paramName}}` and keep the rest as fixed literal text; see ' +
198
232
  'the "bodySniff" section of the customInjection field\'s guide ' +
199
- 'for the exact mechanics and a worked example.',
233
+ 'for the exact mechanics and a worked example. ' +
234
+ '⚠️ BEFORE populating this to gate ' +
235
+ '`verifyProof(proof, { providerId })`: this whole entry (url, ' +
236
+ 'method, body, responseMatches, responseRedactions) feeds a ' +
237
+ 'client-side hash-equality check, and `url` is hashed VERBATIM ' +
238
+ '— no regex/template resolution, even with `urlType: REGEX` or ' +
239
+ '`TEMPLATE`. If the real captured URL contains ANY per-session ' +
240
+ 'part (a tracking nonce, CSRF token, cache-busting timestamp), ' +
241
+ 'no declared spec can EVER hash-match a real proof, no matter ' +
242
+ 'how correct the rest of this entry is — populating' +
243
+ 'it for such ' +
244
+ 'a provider makes verifyProof reject every real proof,' +
245
+ 'which is ' +
246
+ 'worse than leaving this field empty. Diff the same endpoint\'s' +
247
+ ' captured URL across two separate sessions before wiring this ' +
248
+ 'up. If provider-hash validation keeps causing problems ' +
249
+ '(unstable url, or any other persistent mismatch), the ' +
250
+ 'fallback on the verifying side is `verifyProof(proof, { ' +
251
+ 'dangerouslyDisableContentValidation: true })` — trusts only ' +
252
+ 'the attestor\'s signature, skips the hash-equality check ' +
253
+ 'entirely; see the "Provider-hash validation" section of the ' +
254
+ 'customInjection field\'s guide for the full mechanics and ' +
255
+ 'when to reach for it.',
200
256
  items: {
201
257
  type: 'object',
202
258
  properties: {
203
259
  url: { type: 'string' },
204
260
  method: { type: 'string' },
261
+ urlType: {
262
+ type: 'string',
263
+ enum: ['REGEX', 'CONSTANT', 'TEMPLATE'],
264
+ description: 'How the backend treats this URL. Default derived ' +
265
+ 'from {{placeholders}} (TEMPLATE) like a normal ' +
266
+ 'request entry.',
267
+ },
205
268
  body: {
206
269
  type: 'string',
207
270
  description: 'Becomes `bodySniff: { enabled: true, template: <this ' +
@@ -315,6 +378,14 @@ export function createVersionFromCaptureTool(client) {
315
378
  description: 'Free-form JSON passed through to the client. Add-version ' +
316
379
  'ONLY.',
317
380
  },
381
+ disableRequestReplay: {
382
+ type: 'boolean',
383
+ description: 'Skip replaying the main document request (HAWKEYE ' +
384
+ 'provider-level flag). Honored on add-version; for a ' +
385
+ 'provider (no `providerId`) the register endpoint ignores ' +
386
+ 'it, so when true the tool automatically follows the ' +
387
+ 'register up with an add-version call that applies it.',
388
+ },
318
389
  },
319
390
  required: ['initialUrl'],
320
391
  },
@@ -348,6 +419,7 @@ export function createVersionFromCaptureTool(client) {
348
419
  stepsToFollow: args.stepsToFollow,
349
420
  useIncognitoWebview: args.useIncognitoWebview,
350
421
  extensionConfig: args.extensionConfig,
422
+ disableRequestReplay: args.disableRequestReplay,
351
423
  allowedInjectedRequestData: args.allowedInjectedRequestData
352
424
  ? injectedRequestDataFrom(args.allowedInjectedRequestData)
353
425
  : undefined
@@ -372,6 +444,11 @@ export function createVersionFromCaptureTool(client) {
372
444
  'every page load (before navigating to initialUrl), matching ' +
373
445
  'the builder\'s jsUserScripts.');
374
446
  }
447
+ // disableRequestReplay is honored ONLY on add-version, so a
448
+ // register can't set it — when true we immediately follow the
449
+ // register with an add-version call (which also lands any other
450
+ // register-ignored fields).
451
+ const followUp = args.disableRequestReplay === true;
375
452
  const REGISTER_IGNORED_KEYS = [
376
453
  'allowedInjectedRequestData',
377
454
  'useIncognitoWebview',
@@ -382,13 +459,36 @@ export function createVersionFromCaptureTool(client) {
382
459
  if (ignoredOnRegister.length) {
383
460
  notes.push(`${ignoredOnRegister.join(', ')} ` +
384
461
  `${ignoredOnRegister.length > 1 ? 'are' : 'is'} ` +
385
- 'ignored on register (no providerId) — the old backend only ' +
386
- 'reads them when adding a version. Publish once to create ' +
387
- 'the provider, then call this again WITH the returned ' +
388
- 'providerId to set them.');
462
+ 'ignored on register (no providerId) — the devtools backend only ' +
463
+ 'reads them when adding a version. ' +
464
+ (followUp
465
+ ? 'They were applied by the follow-up version the tool ' +
466
+ 'created to set disableRequestReplay.'
467
+ : 'Publish once to create the provider, then call this ' +
468
+ 'again WITH the returned providerId to set them.'));
389
469
  }
390
470
  const body = providerToRegisterBody(providers, opts);
391
- return withNotes(await client.registerProvider(body));
471
+ const registered = await client.registerProvider(body);
472
+ if (followUp) {
473
+ const newProviderId = extractProviderId(registered);
474
+ if (newProviderId) {
475
+ const versionsResp = await client.listVersions(newProviderId);
476
+ const followVersion = bumpPatchString(extractVersions(versionsResp));
477
+ const configBody = providerToConfigBody(providers, { ...opts, disableRequestReplay: true }, followVersion, DEFAULT_VERSION_INFO);
478
+ await client.updateProviderConfig(newProviderId, configBody);
479
+ notes.push('disableRequestReplay is ignored by the register endpoint — ' +
480
+ `applied it via a follow-up version (${followVersion}) on ` +
481
+ 'the new provider, which also lands any register-ignored ' +
482
+ 'fields you passed.');
483
+ }
484
+ else {
485
+ notes.push('Could not apply disableRequestReplay: the register ' +
486
+ 'response did not include the new providerId. Call this ' +
487
+ 'tool again WITH `providerId` and `disableRequestReplay: ' +
488
+ 'true` to set it.');
489
+ }
490
+ }
491
+ return withNotes(registered);
392
492
  }
393
493
  // New version of an existing provider — pick the next patch above
394
494
  // the highest existing version. A provider you don't own (e.g. a
@@ -37,4 +37,24 @@ export interface DraftResult {
37
37
  * `{{param}}`. Throwing discards the draft so the agent re-captures or
38
38
  * re-proposes rather than shipping a provider pinned to this one user. */
39
39
  export declare function assertPortableMatches(matches: ResponseMatch[], redactions: ResponseRedaction[], targets: DraftTarget[]): void;
40
+ /** Reject a draft that OPRF-hashes a target whose value ALSO appears in the
41
+ * request URL. The whole point of `hash` is that the attestor never sees
42
+ * the value in cleartext — but URL `paramValues` are always signed into the
43
+ * attestor's context in the clear (attestor-core: "those in URL... will be
44
+ * put into context and signed"), so a value that's both hashed in the
45
+ * response AND present in the URL is visible there anyway. This isn't a
46
+ * privacy nuance the author can weigh (unlike {@link assertPortableMatches}
47
+ * bare-literal cases) — it doesn't verify. Throwing here, at draft time,
48
+ * surfaces that immediately instead of a confusing later proof failure. */
49
+ export declare function assertNoOprfUrlConflict(url: string, targets: DraftTarget[], effectiveHashes: (OprfMode | undefined)[]): void;
50
+ /** Reject a draft whose URL bakes in a secret header/cookie's value beyond
51
+ * what write-redaction can actually hide there. A secret value used in the
52
+ * URL is NOT an automatic leak — write-redaction (`key-update`/`zk`) can
53
+ * redact it from the attestor's view up to {@link secretUrlCharBudget}
54
+ * characters, summed across every secret value that appears in that URL
55
+ * (not per-value). Below the budget the header buys real privacy; at or
56
+ * over it, the value is effectively visible regardless of hiding the
57
+ * header (same class of problem as {@link assertNoOprfUrlConflict} for
58
+ * OPRF-hashed targets, which has no such budget at all). */
59
+ export declare function assertNoSecretUrlConflict(url: string, secrets: Record<string, string>, writeRedactionMode?: ReclaimProvider['writeRedactionMode']): void;
40
60
  export declare function draftProvider(req: CapturedRequest, targets: DraftTarget[], name: string, hash?: OprfMode): DraftResult;
@@ -1,5 +1,6 @@
1
1
  import { isOprfHashable, OPRF_MAX_BYTES, synthesizeMatcher } from "./matcher.js";
2
- import { splitSecrets } from "./secrets.js";
2
+ import { normalizeCandidates } from "./normalize.js";
3
+ import { secretUrlCharBudget, splitSecrets, stripTransportHeaders, } from "./secrets.js";
3
4
  /** OPRF mode for the auto-hash floor — TEE-native (attestor rejects oprf-raw,
4
5
  * isn't set up for gnark oprf). */
5
6
  const DEFAULT_SENSITIVE_HASH = 'oprf-mpc';
@@ -29,49 +30,6 @@ export function defaultSensitiveHash(value) {
29
30
  && Buffer.byteLength(value, 'utf8') <= OPRF_MAX_BYTES;
30
31
  return hashable ? DEFAULT_SENSITIVE_HASH : undefined;
31
32
  }
32
- /** Request headers that must NOT survive into the provider params. Three kinds:
33
- * - Conditional headers (`if-none-match`/`if-modified-since`/…): a captured
34
- * cached ETag/date makes the origin answer 304/412 when the attestor
35
- * re-issues, and the attestor treats non-2xx as failure. We always want the
36
- * full fresh response.
37
- * - Transfer headers the ATTESTOR sets itself (`connection: close`,
38
- * `accept-encoding: identity`). Passing our own duplicates/collides with the
39
- * request the attestor builds, so leave them out — the attestor already
40
- * forces `identity` (uncompressed, so its byte-matching works).
41
- * - Host / framing headers the TRANSPORT owns (`host`, `content-length`,
42
- * `transfer-encoding`) and HTTP/2 pseudo-headers (`:authority`/`:method`/…,
43
- * which CDP surfaces on h2 captures). The attestor derives `Host` from the
44
- * URL and sets its own; a baked `Host` produces a DUPLICATE
45
- * (`found: h,h`) that fails the receipt check / 400s the origin. A stale
46
- * `content-length` breaks a re-serialized body. Pseudo-headers aren't valid
47
- * real headers at all. */
48
- const STRIP_REQUEST_HEADERS = new Set([
49
- 'if-none-match',
50
- 'if-modified-since',
51
- 'if-match',
52
- 'if-unmodified-since',
53
- 'if-range',
54
- 'connection',
55
- 'accept-encoding',
56
- 'host',
57
- 'content-length',
58
- 'transfer-encoding',
59
- // Per-session anti-CSRF tokens. Not caught by splitSecrets (no `-token`
60
- // suffix), so without this they baked in as PUBLIC headers and went stale at
61
- // verify time — the attestor replayed one user's captured CSRF for everyone,
62
- // which the origin rejects. A CSRF token is never portable; drop it.
63
- 'csrf',
64
- 'x-csrf',
65
- 'xsrf',
66
- 'x-xsrf',
67
- 'x-csrf-token',
68
- 'xsrf-token',
69
- ]);
70
- /** True for an HTTP/2 pseudo-header (`:authority`, `:method`, `:path`,
71
- * `:scheme`) — never a valid request header the attestor can replay. */
72
- function isPseudoHeader(name) {
73
- return name.startsWith(':');
74
- }
75
33
  /** A header VALUE that is a per-session/request token — a UUID or a long opaque
76
34
  * hex/base64 blob. Such a value is captured-user-specific: baked into the
77
35
  * provider it goes stale and the origin rejects the replay for everyone else.
@@ -84,15 +42,17 @@ function isVolatileHeaderValue(value) {
84
42
  || /^[0-9a-f]{16,}$/i.test(v)
85
43
  || /^[A-Za-z0-9_-]{32,}$/.test(v);
86
44
  }
87
- /** Drop the headers the attestor manages or that would 304 at proof time
88
- * (see {@link STRIP_REQUEST_HEADERS}); keep every other captured header so the
89
- * attestor's replay looks like the browser's request. */
45
+ /** Drop a PUBLIC header whose value is per-session/request volatile (see
46
+ * {@link isVolatileHeaderValue}) — public headers get frozen into the
47
+ * persisted provider, so a volatile one goes stale and fails every other
48
+ * user's replay. Secret headers are re-captured fresh per verification
49
+ * (never persisted), so this doesn't apply to them; keep every other
50
+ * captured public header so the attestor's replay looks like the browser's
51
+ * request. */
90
52
  function sanitizeRequestHeaders(headers) {
91
53
  const out = {};
92
54
  for (const [k, v] of Object.entries(headers)) {
93
- if (STRIP_REQUEST_HEADERS.has(k.toLowerCase())
94
- || isPseudoHeader(k)
95
- || isVolatileHeaderValue(v)) {
55
+ if (isVolatileHeaderValue(v)) {
96
56
  continue;
97
57
  }
98
58
  out[k] = v;
@@ -167,6 +127,63 @@ export function assertPortableMatches(matches, redactions, targets) {
167
127
  }
168
128
  }
169
129
  }
130
+ /** Reject a draft that OPRF-hashes a target whose value ALSO appears in the
131
+ * request URL. The whole point of `hash` is that the attestor never sees
132
+ * the value in cleartext — but URL `paramValues` are always signed into the
133
+ * attestor's context in the clear (attestor-core: "those in URL... will be
134
+ * put into context and signed"), so a value that's both hashed in the
135
+ * response AND present in the URL is visible there anyway. This isn't a
136
+ * privacy nuance the author can weigh (unlike {@link assertPortableMatches}
137
+ * bare-literal cases) — it doesn't verify. Throwing here, at draft time,
138
+ * surfaces that immediately instead of a confusing later proof failure. */
139
+ export function assertNoOprfUrlConflict(url, targets, effectiveHashes) {
140
+ for (const [i, t] of targets.entries()) {
141
+ const hash = effectiveHashes[i];
142
+ if (!hash) {
143
+ continue;
144
+ }
145
+ const inUrl = [t.value, ...normalizeCandidates(t.value)]
146
+ .some((v) => url.includes(v));
147
+ if (inUrl) {
148
+ throw new Error(`target "${t.name}" is OPRF-hashed (${hash}) but its value also `
149
+ + `appears in the request URL ("${url}") — the attestor signs URL `
150
+ + 'param values into context in the clear, so it would see this '
151
+ + 'value regardless of the hash. Drop `hash` for this target, or '
152
+ + "capture a request whose URL doesn't carry the value.");
153
+ }
154
+ }
155
+ }
156
+ /** Reject a draft whose URL bakes in a secret header/cookie's value beyond
157
+ * what write-redaction can actually hide there. A secret value used in the
158
+ * URL is NOT an automatic leak — write-redaction (`key-update`/`zk`) can
159
+ * redact it from the attestor's view up to {@link secretUrlCharBudget}
160
+ * characters, summed across every secret value that appears in that URL
161
+ * (not per-value). Below the budget the header buys real privacy; at or
162
+ * over it, the value is effectively visible regardless of hiding the
163
+ * header (same class of problem as {@link assertNoOprfUrlConflict} for
164
+ * OPRF-hashed targets, which has no such budget at all). */
165
+ export function assertNoSecretUrlConflict(url, secrets, writeRedactionMode) {
166
+ let redactedChars = 0;
167
+ const overlapping = [];
168
+ for (const [name, value] of Object.entries(secrets)) {
169
+ const inUrl = [value, ...normalizeCandidates(value)]
170
+ .some((v) => url.includes(v));
171
+ if (inUrl) {
172
+ redactedChars += value.length;
173
+ overlapping.push(name);
174
+ }
175
+ }
176
+ const budget = secretUrlCharBudget(writeRedactionMode);
177
+ if (redactedChars > budget) {
178
+ throw new Error(`secret header(s) ${overlapping.map((n) => `"${n}"`).join(', ')} `
179
+ + `also appear in the request URL ("${url}"), totaling `
180
+ + `${redactedChars} character(s) to hide — over the ${budget}-`
181
+ + `character budget write-redaction (${writeRedactionMode
182
+ ?? 'key-update'}) can actually hide there. Drop the header, `
183
+ + "shorten/remove the value's overlap with the URL, or switch to "
184
+ + "'zk' if that raises the budget enough.");
185
+ }
186
+ }
170
187
  /** Derive a `paramValues` key (and capture-group name) from the provider
171
188
  * name's last alphanumeric token. `"GitHub Username"` → `"username"`,
172
189
  * `"x-followers"` → `"followers"`. */
@@ -188,6 +205,7 @@ export function draftProvider(req, targets, name, hash) {
188
205
  const matches = [];
189
206
  const redactions = [];
190
207
  const extractions = [];
208
+ const effectiveHashes = [];
191
209
  let paramValues;
192
210
  const seenParams = new Set();
193
211
  for (const t of targets) {
@@ -202,6 +220,7 @@ export function draftProvider(req, targets, name, hash) {
202
220
  // The model makes the contextual calls (which named fields are sensitive)
203
221
  // via an explicit per-target hash; this floor only guarantees the obvious.
204
222
  const effectiveHash = t.hash ?? hash ?? defaultSensitiveHash(t.value);
223
+ effectiveHashes.push(effectiveHash);
205
224
  const synthesis = synthesizeMatcher(req.responseBody, req.contentType, t.value, paramName, effectiveHash);
206
225
  if (!synthesis) {
207
226
  throw new Error(`target value "${t.value}" not found in`
@@ -222,7 +241,12 @@ export function draftProvider(req, targets, name, hash) {
222
241
  }
223
242
  // Portability gate: never ship a draft that bakes a per-user value literally.
224
243
  assertPortableMatches(matches, redactions, targets);
225
- const { secrets, publics } = splitSecrets(req.requestHeaders);
244
+ // OPRF gate: never ship a draft that hashes a value the URL also reveals.
245
+ assertNoOprfUrlConflict(req.url, targets, effectiveHashes);
246
+ const { secrets, publics } = splitSecrets(stripTransportHeaders(req.requestHeaders));
247
+ // Secret-header gate: never ship a draft whose URL reveals the same value a
248
+ // stripped secret header carries — hiding the header buys nothing then.
249
+ assertNoSecretUrlConflict(req.url, secrets);
226
250
  // Sanitize the public headers before they enter the provider. Two problems
227
251
  // a captured browser request carries: (1) `accept-encoding: gzip, …`, which
228
252
  // makes the attestor match against compressed bytes (it does no transparent
@@ -3,7 +3,7 @@ export type { DraftResult } from './draft.ts';
3
3
  export { detectConcerns } from './constraints.ts';
4
4
  export { extractGraphqlOp, looksLikeGraphqlEndpoint } from './graphql.ts';
5
5
  export type { CapturedRequest, HttpMethod, OprfMode, ReclaimProvider, RequestConcern, ResponseMatch, ResponseRedaction, SecretParamRef, } from './schema.ts';
6
- export { classifyHeader, splitSecrets } from './secrets.ts';
6
+ export { assertSecretParamValuesFitUrlBudget, classifyHeader, classifyParamName, secretUrlCharBudget, splitSecretParamValues, splitSecrets, stripTransportHeaders, } from './secrets.ts';
7
7
  export { providerToRequestSelection } from './to-version.ts';
8
8
  export { hashProviderParams, providers } from '@reclaimprotocol/attestor-core';
9
9
  export type { ProviderParams } from '@reclaimprotocol/attestor-core';
@@ -6,7 +6,7 @@
6
6
  export { draftProvider } from "./draft.js";
7
7
  export { detectConcerns } from "./constraints.js";
8
8
  export { extractGraphqlOp, looksLikeGraphqlEndpoint } from "./graphql.js";
9
- export { classifyHeader, splitSecrets } from "./secrets.js";
9
+ export { assertSecretParamValuesFitUrlBudget, classifyHeader, classifyParamName, secretUrlCharBudget, splitSecretParamValues, splitSecrets, stripTransportHeaders, } from "./secrets.js";
10
10
  // Recipe → version `requests[]` (a `RequestSelection`) — the same mapping the
11
11
  // MCP publish uses, so the cloud authoring publish stays byte-identical.
12
12
  export { providerToRequestSelection } from "./to-version.js";