@reclaimprotocol/agent 0.1.29 → 0.1.31

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 (89) hide show
  1. package/README.md +28 -9
  2. package/lib/consts.d.ts +5 -5
  3. package/lib/consts.js +7 -7
  4. package/lib/live/tunnel-watchdog.d.ts +1 -0
  5. package/lib/live/tunnel-watchdog.js +66 -0
  6. package/lib/live/tunnel.d.ts +30 -7
  7. package/lib/live/tunnel.js +217 -40
  8. package/lib/mcp/api/operations.gen.d.ts +12 -0
  9. package/lib/mcp/api/operations.gen.js +537 -59
  10. package/lib/mcp/api/tools.js +27 -5
  11. package/lib/mcp/instructions.js +23 -32
  12. package/lib/mcp/remap_errors.js +6 -0
  13. package/lib/mcp/server.js +2 -8
  14. package/lib/mcp/start.js +3 -5
  15. package/lib/mcp/success-response.d.ts +5 -0
  16. package/lib/mcp/success-response.js +21 -0
  17. package/lib/mcp/tools/agent/attach.js +9 -66
  18. package/lib/mcp/tools/agent/build.d.ts +0 -3
  19. package/lib/mcp/tools/agent/build.js +3 -4
  20. package/lib/mcp/tools/agent/how-it-works.d.ts +5 -15
  21. package/lib/mcp/tools/agent/how-it-works.js +9 -22
  22. package/lib/mcp/tools/agent/live-view.d.ts +1 -15
  23. package/lib/mcp/tools/agent/live-view.js +71 -96
  24. package/lib/mcp/tools/agent/propose.js +7 -23
  25. package/lib/mcp/tools/agent/publish.js +80 -18
  26. package/lib/mcp/tools/agent/session.d.ts +4 -7
  27. package/lib/mcp/tools/agent/session.js +8 -23
  28. package/lib/mcp/tools/verification/result.d.ts +2 -1
  29. package/lib/mcp/tools/verification/result.js +29 -28
  30. package/lib/old/client.d.ts +2 -2
  31. package/lib/old/flag.d.ts +2 -5
  32. package/lib/old/flag.js +2 -5
  33. package/lib/old/instructions.d.ts +1 -1
  34. package/lib/old/instructions.js +1 -1
  35. package/lib/old/to-register.d.ts +4 -3
  36. package/lib/old/tools/publish.js +11 -14
  37. package/lib/proof/attestor-auth.d.ts +9 -1
  38. package/lib/proof/attestor-auth.js +15 -2
  39. package/lib/proof/attestor.d.ts +2 -2
  40. package/lib/proof/attestor.js +20 -2
  41. package/lib/proof/pod-prover.js +1 -1
  42. package/lib/proof/proof-core.d.ts +4 -0
  43. package/lib/proof/proof-core.js +20 -0
  44. package/lib/provider/injection-doc.d.ts +3 -2
  45. package/lib/provider/injection-doc.js +12 -10
  46. package/lib/provider/matcher.js +19 -15
  47. package/lib/provider/to-version.d.ts +3 -0
  48. package/lib/provider/to-version.js +2 -1
  49. package/package.json +3 -3
  50. package/skill/SKILL.builder.md +30 -83
  51. package/skill/SKILL.common.md +38 -22
  52. package/skill/SKILL.old.md +17 -3
  53. package/skill/builder-mode.md +37 -4
  54. package/skill/community-contributions.md +12 -3
  55. package/skill/hash-validation.md +9 -9
  56. package/skill/interception.md +39 -46
  57. package/skill/old-mode.md +13 -0
  58. package/skill/user-script.md +24 -8
  59. package/skill/verification-client-integration.md +130 -0
  60. package/src/consts.ts +9 -7
  61. package/src/mcp/api/operations.gen.ts +539 -59
  62. package/src/mcp/api/tools.ts +38 -7
  63. package/src/mcp/instructions.ts +33 -32
  64. package/src/mcp/remap_errors.ts +6 -0
  65. package/src/mcp/server.ts +2 -8
  66. package/src/mcp/start.ts +3 -5
  67. package/src/mcp/success-response.ts +26 -0
  68. package/src/mcp/tools/agent/attach.ts +9 -71
  69. package/src/mcp/tools/agent/build.ts +4 -7
  70. package/src/mcp/tools/agent/how-it-works.ts +9 -22
  71. package/src/mcp/tools/agent/live-view.ts +76 -116
  72. package/src/mcp/tools/agent/propose.ts +7 -23
  73. package/src/mcp/tools/agent/publish.ts +89 -18
  74. package/src/mcp/tools/agent/session.ts +6 -23
  75. package/src/mcp/tools/verification/result.ts +30 -29
  76. package/src/old/client.ts +2 -2
  77. package/src/old/flag.ts +2 -5
  78. package/src/old/instructions.ts +1 -1
  79. package/src/old/to-register.ts +4 -3
  80. package/src/old/tools/publish.ts +16 -14
  81. package/src/proof/attestor-auth.ts +18 -1
  82. package/src/proof/attestor.ts +24 -3
  83. package/src/proof/pod-prover.ts +1 -1
  84. package/src/proof/proof-core.ts +28 -0
  85. package/src/provider/injection-doc.ts +12 -10
  86. package/src/provider/matcher.ts +25 -17
  87. package/src/provider/to-version.ts +7 -1
  88. package/src/live/cloudflared.ts +0 -348
  89. package/src/live/tunnel.ts +0 -254
