@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
@@ -2,10 +2,10 @@ import {
2
2
  createReclaim,
3
3
  decryptCallback,
4
4
  type DecryptionKey,
5
- DEFAULT_BASE_URL,
6
5
  verifyResultFull,
7
6
  } from '@reclaimprotocol/client'
8
7
  import assert from 'node:assert'
8
+ import { apiUrl, orgSecret } from '../../../consts.ts'
9
9
  import { defineTool, type RegisteredTool } from '../../server.ts'
10
10
  import { resolveDecryptionKey } from '../authenticate/decrypt-key.ts'
11
11
 
@@ -19,7 +19,7 @@ const TERMINAL = new Set(['success', 'rejected', 'error'])
19
19
  * `RECLAIM_ORG_SECRET`.
20
20
  */
21
21
  function resolveOrgSecret(): string {
22
- const secret = process.env.RECLAIM_ORG_SECRET?.trim()
22
+ const secret = orgSecret()?.trim()
23
23
  assert(
24
24
  secret,
25
25
  new Error(
@@ -35,11 +35,10 @@ function resolveOrgSecret(): string {
35
35
  * `get_verification_result` — check a session's status and, once a result has
36
36
  * been submitted, pull the stored payload(s) and verify locally. Auth is the
37
37
  * org token; delivery is org-scoped. Each stored payload is either the
38
- * plaintext signed JWS (when the org has no encryption credential) or a compact
39
- * JWE (RFC 7516, ECDH-ES+A256GCM over X25519) that we decrypt here with the
40
- * optional X25519 key (the `reclaim-decrypt.pem` PEM in the project). The
41
- * Builder only ever relays the payload — decryption, when needed, happens
42
- * here.
38
+ * plaintext signed JWS (when the org's encryption is off) or an ECIES
39
+ * ciphertext that we decrypt here with the optional eth key
40
+ * (`RECLAIM_DECRYPT_KEY` or the `reclaim-decrypt.key` hex file). The Builder
41
+ * only ever relays the payload — decryption, when needed, happens here.
43
42
  */
44
43
  export function verificationResultTool(): RegisteredTool {
45
44
  return defineTool<{ sessionId: string }>(
@@ -50,9 +49,9 @@ export function verificationResultTool(): RegisteredTool {
50
49
  'Authenticates with the org secret (RECLAIM_ORG_SECRET). Fetches the ' +
51
50
  'session status; if it is terminal and a result has been submitted, ' +
52
51
  'pulls the stored result(s) and verifies them locally. Encrypted ' +
53
- 'deliveries (compact JWE over X25519) are decrypted with your ' +
54
- 'optional decryption key (the reclaim-decrypt.pem PEM in the ' +
55
- 'project); plaintext deliveries are read directly. Returns ' +
52
+ 'deliveries (ECIES over secp256k1) are decrypted with your optional ' +
53
+ 'eth key (RECLAIM_DECRYPT_KEY or reclaim-decrypt.key); plaintext ' +
54
+ 'deliveries are read directly. Returns ' +
56
55
  '`ready: false` while the session is still pending or no result has ' +
57
56
  'been delivered yet. Use list_verification_events for the full ' +
58
57
  'lifecycle log.',
@@ -69,14 +68,14 @@ export function verificationResultTool(): RegisteredTool {
69
68
  },
70
69
  },
71
70
  async(args) => {
72
- const orgSecret = resolveOrgSecret()
73
- // Optional X25519 decryption key — absent ⇒ plaintext-only.
71
+ const secret = resolveOrgSecret()
72
+ // Optional eth decryption key — absent ⇒ plaintext-only.
74
73
  const decryptionKey: DecryptionKey | undefined =
75
74
  await resolveDecryptionKey()
76
75
 
77
76
  const reclaim = createReclaim({
78
- baseUrl: process.env.RECLAIM_API_URL || DEFAULT_BASE_URL,
79
- orgSecret,
77
+ baseUrl: apiUrl(),
78
+ orgSecret: secret,
80
79
  })
81
80
 
82
81
  const sessionRes = await reclaim.api('GetVerificationSession', {
@@ -137,9 +136,9 @@ export function verificationResultTool(): RegisteredTool {
137
136
  ready: false,
138
137
  note:
139
138
  'A result was stored, but no payload could be read — it is ' +
140
- 'corrupt, or encrypted (JWE) to a key this consumer does not ' +
141
- 'hold. Place an X25519 PEM (reclaim-decrypt.pem) in the ' +
142
- 'project matching the org credential.',
139
+ 'corrupt, or encrypted (ECIES) to a key this consumer does not ' +
140
+ 'hold. Set RECLAIM_DECRYPT_KEY (or reclaim-decrypt.key) to the ' +
141
+ 'eth private key matching the org keypair.',
143
142
  }
144
143
  }
145
144
 
package/src/old/flag.ts 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(): boolean {
12
- return (process.env.USE_OLD_DEVTOOLS ?? '').trim().toLowerCase() !== 'false'
13
- }
14
+ export { useOldDevtools } from '../consts.ts'
@@ -1,5 +1,6 @@
1
1
  import { deriveFromPrivateKey } from '@reclaimprotocol/client'
2
2
  import { ConfigStore } from '../config/store.ts'
3
+ import { oldApiToken, oldEthUid, privateKey } from '../consts.ts'
3
4
  import type { OldIdentity } from './client.ts'
4
5
 
5
6
  /** Normalize an eth uid input to `eth:<lowercase-address>`. Accepts a bare
@@ -17,27 +18,27 @@ export function toEthUid(addressOrUid: string): OldIdentity {
17
18
  * 3. `RECLAIM_PRIVATE_KEY` — derive the eth uid from the local app key.
18
19
  * Returns undefined when none is set.
19
20
  */
20
- export function resolveIdentityFromEnv(): OldIdentity | undefined {
21
- const token = process.env.RECLAIM_OLD_API_TOKEN?.trim()
21
+ function resolveIdentityFromEnv(): OldIdentity | undefined {
22
+ const token = oldApiToken()?.trim()
22
23
  if(token) {
23
24
  return { kind: 'bearer', token }
24
25
  }
25
26
 
26
- const ethUid = process.env.RECLAIM_OLD_ETH_UID?.trim()
27
+ const ethUid = oldEthUid()?.trim()
27
28
  if(ethUid) {
28
29
  return toEthUid(ethUid)
29
30
  }
30
31
 
31
- const privateKey = process.env.RECLAIM_PRIVATE_KEY?.trim()
32
- if(privateKey) {
33
- return toEthUid(deriveFromPrivateKey(privateKey).address)
32
+ const key = privateKey()?.trim()
33
+ if(key) {
34
+ return toEthUid(deriveFromPrivateKey(key).address)
34
35
  }
35
36
 
36
37
  return undefined
37
38
  }
38
39
 
39
40
  /** Read the identity persisted by a prior `reclaim_authenticate` call. */
40
- export function loadPersistedIdentity(): OldIdentity | undefined {
41
+ function loadPersistedIdentity(): OldIdentity | undefined {
41
42
  return new ConfigStore().read()?.old_devtools
42
43
  }
43
44
 
@@ -5,10 +5,7 @@ import {
5
5
  waitForCdpReady,
6
6
  } from '../cdp/launcher.ts'
7
7
  import { ConfigStore } from '../config/store.ts'
8
-
9
- /** The devtools dashboard login page. Override per-call or via
10
- * `RECLAIM_OLD_LOGIN_URL`. */
11
- export const DEFAULT_LOGIN_URL = 'https://dev.reclaimprotocol.org'
8
+ import { DEFAULT_LOGIN_URL, oldLoginUrl } from '../consts.ts'
12
9
 
13
10
  /**
14
11
  * Read the Firebase ID token the dashboard persisted after login.
@@ -84,7 +81,7 @@ export async function captureFirebaseTokenViaCdp(opts: {
84
81
  pollMs?: number
85
82
  }): Promise<CdpLoginResult> {
86
83
  const loginUrl =
87
- opts.loginUrl || process.env.RECLAIM_OLD_LOGIN_URL || DEFAULT_LOGIN_URL
84
+ opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL
88
85
  const timeoutMs = opts.timeoutMs ?? 180_000
89
86
  const pollMs = opts.pollMs ?? 1000
90
87
 
@@ -1,4 +1,4 @@
1
- import type { ReclaimProvider, ResponseRedaction } from '../provider/schema.ts'
1
+ import type { ReclaimProvider } from '../provider/schema.ts'
2
2
  import type {
3
3
  RegisterProviderBody,
4
4
  UpdateProviderConfigBody,
@@ -97,15 +97,12 @@ function requestDataFromProvider(provider: ReclaimProvider): OldRequestData {
97
97
  )
98
98
 
99
99
  // Old devtools wants `responseMatches` and `responseRedactions` as PARALLEL
100
- // arrays. A drafted provider may instead carry each redaction nested in its
101
- // match's `extract` (the builder shape). Prefer the explicit parallel array;
102
- // fall back to the nested `extract`s so redactions are never dropped.
103
- const nestedRedactions = provider.responseMatches
104
- .map((m) => (m as { extract?: ResponseRedaction }).extract)
105
- .filter((e): e is ResponseRedaction => Boolean(e))
106
- const sourceRedactions = provider.responseRedactions?.length
107
- ? provider.responseRedactions
108
- : nestedRedactions
100
+ // arrays — same shape the builder uses, so pass them through directly. We do
101
+ // NOT enforce any match/redaction count relationship here (and never pad —
102
+ // empty placeholder matches break proof generation): the author keeps full
103
+ // control. The publish tool surfaces a non-blocking warning when matches <
104
+ // redactions (see `matchRedactionWarnings`), and the dev decides.
105
+ const sourceRedactions = provider.responseRedactions ?? []
109
106
 
110
107
  const responseRedactions: OldResponseRedaction[] = sourceRedactions.map(
111
108
  (r, i) => {
@@ -153,19 +150,28 @@ function requestDataFromProvider(provider: ReclaimProvider): OldRequestData {
153
150
  return requestData
154
151
  }
155
152
 
156
- /** Body for `POST /api/providers/register` — new provider + v1.0.0. */
153
+ /** One `requestData` entry per drafted provider. A multi-request provider
154
+ * becomes parallel entries that the old backend runs together at verification
155
+ * (the same way the builder version carries multiple `requests`). */
156
+ function requestDataFrom(providers: ReclaimProvider[]): OldRequestData[] {
157
+ return providers.map(requestDataFromProvider)
158
+ }
159
+
160
+ /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
161
+ * drafted request becomes a `requestData` entry; provider-level fields (name)
162
+ * come from the first draft. */
157
163
  export function providerToRegisterBody(
158
- provider: ReclaimProvider,
164
+ providers: ReclaimProvider[],
159
165
  opts: OldProviderOpts,
160
166
  ): RegisterProviderBody {
161
167
  const body: RegisterProviderBody = {
162
- name: provider.name,
168
+ name: providers[0].name,
163
169
  loginUrl: opts.initialUrl,
164
170
  providerType: opts.providerType ?? 'PRIVATE',
165
171
  // Web-settings defaults mirror the builder openapi defaults.
166
172
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
167
173
  useProxy: opts.useProxy ?? DEFAULT_USE_PROXY,
168
- requestData: [requestDataFromProvider(provider)],
174
+ requestData: requestDataFrom(providers),
169
175
  }
170
176
  if(opts.description !== undefined) {
171
177
  body.description = opts.description
@@ -178,7 +184,7 @@ export function providerToRegisterBody(
178
184
  * an EXISTING provider. `version` must not already exist; `versionInfo` is
179
185
  * required and must be ≥10 chars (enforced by the backend). */
180
186
  export function providerToConfigBody(
181
- provider: ReclaimProvider,
187
+ providers: ReclaimProvider[],
182
188
  opts: OldProviderOpts,
183
189
  version: string,
184
190
  versionInfo: string,
@@ -189,7 +195,7 @@ export function providerToConfigBody(
189
195
  const providerConfig: Record<string, unknown> = {
190
196
  loginUrl: opts.initialUrl,
191
197
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
192
- requestData: [requestDataFromProvider(provider)],
198
+ requestData: requestDataFrom(providers),
193
199
  }
194
200
 
195
201
  return { providerConfig, version, versionInfo }
@@ -1,3 +1,4 @@
1
+ import { agentDisabled } from '../../consts.ts'
1
2
  import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
2
3
  import type { OldIdentity, ReclaimOldClient } from '../client.ts'
3
4
  import { persistIdentity, toEthUid } from '../identity.ts'
@@ -128,7 +129,7 @@ export function authenticateTool(client: ReclaimOldClient): RegisteredTool {
128
129
  }
129
130
 
130
131
  // CDP browser login (the default headline flow).
131
- if(process.env.RECLAIM_AGENT_DISABLED === '1') {
132
+ if(agentDisabled()) {
132
133
  throw new Error(
133
134
  'Browser login is unavailable (RECLAIM_AGENT_DISABLED=1). Pass a ' +
134
135
  '`token` (Firebase ID token) or `ethAddress` instead.',
@@ -1,4 +1,9 @@
1
+ import { Boom, isBoom } from '@hapi/boom'
1
2
  import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
3
+ import {
4
+ countsFor,
5
+ matchRedactionWarnings,
6
+ } from '../../provider/match-redaction-warning.ts'
2
7
  import type { ReclaimProvider } from '../../provider/schema.ts'
3
8
  import type { ReclaimOldClient } from '../client.ts'
4
9
  import {
@@ -11,6 +16,35 @@ import {
11
16
 
12
17
  const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture'
13
18
 
19
+ /**
20
+ * The old backend lets you add a version only to a provider YOU own. A
21
+ * `providerId` for a public provider (or anyone else's) comes back as
22
+ * 401/403/404 — the dev's likely intent is to publish their OWN version of that
23
+ * site, which on this backend means registering a separate copy. Rethrow those
24
+ * failures with that actionable suggestion; leave other errors (e.g. a 400
25
+ * version collision) untouched so their real detail surfaces.
26
+ */
27
+ function rethrowOwnershipError(err: unknown, providerId: string): never {
28
+ const status = isBoom(err) ? err.output.statusCode : undefined
29
+ if(status !== 401 && status !== 403 && status !== 404) {
30
+ throw err
31
+ }
32
+
33
+ const base = isBoom(err) ? err.message : String(err)
34
+ throw new Boom(
35
+ `${base} — could not add a version to provider ${providerId}. You can ` +
36
+ 'only publish new versions on providers you own. If this is a public ' +
37
+ "provider (or one you don't own), re-run " +
38
+ 'create_provider_version_from_capture WITHOUT `providerId` to register ' +
39
+ "your own copy. If it IS yours, confirm you're authenticated " +
40
+ '(reclaim_authenticate) as the account that created it.',
41
+ {
42
+ statusCode: status,
43
+ data: isBoom(err) ? err.data : { status, detail: base },
44
+ },
45
+ )
46
+ }
47
+
14
48
  /**
15
49
  * Publish a drafted provider to the OLD devtools backend. Keeps the builder
16
50
  * tool NAME (`create_provider_version_from_capture`) so the skill flow is
@@ -28,7 +62,8 @@ export function createVersionFromCaptureTool(
28
62
  client: ReclaimOldClient,
29
63
  ): RegisteredTool {
30
64
  return defineTool<{
31
- provider: ReclaimProvider
65
+ provider?: ReclaimProvider
66
+ providers?: ReclaimProvider[]
32
67
  initialUrl: string
33
68
  providerId?: string
34
69
  description?: string
@@ -40,14 +75,23 @@ export function createVersionFromCaptureTool(
40
75
  {
41
76
  name: 'create_provider_version_from_capture',
42
77
  description:
43
- 'Publish a drafted ReclaimProvider to the OLD devtools backend. ' +
78
+ 'Publish drafted ReclaimProvider(s) to the OLD devtools backend. ' +
44
79
  'Pass the `provider` object from propose_provider VERBATIM and the ' +
45
80
  '`initialUrl` the user landed on (the login/landing page, NOT the ' +
46
81
  'API url). \n\n' +
82
+ 'For a MULTI-REQUEST provider, pass `providers: [...]` (each entry a ' +
83
+ 'proven draft from propose_provider) — every entry becomes one ' +
84
+ '`requestData` entry on the SAME version, and all are verified ' +
85
+ 'together. Pass a single `provider` for the one-request case ' +
86
+ '(provide exactly one of `provider` / `providers`). \n\n' +
47
87
  'Omit `providerId` to REGISTER A NEW provider (creates it + version ' +
48
88
  '1.0.0; re-running creates a duplicate). Pass `providerId` (from ' +
49
89
  'get_me_providers) to add a NEW VERSION to that existing provider ' +
50
90
  '(the patch is auto-bumped). \n\n' +
91
+ 'You can only add versions to a provider YOU own — the `providerId` ' +
92
+ 'must come from get_me_providers. To publish your own version of a ' +
93
+ "PUBLIC provider you don't own, omit `providerId` to register your " +
94
+ 'own copy (you cannot edit someone else\'s provider here). \n\n' +
51
95
  'Captured requests must be GET/POST. OPRF redactions are supported.',
52
96
  inputSchema: {
53
97
  type: 'object',
@@ -55,7 +99,8 @@ export function createVersionFromCaptureTool(
55
99
  provider: {
56
100
  type: 'object',
57
101
  description:
58
- 'The `provider` object from propose_provider, verbatim.',
102
+ 'A single `provider` object from propose_provider, verbatim. ' +
103
+ 'For multiple requests use `providers` instead.',
59
104
  properties: {
60
105
  name: { type: 'string' },
61
106
  url: { type: 'string' },
@@ -68,6 +113,29 @@ export function createVersionFromCaptureTool(
68
113
  },
69
114
  required: ['name', 'url', 'method', 'responseMatches'],
70
115
  },
116
+ providers: {
117
+ type: 'array',
118
+ description:
119
+ 'Multiple proven drafts → one multi-request version (each ' +
120
+ 'becomes a parallel `requestData` entry, all verified ' +
121
+ 'together). Each item has the same shape as `provider`. Pass ' +
122
+ 'this OR `provider`, not both.',
123
+ minItems: 1,
124
+ items: {
125
+ type: 'object',
126
+ properties: {
127
+ name: { type: 'string' },
128
+ url: { type: 'string' },
129
+ method: { type: 'string' },
130
+ headers: { type: 'object' },
131
+ body: { type: 'string' },
132
+ responseMatches: { type: 'array' },
133
+ responseRedactions: { type: 'array' },
134
+ paramValues: { type: 'object' },
135
+ },
136
+ required: ['name', 'url', 'method', 'responseMatches'],
137
+ },
138
+ },
71
139
  initialUrl: {
72
140
  type: 'string',
73
141
  description:
@@ -104,10 +172,24 @@ export function createVersionFromCaptureTool(
104
172
  'chars or a default is used).',
105
173
  },
106
174
  },
107
- required: ['provider', 'initialUrl'],
175
+ required: ['initialUrl'],
108
176
  },
109
177
  },
110
178
  async(args) => {
179
+ // Exactly one of `provider` / `providers` — normalize to an array so a
180
+ // multi-request capture publishes every request, not just the first.
181
+ const hasMany = Array.isArray(args.providers) && args.providers.length > 0
182
+ const hasOne = !!args.provider
183
+ if(hasMany === hasOne) {
184
+ throw new Boom(
185
+ 'Pass exactly one of `provider` (a single draft) or `providers` ' +
186
+ '(an array of 1+ drafts).',
187
+ { statusCode: 400 },
188
+ )
189
+ }
190
+
191
+ const providers = hasMany ? args.providers! : [args.provider!]
192
+
111
193
  const opts: OldProviderOpts = { initialUrl: args.initialUrl }
112
194
  if(args.description !== undefined) {
113
195
  opts.description = args.description
@@ -125,27 +207,49 @@ export function createVersionFromCaptureTool(
125
207
  opts.providerType = args.providerType
126
208
  }
127
209
 
210
+ // Non-blocking advisory per request — never alters or blocks the
211
+ // publish; the author keeps full control. Attaches under `_notes`.
212
+ const notes = matchRedactionWarnings(
213
+ providers.map((p, i) => countsFor(`Request ${i + 1}`, p)),
214
+ )
215
+ const withNotes = (result: unknown) => {
216
+ if(notes.length && result && typeof result === 'object') {
217
+ return { ...result, _notes: notes }
218
+ }
219
+
220
+ return result
221
+ }
222
+
128
223
  // New provider.
129
224
  if(!args.providerId) {
130
- const body = providerToRegisterBody(args.provider, opts)
131
- return client.registerProvider(body)
225
+ const body = providerToRegisterBody(providers, opts)
226
+ return withNotes(await client.registerProvider(body))
132
227
  }
133
228
 
134
229
  // New version of an existing provider — pick the next patch above
135
- // the highest existing version.
136
- const versionsResp = await client.listVersions(args.providerId)
137
- const version = bumpPatchString(extractVersions(versionsResp))
138
- const versionInfo =
139
- args.notes && args.notes.trim().length >= 10
140
- ? args.notes.trim()
141
- : DEFAULT_VERSION_INFO
142
- const body = providerToConfigBody(
143
- args.provider,
144
- opts,
145
- version,
146
- versionInfo,
147
- )
148
- return client.updateProviderConfig(args.providerId, body)
230
+ // the highest existing version. A provider you don't own (e.g. a
231
+ // public one) fails here with 401/403/404; surface the register-a-copy
232
+ // workaround instead of a bare permission error.
233
+ const providerId = args.providerId
234
+ try {
235
+ const versionsResp = await client.listVersions(providerId)
236
+ const version = bumpPatchString(extractVersions(versionsResp))
237
+ const versionInfo =
238
+ args.notes && args.notes.trim().length >= 10
239
+ ? args.notes.trim()
240
+ : DEFAULT_VERSION_INFO
241
+ const body = providerToConfigBody(
242
+ providers,
243
+ opts,
244
+ version,
245
+ versionInfo,
246
+ )
247
+ return withNotes(
248
+ await client.updateProviderConfig(providerId, body),
249
+ )
250
+ } catch(err) {
251
+ rethrowOwnershipError(err, providerId)
252
+ }
149
253
  },
150
254
  )
151
255
  }
package/src/paths.ts CHANGED
@@ -1,13 +1,13 @@
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.ts'
4
4
 
5
5
  /**
6
6
  * Resolve the Reclaim cache directory (`~/.reclaim`). Honours the
7
7
  * `RECLAIM_HOME` env var as an override so tests can sandbox writes.
8
8
  */
9
9
  export function reclaimHomeDirPath() {
10
- return process.env.RECLAIM_HOME || join(homedir(), '.reclaim')
10
+ return reclaimHome()
11
11
  }
12
12
 
13
13
  /**
@@ -10,6 +10,7 @@ import { bytesToHex } from '@noble/hashes/utils.js'
10
10
  import {
11
11
  createClaimOnAttestor, type Logger,
12
12
  } from '@reclaimprotocol/attestor-core'
13
+ import { agentTraceEnabled } from '../consts.ts'
13
14
  import { resolveEthKey } from '../mcp/tools/authenticate/eth-key.ts'
14
15
  import type { ReclaimProvider } from '../provider/schema.ts'
15
16
  import {
@@ -244,7 +245,7 @@ export async function runAttestorProof(
244
245
  // trace/debug entries during ZK setup). Flip RECLAIM_AGENT_TRACE=1 to
245
246
  // turn it back on when something's broken; logs flow into the proof
246
247
  // response and into the on-disk proof-runs dump.
247
- const traceEnabled = process.env['RECLAIM_AGENT_TRACE'] === '1'
248
+ const traceEnabled = agentTraceEnabled()
248
249
  const capture = traceEnabled ? makeCaptureLogger() : undefined
249
250
  const attestorLogs = capture?.entries
250
251
 
@@ -9,9 +9,9 @@
9
9
  import type { ProviderParams } from '@reclaimprotocol/attestor-core'
10
10
  import { setCryptoImplementation } from '@reclaimprotocol/tls'
11
11
  import { webcryptoCrypto } from '@reclaimprotocol/tls/webcrypto'
12
+ import { attestorUrlOverride, FALLBACK_ATTESTOR_URL } from '../consts.ts'
12
13
  import type { ReclaimProvider } from '../provider/schema.ts'
13
-
14
- const FALLBACK_ATTESTOR_URL = 'wss://attestor.reclaimprotocol.org:444/ws'
14
+ import { normalizeGeoLocation } from '../provider/schema.ts'
15
15
 
16
16
  let cryptoInitialized = false
17
17
 
@@ -34,7 +34,7 @@ export function ensureAttestorCrypto(): void {
34
34
  * effect without restarting. */
35
35
  export function resolveAttestorUrl(override?: string): string {
36
36
  return (
37
- override ?? process.env['RECLAIM_ATTESTOR_URL'] ?? FALLBACK_ATTESTOR_URL
37
+ override ?? attestorUrlOverride() ?? FALLBACK_ATTESTOR_URL
38
38
  )
39
39
  }
40
40
 
@@ -43,6 +43,9 @@ export function resolveAttestorUrl(override?: string): string {
43
43
  export function buildHttpParams(
44
44
  provider: ReclaimProvider,
45
45
  ): ProviderParams<'http'> {
46
+ // A blank egress is treated as unset: omit it so the local proof uses the
47
+ // tester's own egress rather than shipping a blank or an unresolved template.
48
+ const geoLocation = normalizeGeoLocation(provider.geoLocation)
46
49
  return {
47
50
  url: provider.url,
48
51
  method: provider.method,
@@ -53,6 +56,13 @@ export function buildHttpParams(
53
56
  ...(provider.paramValues !== undefined && {
54
57
  paramValues: provider.paramValues,
55
58
  }),
59
+ ...(provider.writeRedactionMode !== undefined && {
60
+ writeRedactionMode: provider.writeRedactionMode,
61
+ }),
62
+ ...(provider.additionalClientOptions !== undefined && {
63
+ additionalClientOptions: provider.additionalClientOptions,
64
+ }),
65
+ ...(geoLocation !== undefined && { geoLocation }),
56
66
  }
57
67
  }
58
68
 
@@ -0,0 +1,21 @@
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
+
18
+ export const INJECTION_GUIDE = readFileSync(
19
+ fileURLToPath(new URL('../../skill/injection-api.md', import.meta.url)),
20
+ 'utf8',
21
+ ).trim()
@@ -0,0 +1,53 @@
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
+
13
+ /** One request's match/redaction counts, with a human label for the message. */
14
+ export interface RequestCounts {
15
+ label: string
16
+ matchCount: number
17
+ redactionCount: number
18
+ }
19
+
20
+ /** Build advisory warnings for any request with matchCount < redactionCount.
21
+ * Returns an empty array when everything is fine. */
22
+ export function matchRedactionWarnings(requests: RequestCounts[]): string[] {
23
+ const warnings: string[] = []
24
+ for(const req of requests) {
25
+ if(req.matchCount < req.redactionCount) {
26
+ warnings.push(
27
+ `${req.label}: ${req.matchCount} response match(es) but ` +
28
+ `${req.redactionCount} response redaction(s). Some verification ` +
29
+ 'clients (e.g. the InApp SDK) may not work correctly when ' +
30
+ 'responseMatches is shorter than responseRedactions — they pair the ' +
31
+ 'two by index. This is allowed; add a functional match per redaction ' +
32
+ 'if you hit verification issues.',
33
+ )
34
+ }
35
+ }
36
+
37
+ return warnings
38
+ }
39
+
40
+ /** Counts for a single drafted request shape (provider or injected request). */
41
+ export function countsFor(
42
+ label: string,
43
+ req: {
44
+ responseMatches?: unknown[]
45
+ responseRedactions?: unknown[]
46
+ },
47
+ ): RequestCounts {
48
+ return {
49
+ label,
50
+ matchCount: req.responseMatches?.length ?? 0,
51
+ redactionCount: req.responseRedactions?.length ?? 0,
52
+ }
53
+ }
@@ -69,6 +69,29 @@ export interface ReclaimProvider {
69
69
  responseRedactions?: ResponseRedaction[]
70
70
  /** Public param refs (templated into URL/body), populated by draft layer. */
71
71
  paramValues?: Record<string, string>
72
+ /** Redaction mode for the transcript (attestor default: 'key-update'). */
73
+ writeRedactionMode?: HttpParams['writeRedactionMode']
74
+ /** TLS client options applied to the attestor tunnel. */
75
+ additionalClientOptions?: HttpParams['additionalClientOptions']
76
+ /** Proxy egress: 2-letter ISO country or `{{param}}`. Blank/omitted ⇒
77
+ * `{{DYNAMIC_GEO}}` on publish (resolved to the verifying user's country).
78
+ */
79
+ geoLocation?: HttpParams['geoLocation']
80
+ }
81
+
82
+ /** Egress placeholder resolved to the verifying user's country at
83
+ * verification time — the default when a provider declares no explicit egress.
84
+ */
85
+ export const DYNAMIC_GEO = '{{DYNAMIC_GEO}}'
86
+
87
+ /** `geoLocation` must never be blank. Trims the input and treats an empty
88
+ * string as unset so callers apply their own default (publish → `DYNAMIC_GEO`;
89
+ * a local proof omits it and uses the tester's egress). */
90
+ export function normalizeGeoLocation(
91
+ value: string | undefined,
92
+ ): string | undefined {
93
+ const trimmed = value?.trim()
94
+ return trimmed ? trimmed : undefined
72
95
  }
73
96
 
74
97
  /** A reference to a secret param. Raw value lives only in server memory. */