@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,132 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { defineTool } from "../../mcp/server.js";
|
|
4
|
+
import { findProjectRoot } from "../../paths.js";
|
|
5
|
+
function readEnvFile(dir) {
|
|
6
|
+
const path = join(dir, '.env');
|
|
7
|
+
if (!existsSync(path)) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
12
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
13
|
+
if (m) {
|
|
14
|
+
out[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Health-check a project's @reclaimprotocol/js-sdk setup: SDK installed,
|
|
21
|
+
* RECLAIM_APP_ID/RECLAIM_APP_SECRET present (env or .env), and (if an appId
|
|
22
|
+
* is found) whether that app is actually linked/has quota, via the real
|
|
23
|
+
* check_app_status endpoint — not just static file checks.
|
|
24
|
+
*/
|
|
25
|
+
export function verifySdkSetupTool(client) {
|
|
26
|
+
return defineTool({
|
|
27
|
+
name: 'verify_reclaim_sdk_setup',
|
|
28
|
+
description: 'Health-check a project\'s @reclaimprotocol/js-sdk integration: SDK ' +
|
|
29
|
+
'installed, RECLAIM_APP_ID/RECLAIM_APP_SECRET present, and (if an ' +
|
|
30
|
+
'appId is found) whether that app is actually linked and has quota ' +
|
|
31
|
+
'left, by calling the real status endpoint.',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
projectDir: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Project directory (default: detected project root).',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}, async ({ projectDir }) => {
|
|
42
|
+
const dir = projectDir ?? findProjectRoot(process.cwd());
|
|
43
|
+
const checks = [];
|
|
44
|
+
const pkgPath = join(dir, 'package.json');
|
|
45
|
+
let sdkInPkg = false;
|
|
46
|
+
if (existsSync(pkgPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
49
|
+
sdkInPkg = Boolean(pkg.dependencies?.['@reclaimprotocol/js-sdk'] ||
|
|
50
|
+
pkg.devDependencies?.['@reclaimprotocol/js-sdk']);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// malformed package.json — fall through, reported below
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const sdkInstalled = existsSync(join(dir, 'node_modules', '@reclaimprotocol', 'js-sdk'));
|
|
57
|
+
checks.push({
|
|
58
|
+
name: 'sdk_dependency',
|
|
59
|
+
ok: sdkInPkg,
|
|
60
|
+
detail: sdkInPkg
|
|
61
|
+
? '@reclaimprotocol/js-sdk is listed in package.json'
|
|
62
|
+
: '@reclaimprotocol/js-sdk is NOT in package.json — run ' +
|
|
63
|
+
'`npm install @reclaimprotocol/js-sdk`.',
|
|
64
|
+
});
|
|
65
|
+
checks.push({
|
|
66
|
+
name: 'sdk_installed',
|
|
67
|
+
ok: sdkInstalled,
|
|
68
|
+
detail: sdkInstalled
|
|
69
|
+
? '@reclaimprotocol/js-sdk found in node_modules'
|
|
70
|
+
: 'Not found in node_modules — run your package manager\'s ' +
|
|
71
|
+
'install command.',
|
|
72
|
+
});
|
|
73
|
+
const env = { ...readEnvFile(dir), ...process.env };
|
|
74
|
+
const appId = env.RECLAIM_APP_ID;
|
|
75
|
+
const appSecret = env.RECLAIM_APP_SECRET;
|
|
76
|
+
checks.push({
|
|
77
|
+
name: 'app_id',
|
|
78
|
+
ok: Boolean(appId),
|
|
79
|
+
detail: appId
|
|
80
|
+
? `RECLAIM_APP_ID set (${appId})`
|
|
81
|
+
: 'RECLAIM_APP_ID missing (env or .env) — run ' +
|
|
82
|
+
'issue_app_credentials or link_app_to_account.',
|
|
83
|
+
});
|
|
84
|
+
checks.push({
|
|
85
|
+
name: 'app_secret',
|
|
86
|
+
ok: Boolean(appSecret),
|
|
87
|
+
detail: appSecret
|
|
88
|
+
? 'RECLAIM_APP_SECRET set'
|
|
89
|
+
: 'RECLAIM_APP_SECRET missing (env or .env).',
|
|
90
|
+
});
|
|
91
|
+
// Informational only — PUBLIC_URL is only needed for the async
|
|
92
|
+
// webhook callback, not the frontend-only flow, so a missing value
|
|
93
|
+
// doesn't fail the overall check.
|
|
94
|
+
const publicUrl = env.PUBLIC_URL;
|
|
95
|
+
checks.push({
|
|
96
|
+
name: 'public_url',
|
|
97
|
+
ok: true,
|
|
98
|
+
detail: publicUrl
|
|
99
|
+
? `PUBLIC_URL set (${publicUrl}) — used as the webhook callback ` +
|
|
100
|
+
'base for setAppCallbackUrl.'
|
|
101
|
+
: 'PUBLIC_URL not set — fine if you only read the proof from the ' +
|
|
102
|
+
'frontend flow, but the async webhook callback needs a ' +
|
|
103
|
+
'publicly-reachable URL or it will never fire. Get one via a ' +
|
|
104
|
+
'tunnel (e.g. `ngrok http <port>`) or by deploying to a ' +
|
|
105
|
+
'dev/staging server you already have — ask the user which they ' +
|
|
106
|
+
'prefer.',
|
|
107
|
+
});
|
|
108
|
+
if (appId) {
|
|
109
|
+
try {
|
|
110
|
+
const status = await client.getApplicationStatus(appId);
|
|
111
|
+
const linked = status?.isLinked === true;
|
|
112
|
+
checks.push({
|
|
113
|
+
name: 'app_status',
|
|
114
|
+
ok: status?.available !== false,
|
|
115
|
+
detail: `isLinked=${linked}, sandboxMode=${status?.sandboxMode}, ` +
|
|
116
|
+
`available=${status?.available}` +
|
|
117
|
+
(linked
|
|
118
|
+
? ''
|
|
119
|
+
: ' — run link_app_to_account to lift the sandbox limit.'),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
checks.push({
|
|
124
|
+
name: 'app_status',
|
|
125
|
+
ok: false,
|
|
126
|
+
detail: `Could not reach status endpoint: ${err instanceof Error ? err.message : String(err)}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { projectDir: dir, checks, ok: checks.every((c) => c.ok) };
|
|
131
|
+
});
|
|
132
|
+
}
|
package/lib/proof/attestor.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// revealed slice; jsonPath/xPath live on responseRedactions (what the prover
|
|
7
7
|
// reveals), never on responseMatches.
|
|
8
8
|
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
9
|
-
import { createClaimOnAttestor
|
|
9
|
+
import { createClaimOnAttestor } from '@reclaimprotocol/attestor-core';
|
|
10
10
|
// Side-effect: make external domhandler nodes DOM-like so attestor-core's
|
|
11
11
|
// bundled `xpath` accepts them (see patch-domhandler.ts). Must load before any
|
|
12
12
|
// proof runs its response-redaction extraction.
|
|
@@ -14,7 +14,7 @@ import "./patch-domhandler.js";
|
|
|
14
14
|
import { agentTraceEnabled } from "../consts.js";
|
|
15
15
|
import { resolveEthKey } from "../mcp/tools/authenticate/eth-key.js";
|
|
16
16
|
import { installResponseDump } from "./debug-response-dump.js";
|
|
17
|
-
import { buildHttpParams, ensureAttestorCrypto, parseClaim, resolveAttestorUrl
|
|
17
|
+
import { buildHttpParams, ensureAttestorCrypto, parseClaim, resolveAttestorUrl } from "./proof-core.js";
|
|
18
18
|
import { ensureZkFiles } from "./zk-files.js";
|
|
19
19
|
/** Trim a captured log stream to the last N entries — for shrinking the
|
|
20
20
|
* LLM-visible response while the full stream stays in the on-disk dump. */
|
package/lib/provider/draft.js
CHANGED
|
@@ -56,19 +56,43 @@ const STRIP_REQUEST_HEADERS = new Set([
|
|
|
56
56
|
'host',
|
|
57
57
|
'content-length',
|
|
58
58
|
'transfer-encoding',
|
|
59
|
+
// Per-session anti-CSRF tokens. Not caught by splitSecrets (no `-token`
|
|
60
|
+
// suffix), so without this they baked in as PUBLIC headers and went stale at
|
|
61
|
+
// verify time — the attestor replayed one user's captured CSRF for everyone,
|
|
62
|
+
// which the origin rejects. A CSRF token is never portable; drop it.
|
|
63
|
+
'csrf',
|
|
64
|
+
'x-csrf',
|
|
65
|
+
'xsrf',
|
|
66
|
+
'x-xsrf',
|
|
67
|
+
'x-csrf-token',
|
|
68
|
+
'xsrf-token',
|
|
59
69
|
]);
|
|
60
70
|
/** True for an HTTP/2 pseudo-header (`:authority`, `:method`, `:path`,
|
|
61
71
|
* `:scheme`) — never a valid request header the attestor can replay. */
|
|
62
72
|
function isPseudoHeader(name) {
|
|
63
73
|
return name.startsWith(':');
|
|
64
74
|
}
|
|
75
|
+
/** A header VALUE that is a per-session/request token — a UUID or a long opaque
|
|
76
|
+
* hex/base64 blob. Such a value is captured-user-specific: baked into the
|
|
77
|
+
* provider it goes stale and the origin rejects the replay for everyone else.
|
|
78
|
+
* Stable identifiers (a tenant/space id like `1237`, a timezone, client hints)
|
|
79
|
+
* are short/structured and don't match, so they survive. */
|
|
80
|
+
function isVolatileHeaderValue(value) {
|
|
81
|
+
const v = value.trim();
|
|
82
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
83
|
+
.test(v)
|
|
84
|
+
|| /^[0-9a-f]{16,}$/i.test(v)
|
|
85
|
+
|| /^[A-Za-z0-9_-]{32,}$/.test(v);
|
|
86
|
+
}
|
|
65
87
|
/** Drop the headers the attestor manages or that would 304 at proof time
|
|
66
88
|
* (see {@link STRIP_REQUEST_HEADERS}); keep every other captured header so the
|
|
67
89
|
* attestor's replay looks like the browser's request. */
|
|
68
90
|
function sanitizeRequestHeaders(headers) {
|
|
69
91
|
const out = {};
|
|
70
92
|
for (const [k, v] of Object.entries(headers)) {
|
|
71
|
-
if (STRIP_REQUEST_HEADERS.has(k.toLowerCase())
|
|
93
|
+
if (STRIP_REQUEST_HEADERS.has(k.toLowerCase())
|
|
94
|
+
|| isPseudoHeader(k)
|
|
95
|
+
|| isVolatileHeaderValue(v)) {
|
|
72
96
|
continue;
|
|
73
97
|
}
|
|
74
98
|
out[k] = v;
|
|
@@ -86,6 +110,32 @@ function sanitizeRequestHeaders(headers) {
|
|
|
86
110
|
* `{{param}}`. Throwing discards the draft so the agent re-captures or
|
|
87
111
|
* re-proposes rather than shipping a provider pinned to this one user. */
|
|
88
112
|
export function assertPortableMatches(matches, redactions, targets) {
|
|
113
|
+
// A baked email LOCAL-PART ("jane.doe@…") pins the provider to one user
|
|
114
|
+
// even when only the domain is captured as a param — the classic result of
|
|
115
|
+
// proving an email's domain substring. Reject it regardless of the declared
|
|
116
|
+
// targets (the local part is usually not itself a target, so the value scan
|
|
117
|
+
// below wouldn't catch it). Strip params + named groups + regex escapes
|
|
118
|
+
// first, then look for `localpart@` as a surviving literal.
|
|
119
|
+
const EMAIL_LOCAL_BAKED = /[A-Za-z0-9._%+-]{3,}@/;
|
|
120
|
+
const deregex = (s) => s
|
|
121
|
+
.replace(/\{\{[^}]*\}\}/g, '')
|
|
122
|
+
.replace(/\(\?<[^>]*>[^)]*\)/g, '')
|
|
123
|
+
.replace(/\\(.)/g, '$1');
|
|
124
|
+
const scanEmail = (text, where) => {
|
|
125
|
+
if (EMAIL_LOCAL_BAKED.test(deregex(text))) {
|
|
126
|
+
throw new Error(`drafted ${where} bakes a per-user email local-part as a literal — `
|
|
127
|
+
+ 'the provider would be pinned to this user. Capture the WHOLE email '
|
|
128
|
+
+ 'as a {{param}} (optionally OPRF-hashed), not just its domain.');
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
for (const [i, m] of matches.entries()) {
|
|
132
|
+
scanEmail(m.value, `responseMatch for "${targets[i]?.name ?? '?'}"`);
|
|
133
|
+
}
|
|
134
|
+
for (const r of redactions) {
|
|
135
|
+
if (r.regex) {
|
|
136
|
+
scanEmail(r.regex, 'responseRedaction regex');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
89
139
|
// Guard only values long enough to be plausibly user-specific; shorter runs
|
|
90
140
|
// (single digits, 2-3 char tokens) collide with ordinary structural bytes.
|
|
91
141
|
const MIN_ALNUM = 4;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/agent",
|
|
3
3
|
"title": "Reclaim Agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.13",
|
|
5
5
|
"description": "Reclaim Protocol's tools for agents — Reclaim Protocol lets your app verify **data points** about its users — a follower count, an account balance, an order history. This package gives your AI coding agent the tools to **find, build, test and fix the _data verification providers_** (the recipes that verify a data point) by inspecting and replaying captured requests, checking sessions, evaluate proofs, publishing updates, and much more.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"src",
|
|
42
42
|
"scripts",
|
|
43
43
|
"skill",
|
|
44
|
+
"samples",
|
|
44
45
|
"README.md"
|
|
45
46
|
],
|
|
46
47
|
"engines": {
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"dependencies": {
|
|
84
85
|
"@hapi/boom": "^9.1.4",
|
|
85
86
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
87
|
+
"@noble/hashes": "^2.2.0",
|
|
86
88
|
"@reclaimprotocol/attestor-core": "^5.0.8",
|
|
87
89
|
"@reclaimprotocol/client": "*",
|
|
88
90
|
"@reclaimprotocol/tls": "^0.1.3",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
RECLAIM_APP_ID=
|
|
2
|
+
RECLAIM_APP_SECRET=
|
|
3
|
+
PORT=3000
|
|
4
|
+
# REQUIRED for the webhook callback to land — Reclaim's servers can't reach
|
|
5
|
+
# localhost. Pick ONE:
|
|
6
|
+
# - ngrok (or similar tunnel): run `ngrok http 3000` in a separate
|
|
7
|
+
# terminal and paste the https://<id>.ngrok-free.app URL it prints here.
|
|
8
|
+
# - A dev/staging server you can deploy this to quickly: point this at
|
|
9
|
+
# that server's public URL instead.
|
|
10
|
+
# Without either, only the frontend-side flow (opening requestUrl) works —
|
|
11
|
+
# the webhook never fires.
|
|
12
|
+
PUBLIC_URL=
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Reclaim demo server
|
|
2
|
+
|
|
3
|
+
1. `npm install`
|
|
4
|
+
2. Fill in `.env` (copy from `.env.example`) — get `RECLAIM_APP_ID`/
|
|
5
|
+
`RECLAIM_APP_SECRET` from the `issue_app_credentials` MCP tool, or your
|
|
6
|
+
existing app.
|
|
7
|
+
3. Set `PUBLIC_URL` in `.env` to a URL Reclaim's servers can reach — pick
|
|
8
|
+
whichever fits:
|
|
9
|
+
- **ngrok**: in a separate terminal, run `ngrok http 3000` (install from
|
|
10
|
+
https://ngrok.com if needed) and copy the
|
|
11
|
+
`https://<id>.ngrok-free.app` URL it prints.
|
|
12
|
+
- **A dev/staging server you can deploy to quickly**: if you already have
|
|
13
|
+
one reachable from the internet, deploy this there and use its URL
|
|
14
|
+
instead — no local tunnel needed.
|
|
15
|
+
This step is REQUIRED — `localhost` is never reachable from Reclaim's
|
|
16
|
+
servers.
|
|
17
|
+
4. `npm start`, then open http://localhost:3000/verify (or your deployed
|
|
18
|
+
URL's `/verify`) and click "Start verification".
|
|
19
|
+
5. Watch the server logs for the `/webhooks/reclaim` payload once you
|
|
20
|
+
complete verification.
|
|
21
|
+
|
|
22
|
+
This demo defaults to providerId `"example"` — a built-in smoke-test
|
|
23
|
+
provider, so you can confirm the whole loop (request → open → callback)
|
|
24
|
+
works before wiring in a real provider.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { ReclaimProofRequest, verifyProof } from "@reclaimprotocol/js-sdk";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
const port = process.env.PORT || 3000;
|
|
8
|
+
const providerId = __PROVIDER_ID__;
|
|
9
|
+
// Left unpinned by default — set this if you need to pin a specific version.
|
|
10
|
+
const providerVersion = __PROVIDER_VERSION__;
|
|
11
|
+
|
|
12
|
+
app.set("trust proxy", true);
|
|
13
|
+
app.use(express.json({ limit: "2mb" }));
|
|
14
|
+
|
|
15
|
+
app.get("/health", (_req, res) => {
|
|
16
|
+
res.json({ ok: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.get("/verify", (_req, res) => {
|
|
20
|
+
res.type("html").send(`<!doctype html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="utf-8" />
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
25
|
+
<title>Reclaim demo verification</title>
|
|
26
|
+
<style>
|
|
27
|
+
body {
|
|
28
|
+
font-family: system-ui, sans-serif; max-width: 680px;
|
|
29
|
+
margin: 64px auto; padding: 0 20px; line-height: 1.5;
|
|
30
|
+
}
|
|
31
|
+
code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; }
|
|
32
|
+
button {
|
|
33
|
+
background: #111827; border: 0; border-radius: 8px; color: white;
|
|
34
|
+
cursor: pointer; font: inherit; padding: 12px 18px;
|
|
35
|
+
}
|
|
36
|
+
button:disabled { cursor: wait; opacity: 0.7; }
|
|
37
|
+
pre {
|
|
38
|
+
background: #f3f4f6; border-radius: 8px; overflow: auto;
|
|
39
|
+
padding: 12px;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<h1>Reclaim demo verification</h1>
|
|
45
|
+
<p>Uses provider <code>${providerId}</code>.</p>
|
|
46
|
+
<button id="start">Start verification</button>
|
|
47
|
+
<pre id="status">Ready.</pre>
|
|
48
|
+
<p>Webhook endpoint: <code>/webhooks/reclaim</code></p>
|
|
49
|
+
<script>
|
|
50
|
+
const button = document.getElementById("start");
|
|
51
|
+
const status = document.getElementById("status");
|
|
52
|
+
button.addEventListener("click", async () => {
|
|
53
|
+
button.disabled = true;
|
|
54
|
+
status.textContent = "Creating verification request...";
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch("/api/reclaim/request", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) throw new Error(await response.text());
|
|
60
|
+
const { requestUrl } = await response.json();
|
|
61
|
+
status.textContent = "Opening Reclaim verification...";
|
|
62
|
+
window.open(requestUrl, "_blank", "noopener,noreferrer");
|
|
63
|
+
status.textContent =
|
|
64
|
+
"Verification opened. Complete it in the Reclaim window.";
|
|
65
|
+
} catch (error) {
|
|
66
|
+
status.textContent =
|
|
67
|
+
error instanceof Error ? error.message : String(error);
|
|
68
|
+
button.disabled = false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
app.post("/api/reclaim/request", async (req, res, next) => {
|
|
77
|
+
try {
|
|
78
|
+
const appId = process.env.RECLAIM_APP_ID;
|
|
79
|
+
const appSecret = process.env.RECLAIM_APP_SECRET;
|
|
80
|
+
if (!appId || !appSecret) {
|
|
81
|
+
return res.status(500).json({
|
|
82
|
+
error: "Missing RECLAIM_APP_ID or RECLAIM_APP_SECRET",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const origin =
|
|
87
|
+
process.env.PUBLIC_URL || `${req.protocol}://${req.get("host")}`;
|
|
88
|
+
const reclaimProofRequest = await ReclaimProofRequest.init(
|
|
89
|
+
appId,
|
|
90
|
+
appSecret,
|
|
91
|
+
providerId,
|
|
92
|
+
{ providerVersion },
|
|
93
|
+
);
|
|
94
|
+
reclaimProofRequest.setAppCallbackUrl(
|
|
95
|
+
`${origin}/webhooks/reclaim`,
|
|
96
|
+
true,
|
|
97
|
+
);
|
|
98
|
+
reclaimProofRequest.setContext(
|
|
99
|
+
randomUUID(),
|
|
100
|
+
JSON.stringify({ createdAt: new Date().toISOString() }),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// getRequestUrl() is ASYNC in the current SDK — must be awaited.
|
|
104
|
+
const requestUrl = await reclaimProofRequest.getRequestUrl();
|
|
105
|
+
|
|
106
|
+
res.json({
|
|
107
|
+
request: reclaimProofRequest.toJsonString(),
|
|
108
|
+
requestUrl,
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
next(error);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
app.post("/webhooks/reclaim", async (req, res, next) => {
|
|
116
|
+
try {
|
|
117
|
+
// With jsonProofResponse: true + express.json() above, req.body is
|
|
118
|
+
// already a parsed Proof/Proof[]. Without either of those, the raw body
|
|
119
|
+
// is text and must be JSON.parse()'d (and URL-decoded if form-encoded)
|
|
120
|
+
// before calling verifyProof.
|
|
121
|
+
const proof = req.body;
|
|
122
|
+
|
|
123
|
+
// The second argument is REQUIRED — must include providerId.
|
|
124
|
+
const result = await verifyProof(proof, { providerId, providerVersion });
|
|
125
|
+
|
|
126
|
+
console.log("Received Reclaim verification:");
|
|
127
|
+
console.log(JSON.stringify(
|
|
128
|
+
{ isVerified: result.isVerified, data: result.data, error: result.error },
|
|
129
|
+
null,
|
|
130
|
+
2,
|
|
131
|
+
));
|
|
132
|
+
|
|
133
|
+
res.status(200).json({ received: true, verified: result.isVerified });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
next(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.use((err, _req, res, _next) => {
|
|
140
|
+
console.error(err);
|
|
141
|
+
res.status(500).json({
|
|
142
|
+
error: err instanceof Error ? err.message : "Internal server error",
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
app.listen(port, () => {
|
|
147
|
+
console.log(`Reclaim demo server listening on http://localhost:${port}`);
|
|
148
|
+
console.log(`Verify page: http://localhost:${port}/verify`);
|
|
149
|
+
console.log(`Webhook endpoint: http://localhost:${port}/webhooks/reclaim`);
|
|
150
|
+
if (!process.env.PUBLIC_URL) {
|
|
151
|
+
console.log(
|
|
152
|
+
"Set PUBLIC_URL (e.g. an ngrok URL) so the webhook is reachable " +
|
|
153
|
+
"from Reclaim's servers.",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @reclaimprotocol/js-sdk — backend integration guide
|
|
2
|
+
|
|
3
|
+
Keep `appSecret` on your BACKEND only. Everything below runs server-side;
|
|
4
|
+
the browser only ever sees the `requestUrl`.
|
|
5
|
+
|
|
6
|
+
## Testing locally, without deploying anything
|
|
7
|
+
|
|
8
|
+
- **providerId `"example"`**: a canonical built-in smoke-test provider —
|
|
9
|
+
no need to author/publish your own provider just to confirm the SDK
|
|
10
|
+
plumbing works end-to-end. Swap in your real providerId once this passes.
|
|
11
|
+
- **A public callback URL**: `setAppCallbackUrl` needs a URL Reclaim's
|
|
12
|
+
servers can reach over the public internet — `localhost` will never
|
|
13
|
+
receive the callback. Two ways to get one, pick whichever fits:
|
|
14
|
+
- **ngrok** (or a similar tunnel): `ngrok http 3000` (or your port)
|
|
15
|
+
alongside your local server, then set `PUBLIC_URL` to the
|
|
16
|
+
`https://<id>.ngrok-free.app` URL it prints. Zero-deploy, but the tunnel
|
|
17
|
+
URL is temporary.
|
|
18
|
+
- **A dev/staging server you can deploy to quickly**: if you already have
|
|
19
|
+
one reachable from the internet, just point `PUBLIC_URL` at it instead —
|
|
20
|
+
skips the extra local tool.
|
|
21
|
+
Without either, only the frontend-side flow (opening `requestUrl` and
|
|
22
|
+
reading the result client-side) works — the async webhook never lands.
|
|
23
|
+
- `scaffold_reclaim_demo_server` defaults `providerId` to `"example"`; its
|
|
24
|
+
README covers both callback-URL options above — use it to get a working
|
|
25
|
+
local loop before wiring in a real provider.
|
|
26
|
+
|
|
27
|
+
## 1. Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @reclaimprotocol/js-sdk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 2. Create a proof request (backend)
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import { ReclaimProofRequest } from "@reclaimprotocol/js-sdk";
|
|
37
|
+
|
|
38
|
+
const reclaimProofRequest = await ReclaimProofRequest.init(
|
|
39
|
+
process.env.RECLAIM_APP_ID,
|
|
40
|
+
process.env.RECLAIM_APP_SECRET,
|
|
41
|
+
providerId,
|
|
42
|
+
{
|
|
43
|
+
// acceptAiProviders: true, // allow AI-verified providers
|
|
44
|
+
// useAppClip: true, // mobile app-clip flow, not a browser popup
|
|
45
|
+
// providerVersion: "1.0.0", // pin a specific provider version
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Deliver the verified proof back to your server asynchronously instead of
|
|
50
|
+
// (or in addition to) reading it from the frontend callback:
|
|
51
|
+
reclaimProofRequest.setAppCallbackUrl(
|
|
52
|
+
`${process.env.PUBLIC_URL}/webhooks/reclaim`,
|
|
53
|
+
true, // jsonProofResponse: send the callback body as JSON, not form-encoded
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Optional: attach your own correlation id / metadata, retrievable later.
|
|
57
|
+
reclaimProofRequest.setContext(
|
|
58
|
+
sessionId,
|
|
59
|
+
JSON.stringify({ createdAt: Date.now() }),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// IMPORTANT: getRequestUrl() is ASYNC — await it.
|
|
63
|
+
const requestUrl = await reclaimProofRequest.getRequestUrl();
|
|
64
|
+
|
|
65
|
+
// Hand the requestUrl to the frontend to open in a new tab/window/webview.
|
|
66
|
+
// If you need to rehydrate this request object later (e.g. in the webhook
|
|
67
|
+
// handler, in a different process), persist reclaimProofRequest.toJsonString()
|
|
68
|
+
// (sync) and rehydrate with:
|
|
69
|
+
// const req = await ReclaimProofRequest.fromJsonString(json); // ASYNC
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 3. Frontend: open the request URL
|
|
73
|
+
|
|
74
|
+
Fetch `requestUrl` from your backend, then open it (popup/new tab/QR code) —
|
|
75
|
+
never construct it in the browser, since that would require the appSecret
|
|
76
|
+
client-side. Trigger the fetch+open from a real user click (not a page-load
|
|
77
|
+
effect) or some browsers block the popup.
|
|
78
|
+
|
|
79
|
+
## 4. Verify the callback (backend webhook)
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { verifyProof } from "@reclaimprotocol/js-sdk";
|
|
83
|
+
|
|
84
|
+
app.post("/webhooks/reclaim", express.json(), async (req, res) => {
|
|
85
|
+
// req.body is already a parsed object here because jsonProofResponse was
|
|
86
|
+
// true above AND express.json() parsed it. If you did NOT set
|
|
87
|
+
// jsonProofResponse (or aren't using express.json()), the body arrives as
|
|
88
|
+
// form-encoded/raw text and must be JSON.parse()'d (and decodeURIComponent'd
|
|
89
|
+
// if form-encoded) into a Proof / Proof[] before calling verifyProof.
|
|
90
|
+
const proof = req.body;
|
|
91
|
+
|
|
92
|
+
// The second argument is REQUIRED — at minimum providerId.
|
|
93
|
+
const result = await verifyProof(proof, {
|
|
94
|
+
providerId,
|
|
95
|
+
// providerVersion, // optional, pin if you pinned it in init()
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!result.isVerified) { // NOTE: isVerified, not is_verified
|
|
99
|
+
console.error("Proof verification failed:", result.error);
|
|
100
|
+
return res.status(400).json({ verified: false, error: result.error });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log("Verified data:", result.data, result.publicData);
|
|
104
|
+
res.status(200).json({ verified: true });
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Gotchas
|
|
109
|
+
|
|
110
|
+
- `getRequestUrl()` and `ReclaimProofRequest.fromJsonString()` are async —
|
|
111
|
+
a missing `await` silently hands you a Promise instead of a string/instance.
|
|
112
|
+
- `verifyProof`'s config arg is mandatory; omitting `providerId` throws.
|
|
113
|
+
- Prefer `setContext` over the deprecated `addContext` (same signature).
|
|
114
|
+
- `ReclaimProofRequest.init` needs network access to the Reclaim backend at
|
|
115
|
+
call time — don't call it at module load / cold-start if your platform pools
|
|
116
|
+
processes long-term; call it per-request instead.
|
|
117
|
+
- Use `check_app_status` (this MCP server) to confirm your app is linked
|
|
118
|
+
and not still sandbox-limited before debugging "verification never lands"
|
|
119
|
+
issues.
|
package/src/cdp/launcher.ts
CHANGED
|
@@ -28,42 +28,42 @@ export interface LaunchOptions {
|
|
|
28
28
|
*/
|
|
29
29
|
function browserCandidates(): string[] {
|
|
30
30
|
switch (platform()) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
case 'darwin':
|
|
32
|
+
return [
|
|
33
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
34
|
+
join(
|
|
35
|
+
homedir(),
|
|
36
|
+
'Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
37
|
+
),
|
|
38
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
39
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
40
|
+
]
|
|
41
|
+
case 'win32': {
|
|
42
|
+
const appData =
|
|
43
43
|
localAppData() ?? join(homedir(), 'AppData', 'Local')
|
|
44
|
-
|
|
44
|
+
return [
|
|
45
45
|
// System-wide Chrome
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
47
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
48
|
+
// Per-user Chrome ("Install for me only")
|
|
49
|
+
join(appData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
50
|
+
// Edge — Chromium-based, preinstalled on Win 10/11
|
|
51
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
52
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
case 'linux':
|
|
57
|
+
return [
|
|
58
|
+
'/usr/bin/google-chrome',
|
|
59
|
+
'/usr/bin/google-chrome-stable',
|
|
60
|
+
'/usr/bin/chromium',
|
|
61
|
+
'/usr/bin/chromium-browser',
|
|
62
|
+
'/snap/bin/chromium',
|
|
63
|
+
'/usr/bin/microsoft-edge',
|
|
64
|
+
]
|
|
65
|
+
default:
|
|
66
|
+
return []
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
package/src/mcp/api/tools.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import type { ReclaimClient } from '@reclaimprotocol/client'
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
IOperationArgs, IOperationId
|
|
4
|
+
IOperationArgs,
|
|
5
|
+
IOperationId
|
|
7
6
|
} from '@reclaimprotocol/client/openapi'
|
|
8
7
|
import { remapErrorAsResponse } from '../remap_errors.ts'
|
|
9
8
|
import { MCP_TOOLS } from './operations.gen.ts'
|