@reclaimprotocol/agent 0.1.5 → 0.1.6

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 (117) hide show
  1. package/lib/authoring/backend.d.ts +0 -24
  2. package/lib/authoring/find.d.ts +3 -0
  3. package/lib/authoring/find.js +54 -0
  4. package/lib/authoring/index.d.ts +3 -3
  5. package/lib/authoring/index.js +4 -3
  6. package/lib/authoring/propose.d.ts +3 -0
  7. package/lib/authoring/propose.js +109 -0
  8. package/lib/authoring/replay.d.ts +10 -0
  9. package/lib/authoring/replay.js +34 -0
  10. package/lib/authoring/tools.d.ts +13 -6
  11. package/lib/authoring/tools.js +108 -293
  12. package/lib/authoring/util.d.ts +27 -0
  13. package/lib/authoring/util.js +21 -0
  14. package/lib/consts.d.ts +6 -1
  15. package/lib/consts.js +9 -2
  16. package/lib/mcp/server.d.ts +2 -41
  17. package/lib/mcp/server.js +6 -34
  18. package/lib/mcp/start.js +4 -2
  19. package/lib/mcp/tools/agent/analyze.js +3 -3
  20. package/lib/mcp/tools/agent/build.d.ts +4 -4
  21. package/lib/mcp/tools/agent/build.js +13 -15
  22. package/lib/mcp/tools/agent/capture.d.ts +13 -1
  23. package/lib/mcp/tools/agent/capture.js +18 -85
  24. package/lib/mcp/tools/agent/eval.d.ts +5 -2
  25. package/lib/mcp/tools/agent/eval.js +11 -121
  26. package/lib/mcp/tools/agent/find.d.ts +10 -3
  27. package/lib/mcp/tools/agent/find.js +25 -31
  28. package/lib/mcp/tools/agent/proof.d.ts +13 -3
  29. package/lib/mcp/tools/agent/proof.js +81 -132
  30. package/lib/mcp/tools/agent/propose.d.ts +10 -9
  31. package/lib/mcp/tools/agent/propose.js +87 -64
  32. package/lib/mcp/tools/agent/replay.d.ts +10 -3
  33. package/lib/mcp/tools/agent/replay.js +29 -36
  34. package/lib/mcp/tools/agent/session.d.ts +1 -3
  35. package/lib/mcp/tools/agent/session.js +5 -5
  36. package/lib/mcp/tools/agent/shared.d.ts +5 -3
  37. package/lib/mcp/tools/agent/shared.js +29 -19
  38. package/lib/old/client.d.ts +35 -0
  39. package/lib/old/client.js +18 -0
  40. package/lib/old/login-cdp.d.ts +24 -5
  41. package/lib/old/login-cdp.js +29 -15
  42. package/lib/old/sdk-client.d.ts +39 -0
  43. package/lib/old/sdk-client.js +85 -0
  44. package/lib/old/to-register.d.ts +69 -0
  45. package/lib/old/to-register.js +52 -2
  46. package/lib/old/tools/authenticate.js +3 -7
  47. package/lib/old/tools/build.d.ts +6 -1
  48. package/lib/old/tools/build.js +11 -1
  49. package/lib/old/tools/get-provider-info.d.ts +16 -0
  50. package/lib/old/tools/get-provider-info.js +73 -0
  51. package/lib/old/tools/get-version-config.d.ts +17 -0
  52. package/lib/old/tools/get-version-config.js +47 -0
  53. package/lib/old/tools/publish.js +189 -2
  54. package/lib/old/tools/update-provider-metadata.d.ts +16 -0
  55. package/lib/old/tools/update-provider-metadata.js +61 -0
  56. package/lib/proof/attestor.d.ts +6 -3
  57. package/lib/proof/attestor.js +10 -1
  58. package/lib/proof/debug-response-dump.d.ts +4 -0
  59. package/lib/proof/debug-response-dump.js +55 -0
  60. package/lib/proof/patch-domhandler.d.ts +1 -0
  61. package/lib/proof/patch-domhandler.js +59 -0
  62. package/lib/proof/zk-files.js +7 -6
  63. package/lib/provider/dom-xpath.d.ts +17 -0
  64. package/lib/provider/dom-xpath.js +187 -0
  65. package/lib/provider/draft.d.ts +30 -2
  66. package/lib/provider/draft.js +117 -28
  67. package/lib/provider/index.d.ts +4 -0
  68. package/lib/provider/index.js +7 -0
  69. package/lib/provider/injection-doc.js +6 -0
  70. package/lib/provider/matcher.d.ts +17 -20
  71. package/lib/provider/matcher.js +394 -280
  72. package/lib/provider/schema.d.ts +7 -5
  73. package/lib/tools/registry.d.ts +14 -0
  74. package/lib/tools/registry.js +32 -0
  75. package/package.json +9 -1
  76. package/skill/SKILL.builder.md +42 -23
  77. package/skill/SKILL.old.md +6 -2
  78. package/skill/builder-mode.md +1 -1
  79. package/skill/injection-api.md +117 -0
  80. package/src/authoring/backend.ts +98 -0
  81. package/src/authoring/index.ts +20 -0
  82. package/src/authoring/tools.ts +273 -0
  83. package/src/authoring/util.ts +42 -0
  84. package/src/consts.ts +15 -2
  85. package/src/mcp/server.ts +11 -78
  86. package/src/mcp/start.ts +4 -1
  87. package/src/mcp/tools/agent/analyze.ts +3 -3
  88. package/src/mcp/tools/agent/build.ts +13 -21
  89. package/src/mcp/tools/agent/capture.ts +31 -115
  90. package/src/mcp/tools/agent/eval.ts +19 -155
  91. package/src/mcp/tools/agent/find.ts +33 -36
  92. package/src/mcp/tools/agent/proof.ts +87 -160
  93. package/src/mcp/tools/agent/propose.ts +93 -85
  94. package/src/mcp/tools/agent/replay.ts +32 -42
  95. package/src/mcp/tools/agent/session.ts +4 -5
  96. package/src/mcp/tools/agent/shared.ts +152 -0
  97. package/src/old/client.ts +56 -0
  98. package/src/old/login-cdp.ts +29 -23
  99. package/src/old/sdk-client.ts +105 -0
  100. package/src/old/to-register.ts +98 -6
  101. package/src/old/tools/authenticate.ts +3 -11
  102. package/src/old/tools/build.ts +12 -0
  103. package/src/old/tools/get-provider-info.ts +91 -0
  104. package/src/old/tools/get-version-config.ts +56 -0
  105. package/src/old/tools/publish.ts +244 -1
  106. package/src/old/tools/update-provider-metadata.ts +72 -0
  107. package/src/proof/attestor.ts +16 -4
  108. package/src/proof/debug-response-dump.ts +70 -0
  109. package/src/proof/patch-domhandler.ts +68 -0
  110. package/src/proof/zk-files.ts +7 -6
  111. package/src/provider/dom-xpath.ts +225 -0
  112. package/src/provider/draft.ts +168 -35
  113. package/src/provider/index.ts +8 -0
  114. package/src/provider/injection-doc.ts +6 -0
  115. package/src/provider/matcher.ts +477 -357
  116. package/src/provider/schema.ts +7 -5
  117. package/src/tools/registry.ts +92 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Build the shared authoring tools for the MCP, bound to a per-capture LOCAL
