@reclaimprotocol/agent 0.1.0-dev.1
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/README.md +287 -0
- package/lib/cdp/capture.d.ts +22 -0
- package/lib/cdp/capture.js +159 -0
- package/lib/cdp/client.d.ts +9 -0
- package/lib/cdp/client.js +11 -0
- package/lib/cdp/eviction.d.ts +16 -0
- package/lib/cdp/eviction.js +38 -0
- package/lib/cdp/launcher.d.ts +14 -0
- package/lib/cdp/launcher.js +102 -0
- package/lib/cdp/tabs.d.ts +6 -0
- package/lib/cdp/tabs.js +10 -0
- package/lib/cli/args.d.ts +14 -0
- package/lib/cli/args.js +19 -0
- package/lib/cli/commands.d.ts +1 -0
- package/lib/cli/commands.js +19 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +7 -0
- package/lib/config/store.d.ts +45 -0
- package/lib/config/store.js +46 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +7 -0
- package/lib/logger.d.ts +2 -0
- package/lib/logger.js +7 -0
- package/lib/mcp/api/operations.gen.d.ts +152 -0
- package/lib/mcp/api/operations.gen.js +2300 -0
- package/lib/mcp/api/tools.d.ts +3 -0
- package/lib/mcp/api/tools.js +36 -0
- package/lib/mcp/remap_errors.d.ts +2 -0
- package/lib/mcp/remap_errors.js +27 -0
- package/lib/mcp/server.d.ts +38 -0
- package/lib/mcp/server.js +76 -0
- package/lib/mcp/start.d.ts +1 -0
- package/lib/mcp/start.js +63 -0
- package/lib/mcp/tools/agent/analyze.d.ts +3 -0
- package/lib/mcp/tools/agent/analyze.js +26 -0
- package/lib/mcp/tools/agent/attach.d.ts +11 -0
- package/lib/mcp/tools/agent/attach.js +72 -0
- package/lib/mcp/tools/agent/build.d.ts +33 -0
- package/lib/mcp/tools/agent/build.js +74 -0
- package/lib/mcp/tools/agent/capture.d.ts +10 -0
- package/lib/mcp/tools/agent/capture.js +142 -0
- package/lib/mcp/tools/agent/eval.d.ts +5 -0
- package/lib/mcp/tools/agent/eval.js +127 -0
- package/lib/mcp/tools/agent/find.d.ts +3 -0
- package/lib/mcp/tools/agent/find.js +60 -0
- package/lib/mcp/tools/agent/proof.d.ts +3 -0
- package/lib/mcp/tools/agent/proof.js +214 -0
- package/lib/mcp/tools/agent/propose.d.ts +9 -0
- package/lib/mcp/tools/agent/propose.js +75 -0
- package/lib/mcp/tools/agent/publish.d.ts +3 -0
- package/lib/mcp/tools/agent/publish.js +122 -0
- package/lib/mcp/tools/agent/replay.d.ts +3 -0
- package/lib/mcp/tools/agent/replay.js +46 -0
- package/lib/mcp/tools/agent/session.d.ts +10 -0
- package/lib/mcp/tools/agent/session.js +29 -0
- package/lib/mcp/tools/credentials/authenticate.d.ts +3 -0
- package/lib/mcp/tools/credentials/authenticate.js +108 -0
- package/lib/mcp/tools/credentials/build.d.ts +16 -0
- package/lib/mcp/tools/credentials/build.js +33 -0
- package/lib/mcp/tools/credentials/cache.d.ts +70 -0
- package/lib/mcp/tools/credentials/cache.js +114 -0
- package/lib/mcp/tools/credentials/env-file.d.ts +25 -0
- package/lib/mcp/tools/credentials/env-file.js +71 -0
- package/lib/mcp/tools/credentials/import.d.ts +2 -0
- package/lib/mcp/tools/credentials/import.js +52 -0
- package/lib/mcp/tools/credentials/issue.d.ts +2 -0
- package/lib/mcp/tools/credentials/issue.js +84 -0
- package/lib/mcp/tools/credentials/list-cached.d.ts +2 -0
- package/lib/mcp/tools/credentials/list-cached.js +55 -0
- package/lib/mcp/tools/credentials/register.d.ts +18 -0
- package/lib/mcp/tools/credentials/register.js +30 -0
- package/lib/mcp/tools/credentials/resolve-owner-key.d.ts +12 -0
- package/lib/mcp/tools/credentials/resolve-owner-key.js +68 -0
- package/lib/mcp/tools/credentials/session-cache.d.ts +14 -0
- package/lib/mcp/tools/credentials/session-cache.js +19 -0
- package/lib/mcp/tools/credentials/status.d.ts +3 -0
- package/lib/mcp/tools/credentials/status.js +62 -0
- package/lib/mcp-bin.d.ts +1 -0
- package/lib/mcp-bin.js +7 -0
- package/lib/old/client.d.ts +67 -0
- package/lib/old/client.js +146 -0
- package/lib/old/flag.d.ts +11 -0
- package/lib/old/flag.js +13 -0
- package/lib/old/identity.d.ts +22 -0
- package/lib/old/identity.js +47 -0
- package/lib/old/instructions.d.ts +7 -0
- package/lib/old/instructions.js +29 -0
- package/lib/old/login-cdp.d.ts +23 -0
- package/lib/old/login-cdp.js +125 -0
- package/lib/old/to-register.d.ts +47 -0
- package/lib/old/to-register.js +133 -0
- package/lib/old/tools/authenticate.d.ts +18 -0
- package/lib/old/tools/authenticate.js +127 -0
- package/lib/old/tools/build.d.ts +9 -0
- package/lib/old/tools/build.js +16 -0
- package/lib/old/tools/me-providers.d.ts +10 -0
- package/lib/old/tools/me-providers.js +32 -0
- package/lib/old/tools/publish.d.ts +16 -0
- package/lib/old/tools/publish.js +97 -0
- package/lib/paths.d.ts +11 -0
- package/lib/paths.js +32 -0
- package/lib/pkg.d.ts +3 -0
- package/lib/pkg.js +13 -0
- package/lib/proof/attestor.d.ts +39 -0
- package/lib/proof/attestor.js +207 -0
- package/lib/proof/replay.d.ts +8 -0
- package/lib/proof/replay.js +73 -0
- package/lib/provider/constraints.d.ts +2 -0
- package/lib/provider/constraints.js +122 -0
- package/lib/provider/draft.d.ts +6 -0
- package/lib/provider/draft.js +68 -0
- package/lib/provider/graphql.d.ts +2 -0
- package/lib/provider/graphql.js +35 -0
- package/lib/provider/matcher.d.ts +36 -0
- package/lib/provider/matcher.js +436 -0
- package/lib/provider/normalize.d.ts +4 -0
- package/lib/provider/normalize.js +51 -0
- package/lib/provider/schema.d.ts +76 -0
- package/lib/provider/schema.js +1 -0
- package/lib/provider/secrets.d.ts +5 -0
- package/lib/provider/secrets.js +22 -0
- package/lib/provider/to-version.d.ts +22 -0
- package/lib/provider/to-version.js +68 -0
- package/lib/types/openapi.d.ts +4 -0
- package/lib/types/openapi.js +1 -0
- package/package.json +88 -0
- package/scripts/download-zk-files.ts +42 -0
- package/scripts/prepend-shebang.ts +17 -0
- package/skill/SKILL.md +248 -0
- package/src/cdp/capture.ts +215 -0
- package/src/cdp/client.ts +20 -0
- package/src/cdp/eviction.ts +52 -0
- package/src/cdp/launcher.ts +133 -0
- package/src/cdp/tabs.ts +22 -0
- package/src/cli/args.ts +20 -0
- package/src/cli/commands.ts +27 -0
- package/src/cli.ts +7 -0
- package/src/config/store.ts +86 -0
- package/src/index.ts +12 -0
- package/src/logger.ts +8 -0
- package/src/mcp/api/operations.gen.ts +2397 -0
- package/src/mcp/api/tools.ts +45 -0
- package/src/mcp/remap_errors.ts +35 -0
- package/src/mcp/server.ts +119 -0
- package/src/mcp/start.ts +78 -0
- package/src/mcp/tools/agent/analyze.ts +34 -0
- package/src/mcp/tools/agent/attach.ts +95 -0
- package/src/mcp/tools/agent/build.ts +99 -0
- package/src/mcp/tools/agent/capture.ts +202 -0
- package/src/mcp/tools/agent/eval.ts +161 -0
- package/src/mcp/tools/agent/find.ts +73 -0
- package/src/mcp/tools/agent/proof.ts +272 -0
- package/src/mcp/tools/agent/propose.ts +111 -0
- package/src/mcp/tools/agent/publish.ts +155 -0
- package/src/mcp/tools/agent/replay.ts +58 -0
- package/src/mcp/tools/agent/session.ts +46 -0
- package/src/mcp/tools/credentials/authenticate.ts +135 -0
- package/src/mcp/tools/credentials/build.ts +37 -0
- package/src/mcp/tools/credentials/cache.ts +161 -0
- package/src/mcp/tools/credentials/env-file.ts +101 -0
- package/src/mcp/tools/credentials/import.ts +72 -0
- package/src/mcp/tools/credentials/issue.ts +107 -0
- package/src/mcp/tools/credentials/list-cached.ts +66 -0
- package/src/mcp/tools/credentials/register.ts +49 -0
- package/src/mcp/tools/credentials/resolve-owner-key.ts +78 -0
- package/src/mcp/tools/credentials/session-cache.ts +31 -0
- package/src/mcp/tools/credentials/status.ts +82 -0
- package/src/mcp-bin.ts +7 -0
- package/src/old/client.ts +236 -0
- package/src/old/flag.ts +13 -0
- package/src/old/identity.ts +56 -0
- package/src/old/instructions.ts +30 -0
- package/src/old/login-cdp.ts +153 -0
- package/src/old/to-register.ts +234 -0
- package/src/old/tools/authenticate.ts +152 -0
- package/src/old/tools/build.ts +21 -0
- package/src/old/tools/me-providers.ts +42 -0
- package/src/old/tools/publish.ts +135 -0
- package/src/paths.ts +37 -0
- package/src/pkg.ts +24 -0
- package/src/proof/attestor.ts +286 -0
- package/src/proof/replay.ts +102 -0
- package/src/provider/constraints.ts +150 -0
- package/src/provider/draft.ts +105 -0
- package/src/provider/graphql.ts +42 -0
- package/src/provider/matcher.ts +616 -0
- package/src/provider/normalize.ts +65 -0
- package/src/provider/schema.ts +92 -0
- package/src/provider/secrets.ts +29 -0
- package/src/provider/to-version.ts +90 -0
- package/src/types/openapi.ts +7 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { defineTool } from "../../mcp/server.js";
|
|
2
|
+
import { bumpPatchString, extractVersions, providerToConfigBody, providerToRegisterBody, } from "../to-register.js";
|
|
3
|
+
const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture';
|
|
4
|
+
/**
|
|
5
|
+
* Publish a drafted provider to the OLD devtools backend. Keeps the builder
|
|
6
|
+
* tool NAME (`create_provider_version_from_capture`) so the skill flow is
|
|
7
|
+
* unchanged, but the behaviour differs:
|
|
8
|
+
*
|
|
9
|
+
* - WITHOUT `providerId` → `POST /api/providers/register`: creates a
|
|
10
|
+
* brand-new provider + version 1.0.0. Re-running creates a DUPLICATE.
|
|
11
|
+
* - WITH `providerId` → `POST /api/providers/:providerId/config`: adds a
|
|
12
|
+
* new version to an existing provider (auto-bumps the patch above the
|
|
13
|
+
* highest existing version).
|
|
14
|
+
*
|
|
15
|
+
* Get a `providerId` from `get_me_providers`.
|
|
16
|
+
*/
|
|
17
|
+
export function createVersionFromCaptureTool(client) {
|
|
18
|
+
return defineTool({
|
|
19
|
+
name: 'create_provider_version_from_capture',
|
|
20
|
+
description: 'Publish a drafted ReclaimProvider to the OLD devtools backend. ' +
|
|
21
|
+
'Pass the `provider` object from propose_provider VERBATIM and the ' +
|
|
22
|
+
'`initialUrl` the user landed on (the login/landing page, NOT the ' +
|
|
23
|
+
'API url). \n\n' +
|
|
24
|
+
'Omit `providerId` to REGISTER A NEW provider (creates it + version ' +
|
|
25
|
+
'1.0.0; re-running creates a duplicate). Pass `providerId` (from ' +
|
|
26
|
+
'get_me_providers) to add a NEW VERSION to that existing provider ' +
|
|
27
|
+
'(the patch is auto-bumped). \n\n' +
|
|
28
|
+
'Captured requests must be GET/POST. OPRF redactions are supported.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
provider: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
description: 'The `provider` object from propose_provider, verbatim.',
|
|
35
|
+
properties: {
|
|
36
|
+
name: { type: 'string' },
|
|
37
|
+
url: { type: 'string' },
|
|
38
|
+
method: { type: 'string' },
|
|
39
|
+
headers: { type: 'object' },
|
|
40
|
+
body: { type: 'string' },
|
|
41
|
+
responseMatches: { type: 'array' },
|
|
42
|
+
responseRedactions: { type: 'array' },
|
|
43
|
+
paramValues: { type: 'object' },
|
|
44
|
+
},
|
|
45
|
+
required: ['name', 'url', 'method', 'responseMatches'],
|
|
46
|
+
},
|
|
47
|
+
initialUrl: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'The website URL the user landed on to trigger the request ' +
|
|
50
|
+
'(maps to the provider `loginUrl`).',
|
|
51
|
+
},
|
|
52
|
+
providerId: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Existing provider to add a new version to. Omit to register ' +
|
|
55
|
+
'a brand-new provider.',
|
|
56
|
+
},
|
|
57
|
+
description: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Provider description (new-provider register only).',
|
|
60
|
+
},
|
|
61
|
+
geoLocation: { type: 'string' },
|
|
62
|
+
providerType: { type: 'string', enum: ['PRIVATE', 'PUBLIC'] },
|
|
63
|
+
notes: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Change note for the new version (update only; must be ≥10 ' +
|
|
66
|
+
'chars or a default is used).',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['provider', 'initialUrl'],
|
|
70
|
+
},
|
|
71
|
+
}, async (args) => {
|
|
72
|
+
const opts = { initialUrl: args.initialUrl };
|
|
73
|
+
if (args.description !== undefined) {
|
|
74
|
+
opts.description = args.description;
|
|
75
|
+
}
|
|
76
|
+
if (args.geoLocation !== undefined) {
|
|
77
|
+
opts.geoLocation = args.geoLocation;
|
|
78
|
+
}
|
|
79
|
+
if (args.providerType !== undefined) {
|
|
80
|
+
opts.providerType = args.providerType;
|
|
81
|
+
}
|
|
82
|
+
// New provider.
|
|
83
|
+
if (!args.providerId) {
|
|
84
|
+
const body = providerToRegisterBody(args.provider, opts);
|
|
85
|
+
return client.registerProvider(body);
|
|
86
|
+
}
|
|
87
|
+
// New version of an existing provider — pick the next patch above
|
|
88
|
+
// the highest existing version.
|
|
89
|
+
const versionsResp = await client.listVersions(args.providerId);
|
|
90
|
+
const version = bumpPatchString(extractVersions(versionsResp));
|
|
91
|
+
const versionInfo = args.notes && args.notes.trim().length >= 10
|
|
92
|
+
? args.notes.trim()
|
|
93
|
+
: DEFAULT_VERSION_INFO;
|
|
94
|
+
const body = providerToConfigBody(args.provider, opts, version, versionInfo);
|
|
95
|
+
return client.updateProviderConfig(args.providerId, body);
|
|
96
|
+
});
|
|
97
|
+
}
|
package/lib/paths.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the Reclaim cache directory (`~/.reclaim`). Honours the
|
|
3
|
+
* `RECLAIM_HOME` env var as an override so tests can sandbox writes.
|
|
4
|
+
*/
|
|
5
|
+
export declare function reclaimHomeDirPath(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Walk up from `start` until we find a directory containing one of
|
|
8
|
+
* the project-root markers, then return that directory. Falls back
|
|
9
|
+
* to `start` if no marker is found within 8 levels.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findProjectRoot(start?: string): string;
|
package/lib/paths.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the Reclaim cache directory (`~/.reclaim`). Honours the
|
|
6
|
+
* `RECLAIM_HOME` env var as an override so tests can sandbox writes.
|
|
7
|
+
*/
|
|
8
|
+
export function reclaimHomeDirPath() {
|
|
9
|
+
return process.env.RECLAIM_HOME || join(homedir(), '.reclaim');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Walk up from `start` until we find a directory containing one of
|
|
13
|
+
* the project-root markers, then return that directory. Falls back
|
|
14
|
+
* to `start` if no marker is found within 8 levels.
|
|
15
|
+
*/
|
|
16
|
+
export function findProjectRoot(start = process.cwd()) {
|
|
17
|
+
const markers = ['.git', 'package.json', 'pyproject.toml', 'Cargo.toml'];
|
|
18
|
+
let dir = resolve(start);
|
|
19
|
+
for (let i = 0; i < 8; i++) {
|
|
20
|
+
for (const m of markers) {
|
|
21
|
+
if (existsSync(join(dir, m))) {
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const parent = dirname(dir);
|
|
26
|
+
if (parent === dir) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
return start;
|
|
32
|
+
}
|
package/lib/pkg.d.ts
ADDED
package/lib/pkg.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
// package.json lives outside `rootDir: src`, so we can't `import` it
|
|
5
|
+
// directly without widening rootDir and corrupting the build output
|
|
6
|
+
// tree. Read it at runtime instead — relative to this module's URL so
|
|
7
|
+
// the path resolves identically in dev (src/pkg.ts) and after build
|
|
8
|
+
// (build/pkg.js), both one level below the package root.
|
|
9
|
+
const PKG_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
10
|
+
const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8'));
|
|
11
|
+
export const NAME = pkg.name;
|
|
12
|
+
export const VERSION = pkg.version;
|
|
13
|
+
export const DESCRIPTION = pkg.description || '';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReclaimProvider } from '../provider/schema.ts';
|
|
2
|
+
export interface AttestorLogEntry {
|
|
3
|
+
level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
4
|
+
msg: string;
|
|
5
|
+
ctx?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface ProofResult {
|
|
8
|
+
proof: unknown;
|
|
9
|
+
extractedValue: string;
|
|
10
|
+
extractedParameters: Record<string, string>;
|
|
11
|
+
identifier?: string;
|
|
12
|
+
owner?: string;
|
|
13
|
+
verified: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
/** Set to `'missing-owner-key'` when no credential was available to
|
|
16
|
+
* sign with — distinguishes this recoverable case from other proof
|
|
17
|
+
* errors so the skill can point the dev at `issue_credentials` /
|
|
18
|
+
* `list_cached_credentials` rather than guessing. */
|
|
19
|
+
errorKind?: 'missing-owner-key';
|
|
20
|
+
/** Pino-style log entries captured from the attestor SDK during the
|
|
21
|
+
* proof attempt. Full stream on the result; the MCP layer trims it
|
|
22
|
+
* for the LLM-visible response and dumps the whole thing to disk
|
|
23
|
+
* via `runLogPath`. */
|
|
24
|
+
attestorLogs?: AttestorLogEntry[];
|
|
25
|
+
/** The exact `params` object passed to `createClaimOnAttestor` for
|
|
26
|
+
* this attempt. Only included on error so the LLM/dev can confirm
|
|
27
|
+
* what the attestor actually saw vs what they thought they sent
|
|
28
|
+
* (catches mutation bugs / template-substitution surprises). Never
|
|
29
|
+
* carries the captured secrets — those live in `secretParams` which
|
|
30
|
+
* is not echoed here. */
|
|
31
|
+
sentParams?: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** Trim a captured log stream to the last N entries — for shrinking the
|
|
34
|
+
* LLM-visible response while the full stream stays in the on-disk dump. */
|
|
35
|
+
export declare function tailLogs(entries: AttestorLogEntry[], n?: number): AttestorLogEntry[];
|
|
36
|
+
/** Filter to just warn/error/fatal — for the LLM-visible response on
|
|
37
|
+
* successful proofs, where chatty trace/info would just waste context. */
|
|
38
|
+
export declare function warnsAndErrors(entries: AttestorLogEntry[]): AttestorLogEntry[];
|
|
39
|
+
export declare function runAttestorProof(provider: ReclaimProvider, secrets: Record<string, string>, attestorUrl?: string, ownerAddress?: string): Promise<ProofResult>;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/proof/attestor.ts
|
|
2
|
+
//
|
|
3
|
+
// Adapter over @reclaimprotocol/attestor-core's createClaimOnAttestor.
|
|
4
|
+
//
|
|
5
|
+
// responseMatches are the regex/contains checks the attestor runs on the
|
|
6
|
+
// revealed slice; jsonPath/xPath live on responseRedactions (what the prover
|
|
7
|
+
// reveals), never on responseMatches.
|
|
8
|
+
import { createClaimOnAttestor, } from '@reclaimprotocol/attestor-core';
|
|
9
|
+
import { resolveOwnerPrivateKey } from "../mcp/tools/credentials/cache.js";
|
|
10
|
+
/** Trim a captured log stream to the last N entries — for shrinking the
|
|
11
|
+
* LLM-visible response while the full stream stays in the on-disk dump. */
|
|
12
|
+
export function tailLogs(entries, n = LOG_TAIL_LINES) {
|
|
13
|
+
if (entries.length <= n) {
|
|
14
|
+
return entries;
|
|
15
|
+
}
|
|
16
|
+
return entries.slice(entries.length - n);
|
|
17
|
+
}
|
|
18
|
+
/** Filter to just warn/error/fatal — for the LLM-visible response on
|
|
19
|
+
* successful proofs, where chatty trace/info would just waste context. */
|
|
20
|
+
export function warnsAndErrors(entries) {
|
|
21
|
+
return entries.filter((e) => e.level === 'warn' || e.level === 'error' || e.level === 'fatal');
|
|
22
|
+
}
|
|
23
|
+
/** How many log entries to keep from the attestor SDK on an error. The
|
|
24
|
+
* early ones describe TLS setup; the late ones (right before the
|
|
25
|
+
* throw) usually pinpoint the actual failure. Keeping the tail covers
|
|
26
|
+
* the latter; the cap stops a chatty session from blowing the LLM's
|
|
27
|
+
* context window. */
|
|
28
|
+
const LOG_TAIL_LINES = 80;
|
|
29
|
+
/** Build a pino-compatible capturing logger. Each `trace`/`debug`/…
|
|
30
|
+
* call records an entry; `child` returns a derived capturer bound to
|
|
31
|
+
* the parent's entries array so nested SDK loggers all flow into one
|
|
32
|
+
* ordered list. */
|
|
33
|
+
function makeCaptureLogger() {
|
|
34
|
+
const entries = [];
|
|
35
|
+
const make = (baseCtx) => {
|
|
36
|
+
const log = (level) => (...args) => {
|
|
37
|
+
if (args.length === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let msg = '';
|
|
41
|
+
let ctx;
|
|
42
|
+
if (args[0] instanceof Error) {
|
|
43
|
+
// pino-style `logger.error(err, 'msg')` — Error objects
|
|
44
|
+
// don't JSON-serialize on their own, so extract the
|
|
45
|
+
// useful fields manually.
|
|
46
|
+
const err = args[0];
|
|
47
|
+
ctx = {
|
|
48
|
+
error: { name: err.name, message: err.message, stack: err.stack },
|
|
49
|
+
};
|
|
50
|
+
msg = typeof args[1] === 'string' ? args[1] : err.message;
|
|
51
|
+
}
|
|
52
|
+
else if (typeof args[0] === 'string') {
|
|
53
|
+
msg = args[0];
|
|
54
|
+
}
|
|
55
|
+
else if (args[0] && typeof args[0] === 'object') {
|
|
56
|
+
ctx = args[0];
|
|
57
|
+
msg = typeof args[1] === 'string' ? args[1] : '';
|
|
58
|
+
}
|
|
59
|
+
const merged = Object.keys(baseCtx).length || ctx
|
|
60
|
+
? { ...baseCtx, ...(ctx ?? {}) }
|
|
61
|
+
: undefined;
|
|
62
|
+
const entry = { level, msg };
|
|
63
|
+
if (merged) {
|
|
64
|
+
entry.ctx = merged;
|
|
65
|
+
}
|
|
66
|
+
entries.push(entry);
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
trace: log('trace'),
|
|
70
|
+
debug: log('debug'),
|
|
71
|
+
info: log('info'),
|
|
72
|
+
warn: log('warn'),
|
|
73
|
+
error: log('error'),
|
|
74
|
+
fatal: log('fatal'),
|
|
75
|
+
child: (childCtx) => {
|
|
76
|
+
return make({ ...baseCtx, ...childCtx });
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
return { logger: make({}), entries };
|
|
81
|
+
}
|
|
82
|
+
/** Default public attestor websocket URL. Resolved per-call so an env-var
|
|
83
|
+
* change or per-request override takes effect without restarting the
|
|
84
|
+
* MCP server. */
|
|
85
|
+
const FALLBACK_ATTESTOR_URL = 'wss://attestor.reclaimprotocol.org:444/ws';
|
|
86
|
+
function resolveAttestorUrl(override) {
|
|
87
|
+
return (override ?? process.env['RECLAIM_ATTESTOR_URL'] ?? FALLBACK_ATTESTOR_URL);
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Main entry point
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export async function runAttestorProof(provider, secrets, attestorUrl, ownerAddress) {
|
|
93
|
+
// The owner is chosen up front (resolve_owner_key) and passed as
|
|
94
|
+
// `ownerAddress`; here we just fetch its private key and sign.
|
|
95
|
+
const ownerPrivateKey = ownerAddress
|
|
96
|
+
? resolveOwnerPrivateKey(ownerAddress)
|
|
97
|
+
: undefined;
|
|
98
|
+
if (!ownerPrivateKey) {
|
|
99
|
+
return {
|
|
100
|
+
proof: undefined,
|
|
101
|
+
extractedValue: '',
|
|
102
|
+
extractedParameters: {},
|
|
103
|
+
verified: false,
|
|
104
|
+
error: `No private key available for ownerAddress "${ownerAddress ?? ''}". ` +
|
|
105
|
+
'Call resolve_owner_key to pick a credential (or ' +
|
|
106
|
+
'issue_credentials / import_credentials to set one up), then ' +
|
|
107
|
+
'retry run_proof with its address as ownerAddress.',
|
|
108
|
+
errorKind: 'missing-owner-key',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const params = {
|
|
113
|
+
url: provider.url,
|
|
114
|
+
method: provider.method,
|
|
115
|
+
...(provider.headers !== undefined && { headers: provider.headers }),
|
|
116
|
+
...(provider.body !== undefined && { body: provider.body }),
|
|
117
|
+
responseMatches: provider.responseMatches,
|
|
118
|
+
responseRedactions: provider.responseRedactions ?? [],
|
|
119
|
+
...(provider.paramValues !== undefined && {
|
|
120
|
+
paramValues: provider.paramValues,
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
// Diagnostic logger capture is gated behind RECLAIM_AGENT_TRACE so the
|
|
124
|
+
// happy path doesn't pay the cost (the attestor SDK emits a *lot* of
|
|
125
|
+
// trace/debug entries during ZK setup). Flip RECLAIM_AGENT_TRACE=1 to
|
|
126
|
+
// turn it back on when something's broken; logs flow into the proof
|
|
127
|
+
// response and into the on-disk proof-runs dump.
|
|
128
|
+
const traceEnabled = process.env['RECLAIM_AGENT_TRACE'] === '1';
|
|
129
|
+
const capture = traceEnabled ? makeCaptureLogger() : undefined;
|
|
130
|
+
const attestorLogs = capture?.entries;
|
|
131
|
+
let claim;
|
|
132
|
+
try {
|
|
133
|
+
claim = await createClaimOnAttestor({
|
|
134
|
+
name: 'http',
|
|
135
|
+
params,
|
|
136
|
+
secretParams: {
|
|
137
|
+
headers: secrets,
|
|
138
|
+
},
|
|
139
|
+
ownerPrivateKey,
|
|
140
|
+
client: { url: resolveAttestorUrl(attestorUrl) },
|
|
141
|
+
zkEngine: 'snarkjs',
|
|
142
|
+
...(capture !== undefined && { logger: capture.logger }),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const errorResult = {
|
|
147
|
+
proof: undefined,
|
|
148
|
+
extractedValue: '',
|
|
149
|
+
extractedParameters: {},
|
|
150
|
+
verified: false,
|
|
151
|
+
error: err instanceof Error ? err.message : String(err),
|
|
152
|
+
sentParams: params,
|
|
153
|
+
};
|
|
154
|
+
if (attestorLogs !== undefined) {
|
|
155
|
+
errorResult.attestorLogs = attestorLogs;
|
|
156
|
+
}
|
|
157
|
+
return errorResult;
|
|
158
|
+
}
|
|
159
|
+
// ClaimTunnelResponse carries the claim under `.claim`.
|
|
160
|
+
const claimRecord = claim;
|
|
161
|
+
const claimData = claimRecord['claim'];
|
|
162
|
+
// claimData.context is a JSON-encoded string carrying
|
|
163
|
+
// { extractedParameters: {...}, providerHash, ... }.
|
|
164
|
+
let context = {};
|
|
165
|
+
const rawContext = claimData?.['context'];
|
|
166
|
+
if (typeof rawContext === 'string') {
|
|
167
|
+
try {
|
|
168
|
+
context = JSON.parse(rawContext);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Leave context empty if it isn't valid JSON.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const extractedParameters = context['extractedParameters'] ??
|
|
175
|
+
{};
|
|
176
|
+
const extractedValue = Object.values(extractedParameters)[0] ?? '';
|
|
177
|
+
const result = {
|
|
178
|
+
proof: claim,
|
|
179
|
+
extractedValue,
|
|
180
|
+
extractedParameters,
|
|
181
|
+
verified: true,
|
|
182
|
+
};
|
|
183
|
+
const identifier = claimData?.['identifier'];
|
|
184
|
+
if (typeof identifier === 'string') {
|
|
185
|
+
result.identifier = identifier;
|
|
186
|
+
}
|
|
187
|
+
const owner = claimData?.['owner'];
|
|
188
|
+
if (typeof owner === 'string') {
|
|
189
|
+
result.owner = owner;
|
|
190
|
+
}
|
|
191
|
+
// Only set when RECLAIM_AGENT_TRACE=1. Full stream — the MCP layer
|
|
192
|
+
// trims for the LLM-visible response; the on-disk dump gets it all.
|
|
193
|
+
if (attestorLogs !== undefined && attestorLogs.length > 0) {
|
|
194
|
+
result.attestorLogs = attestorLogs;
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return {
|
|
200
|
+
proof: undefined,
|
|
201
|
+
extractedValue: '',
|
|
202
|
+
extractedParameters: {},
|
|
203
|
+
verified: false,
|
|
204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReclaimProvider } from '../provider/schema.ts';
|
|
2
|
+
export interface ReplayResult {
|
|
3
|
+
status: number;
|
|
4
|
+
body: string;
|
|
5
|
+
matchesOriginal: boolean;
|
|
6
|
+
hints?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function replayProvider(provider: ReclaimProvider, secrets: Record<string, string>): Promise<ReplayResult>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Expand `{{name}}` placeholders from `paramValues`. Matches the attestor
|
|
2
|
+
* SDK's behavior so replay and proof both hit the same final URL. */
|
|
3
|
+
function expandTemplate(s, params) {
|
|
4
|
+
if (!params) {
|
|
5
|
+
return s;
|
|
6
|
+
}
|
|
7
|
+
return s.replace(/\{\{([^{}]+)\}\}/g, (match, name) => {
|
|
8
|
+
const v = params[name];
|
|
9
|
+
return v !== undefined ? v : match;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export async function replayProvider(provider, secrets) {
|
|
13
|
+
const params = provider.paramValues;
|
|
14
|
+
const url = expandTemplate(provider.url, params);
|
|
15
|
+
const expandedHeaders = {};
|
|
16
|
+
for (const [k, v] of Object.entries(provider.headers ?? {})) {
|
|
17
|
+
expandedHeaders[k] = expandTemplate(v, params);
|
|
18
|
+
}
|
|
19
|
+
const headers = { ...expandedHeaders, ...secrets };
|
|
20
|
+
const fetchInit = {
|
|
21
|
+
method: provider.method,
|
|
22
|
+
headers,
|
|
23
|
+
redirect: 'manual',
|
|
24
|
+
};
|
|
25
|
+
if (provider.body !== undefined) {
|
|
26
|
+
fetchInit.body = expandTemplate(provider.body, params);
|
|
27
|
+
}
|
|
28
|
+
const res = await fetch(url, fetchInit);
|
|
29
|
+
const body = await res.text();
|
|
30
|
+
const hints = [];
|
|
31
|
+
if (res.status === 401 || res.status === 403) {
|
|
32
|
+
hints.push('auth/permission failed');
|
|
33
|
+
}
|
|
34
|
+
if (res.status >= 500) {
|
|
35
|
+
hints.push('upstream server error');
|
|
36
|
+
}
|
|
37
|
+
for (const m of provider.responseMatches) {
|
|
38
|
+
// The attestor substitutes paramValues into responseMatches.value
|
|
39
|
+
// at proof time (`{{name}}` → the value, or → the OPRF nullifier
|
|
40
|
+
// when the matching redaction has `hash`). Replay can't OPRF, but
|
|
41
|
+
// it can do the plain substitution so a paramValues-templated match
|
|
42
|
+
// is meaningful here.
|
|
43
|
+
const expanded = {
|
|
44
|
+
...m,
|
|
45
|
+
value: expandTemplate(m.value, params),
|
|
46
|
+
};
|
|
47
|
+
if (!matchesBody(body, expanded)) {
|
|
48
|
+
if (res.status >= 200 && res.status < 300) {
|
|
49
|
+
hints.push(`matcher ${expanded.type} did not match — ` +
|
|
50
|
+
'value may have changed or matcher is too tight');
|
|
51
|
+
}
|
|
52
|
+
const result = {
|
|
53
|
+
status: res.status,
|
|
54
|
+
body,
|
|
55
|
+
matchesOriginal: false,
|
|
56
|
+
};
|
|
57
|
+
if (hints.length > 0) {
|
|
58
|
+
result.hints = hints;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { status: res.status, body, matchesOriginal: res.status < 400 };
|
|
64
|
+
}
|
|
65
|
+
function matchesBody(body, m) {
|
|
66
|
+
if (m.type === 'regex') {
|
|
67
|
+
return new RegExp(m.value).test(body);
|
|
68
|
+
}
|
|
69
|
+
if (m.type === 'contains') {
|
|
70
|
+
return body.includes(m.value);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { looksLikeGraphqlEndpoint } from "./graphql.js";
|
|
2
|
+
const SIGNATURE_HEADERS = [
|
|
3
|
+
'x-signature',
|
|
4
|
+
'x-amz-signature',
|
|
5
|
+
'x-hub-signature',
|
|
6
|
+
'x-hmac',
|
|
7
|
+
'signature',
|
|
8
|
+
'x-request-signature',
|
|
9
|
+
];
|
|
10
|
+
const CSRF_HEADERS = [
|
|
11
|
+
'x-csrf-token',
|
|
12
|
+
'x-xsrf-token',
|
|
13
|
+
'csrf-token',
|
|
14
|
+
'xsrf-token',
|
|
15
|
+
];
|
|
16
|
+
export function detectConcerns(req) {
|
|
17
|
+
const concerns = [];
|
|
18
|
+
const reqHeadersLower = lowerKeys(req.requestHeaders);
|
|
19
|
+
const respHeadersLower = lowerKeys(req.responseHeaders);
|
|
20
|
+
// Signed requests
|
|
21
|
+
for (const h of SIGNATURE_HEADERS) {
|
|
22
|
+
if (reqHeadersLower[h]) {
|
|
23
|
+
concerns.push({
|
|
24
|
+
code: 'signedRequest',
|
|
25
|
+
message: `Request includes a "${h}" header — ` +
|
|
26
|
+
'providers will break when the signing scheme rotates.',
|
|
27
|
+
});
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Bot challenge
|
|
32
|
+
const cfMitigated = respHeadersLower['cf-mitigated'];
|
|
33
|
+
const server = respHeadersLower['server'] ?? '';
|
|
34
|
+
if ((req.status === 403 || req.status === 503) &&
|
|
35
|
+
(cfMitigated === 'challenge' || /cloudflare/i.test(server))) {
|
|
36
|
+
concerns.push({
|
|
37
|
+
code: 'botChallenge',
|
|
38
|
+
message: 'Response looks like a bot-mitigation challenge (Cloudflare). ' +
|
|
39
|
+
'Capture again after solving the challenge in the browser.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// GraphQL
|
|
43
|
+
if (looksLikeGraphqlEndpoint(req.url)) {
|
|
44
|
+
concerns.push({
|
|
45
|
+
code: 'graphql',
|
|
46
|
+
message: 'Endpoint is GraphQL — ' +
|
|
47
|
+
'match on operationName + variables, not URL path.',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// CSRF dependency
|
|
51
|
+
for (const h of CSRF_HEADERS) {
|
|
52
|
+
if (reqHeadersLower[h]) {
|
|
53
|
+
concerns.push({
|
|
54
|
+
code: 'csrfDependency',
|
|
55
|
+
message: `Request includes a "${h}" header — ` +
|
|
56
|
+
'a prior request fetched this token. ' +
|
|
57
|
+
'Replay will fail unless the token is still valid; ' +
|
|
58
|
+
'expect to re-capture for proof.',
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Short-lived token (JWT exp < 30 min from finishedAt)
|
|
64
|
+
const auth = reqHeadersLower['authorization'];
|
|
65
|
+
if (auth) {
|
|
66
|
+
const jwtMatch = auth.match(/^[Bb]earer\s+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/);
|
|
67
|
+
if (jwtMatch) {
|
|
68
|
+
const exp = jwtExpSeconds(jwtMatch[1]);
|
|
69
|
+
const issuedAtMs = req.finishedAt || Date.now();
|
|
70
|
+
if (exp && exp * 1000 - issuedAtMs < 30 * 60 * 1000) {
|
|
71
|
+
concerns.push({
|
|
72
|
+
code: 'shortLivedToken',
|
|
73
|
+
message: 'Authorization is a JWT expiring in under 30 minutes — ' +
|
|
74
|
+
'provider may stop working soon. Re-capture closer to proof time.',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Encrypted / binary body
|
|
80
|
+
if (looksBinary(req.contentType, req.responseBody)) {
|
|
81
|
+
concerns.push({
|
|
82
|
+
code: 'encryptedBody',
|
|
83
|
+
message: `Response content-type "${req.contentType}" is binary/opaque —` +
|
|
84
|
+
' Reclaim providers need plain-text (JSON/HTML) responses.',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return concerns;
|
|
88
|
+
}
|
|
89
|
+
function lowerKeys(h) {
|
|
90
|
+
return Object.fromEntries(Object.entries(h).map(([k, v]) => [k.toLowerCase(), v]));
|
|
91
|
+
}
|
|
92
|
+
function jwtExpSeconds(jwt) {
|
|
93
|
+
const parts = jwt.split('.');
|
|
94
|
+
if (parts.length !== 3) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
99
|
+
return typeof decoded.exp === 'number' ? decoded.exp : undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function looksBinary(contentType, body) {
|
|
106
|
+
if (!body) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (/octet-stream|protobuf|msgpack|cbor/i.test(contentType)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// Heuristic: significant control-char density in first 256 bytes.
|
|
113
|
+
const sample = body.slice(0, 256);
|
|
114
|
+
let ctrl = 0;
|
|
115
|
+
for (const ch of sample) {
|
|
116
|
+
const code = ch.charCodeAt(0);
|
|
117
|
+
if (code < 9 || (code > 13 && code < 32)) {
|
|
118
|
+
ctrl++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return ctrl / sample.length > 0.1;
|
|
122
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CapturedRequest, OprfMode, ReclaimProvider, SecretParamRef } from './schema.ts';
|
|
2
|
+
export interface DraftResult {
|
|
3
|
+
provider: ReclaimProvider;
|
|
4
|
+
secretRefs: SecretParamRef[];
|
|
5
|
+
}
|
|
6
|
+
export declare function draftProvider(req: CapturedRequest, target: string, name: string, hash?: OprfMode): DraftResult;
|