@reclaimprotocol/agent 0.1.11 → 0.1.13

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 (43) hide show
  1. package/lib/mcp/tools/agent/shared.js +30 -1
  2. package/lib/mcp/tools/authenticate/decrypt-key.js +1 -1
  3. package/lib/old/client.d.ts +31 -0
  4. package/lib/old/client.js +33 -0
  5. package/lib/old/tools/build.js +12 -0
  6. package/lib/old/tools/check-app-status.d.ts +8 -0
  7. package/lib/old/tools/check-app-status.js +27 -0
  8. package/lib/old/tools/issue-app-credentials.d.ts +11 -0
  9. package/lib/old/tools/issue-app-credentials.js +35 -0
  10. package/lib/old/tools/js-sdk-integration-guide.d.ts +2 -0
  11. package/lib/old/tools/js-sdk-integration-guide.js +27 -0
  12. package/lib/old/tools/link-app.d.ts +10 -0
  13. package/lib/old/tools/link-app.js +73 -0
  14. package/lib/old/tools/publish.js +26 -11
  15. package/lib/old/tools/scaffold-demo-server.d.ts +2 -0
  16. package/lib/old/tools/scaffold-demo-server.js +94 -0
  17. package/lib/old/tools/verify-sdk-setup.d.ts +9 -0
  18. package/lib/old/tools/verify-sdk-setup.js +132 -0
  19. package/lib/proof/attestor.js +2 -2
  20. package/lib/provider/draft.js +51 -1
  21. package/package.json +3 -1
  22. package/samples/demo-server/.env.example +12 -0
  23. package/samples/demo-server/README.md +24 -0
  24. package/samples/demo-server/package.json +13 -0
  25. package/samples/demo-server/server.js +156 -0
  26. package/skill/js-sdk-integration.md +119 -0
  27. package/src/cdp/launcher.ts +33 -33
  28. package/src/mcp/api/tools.ts +3 -4
  29. package/src/mcp/remap_errors.ts +1 -1
  30. package/src/mcp/tools/agent/shared.ts +35 -1
  31. package/src/mcp/tools/authenticate/authenticate.ts +1 -3
  32. package/src/mcp/tools/authenticate/decrypt-key.ts +1 -4
  33. package/src/old/client.ts +60 -0
  34. package/src/old/tools/build.ts +12 -0
  35. package/src/old/tools/check-app-status.ts +33 -0
  36. package/src/old/tools/issue-app-credentials.ts +44 -0
  37. package/src/old/tools/js-sdk-integration-guide.ts +38 -0
  38. package/src/old/tools/link-app.ts +93 -0
  39. package/src/old/tools/publish.ts +26 -12
  40. package/src/old/tools/scaffold-demo-server.ts +120 -0
  41. package/src/old/tools/verify-sdk-setup.ts +162 -0
  42. package/src/proof/attestor.ts +7 -3
  43. package/src/provider/draft.ts +59 -1