3
+ * backend: a Chrome tab over CDP + the in-process attestor SDK. The tool logic
4
+ * (navigate/inspect/propose/replay/prove) is authored once in `../../../
5
+ * authoring` with the shared `defineTool` primitive; this only supplies the
6
+ * MCP host — resolve the backend from `captureId` (the session handle from
7
+ * `start_capture`, since one capture bundles the tab, its requests, and its
8
+ * drafts) and the rich, credential-aware `run_proof` schema.
9
+ */
10
+ import {
11
+ type AuthoringBackend,
12
+ authoringTools,
13
+ extendTool,
14
+ } from '../../../authoring/index.ts'
15
+ import { runAttestorProof } from '../../../proof/attestor.ts'
16
+ import { replayProvider } from '../../../proof/replay.ts'
17
+ import type { RegisteredTool } from '../../server.ts'
18
+ import type { CaptureEntry, CaptureStore } from './capture.ts'
19
+ import { shapeRichProofResult } from './proof.ts'
20
+
21
+ /** The local backend for one capture: drive its tab over CDP, read its
22
+ * captured requests, prove via the in-process attestor, replay by re-fetch. */
23
+ function localBackend(entry: CaptureEntry): AuthoringBackend {
24
+ const { tabClient, session, drafts } = entry
25
+ // The per-tab CDP client is strongly typed to `keyof Commands`; the shared
26
+ // tools drive it with dynamic method/event names, so view it as string-keyed.
27
+ const client = tabClient as unknown as {
28
+ send: (method: string, params: object) => Promise<unknown>
29
+ once: (e: string, cb: () => void) => void
30
+ removeListener: (e: string, cb: () => void) => void
31
+ }
32
+
33
+ return {
34
+ capture: session,
35
+ drafts,
36
+ sendCdp: (method, params) => client.send(method, params ?? {}),
37
+ waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
38
+ const onEvent = () => {
39
+ cleanup()
40
+ resolve('fired')
41
+ }
42
+
43
+ const timer = setTimeout(() => {
44
+ cleanup()
45
+ resolve('timeout')
46
+ }, timeoutMs)
47
+ const cleanup = () => {
48
+ clearTimeout(timer)
49
+ client.removeListener(method, onEvent)
50
+ }
51
+
52
+ client.once(method, onEvent)
53
+ }),
54
+ replay: async(draft, opts) => {
55
+ // Auth-bound check: re-fetch with the secrets dropped. replayProvider
56
+ // already does a live Node fetch, so empty secrets == anonymous.
57
+ const secrets = opts?.withoutSecrets ? {} : draft.secrets
58
+ const result = await replayProvider(draft.provider, secrets)
59
+ return {
60
+ status: result.status,
61
+ matchesOriginal: result.matchesOriginal,
62
+ bodySnippet: result.body.slice(0, 256),
63
+ ...(result.hints !== undefined ? { hints: result.hints } : {}),
64
+ }
65
+ },
66
+ prove: async(draft, opts) => {
67
+ const result = await runAttestorProof(
68
+ draft.provider,
69
+ draft.secrets,
70
+ {
71
+ ...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
72
+ ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
73
+ },
74
+ )
75
+ return shapeRichProofResult(result, {
76
+ provider: draft.provider,
77
+ hasSecrets: Object.keys(draft.secrets).length > 0,
78
+ ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
79
+ })
80
+ },
81
+ }
82
+ }
83
+
84
+ /** The MCP's rich, credential-aware `run_proof` schema, merged onto the shared
85
+ * tool by the host below (the cloud's TEE signs in-enclave, so it needs none
86
+ * of this). */
87
+ const RUN_PROOF_EXTRAS = {
88
+ description:
89
+ 'Run a full zkTLS proof of the drafted recipe through the Reclaim '
90
+ + 'attestor SDK. Use only after replay_request looks right — attestor '
91
+ + 'calls are expensive. Writes the full claim to '
92
+ + '~/.reclaim/claims/<timestamp>.json and returns identifier + '
93
+ + 'extractedParameters + claimPath + a prettified `claim` (request '
94
+ + 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
95
+ + 'writes a per-call diagnostic dump (input provider, sentParams, full '
96
+ + 'attestor log stream, result) and returns it as `runLogPath`. '
97
+ + '`attestorUrl` overrides the default only when the dev supplies a '
98
+ + 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
99
+ + '(the credential to sign with) is REQUIRED — call resolve_owner_key '
100
+ + 'first. errorKind="missing-owner-key" means that address has no known '
101
+ + 'private key; re-run resolve_owner_key or issue/import a credential.',
102
+ properties: {
103
+ ownerAddress: {
104
+ type: 'string',
105
+ pattern: '^0x[0-9a-fA-F]{40}$',
106
+ description:
107
+ 'ETH address of the credential to sign the claim with. Get it '
108
+ + 'from resolve_owner_key first. The private key is resolved '
109
+ + 'locally (cache / env / .env) — never passed here.',
110
+ },
111
+ attestorUrl: { type: 'string' },
112
+ },
113
+ required: ['ownerAddress'],
114
+ }
115
+
116
+ /** The shared authoring tools mapped to the MCP's shape: resolve each call's
117
+ * local backend from `args.captureId`, and widen the input schema — every tool
118
+ * takes `captureId`; `run_proof` also gets the credential-aware contract. The
119
+ * tools are authored plain in `../../../authoring`; this only wraps them with
120
+ * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
121
+ export function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[] {
122
+ const resolveBackend = (args: Record<string, unknown>): AuthoringBackend => {
123
+ const captureId = String(args.captureId)
124
+ const entry = captures.sessions.get(captureId)
125
+ if(!entry) {
126
+ throw new Error(
127
+ `no capture ${captureId} — call start_capture first `
128
+ + '(it has been reset or never started).',
129
+ )
130
+ }
131
+
132
+ return localBackend(entry)
133
+ }
134
+
135
+ return authoringTools(resolveBackend).map((tool) => {
136
+ if(tool.definition.name === 'run_proof') {
137
+ return extendTool(tool, {
138
+ description: RUN_PROOF_EXTRAS.description,
139
+ properties: {
140
+ captureId: { type: 'string' },
141
+ ...RUN_PROOF_EXTRAS.properties,
142
+ },
143
+ required: ['captureId', ...RUN_PROOF_EXTRAS.required],
144
+ })
145
+ }
146
+
147
+ return extendTool(tool, {
148
+ properties: { captureId: { type: 'string' } },
149
+ required: ['captureId'],
150
+ })
151
+ })
152
+ }
package/src/old/client.ts CHANGED
@@ -34,6 +34,15 @@ export interface RegisterProviderBody {
34
34
  useProxy?: boolean
35
35
  providerType?: 'PRIVATE' | 'PUBLIC'
36
36
  requestData: unknown[]
37
+ /** Page-injection script (old-devtools name for the builder's
38
+ * `webSettings.jsUserScripts`). Top-level on register; nested under
39
+ * `providerConfig` on the config (add-version) body instead. */
40
+ customInjection?: string
41
+ /** Accepted at register too (unlike most `providerConfig` extras, which
42
+ * the register controller only reads on config/add-version). */
43
+ userAgent?: { ios?: string, android?: string }
44
+ /** Accepted at register too. */
45
+ pageTitle?: string
37
46
  }
