@reclaimprotocol/agent 0.1.5 → 0.1.7

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 +39 -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 +55 -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 +208 -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 +60 -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 +102 -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 +264 -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
@@ -32,12 +32,40 @@ export interface RegisterProviderBody {
32
32
  useProxy?: boolean;
33
33
  providerType?: 'PRIVATE' | 'PUBLIC';
34
34
  requestData: unknown[];
35
+ /** Page-injection script (old-devtools name for the builder's
36
+ * `webSettings.jsUserScripts`). Top-level on register; nested under
37
+ * `providerConfig` on the config (add-version) body instead. */
38
+ customInjection?: string;
39
+ /** Accepted at register too (unlike most `providerConfig` extras, which
40
+ * the register controller only reads on config/add-version). */
41
+ userAgent?: {
42
+ ios?: string;
43
+ android?: string;
44
+ };
45
+ /** Accepted at register too. */
46
+ pageTitle?: string;
47
+ /** Accepted at register too — verified straight from the register
48
+ * controller's destructure + its `providerConfig` write, both of which
49
+ * include it alongside `pageTitle`/`userAgent`. */
50
+ stepsToFollow?: string;
35
51
  }
36
52
  export interface UpdateProviderConfigBody {
37
53
  providerConfig: Record<string, unknown>;
38
54
  version: string;
39
55
  versionInfo: string;
40
56
  }