package/src/old/flag.ts CHANGED
@@ -2,11 +2,8 @@
2
2
  * `USE_OLD_DEVTOOLS` — single source of truth for which backend the MCP
3
3
  * server targets.
4
4
  *
5
- * Defaults to OLD devtools (the system still in production) while the new
6
- * "builder" backend is under development. Builder mode must be opted into
7
- * EXPLICITLY by setting `USE_OLD_DEVTOOLS=false`. Anything else — unset,
8
- * empty, `true`, `1`, a typo — keeps old mode on, so a forgotten env var
9
- * can never silently route a developer at the half-built builder backend.
5
+ * Builder is the default. Only `true` or `1` explicitly selects old devtools;
6
+ * an unset value or typo stays on Builder.
10
7
  *
11
8
  * The actual env read lives in `../consts.ts` (the single env-access module);
12
9
  * this widely-imported path re-exports it for back-compat.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MCP server instructions shown to the LLM when running in OLD-devtools mode
3
- * (`USE_OLD_DEVTOOLS` unset or not `false`). Describes the REDUCED tool
3
+ * (`USE_OLD_DEVTOOLS=true` or `1`). Describes the REDUCED tool
4
4
  * surface — there are no org/credential-link/review flows and no generated
5
5
  * API tools here. Both modes' instructions load together in
6
6
  * `mcp/instructions` (alongside the topic-split authoring guide); re-exported