@@ -0,0 +1,162 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
4
+ import { findProjectRoot } from '../../paths.ts'
5
+ import type { ReclaimOldClient } from '../client.ts'
6
+
7
+ interface Check {
8
+ name: string
9
+ ok: boolean
10
+ detail: string
11
+ }
12
+
13
+ function readEnvFile(dir: string): Record<string, string> {
14
+ const path = join(dir, '.env')
15
+ if(!existsSync(path)) {
16
+ return {}
17
+ }
18
+
19
+ const out: Record<string, string> = {}
20
+ for(const line of readFileSync(path, 'utf8').split('\n')) {
21
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/)
22
+ if(m) {
23
+ out[m[1]] = m[2].replace(/^['"]|['"]$/g, '')
24
+ }
25
+ }
26
+
27
+ return out
28
+ }
29
+
30
+ /**
31
+ * Health-check a project's @reclaimprotocol/js-sdk setup: SDK installed,
32
+ * RECLAIM_APP_ID/RECLAIM_APP_SECRET present (env or .env), and (if an appId
33
+ * is found) whether that app is actually linked/has quota, via the real
34
+ * check_app_status endpoint — not just static file checks.
35
+ */
36
+ export function verifySdkSetupTool(client: ReclaimOldClient): RegisteredTool {
37
+ return defineTool<{ projectDir?: string }>(
38
+ {
39
+ name: 'verify_reclaim_sdk_setup',
40
+ description:
41
+ 'Health-check a project\'s @reclaimprotocol/js-sdk integration: SDK ' +
42
+ 'installed, RECLAIM_APP_ID/RECLAIM_APP_SECRET present, and (if an ' +
43
+ 'appId is found) whether that app is actually linked and has quota ' +
44
+ 'left, by calling the real status endpoint.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ projectDir: {
49
+ type: 'string',
50
+ description: 'Project directory (default: detected project root).',
51
+ },
52
+ },
53
+ },
54
+ },
55
+ async({ projectDir }) => {
56
+ const dir = projectDir ?? findProjectRoot(process.cwd())
57
+ const checks: Check[] = []
58
+
59
+ const pkgPath = join(dir, 'package.json')
60
+ let sdkInPkg = false
61
+ if(existsSync(pkgPath)) {
62
+ try {
63
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
64
+ sdkInPkg = Boolean(
65
+ pkg.dependencies?.['@reclaimprotocol/js-sdk'] ||
66
+ pkg.devDependencies?.['@reclaimprotocol/js-sdk'],
67
+ )
68
+ } catch{
69
+ // malformed package.json — fall through, reported below
70
+ }
71
+ }
72
+
73
+ const sdkInstalled = existsSync(
74
+ join(dir, 'node_modules', '@reclaimprotocol', 'js-sdk'),
75
+ )
76
+ checks.push({
77
+ name: 'sdk_dependency',
78
+ ok: sdkInPkg,
79
+ detail: sdkInPkg
80
+ ? '@reclaimprotocol/js-sdk is listed in package.json'
81
+ : '@reclaimprotocol/js-sdk is NOT in package.json — run ' +
82
+ '`npm install @reclaimprotocol/js-sdk`.',
83
+ })
84
+ checks.push({
85
+ name: 'sdk_installed',
86
+ ok: sdkInstalled,
87
+ detail: sdkInstalled
88
+ ? '@reclaimprotocol/js-sdk found in node_modules'
89
+ : 'Not found in node_modules — run your package manager\'s ' +
90
+ 'install command.',
91
+ })
92
+
93
+ const env = { ...readEnvFile(dir), ...process.env }
94
+ const appId = env.RECLAIM_APP_ID
95
+ const appSecret = env.RECLAIM_APP_SECRET
96
+ checks.push({
97
+ name: 'app_id',
98
+ ok: Boolean(appId),
99
+ detail: appId
100
+ ? `RECLAIM_APP_ID set (${appId})`
101
+ : 'RECLAIM_APP_ID missing (env or .env) — run ' +
102
+ 'issue_app_credentials or link_app_to_account.',
103
+ })
104
+ checks.push({
105
+ name: 'app_secret',
106
+ ok: Boolean(appSecret),
107
+ detail: appSecret
108
+ ? 'RECLAIM_APP_SECRET set'
109
+ : 'RECLAIM_APP_SECRET missing (env or .env).',
110
+ })
111
+
112
+ // Informational only — PUBLIC_URL is only needed for the async
113
+ // webhook callback, not the frontend-only flow, so a missing value
114
+ // doesn't fail the overall check.
115
+ const publicUrl = env.PUBLIC_URL
116
+ checks.push({
117
+ name: 'public_url',
118
+ ok: true,
119
+ detail: publicUrl
120
+ ? `PUBLIC_URL set (${publicUrl}) — used as the webhook callback ` +
121
+ 'base for setAppCallbackUrl.'
122
+ : 'PUBLIC_URL not set — fine if you only read the proof from the ' +
123
+ 'frontend flow, but the async webhook callback needs a ' +
124
+ 'publicly-reachable URL or it will never fire. Get one via a ' +
125
+ 'tunnel (e.g. `ngrok http <port>`) or by deploying to a ' +
126
+ 'dev/staging server you already have — ask the user which they ' +
127
+ 'prefer.',
128
+ })
129
+
130
+ if(appId) {
131
+ try {
132
+ const status = await client.getApplicationStatus(appId) as {
133
+ isLinked?: boolean
134
+ sandboxMode?: boolean
135
+ available?: boolean
136
+ }
137
+ const linked = status?.isLinked === true
138
+ checks.push({
139
+ name: 'app_status',
140
+ ok: status?.available !== false,
141
+ detail:
142
+ `isLinked=${linked}, sandboxMode=${status?.sandboxMode}, ` +
143
+ `available=${status?.available}` +
144
+ (linked
145
+ ? ''
146
+ : ' — run link_app_to_account to lift the sandbox limit.'),
147
+ })
148
+ } catch(err) {
149
+ checks.push({
150
+ name: 'app_status',
151
+ ok: false,
152
+ detail: `Could not reach status endpoint: ${
153
+ err instanceof Error ? err.message : String(err)
154
+ }`,
155
+ })
156
+ }
157
+ }
158
+
159
+ return { projectDir: dir, checks, ok: checks.every((c) => c.ok) }
160
+ },
161
+ )
162
+ }
@@ -8,7 +8,8 @@
8
8
 