38
47
 
39
48
  export interface UpdateProviderConfigBody {
@@ -42,6 +51,19 @@ export interface UpdateProviderConfigBody {
42
51
  versionInfo: string
43
52
  }
44
53
 
54
+ /** Body for `POST /api/providers/:providerId` (`updateProviderMetadata`) —
55
+ * a genuine PARTIAL update on the PROVIDER document itself (name/
56
+ * description/providerType/tags/isActive), distinct from both `register`
57
+ * (creates the provider) and `config` (adds an immutable new version). Only
58
+ * the fields present get changed; everything else is left untouched. */
59
+ export interface UpdateProviderMetadataBody {
60
+ name?: string
61
+ description?: string
62
+ providerType?: 'PRIVATE' | 'PUBLIC'
63
+ tags?: string[]
64
+ isActive?: boolean
65
+ }
66
+
45
67
  export interface MyProvidersQuery {
46
68
  pageKey?: number
47
69
  pageSize?: number
@@ -121,6 +143,40 @@ export class ReclaimOldClient {
121
143
  )
122
144
  }
123
145
 
146
+ /** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
147
+ * one version — the latest when `version` is omitted. Readable for ANY
148
+ * non-archived provider (not owner-restricted), same as the dashboard's
149
+ * edit-form prefill fetch. */
150
+ getProviderConfig(
151
+ providerId: string,
152
+ version?: string,
153
+ ): Promise<unknown> {
154
+ return this.#request(
155
+ 'GET',
156
+ `/providers/${encodeURIComponent(providerId)}/config`,
157
+ {
158
+ auth: true,
159
+ query: version ? { versionNumber: version } : undefined,
160
+ },
161
+ )
162
+ }
163
+
164
+ /** Partial update on the PROVIDER document itself — name/description/
165
+ * providerType/tags/isActive, after creation. The backend 403s if you
166
+ * don't own the provider (and aren't admin); other providerConfig-style
167
+ * fields sent here are simply ignored (this endpoint only reads the
168
+ * ones above from the body). */
169
+ updateProviderMetadata(
170
+ providerId: string,
171
+ body: UpdateProviderMetadataBody,
172
+ ): Promise<unknown> {
173
+ return this.#request(
174
+ 'POST',
175
+ `/providers/${encodeURIComponent(providerId)}`,
176
+ { body, auth: true },
177
+ )
178
+ }
179
+
124
180
  /** Analytics logs for a verification session, from the logs service (a
125
181
  * DIFFERENT host than the devtools API — hence its own fetch, not
126
182
  * `#request`). Attaches the caller identity when one is set; queried
@@ -18,16 +18,39 @@ import { DEFAULT_LOGIN_URL, oldLoginUrl } from '../consts.ts'
18
18
  * `firebaseLocalStorage`) in case a build switches persistence. Returns the
19
19
  * token string, or null when the user has not finished logging in yet.
20
20
  *
21
+ * The CDP-launched Chrome reuses a PERSISTENT profile dir across runs, so a
22
+ * stale `firebase:authUser:*` entry from an earlier (now-expired, ~1h TTL)
23
+ * session is often still sitting in storage the instant the page loads —
24
+ * before the developer has clicked anything. Returning that immediately
25
+ * makes the poll "succeed" on tick one, closing the tab before the developer
26
+ * can sign in, and the captured token then fails backend verification. So
27
+ * every candidate's JWT `exp` claim is checked here (in-page, where `atob` is
28
+ * available) and stale ones are skipped — the loop keeps polling until a
29
+ * genuinely fresh token shows up from an interactive sign-in.
30
+ *
21
31
  * Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
22
32
  */
