@reclaimprotocol/agent 0.1.1 → 0.1.3

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 (74) hide show
  1. package/lib/cdp/launcher.js +4 -3
  2. package/lib/consts.d.ts +68 -0
  3. package/lib/consts.js +134 -0
  4. package/lib/logger.js +2 -1
  5. package/lib/mcp/api/operations.gen.d.ts +11 -7
  6. package/lib/mcp/api/operations.gen.js +356 -199
  7. package/lib/mcp/server.d.ts +0 -6
  8. package/lib/mcp/server.js +0 -30
  9. package/lib/mcp/start.js +7 -7
  10. package/lib/mcp/tools/agent/build.d.ts +5 -9
  11. package/lib/mcp/tools/agent/build.js +5 -9
  12. package/lib/mcp/tools/agent/proof.js +2 -1
  13. package/lib/mcp/tools/agent/publish.js +166 -77
  14. package/lib/mcp/tools/authenticate/decrypt-key.d.ts +10 -23
  15. package/lib/mcp/tools/authenticate/decrypt-key.js +23 -26
  16. package/lib/mcp/tools/authenticate/eth-key.d.ts +0 -7
  17. package/lib/mcp/tools/authenticate/eth-key.js +5 -19
  18. package/lib/mcp/tools/authenticate/issue.js +1 -1
  19. package/lib/mcp/tools/verification/build.d.ts +1 -1
  20. package/lib/mcp/tools/verification/build.js +1 -1
  21. package/lib/mcp/tools/verification/result.d.ts +4 -5
  22. package/lib/mcp/tools/verification/result.js +17 -17
  23. package/lib/old/flag.d.ts +4 -1
  24. package/lib/old/flag.js +4 -3
  25. package/lib/old/identity.d.ts +0 -10
  26. package/lib/old/identity.js +8 -7
  27. package/lib/old/login-cdp.d.ts +0 -3
  28. package/lib/old/login-cdp.js +2 -4
  29. package/lib/old/to-register.d.ts +5 -3
  30. package/lib/old/to-register.js +20 -15
  31. package/lib/old/tools/authenticate.js +2 -1
  32. package/lib/old/tools/publish.js +97 -13
  33. package/lib/paths.js +2 -2
  34. package/lib/proof/attestor.js +2 -1
  35. package/lib/proof/proof-core.js +13 -2
  36. package/lib/provider/injection-doc.d.ts +1 -0
  37. package/lib/provider/injection-doc.js +17 -0
  38. package/lib/provider/match-redaction-warning.d.ts +25 -0
  39. package/lib/provider/match-redaction-warning.js +35 -0
  40. package/lib/provider/schema.d.ts +16 -0
  41. package/lib/provider/schema.js +11 -1
  42. package/lib/provider/to-version.d.ts +59 -7
  43. package/lib/provider/to-version.js +79 -28
  44. package/package.json +1 -1
  45. package/skill/builder-mode.md +8 -2
  46. package/skill/injection-api.md +42 -0
  47. package/skill/old-mode.md +4 -2
  48. package/src/cdp/launcher.ts +5 -4
  49. package/src/consts.ts +164 -0
  50. package/src/logger.ts +2 -1
  51. package/src/mcp/api/operations.gen.ts +358 -203
  52. package/src/mcp/server.ts +0 -46
  53. package/src/mcp/start.ts +13 -7
  54. package/src/mcp/tools/agent/build.ts +5 -9
  55. package/src/mcp/tools/agent/proof.ts +2 -1
  56. package/src/mcp/tools/agent/publish.ts +212 -94
  57. package/src/mcp/tools/authenticate/decrypt-key.ts +24 -29
  58. package/src/mcp/tools/authenticate/eth-key.ts +5 -25
  59. package/src/mcp/tools/authenticate/issue.ts +1 -1
  60. package/src/mcp/tools/verification/build.ts +1 -1
  61. package/src/mcp/tools/verification/result.ts +16 -17
  62. package/src/old/flag.ts +4 -3
  63. package/src/old/identity.ts +8 -7
  64. package/src/old/login-cdp.ts +2 -5
  65. package/src/old/to-register.ts +22 -16
  66. package/src/old/tools/authenticate.ts +2 -1
  67. package/src/old/tools/publish.ts +124 -20
  68. package/src/paths.ts +2 -2
  69. package/src/proof/attestor.ts +2 -1
  70. package/src/proof/proof-core.ts +13 -3
  71. package/src/provider/injection-doc.ts +21 -0
  72. package/src/provider/match-redaction-warning.ts +53 -0
  73. package/src/provider/schema.ts +23 -0
  74. package/src/provider/to-version.ts +137 -31
@@ -3,7 +3,7 @@ import type { RegisteredTool } from '../../server.ts';
3
3
  * Builder-mode verification tools: higher-level orchestrations over the
4
4
  * generated API surface. `get_verification_result` authenticates with the org
5
5
  * token, checks status, pulls the stored payload, and decrypts it locally with
