@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.
- package/lib/mcp/tools/agent/shared.js +30 -1
- package/lib/mcp/tools/authenticate/decrypt-key.js +1 -1
- package/lib/old/client.d.ts +31 -0
- package/lib/old/client.js +33 -0
- package/lib/old/tools/build.js +12 -0
- package/lib/old/tools/check-app-status.d.ts +8 -0
- package/lib/old/tools/check-app-status.js +27 -0
- package/lib/old/tools/issue-app-credentials.d.ts +11 -0
- package/lib/old/tools/issue-app-credentials.js +35 -0
- package/lib/old/tools/js-sdk-integration-guide.d.ts +2 -0
- package/lib/old/tools/js-sdk-integration-guide.js +27 -0
- package/lib/old/tools/link-app.d.ts +10 -0
- package/lib/old/tools/link-app.js +73 -0
- package/lib/old/tools/publish.js +26 -11
- package/lib/old/tools/scaffold-demo-server.d.ts +2 -0
- package/lib/old/tools/scaffold-demo-server.js +94 -0
- package/lib/old/tools/verify-sdk-setup.d.ts +9 -0
- package/lib/old/tools/verify-sdk-setup.js +132 -0
- package/lib/proof/attestor.js +2 -2
- package/lib/provider/draft.js +51 -1
- package/package.json +3 -1
- package/samples/demo-server/.env.example +12 -0
- package/samples/demo-server/README.md +24 -0
- package/samples/demo-server/package.json +13 -0
- package/samples/demo-server/server.js +156 -0
- package/skill/js-sdk-integration.md +119 -0
- package/src/cdp/launcher.ts +33 -33
- package/src/mcp/api/tools.ts +3 -4
- package/src/mcp/remap_errors.ts +1 -1
- package/src/mcp/tools/agent/shared.ts +35 -1
- package/src/mcp/tools/authenticate/authenticate.ts +1 -3
- package/src/mcp/tools/authenticate/decrypt-key.ts +1 -4
- package/src/old/client.ts +60 -0
- package/src/old/tools/build.ts +12 -0
- package/src/old/tools/check-app-status.ts +33 -0
- package/src/old/tools/issue-app-credentials.ts +44 -0
- package/src/old/tools/js-sdk-integration-guide.ts +38 -0
- package/src/old/tools/link-app.ts +93 -0
- package/src/old/tools/publish.ts +26 -12
- package/src/old/tools/scaffold-demo-server.ts +120 -0
- package/src/old/tools/verify-sdk-setup.ts +162 -0
- package/src/proof/attestor.ts +7 -3
- 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
|
+
}
|
package/src/proof/attestor.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { bytesToHex } from '@noble/hashes/utils.js'
|
|
10
10
|
import {
|
|
11
|
-
createClaimOnAttestor,
|
|
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,
|
|
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 =
|
package/src/provider/draft.ts
CHANGED
|
@@ -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(
|
|
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
|