23
- const EXTRACT_TOKEN_JS = `(async () => {
33
+ export const EXTRACT_TOKEN_JS = `(async () => {
34
+ const isFresh = (token) => {
35
+ try {
36
+ const payload = JSON.parse(
37
+ atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
38
+ )
39
+ // No exp claim to check — don't block a token we can't evaluate.
40
+ if (typeof payload.exp !== 'number') return true
41
+ // 60s buffer so a token that's about to expire isn't treated as fresh.
42
+ return payload.exp * 1000 > Date.now() + 60000
43
+ } catch (e) {
44
+ return true
45
+ }
46
+ }
24
47
  try {
25
48
  for (let i = 0; i < localStorage.length; i++) {
26
49
  const k = localStorage.key(i)
27
50
  if (k && k.indexOf('firebase:authUser:') === 0) {
28
51
  const v = JSON.parse(localStorage.getItem(k) || 'null')
29
52
  const t = v && v.stsTokenManager && v.stsTokenManager.accessToken
30
- if (t) return t
53
+ if (t && isFresh(t)) return t
31
54
  }
32
55
  }
33
56
  } catch (e) {}
@@ -49,7 +72,9 @@ const EXTRACT_TOKEN_JS = `(async () => {
49
72
  all.onsuccess = () => {
50
73
  for (const row of (all.result || [])) {
51
74
  const m = row && row.value && row.value.stsTokenManager
52
- if (m && m.accessToken) { resolve(m.accessToken); return }
75
+ if (m && m.accessToken && isFresh(m.accessToken)) {
76
+ resolve(m.accessToken); return
77
+ }
53
78
  }
54
79
  resolve(null)
55
80
  }
@@ -79,18 +104,11 @@ export async function captureFirebaseTokenViaCdp(opts: {
79
104
  loginUrl?: string
80
105
  timeoutMs?: number
81
106
  pollMs?: number
82
- /** Called every ~10s while waiting for the human to finish signing in, so
83
- * MCP clients that track progress on this call can reset their own
84
- * request timeout instead of giving up on a login that's still in
85
- * flight (see `ToolContext` in `mcp/server.ts`). */
86
- onProgress?: (message: string) => Promise<void>
87
107
  }): Promise<CdpLoginResult> {
88
108
  const loginUrl =
89
109
  opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL
90
110
  const timeoutMs = opts.timeoutMs ?? 180_000
91
111
  const pollMs = opts.pollMs ?? 1000
92
- const onProgress = opts.onProgress ?? (async() => {})
93
- const progressIntervalMs = 10_000
94
112
 
95
113
  const cfg = new ConfigStore().read()?.browser_agent
96
114
  const mode = cfg?.attachMode ?? 'dedicated'
@@ -121,9 +139,7 @@ export async function captureFirebaseTokenViaCdp(opts: {
121
139
  try {
122
140
  const session = await handles.attachToTab(targetId)
123
141
  const { Runtime } = session
124
- const startedAt = Date.now()
125
- const deadline = startedAt + timeoutMs
126
- let lastProgressAt = startedAt
142
+ const deadline = Date.now() + timeoutMs
127
143
  while(Date.now() < deadline) {
128
144
  const r = await Runtime.evaluate({
129
145
  expression: EXTRACT_TOKEN_JS,
@@ -135,16 +151,6 @@ export async function captureFirebaseTokenViaCdp(opts: {
135
151
  return { token: value, closedBrowser: launchedByUs }
136
152
  }
137
153
 
138
- const now = Date.now()
139
- if(now - lastProgressAt >= progressIntervalMs) {
140
- lastProgressAt = now
141
- const remainingSec = Math.round((deadline - now) / 1000)
142
- await onProgress(
143
- `Still waiting for sign-in at ${loginUrl} ` +
144
- `(${remainingSec}s left before this call times out).`,
145
- )
146
- }
147
-
148
154
  await new Promise((resolve) => setTimeout(resolve, pollMs))
149
155
  }
150
156
 
@@ -0,0 +1,105 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { NAME, VERSION } from '../pkg.ts'
3
+
4
+ /**
5
+ * Default origin of the PUBLIC production read API that the Reclaim
6
+ * verification SDK/attestor itself calls at runtime to fetch a provider's
7
+ * recipe — a DIFFERENT service from the old-devtools dashboard backend
8
+ * (`ReclaimOldClient`, `devapi.reclaimprotocol.org`). Both endpoints below are
9
+ * unauthenticated reads (no `authenticateToken` middleware on this backend's
10
+ * routes), so this client needs no identity. Override with
11
+ * `RECLAIM_SDK_API_URL`.
12
+ */
13
+ export const DEFAULT_SDK_API_URL = 'https://api.reclaimprotocol.org'
14
+
15
+ /**
16
+ * Minimal read-only client for the public provider-info endpoints on the SDK
17
+ * backend (`reclaim-sdk-backend`). Two distinct reads, on purpose:
18
+ *
19
+ * - `getProvider` (`GET /providers/:id`) — the full provider row merged
20
+ * with its version's WHOLE providerConfig, `customInjection` included.
21
+ * - `getProviderConfigs` (`GET /providers/:id/configs`) — the trimmed
22
+ * recipe the attestor actually consumes at verification time: a fixed
23
+ * field projection that explicitly OMITS `customInjection` and adds a
24
+ * computed `isScriptRequestingClaim` flag instead.
25
+ *
26
+ * Errors are thrown as `@hapi/boom` so `remapErrorAsResponse` renders them
27
+ * the same way as every other tool in this package.
28
+ */
29
+ export class ReclaimSdkClient {
30
+ readonly baseUrl: string
31
+ #fetch: typeof fetch
32
+
33
+ constructor(opts: { baseUrl?: string, fetch?: typeof fetch } = {}) {
34
+ this.baseUrl = (opts.baseUrl || DEFAULT_SDK_API_URL).replace(/\/+$/, '')
35
+ this.#fetch = opts.fetch ?? fetch
36
+ }
37
+
38
+ /** Full provider + providerConfig (includes `customInjection`). Latest
39
+ * version when `versionNumber` is omitted. */
40
+ getProvider(providerId: string, versionNumber?: string): Promise<unknown> {
41
+ return this.#get(
42
+ `/providers/${encodeURIComponent(providerId)}`,
43
+ versionNumber,
44
+ )
45
+ }
46
+
47
+ /** Trimmed runtime recipe (no `customInjection`; adds
48
+ * `isScriptRequestingClaim`). Latest version when `versionNumber` is
49
+ * omitted. */
50
+ getProviderConfigs(
51
+ providerId: string,
52
+ versionNumber?: string,
53
+ ): Promise<unknown> {
54
+ return this.#get(
55
+ `/providers/${encodeURIComponent(providerId)}/configs`,
56
+ versionNumber,
57
+ )
58
+ }
59
+
60
+ async #get(path: string, versionNumber?: string): Promise<unknown> {
61
+ let url = `${this.baseUrl}/api${path}`
62
+ if(versionNumber) {
63
+ url += `?versionNumber=${encodeURIComponent(versionNumber)}`
64
+ }
65
+
66
+ const res = await this.#fetch(url, {
67
+ method: 'GET',
68
+ headers: {
69
+ Accept: 'application/json',
70
+ 'User-Agent': `${NAME}/${VERSION}`,
71
+ },
72
+ })
73
+
74
+ let json: unknown
75
+ try {
76
+ json = await res.json()
77
+ } catch{
78
+ json = undefined
79
+ }
80
+
81
+ const failed =
82
+ !res.ok ||
83
+ (json !== undefined &&
84
+ typeof json === 'object' &&
85
+ (json as { isSuccess?: unknown }).isSuccess === false)
86
+ if(failed) {
87
+ const message =
88
+ (json && typeof json === 'object' &&
89
+ typeof (json as { message?: unknown }).message === 'string'
90
+ ? (json as { message: string }).message
91
+ : undefined) || res.statusText || 'SDK backend request failed'
92
+ throw new Boom(message, {
93
+ statusCode: res.ok ? 502 : res.status,
94
+ data: {
95
+ status: res.status,
96
+ title: message,
97
+ detail: message,
98
+ body: json,
99
+ },
100
+ })
101
+ }
102
+
103
+ return json
104
+ }
105
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ReclaimProvider } from '../provider/schema.ts'
2
+ import type { AllowedJsRequest, DraftRequest } from '../provider/to-version.ts'
2
3
  import type {
3
4
  RegisterProviderBody,
4
5
  UpdateProviderConfigBody,
@@ -34,6 +35,30 @@ export interface OldProviderOpts {
34
35
  /** Defaults to `true` when omitted. Register only (Provider-level flag). */
35
36
  useProxy?: boolean
36
37
  providerType?: 'PRIVATE' | 'PUBLIC'
38
+ /** Page-injection script — old-devtools' name for the builder's
39
+ * `webSettings.jsUserScripts`. Top-level on register, nested under
40
+ * `providerConfig` on config (add-version). The backend rejects a few
41
+ * deprecated injection APIs (e.g. `window.payloadData`) with a 400. */
42
+ customInjection?: string
43
+ /** Accepted at register too (unlike the four fields below, which the
44
+ * backend only accepts on config/add-version). */
45
+ userAgent?: { ios?: string, android?: string }
46
+ /** Accepted at register too. */
47
+ pageTitle?: string
48
+ /** Config (add-version) ONLY — the register endpoint hardcodes this to
49
+ * `[]` and ignores whatever is sent. Already old-backend-shaped: build
50
+ * fresh entries with {@link injectedRequestDataFrom}; a carried-forward
51
+ * value (read back from an existing version) passes through unchanged. */
52
+ allowedInjectedRequestData?: OldInjectedRequestData[]
53
+ /** Config (add-version) ONLY — the register endpoint never reads this
54
+ * field from the request body. */
55
+ stepsToFollow?: string
56
+ /** Config (add-version) ONLY — the register endpoint never reads this
57
+ * field from the request body. */
58
+ useIncognitoWebview?: boolean
59
+ /** Config (add-version) ONLY — the register endpoint never reads this
60
+ * field from the request body. Free-form JSON. */
61
+ extensionConfig?: unknown
37
62
  }
38
63
 
39
64
  interface OldResponseMatch {
@@ -63,8 +88,18 @@ interface OldRequestData {
63
88
  bodySniff?: { enabled: boolean, template: string }
64
89
  }
65
90
 
66
- /** Build the single `requestData` entry shared by register + update. */
67
- function requestDataFromProvider(provider: ReclaimProvider): OldRequestData {
91
+ /** One `allowedInjectedRequestData` entry — an `OldRequestData` plus the
92
+ * pagination/optionality flags the old backend stores alongside it. */
93
+ export interface OldInjectedRequestData extends OldRequestData {
94
+ required?: boolean
95
+ multiple?: boolean
96
+ }
97
+
98
+ /** Build the single `requestData` entry shared by register + update. Takes
99
+ * the loose `DraftRequest` shape (not the full `ReclaimProvider`) so it can
100
+ * build BOTH the top-level `requestData` and `allowedInjectedRequestData`
101
+ * entries — a `ReclaimProvider` satisfies it structurally either way. */
102
+ function requestDataFromProvider(provider: DraftRequest): OldRequestData {
68
103
  const method = String(provider.method).toUpperCase()
69
104
  if(method !== 'GET' && method !== 'POST') {
70
105
  throw new Error(
@@ -87,14 +122,13 @@ function requestDataFromProvider(provider: ReclaimProvider): OldRequestData {
87
122
  )
88
123
  }
89
124
 
90
- const responseMatches: OldResponseMatch[] = provider.responseMatches.map(
91
- (m, i) => ({
125
+ const responseMatches: OldResponseMatch[] = (provider.responseMatches ?? [])
126
+ .map((m, i) => ({
92
127
  value: m.value,
93
128
  type: m.type ?? 'contains',
94
129
  isOptional: false,
95
130
  order: i,
96
- }),
97
- )
131
+ }))
98
132
 
99
133
  // Old devtools wants `responseMatches` and `responseRedactions` as PARALLEL
100
134
  // arrays — same shape the builder uses, so pass them through directly. We do
@@ -157,6 +191,22 @@ function requestDataFrom(providers: ReclaimProvider[]): OldRequestData[] {
157
191
  return providers.map(requestDataFromProvider)
158
192
  }
159
193
 
194
+ /** Translate fresh `allowedInjectedRequestData` drafts (the author's input
195
+ * shape, `AllowedJsRequest[]` — same shape the builder translator uses for
196
+ * its own `allowedJsRequests`) into the old backend's shape. A
197
+ * carried-forward value read back from an existing version is ALREADY in
198
+ * this shape and should be passed straight through instead — don't run it
199
+ * through this a second time. */
200
+ export function injectedRequestDataFrom(
201
+ requests: AllowedJsRequest[],
202
+ ): OldInjectedRequestData[] {
203
+ return requests.map((req) => ({
204
+ ...requestDataFromProvider(req),
205
+ required: req.required ?? true,
206
+ multiple: req.multiple ?? true,
207
+ }))
208
+ }
209
+
160
210
  /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
161
211
  * drafted request becomes a `requestData` entry; provider-level fields (name)
162
212
  * come from the first draft. */
@@ -177,6 +227,21 @@ export function providerToRegisterBody(
177
227
  body.description = opts.description
178
228
  }
179
229
 
230
+ if(opts.customInjection !== undefined) {
231
+ body.customInjection = opts.customInjection
232
+ }
233
+
234
+ // Register also accepts these two (unlike allowedInjectedRequestData /
235
+ // stepsToFollow / useIncognitoWebview / extensionConfig, which the
236
+ // register controller never reads from the body at all).
237
+ if(opts.userAgent !== undefined) {
238
+ body.userAgent = opts.userAgent
239
+ }
240
+
241
+ if(opts.pageTitle !== undefined) {
242
+ body.pageTitle = opts.pageTitle
243
+ }
244
+
180
245
  return body
181
246
  }
182
247
 
@@ -197,6 +262,33 @@ export function providerToConfigBody(
197
262
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
198
263
  requestData: requestDataFrom(providers),
199
264
  }
265
+ if(opts.customInjection !== undefined) {
266
+ providerConfig.customInjection = opts.customInjection
267
+ }
268
+
269
+ if(opts.userAgent !== undefined) {
270
+ providerConfig.userAgent = opts.userAgent
271
+ }
272
+
273
+ if(opts.pageTitle !== undefined) {
274
+ providerConfig.pageTitle = opts.pageTitle
275
+ }
276
+
277
+ if(opts.allowedInjectedRequestData !== undefined) {
278
+ providerConfig.allowedInjectedRequestData = opts.allowedInjectedRequestData
279
+ }
280
+
281
+ if(opts.stepsToFollow !== undefined) {
282
+ providerConfig.stepsToFollow = opts.stepsToFollow
283
+ }
284
+
285
+ if(opts.useIncognitoWebview !== undefined) {
286
+ providerConfig.useIncognitoWebview = opts.useIncognitoWebview
287
+ }
288
+
289
+ if(opts.extensionConfig !== undefined) {
290
+ providerConfig.extensionConfig = opts.extensionConfig
291
+ }
200
292
 
201
293
  return { providerConfig, version, versionInfo }
202
294
  }
@@ -1,9 +1,5 @@
1
1
  import { agentDisabled } from '../../consts.ts'
2
- import {
3
- defineTool,
4
- type RegisteredTool,
5
- type ToolContext,
6
- } from '../../mcp/server.ts'
2
+ import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
7
3
  import type { OldIdentity, ReclaimOldClient } from '../client.ts'
8
4
  import { persistIdentity, toEthUid } from '../identity.ts'
9
5
  import { captureFirebaseTokenViaCdp } from '../login-cdp.ts'
@@ -87,7 +83,7 @@ export function authenticateTool(client: ReclaimOldClient): RegisteredTool {
87
83
  },
88
84
  },
89
85
  },
90
- async({ token, ethAddress, loginUrl, timeoutMs, useBrowser }, ctx: ToolContext) => {
86
+ async({ token, ethAddress, loginUrl, timeoutMs, useBrowser }) => {
91
87
  if(token && ethAddress) {
92
88
  throw new Error(
93
89
  'Provide only ONE of `token` or `ethAddress`, not both.',
@@ -140,11 +136,7 @@ export function authenticateTool(client: ReclaimOldClient): RegisteredTool {
140
136
  )
141
137
  }
142
138
 
143
- const result = await captureFirebaseTokenViaCdp({
144
- loginUrl,
145
- timeoutMs,
146
- onProgress: ctx.reportProgress,
147
- })
139
+ const result = await captureFirebaseTokenViaCdp({ loginUrl, timeoutMs })
148
140
  const identity: OldIdentity = { kind: 'bearer', token: result.token }
149
141
  await validate(identity)
150
142
  persistIdentity(identity)
@@ -1,23 +1,35 @@
1
1
  import type { RegisteredTool } from '../../mcp/server.ts'
2
2
  import type { ReclaimOldClient } from '../client.ts'
3
+ import type { ReclaimSdkClient } from '../sdk-client.ts'
3
4
  import { authenticateTool } from './authenticate.ts'
5
+ import { getProviderInfoTool } from './get-provider-info.ts'
6
+ import { getProviderVersionConfigTool } from './get-version-config.ts'
4
7
  import { getMeProvidersTool } from './me-providers.ts'
5
8
  import { createVersionFromCaptureTool } from './publish.ts'
6
9
  import { sessionAnalyticsLogsTool } from './session-analytics-logs.ts'
10
+ import { updateProviderMetadataTool } from './update-provider-metadata.ts'
7
11
 
8
12
  /**
9
13
  * The backend-facing tools for OLD-devtools mode. Mirrors
10
14
  * `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
11
15
  * list for `registerToolList`. These replace the builder credential + publish
12
16
  * tools and the (disabled) generated provider tools.
17
+ *
18
+ * `sdkClient` is a SEPARATE, unauthenticated client for the public
19
+ * production SDK backend (`api.reclaimprotocol.org`) — a different host from
20
+ * `client`'s old-devtools dashboard backend (`devapi.reclaimprotocol.org`).
13
21
  */
14
22
  export function buildOldBackendTools(
15
23
  client: ReclaimOldClient,
24
+ sdkClient: ReclaimSdkClient,
16
25
  ): RegisteredTool[] {
17
26
  return [
18
27
  authenticateTool(client),
19
28
  createVersionFromCaptureTool(client),
20
29
  getMeProvidersTool(client),
30
+ getProviderVersionConfigTool(client),
31
+ getProviderInfoTool(sdkClient),
32
+ updateProviderMetadataTool(client),
21
33
  sessionAnalyticsLogsTool(client),
22
34
  ]
23
35
  }