9
9
  import { bytesToHex } from '@noble/hashes/utils.js'
10
10
  import {
11
- createClaimOnAttestor, type Logger,
11
+ createClaimOnAttestor,
12
+ type Logger
12
13
  } from '@reclaimprotocol/attestor-core'
13
14
  // Side-effect: make external domhandler nodes DOM-like so attestor-core's
14
15
  // bundled `xpath` accepts them (see patch-domhandler.ts). Must load before any
@@ -19,7 +20,10 @@ import { resolveEthKey } from '../mcp/tools/authenticate/eth-key.ts'
19
20
  import type { ReclaimProvider } from '../provider/schema.ts'
20
21
  import { installResponseDump } from './debug-response-dump.ts'
21
22
  import {
22
- buildHttpParams, ensureAttestorCrypto, parseClaim, resolveAttestorUrl,
23
+ buildHttpParams,
24
+ ensureAttestorCrypto,
25
+ parseClaim,
26
+ resolveAttestorUrl
23
27
  } from './proof-core.ts'
24
28
  import { ensureZkFiles } from './zk-files.ts'
25
29
 
@@ -129,7 +133,7 @@ const LOG_TAIL_LINES = 80
129
133
  function makeCaptureLogger(): {
130
134
  logger: unknown
131
135
  entries: AttestorLogEntry[]
132
- } {
136
+ } {
133
137
  const entries: AttestorLogEntry[] = []
134
138
  const make = (baseCtx: Record<string, unknown>): unknown => {
135
139
  const log =
@@ -93,6 +93,16 @@ const STRIP_REQUEST_HEADERS = new Set([
93
93
  'host',
94
94
  'content-length',
95
95
  'transfer-encoding',
96
+ // Per-session anti-CSRF tokens. Not caught by splitSecrets (no `-token`
97
+ // suffix), so without this they baked in as PUBLIC headers and went stale at
98
+ // verify time — the attestor replayed one user's captured CSRF for everyone,
99
+ // which the origin rejects. A CSRF token is never portable; drop it.
100
+ 'csrf',
101
+ 'x-csrf',
102
+ 'xsrf',
103
+ 'x-xsrf',
104
+ 'x-csrf-token',
105
+ 'xsrf-token',
96
106
  ])
97
107
 
98
108
  /** True for an HTTP/2 pseudo-header (`:authority`, `:method`, `:path`,
@@ -101,6 +111,19 @@ function isPseudoHeader(name: string): boolean {
101
111
  return name.startsWith(':')
102
112
  }
103
113
 
114
+ /** A header VALUE that is a per-session/request token — a UUID or a long opaque
115
+ * hex/base64 blob. Such a value is captured-user-specific: baked into the
116
+ * provider it goes stale and the origin rejects the replay for everyone else.
117
+ * Stable identifiers (a tenant/space id like `1237`, a timezone, client hints)
118
+ * are short/structured and don't match, so they survive. */
119
+ function isVolatileHeaderValue(value: string): boolean {
120
+ const v = value.trim()
121
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
122
+ .test(v)
123
+ || /^[0-9a-f]{16,}$/i.test(v)
124
+ || /^[A-Za-z0-9_-]{32,}$/.test(v)
125
+ }
126
+
104
127
  /** Drop the headers the attestor manages or that would 304 at proof time
105
128
  * (see {@link STRIP_REQUEST_HEADERS}); keep every other captured header so the
106
129
  * attestor's replay looks like the browser's request. */
@@ -109,7 +132,11 @@ function sanitizeRequestHeaders(
109
132
  ): Record<string, string> {
110
133
  const out: Record<string, string> = {}
111
134
  for(const [k, v] of Object.entries(headers)) {
112
- if(STRIP_REQUEST_HEADERS.has(k.toLowerCase()) || isPseudoHeader(k)) {
135
+ if(
136
+ STRIP_REQUEST_HEADERS.has(k.toLowerCase())
137
+ || isPseudoHeader(k)
138
+ || isVolatileHeaderValue(v)
139
+ ) {
113
140
  continue
114
141
  }
115
142
 
@@ -134,6 +161,37 @@ export function assertPortableMatches(
134
161
  redactions: ResponseRedaction[],
135
162
  targets: DraftTarget[],
136
163
  ): void {
164
+ // A baked email LOCAL-PART ("jane.doe@…") pins the provider to one user
165
+ // even when only the domain is captured as a param — the classic result of
166
+ // proving an email's domain substring. Reject it regardless of the declared
167
+ // targets (the local part is usually not itself a target, so the value scan
168
+ // below wouldn't catch it). Strip params + named groups + regex escapes
169
+ // first, then look for `localpart@` as a surviving literal.
170
+ const EMAIL_LOCAL_BAKED = /[A-Za-z0-9._%+-]{3,}@/
171
+ const deregex = (s: string): string => s
172
+ .replace(/\{\{[^}]*\}\}/g, '')
173
+ .replace(/\(\?<[^>]*>[^)]*\)/g, '')
174
+ .replace(/\\(.)/g, '$1')
175
+ const scanEmail = (text: string, where: string): void => {
176
+ if(EMAIL_LOCAL_BAKED.test(deregex(text))) {
177
+ throw new Error(
178
+ `drafted ${where} bakes a per-user email local-part as a literal — `
179
+ + 'the provider would be pinned to this user. Capture the WHOLE email '
180
+ + 'as a {{param}} (optionally OPRF-hashed), not just its domain.',
181
+ )
182
+ }
183
+ }
184
+
185
+ for(const [i, m] of matches.entries()) {
186
+ scanEmail(m.value, `responseMatch for "${targets[i]?.name ?? '?'}"`)
187
+ }
188
+
189
+ for(const r of redactions) {
190
+ if(r.regex) {
191
+ scanEmail(r.regex, 'responseRedaction regex')
192
+ }
193
+ }
194
+
137
195
  // Guard only values long enough to be plausibly user-specific; shorter runs
138
196
  // (single digits, 2-3 char tokens) collide with ordinary structural bytes.
139
197
  const MIN_ALNUM = 4