57
+ /** Body for `POST /api/providers/:providerId` (`updateProviderMetadata`) —
58
+ * a genuine PARTIAL update on the PROVIDER document itself (name/
59
+ * description/providerType/tags/isActive), distinct from both `register`
60
+ * (creates the provider) and `config` (adds an immutable new version). Only
61
+ * the fields present get changed; everything else is left untouched. */
62
+ export interface UpdateProviderMetadataBody {
63
+ name?: string;
64
+ description?: string;
65
+ providerType?: 'PRIVATE' | 'PUBLIC';
66
+ tags?: string[];
67
+ isActive?: boolean;
68
+ }
41
69
  export interface MyProvidersQuery {
42
70
  pageKey?: number;
43
71
  pageSize?: number;
@@ -71,6 +99,17 @@ export declare class ReclaimOldClient {
71
99
  registerProvider(body: RegisterProviderBody): Promise<unknown>;
72
100
  updateProviderConfig(providerId: string, body: UpdateProviderConfigBody): Promise<unknown>;
73
101
  listVersions(providerId: string): Promise<unknown>;
102
+ /** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
103
+ * one version — the latest when `version` is omitted. Readable for ANY
104
+ * non-archived provider (not owner-restricted), same as the dashboard's
105
+ * edit-form prefill fetch. */
106
+ getProviderConfig(providerId: string, version?: string): Promise<unknown>;
107
+ /** Partial update on the PROVIDER document itself — name/description/
108
+ * providerType/tags/isActive, after creation. The backend 403s if you
109
+ * don't own the provider (and aren't admin); other providerConfig-style
110
+ * fields sent here are simply ignored (this endpoint only reads the
111
+ * ones above from the body). */
112
+ updateProviderMetadata(providerId: string, body: UpdateProviderMetadataBody): Promise<unknown>;
74
113
  /** Analytics logs for a verification session, from the logs service (a
75
114
  * DIFFERENT host than the devtools API — hence its own fetch, not
76
115
  * `#request`). Attaches the caller identity when one is set; queried
package/lib/old/client.js CHANGED
@@ -49,6 +49,24 @@ export class ReclaimOldClient {
49
49
  listVersions(providerId) {
50
50
  return this.#request('GET', `/providers/${encodeURIComponent(providerId)}/versions`, { auth: true });
51
51
  }
52
+ /** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
53
+ * one version — the latest when `version` is omitted. Readable for ANY
54
+ * non-archived provider (not owner-restricted), same as the dashboard's
55
+ * edit-form prefill fetch. */
56
+ getProviderConfig(providerId, version) {
57
+ return this.#request('GET', `/providers/${encodeURIComponent(providerId)}/config`, {
58
+ auth: true,
59
+ query: version ? { versionNumber: version } : undefined,
60
+ });
61
+ }
62
+ /** Partial update on the PROVIDER document itself — name/description/
63
+ * providerType/tags/isActive, after creation. The backend 403s if you
64
+ * don't own the provider (and aren't admin); other providerConfig-style
65
+ * fields sent here are simply ignored (this endpoint only reads the
66
+ * ones above from the body). */
67
+ updateProviderMetadata(providerId, body) {
68
+ return this.#request('POST', `/providers/${encodeURIComponent(providerId)}`, { body, auth: true });
69
+ }
52
70
  /** Analytics logs for a verification session, from the logs service (a
53
71
  * DIFFERENT host than the devtools API — hence its own fetch, not
54
72
  * `#request`). Attaches the caller identity when one is set; queried
@@ -1,3 +1,27 @@
1
+ /**
2
+ * Read the Firebase ID token the dashboard persisted after login.
3
+ *
4
+ * The frontend uses `browserLocalPersistence`, which the Firebase JS SDK
5
+ * implements on `localStorage` under a `firebase:authUser:<apiKey>:[DEFAULT]`
6
+ * key whose JSON value carries `stsTokenManager.accessToken`. We scan for any
7
+ * such key (so we never hard-code the project apiKey) and fall back to the
8
+ * IndexedDB persistence layout (`firebaseLocalStorageDb` /
9
+ * `firebaseLocalStorage`) in case a build switches persistence. Returns the
10
+ * token string, or null when the user has not finished logging in yet.
11
+ *
12
+ * The CDP-launched Chrome reuses a PERSISTENT profile dir across runs, so a
13
+ * stale `firebase:authUser:*` entry from an earlier (now-expired, ~1h TTL)
14
+ * session is often still sitting in storage the instant the page loads —
15
+ * before the developer has clicked anything. Returning that immediately
16
+ * makes the poll "succeed" on tick one, closing the tab before the developer
17
+ * can sign in, and the captured token then fails backend verification. So
18
+ * every candidate's JWT `exp` claim is checked here (in-page, where `atob` is
19
+ * available) and stale ones are skipped — the loop keeps polling until a
20
+ * genuinely fresh token shows up from an interactive sign-in.
21
+ *
22
+ * Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
23
+ */
24
+ export declare const EXTRACT_TOKEN_JS = "(async () => {\n\tconst isFresh = (token) => {\n\t\ttry {\n\t\t\tconst payload = JSON.parse(\n\t\t\t\tatob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))\n\t\t\t)\n\t\t\t// No exp claim to check \u2014 don't block a token we can't evaluate.\n\t\t\tif (typeof payload.exp !== 'number') return true\n\t\t\t// 60s buffer so a token that's about to expire isn't treated as fresh.\n\t\t\treturn payload.exp * 1000 > Date.now() + 60000\n\t\t} catch (e) {\n\t\t\treturn true\n\t\t}\n\t}\n\ttry {\n\t\tfor (let i = 0; i < localStorage.length; i++) {\n\t\t\tconst k = localStorage.key(i)\n\t\t\tif (k && k.indexOf('firebase:authUser:') === 0) {\n\t\t\t\tconst v = JSON.parse(localStorage.getItem(k) || 'null')\n\t\t\t\tconst t = v && v.stsTokenManager && v.stsTokenManager.accessToken\n\t\t\t\tif (t && isFresh(t)) return t\n\t\t\t}\n\t\t}\n\t} catch (e) {}\n\ttry {\n\t\treturn await new Promise((resolve) => {\n\t\t\tconst req = indexedDB.open('firebaseLocalStorageDb')\n\t\t\treq.onerror = () => resolve(null)\n\t\t\treq.onsuccess = () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst db = req.result\n\t\t\t\t\tif (!db.objectStoreNames.contains('firebaseLocalStorage')) {\n\t\t\t\t\t\tresolve(null); return\n\t\t\t\t\t}\n\t\t\t\t\tconst store = db\n\t\t\t\t\t\t.transaction('firebaseLocalStorage', 'readonly')\n\t\t\t\t\t\t.objectStore('firebaseLocalStorage')\n\t\t\t\t\tconst all = store.getAll()\n\t\t\t\t\tall.onerror = () => resolve(null)\n\t\t\t\t\tall.onsuccess = () => {\n\t\t\t\t\t\tfor (const row of (all.result || [])) {\n\t\t\t\t\t\t\tconst m = row && row.value && row.value.stsTokenManager\n\t\t\t\t\t\t\tif (m && m.accessToken && isFresh(m.accessToken)) {\n\t\t\t\t\t\t\t\tresolve(m.accessToken); return\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve(null)\n\t\t\t\t\t}\n\t\t\t\t} catch (e) { resolve(null) }\n\t\t\t}\n\t\t})\n\t} catch (e) { return null }\n})()";
1
25
  export interface CdpLoginResult {
2
26
  token: string;
3
27
  /** True when we launched (and then closed) a dedicated Chrome just for
@@ -17,9 +41,4 @@ export declare function captureFirebaseTokenViaCdp(opts: {
17
41
  loginUrl?: string;
18
42
  timeoutMs?: number;
19
43
  pollMs?: number;
20
- /** Called every ~10s while waiting for the human to finish signing in, so
21
- * MCP clients that track progress on this call can reset their own
22
- * request timeout instead of giving up on a login that's still in
23
- * flight (see `ToolContext` in `mcp/server.ts`). */
24
- onProgress?: (message: string) => Promise<void>;
25
44
  }): Promise<CdpLoginResult>;
