@reclaimprotocol/agent 0.1.11 → 0.1.12
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/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 +141 -0
- package/lib/old/tools/link-app.d.ts +10 -0
- package/lib/old/tools/link-app.js +72 -0
- package/lib/old/tools/publish.js +26 -6
- package/lib/old/tools/scaffold-demo-server.d.ts +2 -0
- package/lib/old/tools/scaffold-demo-server.js +242 -0
- package/lib/old/tools/verify-sdk-setup.d.ts +9 -0
- package/lib/old/tools/verify-sdk-setup.js +128 -0
- package/package.json +2 -1
- 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 +42 -0
- package/src/old/tools/js-sdk-integration-guide.ts +147 -0
- package/src/old/tools/link-app.ts +88 -0
- package/src/old/tools/publish.ts +30 -6
- package/src/old/tools/scaffold-demo-server.ts +265 -0
- package/src/old/tools/verify-sdk-setup.ts +158 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verified against `@reclaimprotocol/js-sdk` v5.8.1 source
|
|
5
|
+
* (`reclaim-js-sdk/src/Reclaim.ts`) — NOT copied unchecked from the archived
|
|
6
|
+
* `reclaim-mcp-server`'s guide, which predates several of these signatures.
|
|
7
|
+
* Corrections that matter most, all called out inline below:
|
|
8
|
+
* - `getRequestUrl()` and `ReclaimProofRequest.fromJsonString()` are ASYNC.
|
|
9
|
+
* - `setContext` is current; `addContext` is deprecated (calls setContext).
|
|
10
|
+
* - `verifyProof`'s second arg (`{ providerId, providerVersion? }`) is
|
|
11
|
+
* REQUIRED, not optional.
|
|
12
|
+
* - The verified result's flag is `isVerified` (camelCase), not
|
|
13
|
+
* `is_verified`.
|
|
14
|
+
* - A raw webhook body is TEXT — it must be JSON-parsed (and URL-decoded if
|
|
15
|
+
* form-encoded) into a `Proof`/`Proof[]` before calling `verifyProof`.
|
|
16
|
+
*/
|
|
17
|
+
const GUIDE = `# @reclaimprotocol/js-sdk — backend integration guide
|
|
18
|
+
|
|
19
|
+
Keep \`appSecret\` on your BACKEND only. Everything below runs server-side;
|
|
20
|
+
the browser only ever sees the \`requestUrl\`.
|
|
21
|
+
|
|
22
|
+
## Testing locally, without deploying anything
|
|
23
|
+
|
|
24
|
+
- **providerId \`"example"\`**: a canonical built-in smoke-test provider —
|
|
25
|
+
no need to author/publish your own provider just to confirm the SDK
|
|
26
|
+
plumbing works end-to-end. Swap in your real providerId once this passes.
|
|
27
|
+
- **ngrok for the callback URL**: \`setAppCallbackUrl\` needs a URL Reclaim's
|
|
28
|
+
servers can reach over the public internet — \`localhost\` will never
|
|
29
|
+
receive the callback. Run \`ngrok http 3000\` (or your port) alongside your
|
|
30
|
+
local server and set \`PUBLIC_URL\` to the \`https://<id>.ngrok-free.app\`
|
|
31
|
+
URL it prints. Without this, only the frontend-side flow (opening
|
|
32
|
+
\`requestUrl\` and polling/reading the result client-side) will work — the
|
|
33
|
+
async webhook never lands.
|
|
34
|
+
- \`scaffold_reclaim_demo_server\` defaults \`providerId\` to \`"example"\` and
|
|
35
|
+
its README calls out the ngrok step — use it to get a working local loop
|
|
36
|
+
before wiring in a real provider.
|
|
37
|
+
|
|
38
|
+
## 1. Install
|
|
39
|
+
|
|
40
|
+
\`\`\`bash
|
|
41
|
+
npm install @reclaimprotocol/js-sdk
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
## 2. Create a proof request (backend)
|
|
45
|
+
|
|
46
|
+
\`\`\`js
|
|
47
|
+
import { ReclaimProofRequest } from "@reclaimprotocol/js-sdk";
|
|
48
|
+
|
|
49
|
+
const reclaimProofRequest = await ReclaimProofRequest.init(
|
|
50
|
+
process.env.RECLAIM_APP_ID,
|
|
51
|
+
process.env.RECLAIM_APP_SECRET,
|
|
52
|
+
providerId,
|
|
53
|
+
{
|
|
54
|
+
// acceptAiProviders: true, // allow AI-verified providers
|
|
55
|
+
// useAppClip: true, // mobile app-clip flow instead of a browser popup
|
|
56
|
+
// providerVersion: "1.0.0", // pin a specific provider version
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Deliver the verified proof back to your server asynchronously instead of
|
|
61
|
+
// (or in addition to) reading it from the frontend callback:
|
|
62
|
+
reclaimProofRequest.setAppCallbackUrl(
|
|
63
|
+
\`\${process.env.PUBLIC_URL}/webhooks/reclaim\`,
|
|
64
|
+
true, // jsonProofResponse: send the callback body as JSON, not form-encoded
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Optional: attach your own correlation id / metadata, retrievable later.
|
|
68
|
+
reclaimProofRequest.setContext(sessionId, JSON.stringify({ createdAt: Date.now() }));
|
|
69
|
+
|
|
70
|
+
// IMPORTANT: getRequestUrl() is ASYNC — await it.
|
|
71
|
+
const requestUrl = await reclaimProofRequest.getRequestUrl();
|
|
72
|
+
|
|
73
|
+
// Hand the requestUrl to the frontend to open in a new tab/window/webview.
|
|
74
|
+
// If you need to rehydrate this request object later (e.g. in the webhook
|
|
75
|
+
// handler, in a different process), persist reclaimProofRequest.toJsonString()
|
|
76
|
+
// (sync) and rehydrate with:
|
|
77
|
+
// const req = await ReclaimProofRequest.fromJsonString(json); // ASYNC
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## 3. Frontend: open the request URL
|
|
81
|
+
|
|
82
|
+
Fetch \`requestUrl\` from your backend, then open it (popup/new tab/QR code) —
|
|
83
|
+
never construct it in the browser, since that would require the appSecret
|
|
84
|
+
client-side. Trigger the fetch+open from a real user click (not a page-load
|
|
85
|
+
effect) or some browsers block the popup.
|
|
86
|
+
|
|
87
|
+
## 4. Verify the callback (backend webhook)
|
|
88
|
+
|
|
89
|
+
\`\`\`js
|
|
90
|
+
import { verifyProof } from "@reclaimprotocol/js-sdk";
|
|
91
|
+
|
|
92
|
+
app.post("/webhooks/reclaim", express.json(), async (req, res) => {
|
|
93
|
+
// req.body is already a parsed object here because jsonProofResponse was
|
|
94
|
+
// true above AND express.json() parsed it. If you did NOT set
|
|
95
|
+
// jsonProofResponse (or aren't using express.json()), the body arrives as
|
|
96
|
+
// form-encoded/raw text and must be JSON.parse()'d (and decodeURIComponent'd
|
|
97
|
+
// if form-encoded) into a Proof / Proof[] before calling verifyProof.
|
|
98
|
+
const proof = req.body;
|
|
99
|
+
|
|
100
|
+
// The second argument is REQUIRED — at minimum providerId.
|
|
101
|
+
const result = await verifyProof(proof, {
|
|
102
|
+
providerId,
|
|
103
|
+
// providerVersion, // optional, pin if you pinned it in init()
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!result.isVerified) { // NOTE: isVerified, not is_verified
|
|
107
|
+
console.error("Proof verification failed:", result.error);
|
|
108
|
+
return res.status(400).json({ verified: false, error: result.error });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log("Verified data:", result.data, result.publicData);
|
|
112
|
+
res.status(200).json({ verified: true });
|
|
113
|
+
});
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Gotchas
|
|
117
|
+
|
|
118
|
+
- \`getRequestUrl()\` and \`ReclaimProofRequest.fromJsonString()\` are async —
|
|
119
|
+
a missing \`await\` silently hands you a Promise instead of a string/instance.
|
|
120
|
+
- \`verifyProof\`'s config arg is mandatory; omitting \`providerId\` throws.
|
|
121
|
+
- Prefer \`setContext\` over the deprecated \`addContext\` (same signature).
|
|
122
|
+
- \`ReclaimProofRequest.init\` needs network access to the Reclaim backend at
|
|
123
|
+
call time — don't call it at module load / cold-start if your platform pools
|
|
124
|
+
processes long-term; call it per-request instead.
|
|
125
|
+
- Use \`check_app_status\` (this MCP server) to confirm your app is linked
|
|
126
|
+
and not still sandbox-limited before debugging "verification never lands"
|
|
127
|
+
issues.
|
|
128
|
+
`
|
|
129
|
+
|
|
130
|
+
export function getJsSdkIntegrationGuideTool(): RegisteredTool {
|
|
131
|
+
return defineTool<Record<string, never>>(
|
|
132
|
+
{
|
|
133
|
+
name: 'get_js_sdk_integration_guide',
|
|
134
|
+
description:
|
|
135
|
+
'Return a verified, copy-pasteable guide for integrating ' +
|
|
136
|
+
'@reclaimprotocol/js-sdk (backend proof request + callback ' +
|
|
137
|
+
'verification), checked against the current SDK source rather than ' +
|
|
138
|
+
'assumed from older docs. Covers testing locally without deploying: ' +
|
|
139
|
+
'the built-in "example" providerId for a quick smoke test, and using ' +
|
|
140
|
+
'ngrok to get a callback URL Reclaim\'s servers can actually reach. ' +
|
|
141
|
+
'Use scaffold_reclaim_demo_server to generate a runnable local demo ' +
|
|
142
|
+
'instead of copy-pasting by hand.',
|
|
143
|
+
inputSchema: { type: 'object', properties: {} },
|
|
144
|
+
},
|
|
145
|
+
async() => ({ guide: GUIDE }),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Wallet } from 'ethers'
|
|
2
|
+
import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
3
|
+
import type { ReclaimOldClient } from '../client.ts'
|
|
4
|
+
|
|
5
|
+
/** Byte-for-byte the same format as the backend's `buildLinkMessage` in
|
|
6
|
+
* `application.controller.ts` — MUST stay in lockstep with it. */
|
|
7
|
+
function buildLinkMessage(appId: string, userId: string, nonce: string): string {
|
|
8
|
+
return `Link ${appId} to account ${userId} | nonce: ${nonce}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The uid the backend binds into the signed message — `req.user.uid` there.
|
|
12
|
+
* For a bearer identity this comes back from `POST /users/login`; for an eth
|
|
13
|
+
* identity, `OldIdentity.uid` (already `eth:<address>`) IS that uid. */
|
|
14
|
+
async function resolveUserId(client: ReclaimOldClient): Promise<string> {
|
|
15
|
+
const identity = client.getIdentity()
|
|
16
|
+
if(!identity) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Not authenticated against old devtools. Call reclaim_authenticate first.',
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if(identity.kind === 'eth') {
|
|
23
|
+
return identity.uid
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const res = await client.loginWithToken() as { user?: { uid?: string } }
|
|
27
|
+
const uid = res?.user?.uid
|
|
28
|
+
if(!uid) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'Could not resolve your account uid from POST /users/login — ' +
|
|
31
|
+
'unexpected response shape.',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return uid
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Link an unassociated app (minted via `issue_app_credentials`, or any
|
|
40
|
+
* eth-keypair app you already hold the private key for) to the currently
|
|
41
|
+
* authenticated dashboard account. Proves ownership by signing a one-time
|
|
42
|
+
* challenge with the app's private key — the private key itself is only
|
|
43
|
+
* ever used locally to sign, never sent to the server.
|
|
44
|
+
*/
|
|
45
|
+
export function linkAppTool(client: ReclaimOldClient): RegisteredTool {
|
|
46
|
+
return defineTool<{ appId: string, appSecret: string }>(
|
|
47
|
+
{
|
|
48
|
+
name: 'link_app_to_account',
|
|
49
|
+
description:
|
|
50
|
+
'Link an eth-keypair app to your authenticated dashboard account — ' +
|
|
51
|
+
'use this to keep using an appId/appSecret you already have ' +
|
|
52
|
+
'(instead of minting a fresh one via issue_app_credentials). ' +
|
|
53
|
+
'Requires reclaim_authenticate first. Fetches a one-time nonce, ' +
|
|
54
|
+
'signs `Link <appId> to account <userId> | nonce: <nonce>` locally ' +
|
|
55
|
+
'with `appSecret` (EIP-191, never transmitted), and submits the ' +
|
|
56
|
+
'signature. Fails with APP_ALREADY_LINKED if the app is already ' +
|
|
57
|
+
'linked to a (possibly different) account.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
appId: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'The eth-address appId to link.',
|
|
64
|
+
},
|
|
65
|
+
appSecret: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description:
|
|
68
|
+
'The app\'s raw eth private key — used only locally to sign ' +
|
|
69
|
+
'the link challenge, never sent to the server.',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ['appId', 'appSecret'],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
async({ appId, appSecret }) => {
|
|
76
|
+
const userId = await resolveUserId(client)
|
|
77
|
+
const { nonce } = await client.getLinkNonce()
|
|
78
|
+
const message = buildLinkMessage(appId, userId, nonce)
|
|
79
|
+
const signature = await new Wallet(appSecret).signMessage(message)
|
|
80
|
+
await client.linkAppToAccount(appId, signature, nonce)
|
|
81
|
+
return {
|
|
82
|
+
linked: true,
|
|
83
|
+
appId,
|
|
84
|
+
note: 'App linked to your account. Run check_app_status to confirm.',
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
}
|
package/src/old/tools/publish.ts
CHANGED
|
@@ -148,8 +148,15 @@ export function createVersionFromCaptureTool(
|
|
|
148
148
|
'Multiple proven drafts → one multi-request version (each ' +
|
|
149
149
|
'becomes a parallel `requestData` entry, all verified ' +
|
|
150
150
|
'together). Each item has the same shape as `provider`. Pass ' +
|
|
151
|
-
'this OR `provider`, not both.'
|
|
152
|
-
|
|
151
|
+
'this OR `provider`, not both. May be omitted (or empty) ' +
|
|
152
|
+
'together with `provider` ONLY when adding a version to an ' +
|
|
153
|
+
'EXISTING provider (`providerId` set) — this publishes with ' +
|
|
154
|
+
'zero `requestData` entries, for a pure customInjection/' +
|
|
155
|
+
'HAWKEYE provider that relies entirely on ' +
|
|
156
|
+
'`allowedInjectedRequestData` for validation instead of a ' +
|
|
157
|
+
'static requestData stub. Registering a brand-new provider ' +
|
|
158
|
+
'always needs at least one draft (its name comes from the ' +
|
|
159
|
+
'first one).',
|
|
153
160
|
items: {
|
|
154
161
|
type: 'object',
|
|
155
162
|
properties: {
|
|
@@ -301,17 +308,34 @@ export function createVersionFromCaptureTool(
|
|
|
301
308
|
async(args) => {
|
|
302
309
|
// Exactly one of `provider` / `providers` — normalize to an array so a
|
|
303
310
|
// multi-request capture publishes every request, not just the first.
|
|
311
|
+
// Omitting both is allowed too, but ONLY when adding a version to an
|
|
312
|
+
// EXISTING provider (`providerId` set): that publishes with zero
|
|
313
|
+
// `requestData` entries, which only makes sense for a pure
|
|
314
|
+
// customInjection/HAWKEYE provider whose claims are validated entirely
|
|
315
|
+
// via `allowedInjectedRequestData` instead of a static requestData stub.
|
|
316
|
+
// Registering a brand-new provider (no `providerId`) still needs at
|
|
317
|
+
// least one draft — its name comes from the first one.
|
|
304
318
|
const hasMany = Array.isArray(args.providers) && args.providers.length > 0
|
|
305
319
|
const hasOne = !!args.provider
|
|
306
|
-
if(hasMany
|
|
320
|
+
if(hasMany && hasOne) {
|
|
307
321
|
throw new Boom(
|
|
308
|
-
'Pass
|
|
309
|
-
'(an array of 1+ drafts).',
|
|
322
|
+
'Pass at most one of `provider` (a single draft) or `providers` ' +
|
|
323
|
+
'(an array of 1+ drafts) — not both.',
|
|
310
324
|
{ statusCode: 400 },
|
|
311
325
|
)
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
|
|
328
|
+
if(!hasMany && !hasOne && !args.providerId) {
|
|
329
|
+
throw new Boom(
|
|
330
|
+
'Pass at least one of `provider`/`providers` when registering a ' +
|
|
331
|
+
'new provider (its name comes from the first draft). Omitting ' +
|
|
332
|
+
'both is only allowed when adding a version to an EXISTING ' +
|
|
333
|
+
'provider via `providerId`.',
|
|
334
|
+
{ statusCode: 400 },
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const providers = hasMany ? args.providers! : (hasOne ? [args.provider!] : [])
|
|
315
339
|
|
|
316
340
|
const opts: OldProviderOpts = {
|
|
317
341
|
initialUrl: args.initialUrl,
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { isAbsolute, join } from 'node:path'
|
|
3
|
+
import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
4
|
+
import { findProjectRoot } from '../../paths.ts'
|
|
5
|
+
|
|
6
|
+
/** Corrected against `@reclaimprotocol/js-sdk` v5.8.1 (`Reclaim.ts`): awaits
|
|
7
|
+
* `getRequestUrl()`, uses `setContext` (not deprecated `addContext`), JSON-
|
|
8
|
+
* parses the raw webhook body before `verifyProof`, passes the required
|
|
9
|
+
* `{ providerId, providerVersion }` config, and reads `result.isVerified`
|
|
10
|
+
* (not `is_verified`). `providerVersion` is left unpinned (`undefined`) by
|
|
11
|
+
* default — the built-in "example" providerId isn't versioned, so pinning
|
|
12
|
+
* one is only meaningful once you swap in your own provider. */
|
|
13
|
+
function serverJs(providerId: string, providerVersion: string | undefined): string {
|
|
14
|
+
return `import express from "express";
|
|
15
|
+
import "dotenv/config";
|
|
16
|
+
import { ReclaimProofRequest, verifyProof } from "@reclaimprotocol/js-sdk";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
const port = process.env.PORT || 3000;
|
|
21
|
+
const providerId = ${JSON.stringify(providerId)};
|
|
22
|
+
// Left unpinned by default — set this if you need to pin a specific version.
|
|
23
|
+
const providerVersion = ${providerVersion ? JSON.stringify(providerVersion) : 'undefined'};
|
|
24
|
+
|
|
25
|
+
app.set("trust proxy", true);
|
|
26
|
+
app.use(express.json({ limit: "2mb" }));
|
|
27
|
+
|
|
28
|
+
app.get("/health", (_req, res) => {
|
|
29
|
+
res.json({ ok: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.get("/verify", (_req, res) => {
|
|
33
|
+
res.type("html").send(\`<!doctype html>
|
|
34
|
+
<html lang="en">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8" />
|
|
37
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
38
|
+
<title>Reclaim demo verification</title>
|
|
39
|
+
<style>
|
|
40
|
+
body { font-family: system-ui, sans-serif; max-width: 680px; margin: 64px auto; padding: 0 20px; line-height: 1.5; }
|
|
41
|
+
code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; }
|
|
42
|
+
button { background: #111827; border: 0; border-radius: 8px; color: white; cursor: pointer; font: inherit; padding: 12px 18px; }
|
|
43
|
+
button:disabled { cursor: wait; opacity: 0.7; }
|
|
44
|
+
pre { background: #f3f4f6; border-radius: 8px; overflow: auto; padding: 12px; }
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<h1>Reclaim demo verification</h1>
|
|
49
|
+
<p>Uses provider <code>\${providerId}</code>.</p>
|
|
50
|
+
<button id="start">Start verification</button>
|
|
51
|
+
<pre id="status">Ready.</pre>
|
|
52
|
+
<p>Webhook endpoint: <code>/webhooks/reclaim</code></p>
|
|
53
|
+
<script>
|
|
54
|
+
const button = document.getElementById("start");
|
|
55
|
+
const status = document.getElementById("status");
|
|
56
|
+
button.addEventListener("click", async () => {
|
|
57
|
+
button.disabled = true;
|
|
58
|
+
status.textContent = "Creating verification request...";
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch("/api/reclaim/request", { method: "POST" });
|
|
61
|
+
if (!response.ok) throw new Error(await response.text());
|
|
62
|
+
const { requestUrl } = await response.json();
|
|
63
|
+
status.textContent = "Opening Reclaim verification...";
|
|
64
|
+
window.open(requestUrl, "_blank", "noopener,noreferrer");
|
|
65
|
+
status.textContent = "Verification opened. Complete it in the Reclaim window.";
|
|
66
|
+
} catch (error) {
|
|
67
|
+
status.textContent = 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({ error: "Missing RECLAIM_APP_ID or RECLAIM_APP_SECRET" });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const origin = process.env.PUBLIC_URL || \`\${req.protocol}://\${req.get("host")}\`;
|
|
85
|
+
const reclaimProofRequest = await ReclaimProofRequest.init(appId, appSecret, providerId, {
|
|
86
|
+
providerVersion,
|
|
87
|
+
});
|
|
88
|
+
reclaimProofRequest.setAppCallbackUrl(\`\${origin}/webhooks/reclaim\`, true);
|
|
89
|
+
reclaimProofRequest.setContext(
|
|
90
|
+
randomUUID(),
|
|
91
|
+
JSON.stringify({ createdAt: new Date().toISOString() }),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// getRequestUrl() is ASYNC in the current SDK — must be awaited.
|
|
95
|
+
const requestUrl = await reclaimProofRequest.getRequestUrl();
|
|
96
|
+
|
|
97
|
+
res.json({
|
|
98
|
+
request: reclaimProofRequest.toJsonString(),
|
|
99
|
+
requestUrl,
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
next(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.post("/webhooks/reclaim", async (req, res, next) => {
|
|
107
|
+
try {
|
|
108
|
+
// With jsonProofResponse: true + express.json() above, req.body is
|
|
109
|
+
// already a parsed Proof/Proof[]. Without either of those, the raw body
|
|
110
|
+
// is text and must be JSON.parse()'d (and URL-decoded if form-encoded)
|
|
111
|
+
// before calling verifyProof.
|
|
112
|
+
const proof = req.body;
|
|
113
|
+
|
|
114
|
+
// The second argument is REQUIRED — must include providerId.
|
|
115
|
+
const result = await verifyProof(proof, { providerId, providerVersion });
|
|
116
|
+
|
|
117
|
+
console.log("Received Reclaim verification:");
|
|
118
|
+
console.log(JSON.stringify({ isVerified: result.isVerified, data: result.data, error: result.error }, null, 2));
|
|
119
|
+
|
|
120
|
+
res.status(200).json({ received: true, verified: result.isVerified });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
next(error);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
app.use((err, _req, res, _next) => {
|
|
127
|
+
console.error(err);
|
|
128
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "Internal server error" });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
app.listen(port, () => {
|
|
132
|
+
console.log(\`Reclaim demo server listening on http://localhost:\${port}\`);
|
|
133
|
+
console.log(\`Verify page: http://localhost:\${port}/verify\`);
|
|
134
|
+
console.log(\`Webhook endpoint: http://localhost:\${port}/webhooks/reclaim\`);
|
|
135
|
+
if (!process.env.PUBLIC_URL) {
|
|
136
|
+
console.log("Set PUBLIC_URL (e.g. an ngrok URL) so the webhook is reachable from Reclaim's servers.");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const PACKAGE_JSON = (name: string) => JSON.stringify(
|
|
143
|
+
{
|
|
144
|
+
name,
|
|
145
|
+
private: true,
|
|
146
|
+
type: 'module',
|
|
147
|
+
scripts: { start: 'node server.js' },
|
|
148
|
+
dependencies: {
|
|
149
|
+
'@reclaimprotocol/js-sdk': '^5.8.1',
|
|
150
|
+
dotenv: '^16.4.5',
|
|
151
|
+
express: '^4.21.2',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
null,
|
|
155
|
+
2,
|
|
156
|
+
) + '\n'
|
|
157
|
+
|
|
158
|
+
const ENV_EXAMPLE = `RECLAIM_APP_ID=
|
|
159
|
+
RECLAIM_APP_SECRET=
|
|
160
|
+
PORT=3000
|
|
161
|
+
# REQUIRED for the webhook callback to land — Reclaim's servers can't reach
|
|
162
|
+
# localhost. Run: ngrok http 3000 (in a separate terminal) and paste the
|
|
163
|
+
# https://<id>.ngrok-free.app URL it prints here. Without this, only the
|
|
164
|
+
# frontend-side flow works (opening requestUrl) — the webhook never fires.
|
|
165
|
+
PUBLIC_URL=
|
|
166
|
+
`
|
|
167
|
+
|
|
168
|
+
const README = `# Reclaim demo server
|
|
169
|
+
|
|
170
|
+
1. \`npm install\`
|
|
171
|
+
2. Fill in \`.env\` (copy from \`.env.example\`) — get \`RECLAIM_APP_ID\`/
|
|
172
|
+
\`RECLAIM_APP_SECRET\` from the \`issue_app_credentials\` MCP tool, or your
|
|
173
|
+
existing app.
|
|
174
|
+
3. In a separate terminal, run \`ngrok http 3000\` (install from
|
|
175
|
+
https://ngrok.com if needed) and copy the \`https://<id>.ngrok-free.app\`
|
|
176
|
+
URL it prints into \`PUBLIC_URL\` in \`.env\`. This is REQUIRED — Reclaim's
|
|
177
|
+
servers must reach your webhook over the public internet, and
|
|
178
|
+
\`localhost\` is not reachable from there.
|
|
179
|
+
4. \`npm start\`, then open http://localhost:3000/verify and click "Start
|
|
180
|
+
verification".
|
|
181
|
+
5. Watch the server logs for the \`/webhooks/reclaim\` payload once you
|
|
182
|
+
complete verification.
|
|
183
|
+
|
|
184
|
+
This demo defaults to providerId \`"example"\` — a built-in smoke-test
|
|
185
|
+
provider, so you can confirm the whole loop (request → open → callback)
|
|
186
|
+
works before wiring in a real provider.
|
|
187
|
+
`
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Scaffold a small, runnable Express demo that exercises the full
|
|
191
|
+
* @reclaimprotocol/js-sdk flow end-to-end: request a proof (backend), open it
|
|
192
|
+
* (frontend), and verify the async webhook callback (backend). Corrected
|
|
193
|
+
* against the CURRENT SDK API (see js-sdk-integration-guide) rather than
|
|
194
|
+
* older examples that predate several signature changes.
|
|
195
|
+
*/
|
|
196
|
+
const DEFAULT_PROVIDER_ID = 'example'
|
|
197
|
+
|
|
198
|
+
export function scaffoldDemoServerTool(): RegisteredTool {
|
|
199
|
+
return defineTool<{
|
|
200
|
+
targetDir: string
|
|
201
|
+
providerId?: string
|
|
202
|
+
providerVersion?: string
|
|
203
|
+
}>(
|
|
204
|
+
{
|
|
205
|
+
name: 'scaffold_reclaim_demo_server',
|
|
206
|
+
description:
|
|
207
|
+
'Write a small runnable Express demo (server.js, package.json, ' +
|
|
208
|
+
'.env.example, README.md) that requests a Reclaim proof and ' +
|
|
209
|
+
'verifies the async webhook callback, using the CURRENT ' +
|
|
210
|
+
'@reclaimprotocol/js-sdk API. Defaults `providerId` to the built-in ' +
|
|
211
|
+
'"example" smoke-test provider so you can confirm the whole loop ' +
|
|
212
|
+
'without authoring one first, and requires ngrok (or similar) to ' +
|
|
213
|
+
'expose the local server so the webhook callback can reach it — ' +
|
|
214
|
+
'the generated README walks through that step. Use ' +
|
|
215
|
+
'get_js_sdk_integration_guide for the annotated write-up of the ' +
|
|
216
|
+
'same flow.',
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: {
|
|
220
|
+
targetDir: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description:
|
|
223
|
+
'Directory to write the demo into (created if missing). ' +
|
|
224
|
+
'Relative paths resolve against the project root.',
|
|
225
|
+
},
|
|
226
|
+
providerId: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
description:
|
|
229
|
+
'Provider id the demo requests a proof for (default ' +
|
|
230
|
+
'"example", a built-in smoke-test provider — good for ' +
|
|
231
|
+
'confirming the flow works before wiring in a real one).',
|
|
232
|
+
},
|
|
233
|
+
providerVersion: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description:
|
|
236
|
+
'Provider version to pin (default: unpinned — "example" ' +
|
|
237
|
+
'isn\'t versioned, so only set this once you swap in your ' +
|
|
238
|
+
'own provider).',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
required: ['targetDir'],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
async({ targetDir, providerId, providerVersion }) => {
|
|
245
|
+
const dir = isAbsolute(targetDir)
|
|
246
|
+
? targetDir
|
|
247
|
+
: join(findProjectRoot(process.cwd()), targetDir)
|
|
248
|
+
mkdirSync(dir, { recursive: true })
|
|
249
|
+
|
|
250
|
+
const id = providerId ?? DEFAULT_PROVIDER_ID
|
|
251
|
+
writeFileSync(join(dir, 'server.js'), serverJs(id, providerVersion))
|
|
252
|
+
writeFileSync(join(dir, 'package.json'), PACKAGE_JSON('reclaim-demo-server'))
|
|
253
|
+
writeFileSync(join(dir, '.env.example'), ENV_EXAMPLE)
|
|
254
|
+
writeFileSync(join(dir, 'README.md'), README)
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
dir,
|
|
258
|
+
providerId: id,
|
|
259
|
+
files: ['server.js', 'package.json', '.env.example', 'README.md'],
|
|
260
|
+
next: 'cd into the directory, npm install, copy .env.example to .env ' +
|
|
261
|
+
'and fill it in (including PUBLIC_URL from ngrok), then npm start.',
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
}
|