6
- * the optional X25519 key — a flow the generated raw-API tools can't express
6
+ * the optional eth key — a flow the generated raw-API tools can't express
7
7
  * (the decryption key never leaves the consumer). Diagnosis is left to the
8
8
  * agent over the generated tools (`get_verification_session`,
9
9
  * `list_verification_events`, `list_verification_deliveries`), whose
@@ -3,7 +3,7 @@ import { verificationResultTool } from "./result.js";
3
3
  * Builder-mode verification tools: higher-level orchestrations over the
4
4
  * generated API surface. `get_verification_result` authenticates with the org
5
5
  * token, checks status, pulls the stored payload, and decrypts it locally with
6
- * the optional X25519 key — a flow the generated raw-API tools can't express
6
+ * the optional eth key — a flow the generated raw-API tools can't express
7
7
  * (the decryption key never leaves the consumer). Diagnosis is left to the
8
8
  * agent over the generated tools (`get_verification_session`,
9
9
  * `list_verification_events`, `list_verification_deliveries`), whose
@@ -3,10 +3,9 @@ import { type RegisteredTool } from '../../server.ts';
3
3
  * `get_verification_result` — check a session's status and, once a result has
4
4
  * been submitted, pull the stored payload(s) and verify locally. Auth is the
5
5
  * org token; delivery is org-scoped. Each stored payload is either the