@@ -13,16 +13,39 @@ import { DEFAULT_LOGIN_URL, oldLoginUrl } from "../consts.js";
13
13
  * `firebaseLocalStorage`) in case a build switches persistence. Returns the
14
14
  * token string, or null when the user has not finished logging in yet.
15
15
  *
16
+ * The CDP-launched Chrome reuses a PERSISTENT profile dir across runs, so a
17
+ * stale `firebase:authUser:*` entry from an earlier (now-expired, ~1h TTL)
18
+ * session is often still sitting in storage the instant the page loads —
19
+ * before the developer has clicked anything. Returning that immediately
20
+ * makes the poll "succeed" on tick one, closing the tab before the developer
21
+ * can sign in, and the captured token then fails backend verification. So
22
+ * every candidate's JWT `exp` claim is checked here (in-page, where `atob` is
23
+ * available) and stale ones are skipped — the loop keeps polling until a
24
+ * genuinely fresh token shows up from an interactive sign-in.
25
+ *
16
26
  * Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
17
27
  */
18
- const EXTRACT_TOKEN_JS = `(async () => {
28
+ export const EXTRACT_TOKEN_JS = `(async () => {
29
+ const isFresh = (token) => {
30
+ try {
31
+ const payload = JSON.parse(
32
+ atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
33
+ )
34
+ // No exp claim to check — don't block a token we can't evaluate.
35
+ if (typeof payload.exp !== 'number') return true
36
+ // 60s buffer so a token that's about to expire isn't treated as fresh.
37
+ return payload.exp * 1000 > Date.now() + 60000
38
+ } catch (e) {
39
+ return true
40
+ }
41
+ }
19
42
  try {
20
43
  for (let i = 0; i < localStorage.length; i++) {
21
44
  const k = localStorage.key(i)
22
45
  if (k && k.indexOf('firebase:authUser:') === 0) {
23
46
  const v = JSON.parse(localStorage.getItem(k) || 'null')
24
47
  const t = v && v.stsTokenManager && v.stsTokenManager.accessToken
25
- if (t) return t
48
+ if (t && isFresh(t)) return t
26
49
  }
27
50
  }
28
51
  } catch (e) {}
@@ -44,7 +67,9 @@ const EXTRACT_TOKEN_JS = `(async () => {
44
67
  all.onsuccess = () => {
45
68
  for (const row of (all.result || [])) {
46
69
  const m = row && row.value && row.value.stsTokenManager
47
- if (m && m.accessToken) { resolve(m.accessToken); return }
70
+ if (m && m.accessToken && isFresh(m.accessToken)) {
71
+ resolve(m.accessToken); return
72
+ }
48
73
  }
49
74
  resolve(null)
50
75
  }
@@ -65,8 +90,6 @@ export async function captureFirebaseTokenViaCdp(opts) {
65
90
  const loginUrl = opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL;
66
91
  const timeoutMs = opts.timeoutMs ?? 180_000;
67
92
  const pollMs = opts.pollMs ?? 1000;
68
- const onProgress = opts.onProgress ?? (async () => { });
69
- const progressIntervalMs = 10_000;
70
93
  const cfg = new ConfigStore().read()?.browser_agent;
71
94
  const mode = cfg?.attachMode ?? 'dedicated';
72
95
  const port = cfg?.port ?? 9222;
@@ -91,9 +114,7 @@ export async function captureFirebaseTokenViaCdp(opts) {
91
114
  try {
92
115
  const session = await handles.attachToTab(targetId);
93
116
  const { Runtime } = session;
94
- const startedAt = Date.now();
95
- const deadline = startedAt + timeoutMs;
96
- let lastProgressAt = startedAt;
117
+ const deadline = Date.now() + timeoutMs;
97
118
  while (Date.now() < deadline) {
98
119
  const r = await Runtime.evaluate({
99
120
  expression: EXTRACT_TOKEN_JS,
@@ -104,13 +125,6 @@ export async function captureFirebaseTokenViaCdp(opts) {
104
125
  if (typeof value === 'string' && value.length > 0) {
105
126
  return { token: value, closedBrowser: launchedByUs };
106
127
  }
107
- const now = Date.now();
108
- if (now - lastProgressAt >= progressIntervalMs) {
109
- lastProgressAt = now;
110
- const remainingSec = Math.round((deadline - now) / 1000);
111
- await onProgress(`Still waiting for sign-in at ${loginUrl} ` +
112
- `(${remainingSec}s left before this call times out).`);
113
- }
114
128
  await new Promise((resolve) => setTimeout(resolve, pollMs));
115
129
  }
116
130
  throw new Error('Timed out waiting for login. Finish signing in at the opened ' +
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Default origin of the PUBLIC production read API that the Reclaim
3
+ * verification SDK/attestor itself calls at runtime to fetch a provider's
4
+ * recipe — a DIFFERENT service from the old-devtools dashboard backend
5
+ * (`ReclaimOldClient`, `devapi.reclaimprotocol.org`). Both endpoints below are
6
+ * unauthenticated reads (no `authenticateToken` middleware on this backend's
7
+ * routes), so this client needs no identity. Override with
8
+ * `RECLAIM_SDK_API_URL`.
9
+ */
10
+ export declare const DEFAULT_SDK_API_URL = "https://api.reclaimprotocol.org";
11
+ /**
12
+ * Minimal read-only client for the public provider-info endpoints on the SDK
13
+ * backend (`reclaim-sdk-backend`). Two distinct reads, on purpose:
14
+ *
15
+ * - `getProvider` (`GET /providers/:id`) — the full provider row merged
16
+ * with its version's WHOLE providerConfig, `customInjection` included.
17
+ * - `getProviderConfigs` (`GET /providers/:id/configs`) — the trimmed
18
+ * recipe the attestor actually consumes at verification time: a fixed
19
+ * field projection that explicitly OMITS `customInjection` and adds a
20
+ * computed `isScriptRequestingClaim` flag instead.
21
+ *
22
+ * Errors are thrown as `@hapi/boom` so `remapErrorAsResponse` renders them
23
+ * the same way as every other tool in this package.
24
+ */
25
+ export declare class ReclaimSdkClient {
26
+ #private;
27
+ readonly baseUrl: string;
28
+ constructor(opts?: {
29
+ baseUrl?: string;
30
+ fetch?: typeof fetch;
31
+ });
32
+ /** Full provider + providerConfig (includes `customInjection`). Latest
33
+ * version when `versionNumber` is omitted. */
34
+ getProvider(providerId: string, versionNumber?: string): Promise<unknown>;
35
+ /** Trimmed runtime recipe (no `customInjection`; adds
36
+ * `isScriptRequestingClaim`). Latest version when `versionNumber` is
37
+ * omitted. */
38
+ getProviderConfigs(providerId: string, versionNumber?: string): Promise<unknown>;
39
+ }
@@ -0,0 +1,85 @@
1
+ import { Boom } from '@hapi/boom';
2
+ import { NAME, VERSION } from "../pkg.js";
3
+ /**
4
+ * Default origin of the PUBLIC production read API that the Reclaim
5
+ * verification SDK/attestor itself calls at runtime to fetch a provider's
6
+ * recipe — a DIFFERENT service from the old-devtools dashboard backend
7
+ * (`ReclaimOldClient`, `devapi.reclaimprotocol.org`). Both endpoints below are
8
+ * unauthenticated reads (no `authenticateToken` middleware on this backend's
9
+ * routes), so this client needs no identity. Override with
10
+ * `RECLAIM_SDK_API_URL`.
11
+ */
12
+ export const DEFAULT_SDK_API_URL = 'https://api.reclaimprotocol.org';
13
+ /**
14
+ * Minimal read-only client for the public provider-info endpoints on the SDK
15
+ * backend (`reclaim-sdk-backend`). Two distinct reads, on purpose:
16
+ *
17
+ * - `getProvider` (`GET /providers/:id`) — the full provider row merged
18
+ * with its version's WHOLE providerConfig, `customInjection` included.
19
+ * - `getProviderConfigs` (`GET /providers/:id/configs`) — the trimmed
20
+ * recipe the attestor actually consumes at verification time: a fixed
21
+ * field projection that explicitly OMITS `customInjection` and adds a
22
+ * computed `isScriptRequestingClaim` flag instead.
23
+ *
24
+ * Errors are thrown as `@hapi/boom` so `remapErrorAsResponse` renders them
25
+ * the same way as every other tool in this package.
26
+ */
27
+ export class ReclaimSdkClient {
28
+ baseUrl;
29
+ #fetch;
30
+ constructor(opts = {}) {
31
+ this.baseUrl = (opts.baseUrl || DEFAULT_SDK_API_URL).replace(/\/+$/, '');
32
+ this.#fetch = opts.fetch ?? fetch;
33
+ }
34
+ /** Full provider + providerConfig (includes `customInjection`). Latest
35
+ * version when `versionNumber` is omitted. */
36
+ getProvider(providerId, versionNumber) {
37
+ return this.#get(`/providers/${encodeURIComponent(providerId)}`, versionNumber);
38
+ }
39
+ /** Trimmed runtime recipe (no `customInjection`; adds
40
+ * `isScriptRequestingClaim`). Latest version when `versionNumber` is
41
+ * omitted. */
42
+ getProviderConfigs(providerId, versionNumber) {
43
+ return this.#get(`/providers/${encodeURIComponent(providerId)}/configs`, versionNumber);
44
+ }
45
+ async #get(path, versionNumber) {
46
+ let url = `${this.baseUrl}/api${path}`;
47
+ if (versionNumber) {
48
+ url += `?versionNumber=${encodeURIComponent(versionNumber)}`;
49
+ }
50
+ const res = await this.#fetch(url, {
51
+ method: 'GET',
52
+ headers: {
53
+ Accept: 'application/json',
54
+ 'User-Agent': `${NAME}/${VERSION}`,
55
+ },
56
+ });
57
+ let json;
58
+ try {
59
+ json = await res.json();
60
+ }
61
+ catch {
62
+ json = undefined;
63
+ }
64
+ const failed = !res.ok ||
65
+ (json !== undefined &&
66
+ typeof json === 'object' &&
67
+ json.isSuccess === false);
68
+ if (failed) {
69
+ const message = (json && typeof json === 'object' &&
70
+ typeof json.message === 'string'
71
+ ? json.message
72
+ : undefined) || res.statusText || 'SDK backend request failed';
73
+ throw new Boom(message, {
74
+ statusCode: res.ok ? 502 : res.status,
75
+ data: {
76
+ status: res.status,
77
+ title: message,
78
+ detail: message,
79
+ body: json,
80
+ },
81
+ });
82
+ }
83
+ return json;
84
+ }
85
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ReclaimProvider } from '../provider/schema.ts';
2
+ import type { AllowedJsRequest } from '../provider/to-version.ts';
2
3
  import type { RegisterProviderBody, UpdateProviderConfigBody } from './client.ts';
3
4
  /**
4
5
  * Translate a drafted `ReclaimProvider` (from `propose_provider`) into the
@@ -22,7 +23,74 @@ export interface OldProviderOpts {
22
23
  /** Defaults to `true` when omitted. Register only (Provider-level flag). */
23
24
  useProxy?: boolean;
24
25
  providerType?: 'PRIVATE' | 'PUBLIC';
26
+ /** Page-injection script — old-devtools' name for the builder's
27
+ * `webSettings.jsUserScripts`. Top-level on register, nested under
28
+ * `providerConfig` on config (add-version). The backend rejects a few
29
+ * deprecated injection APIs (e.g. `window.payloadData`) with a 400. */
30
+ customInjection?: string;
31
+ /** Accepted at register too (unlike the four fields below, which the
32
+ * backend only accepts on config/add-version). */
33
+ userAgent?: {
34
+ ios?: string;
35
+ android?: string;
36
+ };
37
+ /** Accepted at register too. */
38
+ pageTitle?: string;
39
+ /** Config (add-version) ONLY — the register endpoint hardcodes this to
40
+ * `[]` and ignores whatever is sent. Already old-backend-shaped: build
41
+ * fresh entries with {@link injectedRequestDataFrom}; a carried-forward
42
+ * value (read back from an existing version) passes through unchanged. */
43
+ allowedInjectedRequestData?: OldInjectedRequestData[];
44
+ /** Accepted at register too (verified from the register controller's
45
+ * destructure + providerConfig write — same as `userAgent`/`pageTitle`). */
46
+ stepsToFollow?: string;
47
+ /** Config (add-version) ONLY — the register endpoint never reads this
48
+ * field from the request body. */
49
+ useIncognitoWebview?: boolean;
50
+ /** Config (add-version) ONLY — the register endpoint never reads this
51
+ * field from the request body. Free-form JSON. */
52
+ extensionConfig?: unknown;
25
53
  }
54
+ interface OldResponseMatch {
55
+ value: string;
56
+ type: string;
57
+ isOptional: boolean;
58
+ order: number;
59
+ }
60
+ interface OldResponseRedaction {
61
+ xPath?: string;
62
+ jsonPath?: string;
63
+ regex?: string;
64
+ /** OPRF mode (`oprf` / `oprf-raw` / `oprf-mpc`) — the backend stores it as
65
+ * a free-form string and the attestor interprets it at verification. */
66
+ hash?: string;
67
+ order: number;
68
+ }
69
+ interface OldRequestData {
70
+ url: string;
71
+ urlType: 'CONSTANT' | 'TEMPLATE';
72
+ method: 'GET' | 'POST';
73
+ responseMatches: OldResponseMatch[];
74
+ responseRedactions: OldResponseRedaction[];
75
+ credentials: 'include';
76
+ bodySniff?: {
77
+ enabled: boolean;
78
+ template: string;
79
+ };
80
+ }
81
+ /** One `allowedInjectedRequestData` entry — an `OldRequestData` plus the
82
+ * pagination/optionality flags the old backend stores alongside it. */
83
+ export interface OldInjectedRequestData extends OldRequestData {
84
+ required?: boolean;
85
+ multiple?: boolean;
86
+ }
87
+ /** Translate fresh `allowedInjectedRequestData` drafts (the author's input
88
+ * shape, `AllowedJsRequest[]` — same shape the builder translator uses for
89
+ * its own `allowedJsRequests`) into the old backend's shape. A
90
+ * carried-forward value read back from an existing version is ALREADY in
91
+ * this shape and should be passed straight through instead — don't run it
92
+ * through this a second time. */
93
+ export declare function injectedRequestDataFrom(requests: AllowedJsRequest[]): OldInjectedRequestData[];
26
94
  /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
27
95
  * drafted request becomes a `requestData` entry; provider-level fields (name)
28
96
  * come from the first draft. */
@@ -50,3 +118,4 @@ export declare function extractVersions(resp: unknown): SemverTriplet[];
50
118
  * rather than collide with the guaranteed 1.0.0).
51
119
  */
52
120
  export declare function bumpPatchString(versions: SemverTriplet[]): string;
121
+ export {};
@@ -4,7 +4,10 @@
4
4
  * is a Provider-level flag (register only). */
5
5
  const DEFAULT_GEO_LOCATION = '{{DYNAMIC_GEO}}';
6
6
  const DEFAULT_USE_PROXY = true;
7
- /** Build the single `requestData` entry shared by register + update. */
7
+ /** Build the single `requestData` entry shared by register + update. Takes
8
+ * the loose `DraftRequest` shape (not the full `ReclaimProvider`) so it can
9
+ * build BOTH the top-level `requestData` and `allowedInjectedRequestData`
10
+ * entries — a `ReclaimProvider` satisfies it structurally either way. */
8
11
  function requestDataFromProvider(provider) {
9
12
  const method = String(provider.method).toUpperCase();
10
13
  if (method !== 'GET' && method !== 'POST') {
@@ -22,7 +25,8 @@ function requestDataFromProvider(provider) {
22
25
  'consumer-supplied params to the {{context_<name>}} form ' +
23
26
  'for this backend.');
24
27
  }
25
- const responseMatches = provider.responseMatches.map((m, i) => ({
28
+ const responseMatches = (provider.responseMatches ?? [])
29
+ .map((m, i) => ({
26
30
  value: m.value,
27
31
  type: m.type ?? 'contains',
28
32
  isOptional: false,
@@ -78,6 +82,19 @@ function requestDataFromProvider(provider) {
78
82
  function requestDataFrom(providers) {
79
83
  return providers.map(requestDataFromProvider);
80
84
  }
85
+ /** Translate fresh `allowedInjectedRequestData` drafts (the author's input
86
+ * shape, `AllowedJsRequest[]` — same shape the builder translator uses for
87
+ * its own `allowedJsRequests`) into the old backend's shape. A
88
+ * carried-forward value read back from an existing version is ALREADY in
89
+ * this shape and should be passed straight through instead — don't run it
90
+ * through this a second time. */
91
+ export function injectedRequestDataFrom(requests) {
92
+ return requests.map((req) => ({
93
+ ...requestDataFromProvider(req),
94
+ required: req.required ?? true,
95
+ multiple: req.multiple ?? true,
96
+ }));
97
+ }
81
98
  /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
82
99
  * drafted request becomes a `requestData` entry; provider-level fields (name)
83
100
  * come from the first draft. */
@@ -94,6 +111,21 @@ export function providerToRegisterBody(providers, opts) {
94
111
  if (opts.description !== undefined) {
95
112
  body.description = opts.description;
96
113
  }
114
+ if (opts.customInjection !== undefined) {
115
+ body.customInjection = opts.customInjection;
116
+ }
117
+ // Register also accepts these three (unlike allowedInjectedRequestData /
118
+ // useIncognitoWebview / extensionConfig, which the register controller
119
+ // never reads from the body at all).
120
+ if (opts.userAgent !== undefined) {
121
+ body.userAgent = opts.userAgent;
122
+ }
123
+ if (opts.pageTitle !== undefined) {
124
+ body.pageTitle = opts.pageTitle;
125
+ }
126
+ if (opts.stepsToFollow !== undefined) {
127
+ body.stepsToFollow = opts.stepsToFollow;
128
+ }
97
129
  return body;
98
130
  }
99
131
  /** Body for `POST /api/providers/:providerId/config` — adds a new version to
@@ -108,6 +140,27 @@ export function providerToConfigBody(providers, opts, version, versionInfo) {
108
140
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
109
141
  requestData: requestDataFrom(providers),
110
142
  };
143
+ if (opts.customInjection !== undefined) {
144
+ providerConfig.customInjection = opts.customInjection;
145
+ }
146
+ if (opts.userAgent !== undefined) {
147
+ providerConfig.userAgent = opts.userAgent;
148
+ }
149
+ if (opts.pageTitle !== undefined) {
150
+ providerConfig.pageTitle = opts.pageTitle;
151
+ }
152
+ if (opts.allowedInjectedRequestData !== undefined) {
153
+ providerConfig.allowedInjectedRequestData = opts.allowedInjectedRequestData;
154
+ }
155
+ if (opts.stepsToFollow !== undefined) {
156
+ providerConfig.stepsToFollow = opts.stepsToFollow;
157
+ }
158
+ if (opts.useIncognitoWebview !== undefined) {
159
+ providerConfig.useIncognitoWebview = opts.useIncognitoWebview;
160
+ }
161
+ if (opts.extensionConfig !== undefined) {
162
+ providerConfig.extensionConfig = opts.extensionConfig;
163
+ }
111
164
  return { providerConfig, version, versionInfo };
112
165
  }
113
166
  /**
@@ -1,5 +1,5 @@
1
1
  import { agentDisabled } from "../../consts.js";
2
- import { defineTool, } from "../../mcp/server.js";
2
+ import { defineTool } from "../../mcp/server.js";
3
3
  import { persistIdentity, toEthUid } from "../identity.js";
4
4
  import { captureFirebaseTokenViaCdp } from "../login-cdp.js";
5
5
  /**
@@ -69,7 +69,7 @@ export function authenticateTool(client) {
69
69
  },
70
70
  },
71
71
  },
72
- }, async ({ token, ethAddress, loginUrl, timeoutMs, useBrowser }, ctx) => {
72
+ }, async ({ token, ethAddress, loginUrl, timeoutMs, useBrowser }) => {
73
73
  if (token && ethAddress) {
74
74
  throw new Error('Provide only ONE of `token` or `ethAddress`, not both.');
75
75
  }
@@ -113,11 +113,7 @@ export function authenticateTool(client) {
113
113
  throw new Error('Browser login is unavailable (RECLAIM_AGENT_DISABLED=1). Pass a ' +
114
114
  '`token` (Firebase ID token) or `ethAddress` instead.');
115
115
  }
116
- const result = await captureFirebaseTokenViaCdp({
117
- loginUrl,
118
- timeoutMs,
119
- onProgress: ctx.reportProgress,
120
- });
116
+ const result = await captureFirebaseTokenViaCdp({ loginUrl, timeoutMs });
121
117
  const identity = { kind: 'bearer', token: result.token };
122
118
  await validate(identity);
123
119
  persistIdentity(identity);
@@ -1,9 +1,14 @@
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
  /**
4
5
  * The backend-facing tools for OLD-devtools mode. Mirrors
5
6
  * `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
6
7
  * list for `registerToolList`. These replace the builder credential + publish
7
8
  * tools and the (disabled) generated provider tools.
9
+ *
10
+ * `sdkClient` is a SEPARATE, unauthenticated client for the public
11
+ * production SDK backend (`api.reclaimprotocol.org`) — a different host from
12
+ * `client`'s old-devtools dashboard backend (`devapi.reclaimprotocol.org`).
8
13
  */
9
- export declare function buildOldBackendTools(client: ReclaimOldClient): RegisteredTool[];
14
+ export declare function buildOldBackendTools(client: ReclaimOldClient, sdkClient: ReclaimSdkClient): RegisteredTool[];
@@ -1,18 +1,28 @@
1
1
  import { authenticateTool } from "./authenticate.js";
2
+ import { getProviderInfoTool } from "./get-provider-info.js";
3
+ import { getProviderVersionConfigTool } from "./get-version-config.js";
2
4
  import { getMeProvidersTool } from "./me-providers.js";
3
5
  import { createVersionFromCaptureTool } from "./publish.js";
4
6
  import { sessionAnalyticsLogsTool } from "./session-analytics-logs.js";
7
+ import { updateProviderMetadataTool } from "./update-provider-metadata.js";
5
8
  /**
6
9
  * The backend-facing tools for OLD-devtools mode. Mirrors
7
10
  * `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
8
11
  * list for `registerToolList`. These replace the builder credential + publish
9
12
  * tools and the (disabled) generated provider tools.
13
+ *
14
+ * `sdkClient` is a SEPARATE, unauthenticated client for the public
15
+ * production SDK backend (`api.reclaimprotocol.org`) — a different host from
16
+ * `client`'s old-devtools dashboard backend (`devapi.reclaimprotocol.org`).
10
17
  */
11
- export function buildOldBackendTools(client) {
18
+ export function buildOldBackendTools(client, sdkClient) {
12
19
  return [
13
20
  authenticateTool(client),
14
21
  createVersionFromCaptureTool(client),
15
22
  getMeProvidersTool(client),
23
+ getProviderVersionConfigTool(client),
24
+ getProviderInfoTool(sdkClient),
25
+ updateProviderMetadataTool(client),
16
26
  sessionAnalyticsLogsTool(client),
17
27
  ];
18
28
  }
@@ -0,0 +1,16 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ import type { ReclaimSdkClient } from '../sdk-client.ts';
3
+ /**
4
+ * Read provider info from the PUBLIC production SDK backend
5
+ * (`api.reclaimprotocol.org`) — the service the verification SDK/attestor
6
+ * itself calls at runtime, NOT the old-devtools dashboard backend
7
+ * (`get_provider_version_config`, `devapi.reclaimprotocol.org`, requires
8
+ * auth). Both underlying reads are unauthenticated.
9
+ *
10
+ * Fetches BOTH shapes in one call, since they're complementary:
11
+ * - `provider`: full provider + providerConfig, `customInjection` included.
12
+ * - `configs`: the trimmed runtime recipe the attestor actually consumes —
13
+ * `customInjection` deliberately omitted, `isScriptRequestingClaim`
14
+ * computed instead.
15
+ */
16
+ export declare function getProviderInfoTool(client: ReclaimSdkClient): RegisteredTool;