@@ -72,9 +72,10 @@ export interface OldProviderOpts {
72
72
  * .interceptorOptions.isDocumentRequestReplayEnabled`, is likewise always
73
73
  * force-disabled by `providerToCreateVersionRequest`. */
74
74
  disableRequestReplay?: boolean
75
- /** Interception mechanism — `NONE` (the publish tool's default) for the
76
- * standard capture→replay flow; `HAWKEYE`/`MSWJS`/`XHOOK`/`CDP` for a
77
- * genuine live-interception provider. Old-devtools calls this
75
+ /** Interception mechanism. Omit it to use the backend's `HAWKEYE` default;
76
+ * use `NONE` only when disabling interception after diagnosis.
77
+ * `HAWKEYE`/`MSWJS`/`XHOOK`/`CDP` enable live interception. Old-devtools
78
+ * calls this
78
79
  * `injectionType`; builder's equivalent is `webSettings.clientOptions
79
80
  * .inapp.interceptorOptions.interceptorType` (no `NONE` value there —
80
81
  * omitting the whole object is builder's no-interception signal
@@ -455,20 +455,12 @@ export function createVersionFromCaptureTool(
455
455
  type: 'string',
456
456
  enum: ['NONE', 'MSWJS', 'XHOOK', 'CDP', 'HAWKEYE'],
457
457
  description:
458
- 'Interception mechanism for this version. Omit to leave it ' +
459
- 'unset — the devtools backend\'s own column default ' +
460
- '(`HAWKEYE`) applies, which is inert and harmless for the ' +
461
- 'standard capture→replay flow with no interceptor script ' +
462
- 'installed. Only set `HAWKEYE`/`MSWJS`/`XHOOK` when you are ' +
463
- 'ALSO supplying a `customInjection` script that calls ' +
464
- '`window.reclaimInterceptor.addResponseMiddleware(...)` for ' +
465
- 'genuine live interception, or set `NONE` explicitly to be ' +
466
- 'certain none is configured. Builder calls the equivalent ' +
467
- 'concept `interceptorType` (no `NONE` there — omitting the ' +
468
- 'whole `interceptorOptions` object is its no-interception ' +
469
- 'signal instead). ' + COOKIE_ATTACHMENT_HINT +
470
- 'What each value actually does: ' +
471
- 'how_it_works({ topic: "interception" }).',
458
+ 'Live-interception mechanism. Omit it for the standard flow. ' +
459
+ 'Use `NONE` only to confirm a suspected interception problem; ' +
460
+ 'if that fixes the provider, keep `NONE` and use ' +
461
+ '`window.Reclaim.requestClaim(...)`. For selection rules, call ' +
462
+ 'how_it_works({ topic: "interception" }). ' +
463
+ COOKIE_ATTACHMENT_HINT,
472
464
  },
473
465
  disableRequestReplay: {
474
466
  type: 'boolean',
@@ -499,6 +491,16 @@ export function createVersionFromCaptureTool(
499
491
  'registration starts at the backend-defined version.',
500
492
  ),
501
493
  )
494
+ assert(
495
+ args.injectionType !== 'NONE'
496
+ || !args.customInjection?.includes('addResponseMiddleware'),
497
+ badRequest(
498
+ '`injectionType: NONE` cannot deliver ' +
499
+ '`addResponseMiddleware` traffic. Use an interceptor type, or ' +
500
+ 'keep `NONE` and replace intercepted requests with ' +
501
+ '`window.Reclaim.requestClaim(...)`.',
502
+ ),
503
+ )
502
504
  // `provider` / `providers` are just two spellings of the same input —
503
505
  // fold them into one array. Zero drafts is allowed ONLY when adding a
504
506
  // version to an EXISTING provider (`providerId` set): that publishes
@@ -11,16 +11,33 @@ export interface AttestorAuthentication {
11
11
  /**
12
12
  * Create legacy-compatible attestor authorization when the deployment has an
13
13
  * auth private key. An absent key intentionally means authentication is off.
14
+ *
15
+ * `expiresInSeconds` overrides the attestor library's default lifetime. A
16
+ * zkFetch authorization uses a very short one: the attestor validates expiry
17
+ * once, when it authenticates the connection, and never again for the life of
18
+ * that connection — so the window only has to cover the caller receiving the
19
+ * token and opening its socket, not the proof that follows.
14
20
  */
15
21
  export async function createAttestorAuthentication(
16
22
  sessionId: string,
17
23
  privateKey: string | undefined,
24
+ opts: { expiresInSeconds?: number } = {},
18
25
  ): Promise<AttestorAuthentication | undefined> {
19
26
  if(!privateKey?.trim()) {
20
27
  return undefined
21
28
  }
22
29
 
23
- const request = await createAuthRequest({ id: sessionId }, privateKey)
30
+ const request = await createAuthRequest(
31
+ {
32
+ id: sessionId,
33
+ ...(opts.expiresInSeconds === undefined
34
+ ? {}
35
+ : {
36
+ expiresAt: Math.floor(Date.now() / 1000) + opts.expiresInSeconds,
37
+ }),
38
+ },
39
+ privateKey,
40
+ )
24
41
  const token = Buffer.from(
25
42
  JSON.stringify({
26
43
  ...request,
@@ -22,6 +22,7 @@ import type { ReclaimProvider } from '../provider/schema.ts'
22
22
  import { installResponseDump } from './debug-response-dump.ts'
23
23
  import {
24
24
  buildHttpParams,
25
+ claimRejection,
25
26
  ensureAttestorCrypto,
26
27
  parseClaim,
27
28
  resolveAttestorUrl
@@ -65,7 +66,7 @@ export interface RunAttestorProofOptions {
65
66
  * want gnark's speed pin it explicitly (for example, the verify runner). */
66
67
  zkEngine?: 'gnark' | 'snarkjs' | 'stwo'
67
68
  /** Extract the structured {@link AttestorClaim} (claim + attestor signature)
68
- * onto the result for legacy Proof construction. */
69
+ * onto the result for Proof construction. */
69
70
  includeAttestorClaim?: boolean
70
71
  }
71
72
 
@@ -77,7 +78,7 @@ export interface ProofResult {
77
78
  owner?: string
78
79
  verified: boolean
79
80
  error?: string
80
- /** Structured claim + signature for legacy Proof verification. Set only when
81
+ /** Structured claim + signature for Proof verification. Set only when
81
82
  * `includeAttestorClaim` was requested and the claim carries signatures. */
82
83
  attestorClaim?: AttestorClaim
83
84
  /** Set to `'missing-owner-key'` when no credential was available to
@@ -300,6 +301,26 @@ export async function runAttestorProof(
300
301
  return errorResult
301
302
  }
302
303
 
304
+ // The attestor reports a rejected claim (for example, an invalid ZK proof)
305
+ // in the response rather than by throwing, so a returned value alone is
306
+ // not a proof: it needs a claim, a claim signature, and no error.
307
+ const rejection = claimRejection(claim)
308
+ if(rejection !== undefined) {
309
+ const rejected: ProofResult = {
310
+ proof: claim,
311
+ extractedValue: '',
312
+ extractedParameters: {},
313
+ verified: false,
314
+ error: rejection,
315
+ sentParams: params,
316
+ }
317
+ if(attestorLogs !== undefined) {
318
+ rejected.attestorLogs = attestorLogs
319
+ }
320
+
321
+ return rejected
322
+ }
323
+
303
324
  const { extractedValue, extractedParameters, identifier, owner, claimData }
304
325
  = parseClaim(claim)
305
326
 
@@ -318,7 +339,7 @@ export async function runAttestorProof(
318
339
  }
319
340
 
320
341
  // Structured claim + signature so the result JWS can carry the exact
321
- // legacy Proof the Consumer verifies independently.
342
+ // Proof the Consumer verifies independently.
322
343
  if(opts.includeAttestorClaim) {
323
344
  const sigs = (claim as {
324
345
  signatures?: { attestorAddress?: unknown, claimSignature?: unknown }
@@ -27,7 +27,7 @@
27
27
  * The pod API is unauthenticated (gateway/session is the trust boundary); call
28
28
  * it over the same internal host used for CDP. Consumers verify
29
29
  * `claim_signature` with the legacy SDK's `verifyProof`, so this hands back an
30
- * {@link AttestorClaim} that the app places in the exact legacy `Proof` shape.
30
+ * {@link AttestorClaim} that the app places in the exact `Proof` shape.
31
31
  * `result_signature` is unused by consumers.
32
32
  */
33
33
  import { bytesToHex } from '@noble/hashes/utils.js'
@@ -75,6 +75,34 @@ export interface ParsedClaim {
75
75
  owner?: string
76
76
  }
77
77
 
78
+ /** Why the attestor did not sign `claim`, or `undefined` when it did. The
79
+ * attestor returns rejections (for example, an invalid ZK proof) as a
80
+ * response with `error` set and no claim signature, not by throwing. */
81
+ export function claimRejection(claim: unknown): string | undefined {
82
+ const response = claim as {
83
+ claim?: unknown
84
+ error?: { code?: unknown, message?: unknown }
85
+ signatures?: { claimSignature?: unknown }
86
+ } | null | undefined
87
+ const error = response?.error
88
+ if(error?.message || error?.code) {
89
+ return typeof error.message === 'string' && error.message
90
+ ? error.message
91
+ : `attestor error code ${String(error.code)}`
92
+ }
93
+
94
+ if(!response?.claim) {
95
+ return 'attestor returned no claim'
96
+ }
97
+
98
+ const signature = response.signatures?.claimSignature
99
+ if(!(signature instanceof Uint8Array) || !signature.length) {
100
+ return 'attestor did not sign the claim'
101
+ }
102
+
103
+ return undefined
104
+ }
105
+
78
106
  /** Pull claimData + decoded `context.extractedParameters` (plus identifier /
79
107
  * owner) out of a `createClaimOnAttestor` result. The full result still
80
108
  * carries signatures the caller may need for nested-proof building. */
@@ -33,17 +33,19 @@ export const USER_SCRIPT_HINT =
33
33
 
34
34
  /** Prefixes the `allowedJsRequests` / `allowedInjectedRequestData` field. The
35
35
  * trap it describes is silent and total — a mis-declared entry makes
36
- * `verifyProof({ providerId })` reject EVERY real proof — so the warning
37
- * itself stays in the schema even though its explanation moved to a topic. */
36
+ * `verifyRequestProof({ providerId })` reject EVERY real proof — so the
37
+ * warning itself stays in the schema even though its explanation moved to a
38
+ * topic. */
38
39
  export const ALLOWED_REQUESTS_HINT =
39
40
  '⚠️ Only populate this if you intend to gate '
40
- + '`verifyProof(proof, { providerId })`: each entry feeds a client-side '
41
- + 'hash-equality check and its `url` is hashed VERBATIM (no regex/template '
42
- + 'resolution, even with urlType REGEX/TEMPLATE). If the real captured URL '
43
- + 'carries ANY per-session part (nonce, CSRF token, cache-busting '
44
- + 'timestamp), no declared spec can ever hash-match a real proof — '
45
- + 'populating it then makes verifyProof reject every real proof, which is '
46
- + 'worse than leaving it empty. Diff the same endpoint\'s captured URL '
47
- + 'across two sessions first. Mechanics, the body-templating rules and the '
41
+ + '`verifyRequestProof(proof, { providerId })`: each entry feeds a '
42
+ + 'client-side hash-equality check and its `url` is hashed VERBATIM (no '
43
+ + 'regex/template resolution, even with urlType REGEX/TEMPLATE). If the '
44
+ + 'real captured URL carries ANY per-session part (nonce, CSRF token, '
45
+ + 'cache-busting timestamp), no declared spec can ever hash-match a real '
46
+ + 'proof — populating it then makes verifyRequestProof reject every real '
47
+ + 'proof, which is worse than leaving it empty. Diff the same endpoint\'s '
48
+ + 'captured URL across two sessions first. Mechanics, the '
49
+ + 'body-templating rules and the '
48
50
  + 'dangerouslyDisableContentValidation fallback: '
49
51
  + 'how_it_works({ topic: "hash-validation" }).'
@@ -327,16 +327,32 @@ function findJsonPath(body: string, target: string): string | undefined {
327
327
  }
328
328
 
329
329
  const candidates = normalizeCandidates(target)
330
- return walk(parsed, '$', candidates)
330
+ const paths: string[] = []
331
+ collectJsonPaths(parsed, '$', candidates, paths)
332
+ if(paths.length > 1) {
333
+ throw new Error(
334
+ `target value "${target}" occurs at multiple JSON paths `
335
+ + `(${paths.join(', ')}) — choose a value that identifies one field`,
336
+ )
337
+ }
338
+
339
+ return paths[0]
331
340
  }
332
341
 
333
- function walk(
342
+ function collectJsonPaths(
334
343
  node: unknown,
335
344
  path: string,
336
345
  candidates: string[],
337
- ): string | undefined {
346
+ paths: string[],
347
+ ): void {
348
+ // Two paths are enough to prove ambiguity and keep traversal bounded for a
349
+ // response containing the same primitive many times.
350
+ if(paths.length > 1) {
351
+ return
352
+ }
353
+
338
354
  if(node === null || node === undefined) {
339
- return undefined
355
+ return
340
356
  }
341
357
 
342
358
  if(
@@ -346,21 +362,18 @@ function walk(
346
362
  ) {
347
363
  const str = String(node)
348
364
  if(candidates.includes(str)) {
349
- return path
365
+ paths.push(path)
350
366
  }
351
367
 
352
- return undefined
368
+ return
353
369
  }
354
370
 
355
371
  if(Array.isArray(node)) {
356
372
  for(const [i, element] of node.entries()) {
357
- const found = walk(element, `${path}[${i}]`, candidates)
358
- if(found) {
359
- return found
360
- }
373
+ collectJsonPaths(element, `${path}[${i}]`, candidates, paths)
361
374
  }
362
375
 
363
- return undefined
376
+ return
364
377
  }
365
378
 
366
379
  if(typeof node === 'object') {
@@ -368,14 +381,9 @@ function walk(
368
381
  const childPath = /^[A-Za-z_][A-Za-z0-9_]*$/.test(k)
369
382
  ? `${path}.${k}`
370
383
  : `${path}["${k}"]`
371
- const found = walk(v, childPath, candidates)
372
- if(found) {
373
- return found
374
- }
384
+ collectJsonPaths(v, childPath, candidates, paths)
375
385
  }
376
386
  }
377
-
378
- return undefined
379
387
  }
380
388
 
381
389
  // ---------------------------------------------------------------------------
@@ -74,6 +74,11 @@ export interface CreateVersionOptions {
74
74
  jsUserScripts?: string
75
75
  /** Requests the injection script may fire, as `RequestSelectionTemplate`s. */
76
76
  allowedJsRequests?: AllowedJsRequest[]
77
+ /** Portal-only authenticated actions exposed as
78
+ * `window.Reclaim.browserRequest`. */
79
+ browserRequestActions?: NonNullable<
80
+ CreateProviderVersionRequest['webSettings']['browserRequestActions']
81
+ >
77
82
  /** In-app interception options, mapped to
78
83
  * `webSettings.clientOptions.inapp.interceptorOptions` — ONLY when this
79
84
  * is explicitly passed (builder's schema has no `NONE` interceptorType;
@@ -218,7 +223,7 @@ export function providerToCreateVersionRequest(
218
223
  ): CreateProviderVersionRequest {
219
224
  const {
220
225
  notes, version, requiredContext: requestedContext, jsUserScripts,
221
- allowedJsRequests, interceptorOptions,
226
+ allowedJsRequests, browserRequestActions, interceptorOptions,
222
227
  } = opts
223
228
  const requiredContext = requestedContext ?? deriveRequiredContext(
224
229
  providers,
@@ -279,6 +284,7 @@ export function providerToCreateVersionRequest(
279
284
  },
280
285
  // Only set when non-empty; an empty string would store a no-op script.
281
286
  ...(script ? { jsUserScripts: script } : {}),
287
+ ...(browserRequestActions?.length ? { browserRequestActions } : {}),
282
288
  },
283
289
  // Derived from `{{context.<name>}}` placeholders only — a captured
284
290
  // provider's `paramValues` are extracted response parameters, not