6
- * plaintext signed JWS (when the org has no encryption credential) or a compact
7
- * JWE (RFC 7516, ECDH-ES+A256GCM over X25519) that we decrypt here with the
8
- * optional X25519 key (the `reclaim-decrypt.pem` PEM in the project). The
9
- * Builder only ever relays the payload — decryption, when needed, happens
10
- * here.
6
+ * plaintext signed JWS (when the org's encryption is off) or an ECIES
7
+ * ciphertext that we decrypt here with the optional eth key
8
+ * (`RECLAIM_DECRYPT_KEY` or the `reclaim-decrypt.key` hex file). The Builder
9
+ * only ever relays the payload — decryption, when needed, happens here.
11
10
  */
12
11
  export declare function verificationResultTool(): RegisteredTool;
@@ -1,5 +1,6 @@
1
- import { createReclaim, decryptCallback, DEFAULT_BASE_URL, verifyResultFull, } from '@reclaimprotocol/client';
1
+ import { createReclaim, decryptCallback, verifyResultFull, } from '@reclaimprotocol/client';
2
2
  import assert from 'node:assert';
3
+ import { apiUrl, orgSecret } from "../../../consts.js";
3
4
  import { defineTool } from "../../server.js";
4
5
  import { resolveDecryptionKey } from "../authenticate/decrypt-key.js";
5
6
  /** Statuses that can carry a submitted (terminal) result. `expired`/`pending`
@@ -11,7 +12,7 @@ const TERMINAL = new Set(['success', 'rejected', 'error']);
11
12
  * `RECLAIM_ORG_SECRET`.
12
13
  */
13
14
  function resolveOrgSecret() {
14
- const secret = process.env.RECLAIM_ORG_SECRET?.trim();
15
+ const secret = orgSecret()?.trim();
15
16
  assert(secret, new Error('RECLAIM_ORG_SECRET is not set. Verification reads authenticate with ' +
16
17
  'the org secret (rorg_…) — issue one from the dashboard ' +
17
18
  '(POST /orgs/{orgId}/token) and export it as RECLAIM_ORG_SECRET.'));
@@ -21,11 +22,10 @@ function resolveOrgSecret() {
21
22
  * `get_verification_result` — check a session's status and, once a result has
22
23
  * been submitted, pull the stored payload(s) and verify locally. Auth is the
23
24
  * org token; delivery is org-scoped. Each stored payload is either the
24
- * plaintext signed JWS (when the org has no encryption credential) or a compact
25
- * JWE (RFC 7516, ECDH-ES+A256GCM over X25519) that we decrypt here with the
26
- * optional X25519 key (the `reclaim-decrypt.pem` PEM in the project). The
27
- * Builder only ever relays the payload — decryption, when needed, happens
28
- * here.
25
+ * plaintext signed JWS (when the org's encryption is off) or an ECIES
26
+ * ciphertext that we decrypt here with the optional eth key
27
+ * (`RECLAIM_DECRYPT_KEY` or the `reclaim-decrypt.key` hex file). The Builder
28
+ * only ever relays the payload — decryption, when needed, happens here.
29
29
  */
30
30
  export function verificationResultTool() {
31
31
  return defineTool({
@@ -34,9 +34,9 @@ export function verificationResultTool() {
34
34
  'Authenticates with the org secret (RECLAIM_ORG_SECRET). Fetches the ' +
35
35
  'session status; if it is terminal and a result has been submitted, ' +
36
36
  'pulls the stored result(s) and verifies them locally. Encrypted ' +
37
- 'deliveries (compact JWE over X25519) are decrypted with your ' +
38
- 'optional decryption key (the reclaim-decrypt.pem PEM in the ' +
39
- 'project); plaintext deliveries are read directly. Returns ' +
37
+ 'deliveries (ECIES over secp256k1) are decrypted with your optional ' +
38
+ 'eth key (RECLAIM_DECRYPT_KEY or reclaim-decrypt.key); plaintext ' +
39
+ 'deliveries are read directly. Returns ' +
40
40
  '`ready: false` while the session is still pending or no result has ' +
41
41
  'been delivered yet. Use list_verification_events for the full ' +
42
42
  'lifecycle log.',
@@ -51,12 +51,12 @@ export function verificationResultTool() {
51
51
  },
52
52
  },
53
53
  }, async (args) => {
54
- const orgSecret = resolveOrgSecret();
55
- // Optional X25519 decryption key — absent ⇒ plaintext-only.
54
+ const secret = resolveOrgSecret();
55
+ // Optional eth decryption key — absent ⇒ plaintext-only.
56
56
  const decryptionKey = await resolveDecryptionKey();
57
57
  const reclaim = createReclaim({
58
- baseUrl: process.env.RECLAIM_API_URL || DEFAULT_BASE_URL,
59
- orgSecret,
58
+ baseUrl: apiUrl(),
59
+ orgSecret: secret,
60
60
  });
61
61
  const sessionRes = await reclaim.api('GetVerificationSession', {
62
62
  params: { sessionId: args.sessionId },
@@ -110,9 +110,9 @@ export function verificationResultTool() {
110
110
  status,
111
111
  ready: false,
112
112
  note: 'A result was stored, but no payload could be read — it is ' +
113
- 'corrupt, or encrypted (JWE) to a key this consumer does not ' +
114
- 'hold. Place an X25519 PEM (reclaim-decrypt.pem) in the ' +
115
- 'project matching the org credential.',
113
+ 'corrupt, or encrypted (ECIES) to a key this consumer does not ' +
114
+ 'hold. Set RECLAIM_DECRYPT_KEY (or reclaim-decrypt.key) to the ' +
115
+ 'eth private key matching the org keypair.',
116
116
  };
117
117
  }
118
118
  return {
package/lib/old/flag.d.ts CHANGED
@@ -7,5 +7,8 @@
7
7
  * EXPLICITLY by setting `USE_OLD_DEVTOOLS=false`. Anything else — unset,
8
8
  * empty, `true`, `1`, a typo — keeps old mode on, so a forgotten env var
9
9
  * can never silently route a developer at the half-built builder backend.
10
+ *
11
+ * The actual env read lives in `../consts.ts` (the single env-access module);
12
+ * this widely-imported path re-exports it for back-compat.
10
13
  */
11
- export declare function useOldDevtools(): boolean;
14
+ export { useOldDevtools } from '../consts.ts';
package/lib/old/flag.js CHANGED
@@ -7,7 +7,8 @@
7
7
  * EXPLICITLY by setting `USE_OLD_DEVTOOLS=false`. Anything else — unset,
8
8
  * empty, `true`, `1`, a typo — keeps old mode on, so a forgotten env var
9
9
  * can never silently route a developer at the half-built builder backend.
10
+ *
11
+ * The actual env read lives in `../consts.ts` (the single env-access module);
12
+ * this widely-imported path re-exports it for back-compat.
10
13
  */
11
- export function useOldDevtools() {
12
- return (process.env.USE_OLD_DEVTOOLS ?? '').trim().toLowerCase() !== 'false';
13
- }
14
+ export { useOldDevtools } from "../consts.js";
@@ -2,16 +2,6 @@ import type { OldIdentity } from './client.ts';
2
2
  /** Normalize an eth uid input to `eth:<lowercase-address>`. Accepts a bare
3
3
  * `0x…` address or an already-prefixed `eth:0x…`. */
4
4
  export declare function toEthUid(addressOrUid: string): OldIdentity;
5
- /**
6
- * Resolve a caller identity from the environment, in priority order:
7
- * 1. `RECLAIM_OLD_API_TOKEN` — a dashboard Firebase bearer token.
8
- * 2. `RECLAIM_OLD_ETH_UID` — an `eth:<addr>` (or bare `0x…`) uid.
9
- * 3. `RECLAIM_PRIVATE_KEY` — derive the eth uid from the local app key.
10
- * Returns undefined when none is set.
11
- */
12
- export declare function resolveIdentityFromEnv(): OldIdentity | undefined;
13
- /** Read the identity persisted by a prior `reclaim_authenticate` call. */
14
- export declare function loadPersistedIdentity(): OldIdentity | undefined;
15
5
  /** Persist the identity so it survives an MCP-server restart. */
16
6
  export declare function persistIdentity(identity: OldIdentity): void;
17
7
  /**
@@ -1,5 +1,6 @@
1
1
  import { deriveFromPrivateKey } from '@reclaimprotocol/client';
2
2
  import { ConfigStore } from "../config/store.js";
3
+ import { oldApiToken, oldEthUid, privateKey } from "../consts.js";
3
4
  /** Normalize an eth uid input to `eth:<lowercase-address>`. Accepts a bare
4
5
  * `0x…` address or an already-prefixed `eth:0x…`. */
5
6
  export function toEthUid(addressOrUid) {
@@ -14,23 +15,23 @@ export function toEthUid(addressOrUid) {
14
15
  * 3. `RECLAIM_PRIVATE_KEY` — derive the eth uid from the local app key.
15
16
  * Returns undefined when none is set.
16
17
  */
17
- export function resolveIdentityFromEnv() {
18
- const token = process.env.RECLAIM_OLD_API_TOKEN?.trim();
18
+ function resolveIdentityFromEnv() {
19
+ const token = oldApiToken()?.trim();
19
20
  if (token) {
20
21
  return { kind: 'bearer', token };
21
22
  }
22
- const ethUid = process.env.RECLAIM_OLD_ETH_UID?.trim();
23
+ const ethUid = oldEthUid()?.trim();
23
24
  if (ethUid) {
24
25
  return toEthUid(ethUid);
25
26
  }
26
- const privateKey = process.env.RECLAIM_PRIVATE_KEY?.trim();
27
- if (privateKey) {
28
- return toEthUid(deriveFromPrivateKey(privateKey).address);
27
+ const key = privateKey()?.trim();
28
+ if (key) {
29
+ return toEthUid(deriveFromPrivateKey(key).address);
29
30
  }
30
31
  return undefined;
31
32
  }
32
33
  /** Read the identity persisted by a prior `reclaim_authenticate` call. */
33
- export function loadPersistedIdentity() {
34
+ function loadPersistedIdentity() {
34
35
  return new ConfigStore().read()?.old_devtools;
35
36
  }
36
37
  /** Persist the identity so it survives an MCP-server restart. */
@@ -1,6 +1,3 @@
1
- /** The devtools dashboard login page. Override per-call or via
2
- * `RECLAIM_OLD_LOGIN_URL`. */
3
- export declare const DEFAULT_LOGIN_URL = "https://dev.reclaimprotocol.org";
4
1
  export interface CdpLoginResult {
5
2
  token: string;
6
3
  /** True when we launched (and then closed) a dedicated Chrome just for
@@ -1,9 +1,7 @@
1
1
  import { connectCdp } from "../cdp/client.js";
2
2
  import { defaultProfileDir, launchChrome, waitForCdpReady, } from "../cdp/launcher.js";
3
3
  import { ConfigStore } from "../config/store.js";
4
- /** The devtools dashboard login page. Override per-call or via
5
- * `RECLAIM_OLD_LOGIN_URL`. */
6
- export const DEFAULT_LOGIN_URL = 'https://dev.reclaimprotocol.org';
4
+ import { DEFAULT_LOGIN_URL, oldLoginUrl } from "../consts.js";
7
5
  /**
8
6
  * Read the Firebase ID token the dashboard persisted after login.
9
7
  *
@@ -64,7 +62,7 @@ const EXTRACT_TOKEN_JS = `(async () => {
64
62
  * disrupt it.
65
63
  */
66
64
  export async function captureFirebaseTokenViaCdp(opts) {
67
- const loginUrl = opts.loginUrl || process.env.RECLAIM_OLD_LOGIN_URL || DEFAULT_LOGIN_URL;
65
+ const loginUrl = opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL;
68
66
  const timeoutMs = opts.timeoutMs ?? 180_000;
69
67
  const pollMs = opts.pollMs ?? 1000;
70
68
  const cfg = new ConfigStore().read()?.browser_agent;
@@ -23,12 +23,14 @@ export interface OldProviderOpts {
23
23
  useProxy?: boolean;
24
24
  providerType?: 'PRIVATE' | 'PUBLIC';
25
25
  }
26
- /** Body for `POST /api/providers/register` — new provider + v1.0.0. */
27
- export declare function providerToRegisterBody(provider: ReclaimProvider, opts: OldProviderOpts): RegisterProviderBody;
26
+ /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
27
+ * drafted request becomes a `requestData` entry; provider-level fields (name)
28
+ * come from the first draft. */
29
+ export declare function providerToRegisterBody(providers: ReclaimProvider[], opts: OldProviderOpts): RegisterProviderBody;
28
30
  /** Body for `POST /api/providers/:providerId/config` — adds a new version to
29
31
  * an EXISTING provider. `version` must not already exist; `versionInfo` is
30
32
  * required and must be ≥10 chars (enforced by the backend). */
31
- export declare function providerToConfigBody(provider: ReclaimProvider, opts: OldProviderOpts, version: string, versionInfo: string): UpdateProviderConfigBody;
33
+ export declare function providerToConfigBody(providers: ReclaimProvider[], opts: OldProviderOpts, version: string, versionInfo: string): UpdateProviderConfigBody;
32
34
  /** Minimal version triplet as the old backend stores it. */
33
35
  export interface SemverTriplet {
34
36
  major: number;
@@ -29,15 +29,12 @@ function requestDataFromProvider(provider) {
29
29
  order: i,
30
30
  }));
31
31
  // Old devtools wants `responseMatches` and `responseRedactions` as PARALLEL
32
- // arrays. A drafted provider may instead carry each redaction nested in its
33
- // match's `extract` (the builder shape). Prefer the explicit parallel array;
34
- // fall back to the nested `extract`s so redactions are never dropped.
35
- const nestedRedactions = provider.responseMatches
36
- .map((m) => m.extract)
37
- .filter((e) => Boolean(e));
38
- const sourceRedactions = provider.responseRedactions?.length
39
- ? provider.responseRedactions
40
- : nestedRedactions;
32
+ // arrays — same shape the builder uses, so pass them through directly. We do
33
+ // NOT enforce any match/redaction count relationship here (and never pad —
34
+ // empty placeholder matches break proof generation): the author keeps full
35
+ // control. The publish tool surfaces a non-blocking warning when matches <
36
+ // redactions (see `matchRedactionWarnings`), and the dev decides.
37
+ const sourceRedactions = provider.responseRedactions ?? [];
41
38
  const responseRedactions = sourceRedactions.map((r, i) => {
42
39
  const out = { order: i };
43
40
  if (r.xPath !== undefined) {
@@ -75,16 +72,24 @@ function requestDataFromProvider(provider) {
75
72
  // it ourselves would risk drifting from the server's own extraction.
76
73
  return requestData;
77
74
  }
78
- /** Body for `POST /api/providers/register` — new provider + v1.0.0. */
79
- export function providerToRegisterBody(provider, opts) {
75
+ /** One `requestData` entry per drafted provider. A multi-request provider
76
+ * becomes parallel entries that the old backend runs together at verification
77
+ * (the same way the builder version carries multiple `requests`). */
78
+ function requestDataFrom(providers) {
79
+ return providers.map(requestDataFromProvider);
80
+ }
81
+ /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
82
+ * drafted request becomes a `requestData` entry; provider-level fields (name)
83
+ * come from the first draft. */
84
+ export function providerToRegisterBody(providers, opts) {
80
85
  const body = {
81
- name: provider.name,
86
+ name: providers[0].name,
82
87
  loginUrl: opts.initialUrl,
83
88
  providerType: opts.providerType ?? 'PRIVATE',
84
89
  // Web-settings defaults mirror the builder openapi defaults.
85
90
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
86
91
  useProxy: opts.useProxy ?? DEFAULT_USE_PROXY,
87
- requestData: [requestDataFromProvider(provider)],
92
+ requestData: requestDataFrom(providers),
88
93
  };
89
94
  if (opts.description !== undefined) {
90
95
  body.description = opts.description;
@@ -94,14 +99,14 @@ export function providerToRegisterBody(provider, opts) {
94
99
  /** Body for `POST /api/providers/:providerId/config` — adds a new version to
95
100
  * an EXISTING provider. `version` must not already exist; `versionInfo` is
96
101
  * required and must be ≥10 chars (enforced by the backend). */
97
- export function providerToConfigBody(provider, opts, version, versionInfo) {
102
+ export function providerToConfigBody(providers, opts, version, versionInfo) {
98
103
  // `useProxy` is a Provider-level flag on the old backend, not part of a
99
104
  // version's providerConfig — so the config (new-version) path only carries
100
105
  // `geoLocation`. It defaults the same way as register.
101
106
  const providerConfig = {
102
107
  loginUrl: opts.initialUrl,
103
108
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
104
- requestData: [requestDataFromProvider(provider)],
109
+ requestData: requestDataFrom(providers),
105
110
  };
106
111
  return { providerConfig, version, versionInfo };
107
112
  }
@@ -1,3 +1,4 @@
1
+ import { agentDisabled } from "../../consts.js";
1
2
  import { defineTool } from "../../mcp/server.js";
2
3
  import { persistIdentity, toEthUid } from "../identity.js";
3
4
  import { captureFirebaseTokenViaCdp } from "../login-cdp.js";
@@ -108,7 +109,7 @@ export function authenticateTool(client) {
108
109
  }
109
110
  }
110
111
  // CDP browser login (the default headline flow).
111
- if (process.env.RECLAIM_AGENT_DISABLED === '1') {
112
+ if (agentDisabled()) {
112
113
  throw new Error('Browser login is unavailable (RECLAIM_AGENT_DISABLED=1). Pass a ' +
113
114
  '`token` (Firebase ID token) or `ethAddress` instead.');
114
115
  }
@@ -1,6 +1,32 @@
1
+ import { Boom, isBoom } from '@hapi/boom';
1
2
  import { defineTool } from "../../mcp/server.js";
3
+ import { countsFor, matchRedactionWarnings, } from "../../provider/match-redaction-warning.js";
2
4
  import { bumpPatchString, extractVersions, providerToConfigBody, providerToRegisterBody, } from "../to-register.js";
3
5
  const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture';
6
+ /**
7
+ * The old backend lets you add a version only to a provider YOU own. A
8
+ * `providerId` for a public provider (or anyone else's) comes back as
9
+ * 401/403/404 — the dev's likely intent is to publish their OWN version of that
10
+ * site, which on this backend means registering a separate copy. Rethrow those
11
+ * failures with that actionable suggestion; leave other errors (e.g. a 400
12
+ * version collision) untouched so their real detail surfaces.
13
+ */
14
+ function rethrowOwnershipError(err, providerId) {
15
+ const status = isBoom(err) ? err.output.statusCode : undefined;
16
+ if (status !== 401 && status !== 403 && status !== 404) {
17
+ throw err;
18
+ }
19
+ const base = isBoom(err) ? err.message : String(err);
20
+ throw new Boom(`${base} — could not add a version to provider ${providerId}. You can ` +
21
+ 'only publish new versions on providers you own. If this is a public ' +
22
+ "provider (or one you don't own), re-run " +
23
+ 'create_provider_version_from_capture WITHOUT `providerId` to register ' +
24
+ "your own copy. If it IS yours, confirm you're authenticated " +
25
+ '(reclaim_authenticate) as the account that created it.', {
26
+ statusCode: status,
27
+ data: isBoom(err) ? err.data : { status, detail: base },
28
+ });
29
+ }
4
30
  /**
5
31
  * Publish a drafted provider to the OLD devtools backend. Keeps the builder
6
32
  * tool NAME (`create_provider_version_from_capture`) so the skill flow is
@@ -17,21 +43,31 @@ const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture';
17
43
  export function createVersionFromCaptureTool(client) {
18
44
  return defineTool({
19
45
  name: 'create_provider_version_from_capture',
20
- description: 'Publish a drafted ReclaimProvider to the OLD devtools backend. ' +
46
+ description: 'Publish drafted ReclaimProvider(s) to the OLD devtools backend. ' +
21
47
  'Pass the `provider` object from propose_provider VERBATIM and the ' +
22
48
  '`initialUrl` the user landed on (the login/landing page, NOT the ' +
23
49
  'API url). \n\n' +
50
+ 'For a MULTI-REQUEST provider, pass `providers: [...]` (each entry a ' +
51
+ 'proven draft from propose_provider) — every entry becomes one ' +
52
+ '`requestData` entry on the SAME version, and all are verified ' +
53
+ 'together. Pass a single `provider` for the one-request case ' +
54
+ '(provide exactly one of `provider` / `providers`). \n\n' +
24
55
  'Omit `providerId` to REGISTER A NEW provider (creates it + version ' +
25
56
  '1.0.0; re-running creates a duplicate). Pass `providerId` (from ' +
26
57
  'get_me_providers) to add a NEW VERSION to that existing provider ' +
27
58
  '(the patch is auto-bumped). \n\n' +
59
+ 'You can only add versions to a provider YOU own — the `providerId` ' +
60
+ 'must come from get_me_providers. To publish your own version of a ' +
61
+ "PUBLIC provider you don't own, omit `providerId` to register your " +
62
+ 'own copy (you cannot edit someone else\'s provider here). \n\n' +
28
63
  'Captured requests must be GET/POST. OPRF redactions are supported.',
29
64
  inputSchema: {
30
65
  type: 'object',
31
66
  properties: {
32
67
  provider: {
33
68
  type: 'object',
34
- description: 'The `provider` object from propose_provider, verbatim.',
69
+ description: 'A single `provider` object from propose_provider, verbatim. ' +
70
+ 'For multiple requests use `providers` instead.',
35
71
  properties: {
36
72
  name: { type: 'string' },
37
73
  url: { type: 'string' },
@@ -44,6 +80,28 @@ export function createVersionFromCaptureTool(client) {
44
80
  },
45
81
  required: ['name', 'url', 'method', 'responseMatches'],
46
82
  },
83
+ providers: {
84
+ type: 'array',
85
+ description: 'Multiple proven drafts → one multi-request version (each ' +
86
+ 'becomes a parallel `requestData` entry, all verified ' +
87
+ 'together). Each item has the same shape as `provider`. Pass ' +
88
+ 'this OR `provider`, not both.',
89
+ minItems: 1,
90
+ items: {
91
+ type: 'object',
92
+ properties: {
93
+ name: { type: 'string' },
94
+ url: { type: 'string' },
95
+ method: { type: 'string' },
96
+ headers: { type: 'object' },
97
+ body: { type: 'string' },
98
+ responseMatches: { type: 'array' },
99
+ responseRedactions: { type: 'array' },
100
+ paramValues: { type: 'object' },
101
+ },
102
+ required: ['name', 'url', 'method', 'responseMatches'],
103
+ },
104
+ },
47
105
  initialUrl: {
48
106
  type: 'string',
49
107
  description: 'The website URL the user landed on to trigger the request ' +
@@ -75,9 +133,18 @@ export function createVersionFromCaptureTool(client) {
75
133
  'chars or a default is used).',
76
134
  },
77
135
  },
78
- required: ['provider', 'initialUrl'],
136
+ required: ['initialUrl'],
79
137
  },
80
138
  }, async (args) => {
139
+ // Exactly one of `provider` / `providers` — normalize to an array so a
140
+ // multi-request capture publishes every request, not just the first.
141
+ const hasMany = Array.isArray(args.providers) && args.providers.length > 0;
142
+ const hasOne = !!args.provider;
143
+ if (hasMany === hasOne) {
144
+ throw new Boom('Pass exactly one of `provider` (a single draft) or `providers` ' +
145
+ '(an array of 1+ drafts).', { statusCode: 400 });
146
+ }
147
+ const providers = hasMany ? args.providers : [args.provider];
81
148
  const opts = { initialUrl: args.initialUrl };
82
149
  if (args.description !== undefined) {
83
150
  opts.description = args.description;
@@ -91,19 +158,36 @@ export function createVersionFromCaptureTool(client) {
91
158
  if (args.providerType !== undefined) {
92
159
  opts.providerType = args.providerType;
93
160
  }
161
+ // Non-blocking advisory per request — never alters or blocks the
162
+ // publish; the author keeps full control. Attaches under `_notes`.
163
+ const notes = matchRedactionWarnings(providers.map((p, i) => countsFor(`Request ${i + 1}`, p)));
164
+ const withNotes = (result) => {
165
+ if (notes.length && result && typeof result === 'object') {
166
+ return { ...result, _notes: notes };
167
+ }
168
+ return result;
169
+ };
94
170
  // New provider.
95
171
  if (!args.providerId) {
96
- const body = providerToRegisterBody(args.provider, opts);
97
- return client.registerProvider(body);
172
+ const body = providerToRegisterBody(providers, opts);
173
+ return withNotes(await client.registerProvider(body));
98
174
  }
99
175
  // New version of an existing provider — pick the next patch above
100
- // the highest existing version.
101
- const versionsResp = await client.listVersions(args.providerId);
102
- const version = bumpPatchString(extractVersions(versionsResp));
103
- const versionInfo = args.notes && args.notes.trim().length >= 10
104
- ? args.notes.trim()
105
- : DEFAULT_VERSION_INFO;
106
- const body = providerToConfigBody(args.provider, opts, version, versionInfo);
107
- return client.updateProviderConfig(args.providerId, body);
176
+ // the highest existing version. A provider you don't own (e.g. a
177
+ // public one) fails here with 401/403/404; surface the register-a-copy
178
+ // workaround instead of a bare permission error.
179
+ const providerId = args.providerId;
180
+ try {
181
+ const versionsResp = await client.listVersions(providerId);
182
+ const version = bumpPatchString(extractVersions(versionsResp));
183
+ const versionInfo = args.notes && args.notes.trim().length >= 10
184
+ ? args.notes.trim()
185
+ : DEFAULT_VERSION_INFO;
186
+ const body = providerToConfigBody(providers, opts, version, versionInfo);
187
+ return withNotes(await client.updateProviderConfig(providerId, body));
188
+ }
189
+ catch (err) {
190
+ rethrowOwnershipError(err, providerId);
191
+ }
108
192
  });
109
193
  }
package/lib/paths.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
2
  import { dirname, join, resolve } from 'node:path';
3
+ import { reclaimHome } from "./consts.js";
4
4
  /**
5
5
  * Resolve the Reclaim cache directory (`~/.reclaim`). Honours the
6
6
  * `RECLAIM_HOME` env var as an override so tests can sandbox writes.
7
7
  */
8
8
  export function reclaimHomeDirPath() {
9
- return process.env.RECLAIM_HOME || join(homedir(), '.reclaim');
9
+ return reclaimHome();
10
10
  }
11
11
  /**
12
12
  * Walk up from `start` until we find a directory containing one of
@@ -7,6 +7,7 @@
7
7
  // reveals), never on responseMatches.
8
8
  import { bytesToHex } from '@noble/hashes/utils.js';
9
9
  import { createClaimOnAttestor, } from '@reclaimprotocol/attestor-core';
10
+ import { agentTraceEnabled } from "../consts.js";
10
11
  import { resolveEthKey } from "../mcp/tools/authenticate/eth-key.js";
11
12
  import { buildHttpParams, ensureAttestorCrypto, parseClaim, resolveAttestorUrl, } from "./proof-core.js";
12
13
  import { ensureZkFiles } from "./zk-files.js";
@@ -136,7 +137,7 @@ export async function runAttestorProof(provider, secrets, opts = {}) {
136
137
  // trace/debug entries during ZK setup). Flip RECLAIM_AGENT_TRACE=1 to
137
138
  // turn it back on when something's broken; logs flow into the proof
138
139
  // response and into the on-disk proof-runs dump.
139
- const traceEnabled = process.env['RECLAIM_AGENT_TRACE'] === '1';
140
+ const traceEnabled = agentTraceEnabled();
140
141
  const capture = traceEnabled ? makeCaptureLogger() : undefined;
141
142
  const attestorLogs = capture?.entries;
142
143
  let claim;
@@ -1,6 +1,7 @@
1
1
  import { setCryptoImplementation } from '@reclaimprotocol/tls';
2
2
  import { webcryptoCrypto } from '@reclaimprotocol/tls/webcrypto';
3
- const FALLBACK_ATTESTOR_URL = 'wss://attestor.reclaimprotocol.org:444/ws';
3
+ import { attestorUrlOverride, FALLBACK_ATTESTOR_URL } from "../consts.js";
4
+ import { normalizeGeoLocation } from "../provider/schema.js";
4
5
  let cryptoInitialized = false;
5
6
  /** One-time crypto backend init for `@reclaimprotocol/tls` (idempotent). The
6
7
  * package ships with an empty `crypto = {}` and defers the backend to the
@@ -18,11 +19,14 @@ export function ensureAttestorCrypto() {
18
19
  * default. Resolved per-call so an env change or per-request override takes
19
20
  * effect without restarting. */
20
21
  export function resolveAttestorUrl(override) {
21
- return (override ?? process.env['RECLAIM_ATTESTOR_URL'] ?? FALLBACK_ATTESTOR_URL);
22
+ return (override ?? attestorUrlOverride() ?? FALLBACK_ATTESTOR_URL);
22
23
  }
23
24
  /** Map a verification's provider recipe straight to the attestor's HTTP
24
25
  * params — never synthesized from captured traffic. */
25
26
  export function buildHttpParams(provider) {
27
+ // A blank egress is treated as unset: omit it so the local proof uses the
28
+ // tester's own egress rather than shipping a blank or an unresolved template.
29
+ const geoLocation = normalizeGeoLocation(provider.geoLocation);
26
30
  return {
27
31
  url: provider.url,
28
32
  method: provider.method,
@@ -33,6 +37,13 @@ export function buildHttpParams(provider) {
33
37
  ...(provider.paramValues !== undefined && {
34
38
  paramValues: provider.paramValues,
35
39
  }),
40
+ ...(provider.writeRedactionMode !== undefined && {
41
+ writeRedactionMode: provider.writeRedactionMode,
42
+ }),
43
+ ...(provider.additionalClientOptions !== undefined && {
44
+ additionalClientOptions: provider.additionalClientOptions,
45
+ }),
46
+ ...(geoLocation !== undefined && { geoLocation }),
36
47
  };
37
48
  }
38
49
  /** Pull claimData + decoded `context.extractedParameters` (plus identifier /
@@ -0,0 +1 @@
1
+ export declare const INJECTION_GUIDE: string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Guidance for authoring page-injection scripts (`webSettings.jsUserScripts` on
3
+ * the builder, `customInjection` on old-devtools), surfaced to the authoring
4
+ * model from the publish tool description. The prose lives in
5
+ * `skill/injection-api.md` so it can be edited as markdown without touching
6
+ * code — the same pattern as `mcp/instructions.ts`. The path resolves the same
7
+ * from `src/provider` (dev) and `lib/provider` (published): both are two levels
8
+ * below the package root.
9
+ *
10
+ * The guide leads with plain browser JavaScript (navigation/interaction via
11
+ * standard APIs; the client already intercepts page requests) and treats
12
+ * `window.Reclaim` as a rarely-needed edge-case bridge, pointing at the
13
+ * authoritative `src/support/env.d.ts` rather than reproducing its surface.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ export const INJECTION_GUIDE = readFileSync(fileURLToPath(new URL('../../skill/injection-api.md', import.meta.url)), 'utf8').trim();
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Non-blocking advisory for a request whose `responseMatches` count is less
3
+ * than its `responseRedactions` count.
4
+ *
5
+ * Some verification clients (notably the InApp SDK) pair the two arrays by
6
+ * index and may not behave correctly when there are fewer matches than
7
+ * redactions. This is NOT enforced — the author keeps full control over what
8
+ * they publish in either old-devtools or builder mode. The tools surface these
9
+ * strings so the dev can make an informed decision (e.g. add a functional match
10
+ * per redaction) rather than being blocked.
11
+ */
12
+ /** One request's match/redaction counts, with a human label for the message. */
13
+ export interface RequestCounts {
14
+ label: string;
15
+ matchCount: number;
16
+ redactionCount: number;
17
+ }
18
+ /** Build advisory warnings for any request with matchCount < redactionCount.
19
+ * Returns an empty array when everything is fine. */
20
+ export declare function matchRedactionWarnings(requests: RequestCounts[]): string[];
21
+ /** Counts for a single drafted request shape (provider or injected request). */
22
+ export declare function countsFor(label: string, req: {
23
+ responseMatches?: unknown[];
24
+ responseRedactions?: unknown[];
25
+ }): RequestCounts;