@reclaimprotocol/agent 0.1.9 → 0.1.11

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.
@@ -205,6 +205,82 @@ export function authoringTools(
205
205
  }))
206
206
  },
207
207
  ),
208
+ defineTool<Record<string, unknown>>(
209
+ {
210
+ name: 'wait_for_login',
211
+ description:
212
+ 'Wait for the user to finish logging in. Polls the browser\'s '
213
+ + 'REAL cookies over CDP (httpOnly INCLUDED) until a NEW session '
214
+ + 'cookie appears, ignoring analytics/bot cookies. The ONLY '
215
+ + 'correct login wait — NEVER use wait_for_page for it: page JS '
216
+ + 'can\'t see the httpOnly session cookie (hangs the whole '
217
+ + 'timeout), yet DOES see pre-login tracking cookies (a '
218
+ + 'length/substring check fires instantly, before the user '
219
+ + 'types). Snapshots the baseline itself — call right after '
220
+ + 'navigating to the login page. Does NOT navigate (never yanks '
221
+ + 'the user off login/2FA). Returns { loggedIn, newCookies }.',
222
+ inputSchema: objectSchema({ timeoutMs: { type: 'number' } }),
223
+ },
224
+ async(args) => {
225
+ const backend = resolveBackend(args)
226
+ const deadline = Date.now() + (Number(args.timeoutMs) || 180_000)
227
+ const cookies = async() => {
228
+ const res = (await backend.sendCdp('Network.getCookies')) as {
229
+ cookies?: { name: string, httpOnly?: boolean }[]
230
+ }
231
+ return res.cookies ?? []
232
+ }
233
+
234
+ // Analytics / bot-protection / pre-auth infra cookies appear (and
235
+ // rotate) without any login — never the signal, even when new.
236
+ const JUNK = new RegExp('^(' + [
237
+ '_ga', '_gid', '_gat', '_ym', '__ddg', '__gads', '__gpi', '__eoi',
238
+ '__utm', '_fbp', 'fbp', 'fr', 'FCCDCF', 'pinLogger',
239
+ 'userFirstVisit', 'cf_', '_dd_', 'bcookie', 'bscookie', 'lidc',
240
+ ].join('|') + ')', 'i')
241
+ // A real session/auth cookie name. Deliberately NOT bare `token` — a
242
+ // challenge/CSRF token (e.g. LinkedIn `chp_token`) contains it and is
243
+ // set BEFORE auth completes. Any new httpOnly cookie is also a
244
+ // candidate (server-set session cookies are usually httpOnly).
245
+ const SESSION = /sess|sid|jwt|_session|auth|logged.?in|remember|li_at/i
246
+ // Auth/challenge URL paths — while the page is still on one, login is
247
+ // NOT done (a mid-flow cookie must not count). Cross-site-stable, far
248
+ // more so than cookie names.
249
+ const AUTH_URL = new RegExp([
250
+ 'login', 'signin', 'sign-in', 'challenge', 'checkpoint', '\\buas\\b',
251
+ 'authorize', '\\bsso\\b', 'mfa', '2fa', 'two-factor', 'otp', 'verify',
252
+ 'password',
253
+ ].join('|'), 'i')
254
+ const offAuthUrl = async() => {
255
+ const { value } = await evalInPage(backend, 'location.href')
256
+ const href = typeof value === 'string' ? value : ''
257
+ try {
258
+ const u = new URL(href)
259
+ return !AUTH_URL.test(u.pathname + u.search)
260
+ } catch{
261
+ return false
262
+ }
263
+ }
264
+
265
+ const initial = await cookies()
266
+ const baseline = new Set(initial.map((c) => c.name))
267
+ while(Date.now() < deadline) {
268
+ const current = await cookies()
269
+ const fresh = current
270
+ .filter((c) => !baseline.has(c.name) && !JUNK.test(c.name))
271
+ const hit = fresh.filter((c) => c.httpOnly || SESSION.test(c.name))
272
+ // BOTH required: a new session cookie AND the page has left the auth
273
+ // flow. Either alone false-positives (mid-flow token; stray nav).
274
+ if(hit.length && await offAuthUrl()) {
275
+ return { loggedIn: true, newCookies: hit.map((c) => c.name) }
276
+ }
277
+
278
+ await sleep(1000)
279
+ }
280
+
281
+ return { loggedIn: false, newCookies: [] }
282
+ },
283
+ ),
208
284
  defineTool<Record<string, unknown>>(
209
285
  {
210
286
  name: 'list_requests',
@@ -21,7 +21,12 @@ export function findTool(resolveBackend: ResolveBackend): RegisteredTool {
21
21
  'Search captured responses for any encoding of `value` (raw, '
22
22
  + 'URL/JSON/HTML-escaped, …). Returns a ranked candidate list with '
23
23
  + 'the matched variant, score, and a short snippet — used to locate '
24
- + 'the request that returns a target. Server-side; bodies stay here.',
24
+ + 'the request that returns a target. Also finds SPLIT matches: when '
25
+ + '`value` is multi-word (e.g. a full name), a response that carries '
26
+ + 'every word SEPARATELY (a JSON profile API with first/last name in '
27
+ + 'distinct fields) is returned with `splitMatch:true` — prove those '
28
+ + 'per-field (a redaction per part) rather than as one string. '
29
+ + 'Server-side; bodies stay here.',
25
30
  inputSchema: objectSchema(
26
31
  { value: { type: 'string' }, limit: { type: 'number' } },
27
32
  ['value'],
@@ -29,37 +34,60 @@ export function findTool(resolveBackend: ResolveBackend): RegisteredTool {
29
34
  },
30
35
  async(args) => {
31
36
  const backend = resolveBackend(args)
32
- const variants = normalizeCandidates(String(args.value))
37
+ const value = String(args.value)
38
+ const variants = normalizeCandidates(value)
39
+ // Name-part tokens (≥3 chars) for split-field detection — a JSON API
40
+ // often carries a full name as separate first/last fields, so the whole
41
+ // string never matches even though the value IS there.
42
+ const tokens = value.split(/\s+/).filter((t) => t.length >= 3)
43
+ const snip = (body: string, idx: number, len: number) => {
44
+ const s = Math.max(0, idx - 40)
45
+
46
+ return body.slice(s, Math.min(body.length, idx + len + 40))
47
+ }
48
+
33
49
  const ranked = []
34
50
  for(const r of backend.capture.requests.values()) {
35
51
  if(!r.responseBody) {
36
52
  continue
37
53
  }
38
54
 
55
+ const body = r.responseBody
56
+ // Prefer JSON responses + smaller bodies (likely API endpoints).
57
+ const base = (r.contentType.includes('json') ? 10 : 0)
58
+ + Math.max(0, 5 - Math.log10(Math.max(r.size, 1)))
59
+
39
60
  let hit: { snippet: string, variant: string } | undefined
40
61
  for(const v of variants) {
41
- const idx = r.responseBody.indexOf(v)
62
+ const idx = body.indexOf(v)
42
63
  if(idx >= 0) {
43
- const start = Math.max(0, idx - 40)
44
- const end = Math.min(r.responseBody.length, idx + v.length + 40)
45
- hit = { snippet: r.responseBody.slice(start, end), variant: v }
64
+ hit = { snippet: snip(body, idx, v.length), variant: v }
46
65
  break
47
66
  }
48
67
  }
49
68
 
50
- if(!hit) {
69
+ if(hit) {
70
+ // Whole-value match — ranked above split matches (+5).
71
+ ranked.push({
72
+ ...publicRequest(r),
73
+ matchedVariant: hit.variant,
74
+ snippet: hit.snippet,
75
+ score: base + 5,
76
+ })
51
77
  continue
52
78
  }
53
79
 
54
- // Prefer JSON responses + smaller bodies (likely API endpoints).
55
- const score = (r.contentType.includes('json') ? 10 : 0)
56
- + Math.max(0, 5 - Math.log10(Math.max(r.size, 1)))
57
- ranked.push({
58
- ...publicRequest(r),
59
- matchedVariant: hit.variant,
60
- snippet: hit.snippet,
61
- score,
62
- })
80
+ // Split match: every word present separately (e.g. first/last name
81
+ // in distinct JSON fields). Requires ≥2 tokens so it's specific.
82
+ if(tokens.length >= 2 && tokens.every((t) => body.includes(t))) {
83
+ ranked.push({
84
+ ...publicRequest(r),
85
+ matchedVariant: `split:${tokens.join('+')}`,
86
+ splitMatch: true,
87
+ snippet: snip(body, body.indexOf(tokens[0]), tokens[0].length),
88
+ score: base,
89
+ })
90
+ }
63
91
  }
64
92
 
65
93
  ranked.sort((a, b) => b.score - a.score)
@@ -83,12 +83,12 @@ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
83
83
  }
84
84
 
85
85
  for(const t of targets) {
86
- if((t.hash ?? defaultHash) !== undefined
87
- && t.value.length > OPRF_MAX_BYTES) {
86
+ const bytes = Buffer.byteLength(t.value, 'utf8')
87
+ if((t.hash ?? defaultHash) !== undefined && bytes > OPRF_MAX_BYTES) {
88
88
  throw new Error(
89
89
  `OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; got `
90
- + `${t.value.length}-byte "${t.value}". Use a tighter `
91
- + 'target or drop `hash`.',
90
+ + `${bytes}-byte "${t.value}". Use a tighter target or drop `
91
+ + '`hash`.',
92
92
  )
93
93
  }
94
94
  }
package/src/old/client.ts CHANGED
@@ -147,24 +147,6 @@ export class ReclaimOldClient {
147
147
  )
148
148
  }
149
149
 
150
- /** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
151
- * one version — the latest when `version` is omitted. Readable for ANY
152
- * non-archived provider (not owner-restricted), same as the dashboard's
153
- * edit-form prefill fetch. */
154
- getProviderConfig(
155
- providerId: string,
156
- version?: string,
157
- ): Promise<unknown> {
158
- return this.#request(
159
- 'GET',
160
- `/providers/${encodeURIComponent(providerId)}/config`,
161
- {
162
- auth: true,
163
- query: version ? { versionNumber: version } : undefined,
164
- },
165
- )
166
- }
167
-
168
150
  /** Partial update on the PROVIDER document itself — name/description/
169
151
  * providerType/tags/isActive, after creation. The backend 403s if you
170
152
  * don't own the provider (and aren't admin); other providerConfig-style
@@ -93,6 +93,8 @@ interface OldRequestData {
93
93
  export interface OldInjectedRequestData extends OldRequestData {
94
94
  required?: boolean
95
95
  multiple?: boolean
96
+ templateParams?: string[]
97
+ templateParamsMode?: 'separate' | 'merge'
96
98
  }
97
99
 
98
100
  /** Build the single `requestData` entry shared by register + update. Takes
@@ -200,11 +202,15 @@ function requestDataFrom(providers: ReclaimProvider[]): OldRequestData[] {
200
202
  export function injectedRequestDataFrom(
201
203
  requests: AllowedJsRequest[],
202
204
  ): OldInjectedRequestData[] {
203
- return requests.map((req) => ({
204
- ...requestDataFromProvider(req),
205
- required: req.required ?? true,
206
- multiple: req.multiple ?? true,
207
- }))
205
+ return requests.map((req) => {
206
+ return {
207
+ ...requestDataFromProvider(req),
208
+ required: req.required ?? true,
209
+ multiple: req.multiple ?? true,
210
+ templateParams: req.templateParams,
211
+ templateParamsMode: req.templateParamsMode,
212
+ }
213
+ })
208
214
  }
209
215
 
210
216
  /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
@@ -3,7 +3,6 @@ import type { ReclaimOldClient } from '../client.ts'
3
3
  import type { ReclaimSdkClient } from '../sdk-client.ts'
4
4
  import { authenticateTool } from './authenticate.ts'
5
5
  import { getProviderInfoTool } from './get-provider-info.ts'
6
- import { getProviderVersionConfigTool } from './get-version-config.ts'
7
6
  import { getMeProvidersTool } from './me-providers.ts'
8
7
  import { createVersionFromCaptureTool } from './publish.ts'
9
8
  import { sessionAnalyticsLogsTool } from './session-analytics-logs.ts'
@@ -27,7 +26,6 @@ export function buildOldBackendTools(
27
26
  authenticateTool(client),
28
27
  createVersionFromCaptureTool(client),
29
28
  getMeProvidersTool(client),
30
- getProviderVersionConfigTool(client),
31
29
  getProviderInfoTool(sdkClient),
32
30
  updateProviderMetadataTool(client),
33
31
  sessionAnalyticsLogsTool(client),
@@ -18,14 +18,17 @@ async function attempt(
18
18
  }
19
19
 
20
20
  /**
21
- * Read provider info from the PUBLIC production SDK backend
22
- * (`api.reclaimprotocol.org`) — the service the verification SDK/attestor
23
- * itself calls at runtime, NOT the old-devtools dashboard backend
24
- * (`get_provider_version_config`, `devapi.reclaimprotocol.org`, requires
25
- * auth). Both underlying reads are unauthenticated.
21
+ * The single provider-read tool for old-devtools mode. Reads from the PUBLIC
22
+ * production SDK backend (`api.reclaimprotocol.org`) — the service the
23
+ * verification SDK/attestor itself calls at runtime. No auth required, works
24
+ * for ANY provider.
26
25
  *
27
- * Fetches BOTH shapes in one call, since they're complementary:
28
- * - `provider`: full provider + providerConfig, `customInjection` included.
26
+ * Fetches BOTH shapes in one call, since they're complementary and together
27
+ * cover everything a caller needs before re-publishing a new version:
28
+ * - `provider`: the full provider + providerConfig — `customInjection`,
29
+ * `userAgent`, `allowedInjectedRequestData`, `requestData`, everything.
30
+ * This is what you read before create_provider_version_from_capture so
31
+ * you can pass the whole config back (that tool carries nothing forward).
29
32
  * - `configs`: the trimmed runtime recipe the attestor actually consumes —
30
33
  * `customInjection` deliberately omitted, `isScriptRequestingClaim`
31
34
  * computed instead.
@@ -38,15 +41,18 @@ export function getProviderInfoTool(client: ReclaimSdkClient): RegisteredTool {
38
41
  'Read a PUBLISHED provider from the public production SDK backend ' +
39
42
  '(api.reclaimprotocol.org — what the verification SDK/attestor ' +
40
43
  'itself fetches at runtime), for ANY provider, no auth required. ' +
41
- 'Different from get_provider_version_config, which reads the old ' +
42
- 'devtools DASHBOARD backend and requires auth. \n\n' +
44
+ 'This is the provider-read tool for old-devtools mode. \n\n' +
43
45
  'Returns two complementary reads for the given version (latest ' +
44
- 'when `versionNumber` is omitted): `provider` (the full provider ' +
45
- '+ providerConfig, including `customInjection`) and `configs` (the ' +
46
- 'trimmed recipe the attestor actually consumes — `customInjection` ' +
47
- 'is deliberately OMITTED there, replaced by a computed ' +
48
- '`isScriptRequestingClaim` flag). One endpoint failing (e.g. an ' +
49
- 'invalid `versionNumber`) is reported under `errors` without ' +
46
+ 'when `versionNumber` is omitted): `provider` (the FULL provider ' +
47
+ '+ providerConfig — `customInjection`, `userAgent`, ' +
48
+ '`allowedInjectedRequestData`, `requestData`, everything) and ' +
49
+ '`configs` (the trimmed recipe the attestor actually consumes — ' +
50
+ '`customInjection` is deliberately OMITTED there, replaced by a ' +
51
+ 'computed `isScriptRequestingClaim` flag). Read `provider` here ' +
52
+ 'BEFORE create_provider_version_from_capture so you can pass the ' +
53
+ 'whole existing config back — that tool always creates a new ' +
54
+ 'version and carries nothing forward. One endpoint failing (e.g. ' +
55
+ 'an invalid `versionNumber`) is reported under `errors` without ' +
50
56
  'hiding a result the other endpoint did return.',
51
57
  inputSchema: {
52
58
  type: 'object',
@@ -12,7 +12,6 @@ import {
12
12
  bumpPatchString,
13
13
  extractVersions,
14
14
  injectedRequestDataFrom,
15
- type OldInjectedRequestData,
16
15
  type OldProviderOpts,
17
16
  providerToConfigBody,
18
17
  providerToRegisterBody,
@@ -20,66 +19,6 @@ import {
20
19
 
21
20
  const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture'
22
21
 
23
- /** Fields with no partial-patch equivalent on this backend: a version always
24
- * replaces the WHOLE `providerConfig`, so omitting one of these on
25
- * add-version must carry forward the current value rather than publish it
26
- * blank. `allowedInjectedRequestData` is handled alongside these (further
27
- * down, inline) since it needs translation when freshly authored, unlike
28
- * these — which pass through unchanged either way. */
29
- const PRESERVABLE_KEYS = [
30
- 'customInjection',
31
- 'userAgent',
32
- 'pageTitle',
33
- 'stepsToFollow',
34
- 'useIncognitoWebview',
35
- 'extensionConfig',
36
- ] as const
37
-
38
- /** Pull `providerConfig` out of a `get_provider_version_config` /
39
- * `getProviderConfig` response, tolerant of a missing/malformed shape. */
40
- function extractProviderConfig(
41
- resp: unknown,
42
- ): Record<string, unknown> | undefined {
43
- if(resp && typeof resp === 'object') {
44
- const cfg = (resp as { providerConfig?: unknown }).providerConfig
45
- if(cfg && typeof cfg === 'object') {
46
- return cfg as Record<string, unknown>
47
- }
48
- }
49
-
50
- return undefined
51
- }
52
-
53
- /** Prefer the caller's explicit value; otherwise fall back to `current`'s
54
- * value (if any) and record `fieldName` as carried forward.
55
- *
56
- * `current === null` is treated the SAME as `undefined` (nothing to carry),
57
- * not as a real value to preserve. These are all optional fields with no
58
- * Prisma default — a version where one was never set reads back as
59
- * `null`, not a missing key, because Prisma normalizes an absent optional
60
- * field to `null` on read. Carrying that `null` "forward" would falsely
61
- * claim the field was preserved when really nothing was ever there — it
62
- * most likely means an EARLIER version already lost it (e.g. published
63
- * before this field had carry-forward support at all), not that this call
64
- * is doing anything wrong. */
65
- function resolveField<T>(
66
- explicit: T | undefined,
67
- current: unknown,
68
- fieldName: string,
69
- carried: string[],
70
- ): T | undefined {
71
- if(explicit !== undefined) {
72
- return explicit
73
- }
74
-
75
- if(current !== undefined && current !== null) {
76
- carried.push(fieldName)
77
- return current as T
78
- }
79
-
80
- return undefined
81
- }
82
-
83
22
  /**
84
23
  * The old backend lets you add a version only to a provider YOU own. A
85
24
  * `providerId` for a public provider (or anyone else's) comes back as
@@ -166,23 +105,19 @@ export function createVersionFromCaptureTool(
166
105
  'For data gated behind login/navigation, pass `customInjection` — a ' +
167
106
  'page-injection script (old-devtools\' name for the builder\'s ' +
168
107
  'jsUserScripts) that drives the page to where the request fires. \n\n' +
169
- 'This backend has NO partial-patch endpoint — adding a version ' +
170
- 'always replaces the WHOLE providerConfig. When `providerId` is ' +
171
- 'set, any of `customInjection`, `userAgent`, `pageTitle`, ' +
172
- '`allowedInjectedRequestData`, `stepsToFollow`, ' +
173
- '`useIncognitoWebview`, `extensionConfig` that you OMIT is carried ' +
174
- 'forward unchanged from the currently-published version (so ' +
175
- 're-publishing requestData never wipes a working login script or ' +
176
- 'other config someone set via the dashboard). This can only carry ' +
177
- 'forward what the CURRENT version actually has — if an earlier ' +
178
- 'version already lost a field (e.g. published before this tool ' +
179
- 'carried it forward at all), there is nothing left to preserve and ' +
180
- 'the result legitimately comes back empty/null for that field; ' +
181
- 'that is not this call failing to carry it forward. Pass an ' +
182
- 'explicit value (`""`, `[]`, `false`, ...) to intentionally clear ' +
183
- 'one of them instead, or call get_provider_version_config FIRST ' +
184
- 'to see exactly what is live today, rather than assuming this ' +
185
- 'call recovered something that may already be gone. ' +
108
+ 'BY DESIGN, this backend only ever CREATES A NEW VERSION (POST) — ' +
109
+ 'there is no partial-patch/edit endpoint, and this tool does NOT ' +
110
+ 'read or carry anything forward from the previous version. Every ' +
111
+ 'new version replaces the WHOLE providerConfig with exactly what ' +
112
+ 'you pass in THIS call. Therefore any field you want present in the ' +
113
+ 'new version MUST be supplied here — `customInjection`, `userAgent`, ' +
114
+ '`pageTitle`, `allowedInjectedRequestData`, `stepsToFollow`, ' +
115
+ '`useIncognitoWebview`, `extensionConfig`, and `requestData` (via ' +
116
+ '`provider`/`providers`). Anything you OMIT is published blank/' +
117
+ 'default in the new version, NOT preserved. To change one field ' +
118
+ 'while keeping the rest, first read the current version with ' +
119
+ 'get_provider_info, then pass the full desired config (the existing ' +
120
+ 'values plus your change) back into this call. ' +
186
121
  '`allowedInjectedRequestData`/`useIncognitoWebview`/' +
187
122
  '`extensionConfig` only take effect on add-version — the register ' +
188
123
  'endpoint (no `providerId`) ignores them. \n\n' +
@@ -316,6 +251,28 @@ export function createVersionFromCaptureTool(
316
251
  description:
317
252
  'Fail validation if none match. Default true.',
318
253
  },
254
+ templateParams: {
255
+ type: 'array',
256
+ items: { type: 'string' },
257
+ description:
258
+ 'Names of `${var}` placeholders used in this ' +
259
+ 'template\'s `responseMatches`/`responseRedactions` ' +
260
+ 'that should be substituted at verify time from the ' +
261
+ 'submitted proof\'s own witness params. Omit (or ' +
262
+ 'leave empty) if the template has no such ' +
263
+ 'placeholders.',
264
+ },
265
+ templateParamsMode: {
266
+ type: 'string',
267
+ enum: ['separate', 'merge'],
268
+ description:
269
+ 'How multiple values for `templateParams` expand ' +
270
+ 'into request specs. `separate` (default): one ' +
271
+ 'independent spec per value, each matching its own ' +
272
+ 'proof. `merge`: fold all values into ONE spec with ' +
273
+ 'one match/redaction entry per value, matching a ' +
274
+ 'single proof that bundles them all into one claim.',
275
+ },
319
276
  },
320
277
  required: ['url', 'method'],
321
278
  },
@@ -356,25 +313,23 @@ export function createVersionFromCaptureTool(
356
313
 
357
314
  const providers = hasMany ? args.providers! : [args.provider!]
358
315
 
359
- const opts: OldProviderOpts = { initialUrl: args.initialUrl }
360
- if(args.description !== undefined) {
361
- opts.description = args.description
362
- }
363
-
364
- if(args.geoLocation !== undefined) {
365
- opts.geoLocation = args.geoLocation
366
- }
367
-
368
- if(args.useProxy !== undefined) {
369
- opts.useProxy = args.useProxy
370
- }
371
-
372
- if(args.providerType !== undefined) {
373
- opts.providerType = args.providerType
374
- }
375
-
376
- if(args.customInjection !== undefined) {
377
- opts.customInjection = args.customInjection
316
+ const opts: OldProviderOpts = {
317
+ initialUrl: args.initialUrl,
318
+ description: args.description,
319
+ geoLocation: args.geoLocation,
320
+ useProxy: args.useProxy,
321
+ providerType: args.providerType,
322
+ customInjection: args.customInjection,
323
+ userAgent: args.userAgent,
324
+ pageTitle: args.pageTitle,
325
+ stepsToFollow: args.stepsToFollow,
326
+ useIncognitoWebview: args.useIncognitoWebview,
327
+ extensionConfig: args.extensionConfig,
328
+ allowedInjectedRequestData: args.allowedInjectedRequestData
329
+ ? injectedRequestDataFrom(
330
+ args.allowedInjectedRequestData,
331
+ )
332
+ : undefined
378
333
  }
379
334
 
380
335
  // Non-blocking advisory per request — never alters or blocks the
@@ -414,27 +369,15 @@ export function createVersionFromCaptureTool(
414
369
  .filter((k) => args[k] !== undefined)
415
370
  if(ignoredOnRegister.length) {
416
371
  notes.push(
417
- `${ignoredOnRegister.join(', ')} ${
418
- ignoredOnRegister.length > 1 ? 'are' : 'is'
419
- } ignored on register (no providerId) — the old backend only ` +
372
+ `${ignoredOnRegister.join(', ')} ` +
373
+ `${ignoredOnRegister.length > 1 ? 'are' : 'is'} ` +
374
+ 'ignored on register (no providerId) — the old backend only ' +
420
375
  'reads them when adding a version. Publish once to create ' +
421
376
  'the provider, then call this again WITH the returned ' +
422
377
  'providerId to set them.',
423
378
  )
424
379
  }
425
380
 
426
- if(args.userAgent !== undefined) {
427
- opts.userAgent = args.userAgent
428
- }
429
-
430
- if(args.pageTitle !== undefined) {
431
- opts.pageTitle = args.pageTitle
432
- }
433
-
434
- if(args.stepsToFollow !== undefined) {
435
- opts.stepsToFollow = args.stepsToFollow
436
- }
437
-
438
381
  const body = providerToRegisterBody(providers, opts)
439
382
  return withNotes(await client.registerProvider(body))
440
383
  }
@@ -445,48 +388,12 @@ export function createVersionFromCaptureTool(
445
388
  // workaround instead of a bare permission error.
446
389
  const providerId = args.providerId
447
390
  try {
448
- // This backend has no partial-patch endpoint — a version always
449
- // replaces the WHOLE providerConfig. Anything the caller didn't
450
- // pass at all is carried forward from the currently-published
451
- // version instead of going out blank. Pass an explicit empty
452
- // value ("", [], false) to intentionally clear one.
453
- const needsCurrent = PRESERVABLE_KEYS.some((k) => args[k] === undefined)
454
- || args.allowedInjectedRequestData === undefined
455
- const current = needsCurrent
456
- ? extractProviderConfig(await client.getProviderConfig(providerId))
457
- : undefined
458
-
459
- const carried: string[] = []
460
- for(const key of PRESERVABLE_KEYS) {
461
- const resolved = resolveField(args[key], current?.[key], key, carried)
462
- if(resolved !== undefined) {
463
- (opts as unknown as Record<string, unknown>)[key] = resolved
464
- }
465
- }
466
-
467
- const freshInjected = args.allowedInjectedRequestData !== undefined
468
- ? injectedRequestDataFrom(args.allowedInjectedRequestData)
469
- : undefined
470
- const resolvedInjected = resolveField<OldInjectedRequestData[]>(
471
- freshInjected,
472
- current?.allowedInjectedRequestData,
473
- 'allowedInjectedRequestData',
474
- carried,
475
- )
476
- if(resolvedInjected !== undefined) {
477
- opts.allowedInjectedRequestData = resolvedInjected
478
- }
479
-
480
- if(carried.length) {
481
- notes.push(
482
- 'Not passed, carried forward unchanged from the current ' +
483
- `published version: ${carried.join(', ')}. Pass an explicit ` +
484
- 'value ("", [], false, ...) to override any of them, or ' +
485
- 'call get_provider_version_config first to see what is ' +
486
- 'live today.',
487
- )
488
- }
489
-
391
+ // By design this always POSTs a brand-new version and publishes
392
+ // EXACTLY the config assembled above — nothing is read back or
393
+ // carried forward. Any field the caller omitted is already absent
394
+ // from `opts` and will be blank/default in the new version. To
395
+ // preserve existing fields, the caller must have read them via
396
+ // get_provider_info and passed them back in.
490
397
  if(args.customInjection !== undefined && opts.customInjection?.trim()) {
491
398
  notes.push(
492
399
  'customInjection will run in the verification client before ' +
@@ -495,6 +402,15 @@ export function createVersionFromCaptureTool(
495
402
  )
496
403
  }
497
404
 
405
+ notes.push(
406
+ 'A NEW version was created with only the fields passed in this ' +
407
+ 'call — nothing was carried forward from the previous version. ' +
408
+ 'If you intended to keep other fields (customInjection, ' +
409
+ 'userAgent, allowedInjectedRequestData, ...), read the prior ' +
410
+ 'version with get_provider_info and re-publish with them ' +
411
+ 'included.',
412
+ )
413
+
498
414
  const versionsResp = await client.listVersions(providerId)
499
415
  const version = bumpPatchString(extractVersions(versionsResp))
500
416
  const versionInfo =