@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,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deriveFromPrivateKey,
|
|
3
|
+
type ReclaimClient,
|
|
4
|
+
} from '@reclaimprotocol/client'
|
|
5
|
+
import assert from 'node:assert'
|
|
6
|
+
import { findProjectRoot } from '../../../paths.ts'
|
|
7
|
+
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
8
|
+
import { suggestCredentialForDir } from './cache.ts'
|
|
9
|
+
|
|
10
|
+
export function statusTool(client: ReclaimClient): RegisteredTool {
|
|
11
|
+
return defineTool<{ address?: string }>(
|
|
12
|
+
{
|
|
13
|
+
name: 'reclaim_status',
|
|
14
|
+
description:
|
|
15
|
+
'Report the live mode (SANDBOX | PRODUCTION | REVOKED) ' +
|
|
16
|
+
'of a Reclaim app credential. Pass `address` — or omit it ' +
|
|
17
|
+
'to derive the address from RECLAIM_PRIVATE_KEY, falling ' +
|
|
18
|
+
'back to the best cached credential for this directory. ' +
|
|
19
|
+
'Useful to confirm a credential is linked to an org ' +
|
|
20
|
+
'with billing before running a production flow.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
address: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
pattern: '^0x[0-9a-fA-F]{40}$',
|
|
27
|
+
description:
|
|
28
|
+
'Lowercase ETH address. Defaults to the address ' +
|
|
29
|
+
'derived from RECLAIM_PRIVATE_KEY, then the cached ' +
|
|
30
|
+
'credential suggested for the current directory.',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async(args) => {
|
|
36
|
+
let address = args.address?.toLowerCase()
|
|
37
|
+
if(!address && process.env.RECLAIM_PRIVATE_KEY) {
|
|
38
|
+
address = deriveFromPrivateKey(process.env.RECLAIM_PRIVATE_KEY).address
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if(!address) {
|
|
42
|
+
address = suggestCredentialForDir(findProjectRoot(process.cwd()))
|
|
43
|
+
?.credential.address
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assert(
|
|
47
|
+
address,
|
|
48
|
+
new Error(
|
|
49
|
+
'No address given, RECLAIM_PRIVATE_KEY is not set, and the ' +
|
|
50
|
+
'cache is empty. Run issue_credentials or import_credentials ' +
|
|
51
|
+
'first.',
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const credResult = await client
|
|
56
|
+
.call('GetCredentialByAddress', { params: { address } })
|
|
57
|
+
const cred = credResult.data
|
|
58
|
+
|
|
59
|
+
// A registered credential always carries an `id`; its absence means
|
|
60
|
+
// the server had no row for this address — a local-only credential.
|
|
61
|
+
const localOnly = !cred.id
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
address: cred.address,
|
|
65
|
+
mode: cred.mode,
|
|
66
|
+
orgId: cred.orgId,
|
|
67
|
+
label: cred.label,
|
|
68
|
+
revokedAt: cred.revokedAt,
|
|
69
|
+
lastUsedAt: cred.lastUsedAt,
|
|
70
|
+
...(localOnly ? {
|
|
71
|
+
note:
|
|
72
|
+
'Local-only credential — not registered with the server. '
|
|
73
|
+
+ 'It operates in SANDBOX, which is all you need to publish '
|
|
74
|
+
+ 'a verification (only an auth token is required — no '
|
|
75
|
+
+ 'credential link). Linking it to an org with billing (via '
|
|
76
|
+
+ 'link_credential) is only needed for the upcoming feature '
|
|
77
|
+
+ 'that runs verifications for your own app’s users.',
|
|
78
|
+
} : {}),
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
}
|
package/src/mcp-bin.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { NAME, VERSION } from '../pkg.ts'
|
|
3
|
+
|
|
4
|
+
/** Default origin of the OLD devtools backend. The client appends the
|
|
5
|
+
* `/api` route prefix itself, so this is the bare origin. Override with
|
|
6
|
+
* `RECLAIM_OLD_API_URL`. */
|
|
7
|
+
export const DEFAULT_OLD_API_URL = 'https://devapi.reclaimprotocol.org'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Externally-supplied caller identity. We do NOT mint these — the dev
|
|
11
|
+
* provides one:
|
|
12
|
+
* - `bearer`: a Firebase ID token from the devtools dashboard. Attached
|
|
13
|
+
* as `Authorization: Bearer …`; `POST /api/users/login` provisions the
|
|
14
|
+
* user row on first use.
|
|
15
|
+
* - `eth`: an `eth:<lowercase-address>` uid for an already-provisioned
|
|
16
|
+
* eth user. Attached as the `x-eth-uid` header (no signature — the
|
|
17
|
+
* backend trusts the header for a uid that already exists).
|
|
18
|
+
*/
|
|
19
|
+
export type OldIdentity =
|
|
20
|
+
| { kind: 'bearer', token: string }
|
|
21
|
+
| { kind: 'eth', uid: string }
|
|
22
|
+
|
|
23
|
+
export interface RegisterProviderBody {
|
|
24
|
+
name: string
|
|
25
|
+
description?: string
|
|
26
|
+
loginUrl: string
|
|
27
|
+
geoLocation?: string
|
|
28
|
+
providerType?: 'PRIVATE' | 'PUBLIC'
|
|
29
|
+
requestData: unknown[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UpdateProviderConfigBody {
|
|
33
|
+
providerConfig: Record<string, unknown>
|
|
34
|
+
version: string
|
|
35
|
+
versionInfo: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MyProvidersQuery {
|
|
39
|
+
pageKey?: number
|
|
40
|
+
pageSize?: number
|
|
41
|
+
searchQuery?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RequestOpts {
|
|
45
|
+
query?: Record<string, string | number | undefined>
|
|
46
|
+
body?: unknown
|
|
47
|
+
/** Attach the caller identity header. Throws if no identity is set. */
|
|
48
|
+
auth?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Minimal REST client for the OLD devtools Express backend (base `/api`).
|
|
53
|
+
* Deliberately NOT a clone of `@reclaimprotocol/client`'s operationId
|
|
54
|
+
* machinery — the old backend has no OpenAPI spec, so this exposes a few
|
|
55
|
+
* hand-written typed methods for exactly the flows old mode needs:
|
|
56
|
+
* authenticate, publish (register / update version), and list-own.
|
|
57
|
+
*
|
|
58
|
+
* Errors are thrown as `@hapi/boom` so `remapErrorAsResponse` renders them
|
|
59
|
+
* identically to builder-mode `ProblemError`s.
|
|
60
|
+
*/
|
|
61
|
+
export class ReclaimOldClient {
|
|
62
|
+
readonly baseUrl: string
|
|
63
|
+
#identity?: OldIdentity
|
|
64
|
+
#fetch: typeof fetch
|
|
65
|
+
|
|
66
|
+
constructor(opts: {
|
|
67
|
+
baseUrl?: string
|
|
68
|
+
identity?: OldIdentity
|
|
69
|
+
fetch?: typeof fetch
|
|
70
|
+
} = {}) {
|
|
71
|
+
this.baseUrl = (opts.baseUrl || DEFAULT_OLD_API_URL).replace(/\/+$/, '')
|
|
72
|
+
this.#identity = opts.identity
|
|
73
|
+
this.#fetch = opts.fetch ?? fetch
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setIdentity(identity: OldIdentity | undefined) {
|
|
77
|
+
this.#identity = identity
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getIdentity(): OldIdentity | undefined {
|
|
81
|
+
return this.#identity
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Validate + provision a bearer identity. The backend creates the user
|
|
85
|
+
* row on first login, which is what makes later authed calls work. */
|
|
86
|
+
loginWithToken(): Promise<unknown> {
|
|
87
|
+
return this.#request('POST', '/users/login', { auth: true })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
registerProvider(body: RegisterProviderBody): Promise<unknown> {
|
|
91
|
+
return this.#request('POST', '/providers/register', { body, auth: true })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
updateProviderConfig(
|
|
95
|
+
providerId: string,
|
|
96
|
+
body: UpdateProviderConfigBody,
|
|
97
|
+
): Promise<unknown> {
|
|
98
|
+
return this.#request(
|
|
99
|
+
'POST',
|
|
100
|
+
`/providers/${encodeURIComponent(providerId)}/config`,
|
|
101
|
+
{ body, auth: true },
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
listVersions(providerId: string): Promise<unknown> {
|
|
106
|
+
return this.#request(
|
|
107
|
+
'GET',
|
|
108
|
+
`/providers/${encodeURIComponent(providerId)}/versions`,
|
|
109
|
+
{ auth: true },
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getMyProviders(query: MyProvidersQuery = {}): Promise<unknown> {
|
|
114
|
+
// Every one of these query params is REQUIRED by the backend
|
|
115
|
+
// validator (missing/invalid → 400), so default them all here.
|
|
116
|
+
return this.#request('GET', '/providers/user/paginated', {
|
|
117
|
+
auth: true,
|
|
118
|
+
query: {
|
|
119
|
+
pageKey: query.pageKey ?? 0,
|
|
120
|
+
pageSize: query.pageSize ?? 20,
|
|
121
|
+
searchQuery: query.searchQuery ?? '',
|
|
122
|
+
sortByCreatedLatest: 'true',
|
|
123
|
+
providerType: 'ALL',
|
|
124
|
+
providerStatus: 'ALL',
|
|
125
|
+
providerVisibility: 'ALL',
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async #request(
|
|
131
|
+
method: string,
|
|
132
|
+
path: string,
|
|
133
|
+
opts: RequestOpts = {},
|
|
134
|
+
): Promise<unknown> {
|
|
135
|
+
let url = `${this.baseUrl}/api${path}`
|
|
136
|
+
if(opts.query) {
|
|
137
|
+
const search = new URLSearchParams()
|
|
138
|
+
for(const [k, v] of Object.entries(opts.query)) {
|
|
139
|
+
if(v === undefined) {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
search.append(k, String(v))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const qs = search.toString()
|
|
147
|
+
if(qs) {
|
|
148
|
+
url += `?${qs}`
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const headers: Record<string, string> = {
|
|
153
|
+
Accept: 'application/json',
|
|
154
|
+
'User-Agent': `${NAME}/${VERSION}`,
|
|
155
|
+
}
|
|
156
|
+
if(opts.auth) {
|
|
157
|
+
headers[this.#authHeaderName()] = this.#authHeaderValue()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const init: RequestInit = { method, headers }
|
|
161
|
+
if(method !== 'GET' && method !== 'HEAD') {
|
|
162
|
+
init.body = JSON.stringify(opts.body ?? {})
|
|
163
|
+
headers['Content-Type'] = 'application/json'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const res = await this.#fetch(url, init)
|
|
167
|
+
const json = await this.#parseBody(res)
|
|
168
|
+
|
|
169
|
+
// Old API convention: { message, ...data, isSuccess }. Treat a non-2xx
|
|
170
|
+
// OR an explicit isSuccess:false as an error so a 200-with-failure
|
|
171
|
+
// body never slips through as success.
|
|
172
|
+
const failed =
|
|
173
|
+
!res.ok ||
|
|
174
|
+
(json !== undefined &&
|
|
175
|
+
typeof json === 'object' &&
|
|
176
|
+
(json as { isSuccess?: unknown }).isSuccess === false)
|
|
177
|
+
if(failed) {
|
|
178
|
+
throw this.#asBoom(res, json)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return json
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#authHeaderName(): string {
|
|
185
|
+
const id = this.#requireIdentity()
|
|
186
|
+
return id.kind === 'bearer' ? 'Authorization' : 'x-eth-uid'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#authHeaderValue(): string {
|
|
190
|
+
const id = this.#requireIdentity()
|
|
191
|
+
return id.kind === 'bearer' ? `Bearer ${id.token}` : id.uid
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#requireIdentity(): OldIdentity {
|
|
195
|
+
if(!this.#identity) {
|
|
196
|
+
throw new Boom(
|
|
197
|
+
'Not authenticated against old devtools. Call reclaim_authenticate ' +
|
|
198
|
+
'with a dashboard token (or eth uid) first.',
|
|
199
|
+
{ statusCode: 401, data: { status: 401, detail: 'no identity set' } },
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return this.#identity
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async #parseBody(res: Response): Promise<unknown> {
|
|
207
|
+
if(res.status === 204 || res.headers.get('content-length') === '0') {
|
|
208
|
+
return undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
return await res.json()
|
|
213
|
+
} catch{
|
|
214
|
+
return undefined
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#asBoom(res: Response, json: unknown): Boom {
|
|
219
|
+
const message =
|
|
220
|
+
(json && typeof json === 'object' &&
|
|
221
|
+
typeof (json as { message?: unknown }).message === 'string'
|
|
222
|
+
? (json as { message: string }).message
|
|
223
|
+
: undefined) || res.statusText || 'Old devtools request failed'
|
|
224
|
+
// Shape `data` like a ProblemDetails so remapErrorAsResponse can render
|
|
225
|
+
// `${message}: ${data.detail}` and expose the raw body as structured.
|
|
226
|
+
return new Boom(message, {
|
|
227
|
+
statusCode: res.ok ? 502 : res.status,
|
|
228
|
+
data: {
|
|
229
|
+
status: res.status,
|
|
230
|
+
title: message,
|
|
231
|
+
detail: message,
|
|
232
|
+
body: json,
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/old/flag.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `USE_OLD_DEVTOOLS` — single source of truth for which backend the MCP
|
|
3
|
+
* server targets.
|
|
4
|
+
*
|
|
5
|
+
* Defaults to OLD devtools (the system still in production) while the new
|
|
6
|
+
* "builder" backend is under development. Builder mode must be opted into
|
|
7
|
+
* EXPLICITLY by setting `USE_OLD_DEVTOOLS=false`. Anything else — unset,
|
|
8
|
+
* empty, `true`, `1`, a typo — keeps old mode on, so a forgotten env var
|
|
9
|
+
* can never silently route a developer at the half-built builder backend.
|
|
10
|
+
*/
|
|
11
|
+
export function useOldDevtools(): boolean {
|
|
12
|
+
return (process.env.USE_OLD_DEVTOOLS ?? '').trim().toLowerCase() !== 'false'
|
|
13
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { deriveFromPrivateKey } from '@reclaimprotocol/client'
|
|
2
|
+
import { ConfigStore } from '../config/store.ts'
|
|
3
|
+
import type { OldIdentity } from './client.ts'
|
|
4
|
+
|
|
5
|
+
/** Normalize an eth uid input to `eth:<lowercase-address>`. Accepts a bare
|
|
6
|
+
* `0x…` address or an already-prefixed `eth:0x…`. */
|
|
7
|
+
export function toEthUid(addressOrUid: string): OldIdentity {
|
|
8
|
+
const lower = addressOrUid.trim().toLowerCase()
|
|
9
|
+
const uid = lower.startsWith('eth:') ? lower : `eth:${lower}`
|
|
10
|
+
return { kind: 'eth', uid }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a caller identity from the environment, in priority order:
|
|
15
|
+
* 1. `RECLAIM_OLD_API_TOKEN` — a dashboard Firebase bearer token.
|
|
16
|
+
* 2. `RECLAIM_OLD_ETH_UID` — an `eth:<addr>` (or bare `0x…`) uid.
|
|
17
|
+
* 3. `RECLAIM_PRIVATE_KEY` — derive the eth uid from the local app key.
|
|
18
|
+
* Returns undefined when none is set.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveIdentityFromEnv(): OldIdentity | undefined {
|
|
21
|
+
const token = process.env.RECLAIM_OLD_API_TOKEN?.trim()
|
|
22
|
+
if(token) {
|
|
23
|
+
return { kind: 'bearer', token }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ethUid = process.env.RECLAIM_OLD_ETH_UID?.trim()
|
|
27
|
+
if(ethUid) {
|
|
28
|
+
return toEthUid(ethUid)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const privateKey = process.env.RECLAIM_PRIVATE_KEY?.trim()
|
|
32
|
+
if(privateKey) {
|
|
33
|
+
return toEthUid(deriveFromPrivateKey(privateKey).address)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read the identity persisted by a prior `reclaim_authenticate` call. */
|
|
40
|
+
export function loadPersistedIdentity(): OldIdentity | undefined {
|
|
41
|
+
return new ConfigStore().read()?.old_devtools
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Persist the identity so it survives an MCP-server restart. */
|
|
45
|
+
export function persistIdentity(identity: OldIdentity) {
|
|
46
|
+
new ConfigStore().write({ old_devtools: identity })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The identity the client should start with: an explicit env var wins
|
|
51
|
+
* (useful for headless / CI), otherwise fall back to whatever the dev
|
|
52
|
+
* authenticated with last time.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveInitialIdentity(): OldIdentity | undefined {
|
|
55
|
+
return resolveIdentityFromEnv() ?? loadPersistedIdentity()
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server instructions shown to the LLM when running in OLD-devtools mode
|
|
3
|
+
* (`USE_OLD_DEVTOOLS` unset or not `false`). Describes the REDUCED tool
|
|
4
|
+
* surface — there are no org/credential-link/review flows and no generated
|
|
5
|
+
* API tools here. Must not mention disabled tools.
|
|
6
|
+
*/
|
|
7
|
+
export const OLD_MODE_INSTRUCTIONS =
|
|
8
|
+
'This server is running against the LEGACY Reclaim devtools backend ' +
|
|
9
|
+
'(temporary, while the new builder backend is in development), so its ' +
|
|
10
|
+
'capabilities are intentionally limited.\n\n' +
|
|
11
|
+
'Provider-authoring flow:\n' +
|
|
12
|
+
'1. `reclaim_authenticate` — by default opens the dashboard in Chrome, ' +
|
|
13
|
+
'waits for you to sign in, captures the token automatically, and closes ' +
|
|
14
|
+
'the tab (or paste a `token` / pass an eth `ethAddress` to skip the ' +
|
|
15
|
+
'browser). Do this once before publishing or listing.\n' +
|
|
16
|
+
'2. `attach_chrome` → `start_capture` → drive the page (`navigate`, ' +
|
|
17
|
+
'`wait_for_page`) → inspect (`list_requests`, `get_request`, ' +
|
|
18
|
+
'`find_requests_containing`).\n' +
|
|
19
|
+
'3. `propose_provider` to draft a provider, `replay_request` to ' +
|
|
20
|
+
'pre-flight, `run_proof` to verify with the attestor. OPRF/`hash` ' +
|
|
21
|
+
'redactions are optional and supported.\n' +
|
|
22
|
+
'4. `create_provider_version_from_capture` to publish. With NO ' +
|
|
23
|
+
'`providerId` it REGISTERS A NEW provider (re-running creates a ' +
|
|
24
|
+
'duplicate); pass a ' +
|
|
25
|
+
'`providerId` from `get_me_providers` to add a new version to an existing ' +
|
|
26
|
+
'provider instead.\n' +
|
|
27
|
+
'5. `get_me_providers` lists the providers you own (the only ' +
|
|
28
|
+
'provider-listing tool available in this mode).\n\n' +
|
|
29
|
+
'Only GET/POST endpoints can be captured into providers here. There is no ' +
|
|
30
|
+
'organization, credential-linking, or public-review workflow in this mode.'
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { connectCdp } from '../cdp/client.ts'
|
|
2
|
+
import {
|
|
3
|
+
defaultProfileDir,
|
|
4
|
+
launchChrome,
|
|
5
|
+
waitForCdpReady,
|
|
6
|
+
} from '../cdp/launcher.ts'
|
|
7
|
+
import { ConfigStore } from '../config/store.ts'
|
|
8
|
+
|
|
9
|
+
/** The devtools dashboard login page. Override per-call or via
|
|
10
|
+
* `RECLAIM_OLD_LOGIN_URL`. */
|
|
11
|
+
export const DEFAULT_LOGIN_URL = 'https://dev.reclaimprotocol.org'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read the Firebase ID token the dashboard persisted after login.
|
|
15
|
+
*
|
|
16
|
+
* The frontend uses `browserLocalPersistence`, which the Firebase JS SDK
|
|
17
|
+
* implements on `localStorage` under a `firebase:authUser:<apiKey>:[DEFAULT]`
|
|
18
|
+
* key whose JSON value carries `stsTokenManager.accessToken`. We scan for any
|
|
19
|
+
* such key (so we never hard-code the project apiKey) and fall back to the
|
|
20
|
+
* IndexedDB persistence layout (`firebaseLocalStorageDb` /
|
|
21
|
+
* `firebaseLocalStorage`) in case a build switches persistence. Returns the
|
|
22
|
+
* token string, or null when the user has not finished logging in yet.
|
|
23
|
+
*
|
|
24
|
+
* Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
|
|
25
|
+
*/
|
|
26
|
+
const EXTRACT_TOKEN_JS = `(async () => {
|
|
27
|
+
try {
|
|
28
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
29
|
+
const k = localStorage.key(i)
|
|
30
|
+
if (k && k.indexOf('firebase:authUser:') === 0) {
|
|
31
|
+
const v = JSON.parse(localStorage.getItem(k) || 'null')
|
|
32
|
+
const t = v && v.stsTokenManager && v.stsTokenManager.accessToken
|
|
33
|
+
if (t) return t
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {}
|
|
37
|
+
try {
|
|
38
|
+
return await new Promise((resolve) => {
|
|
39
|
+
const req = indexedDB.open('firebaseLocalStorageDb')
|
|
40
|
+
req.onerror = () => resolve(null)
|
|
41
|
+
req.onsuccess = () => {
|
|
42
|
+
try {
|
|
43
|
+
const db = req.result
|
|
44
|
+
if (!db.objectStoreNames.contains('firebaseLocalStorage')) {
|
|
45
|
+
resolve(null); return
|
|
46
|
+
}
|
|
47
|
+
const store = db
|
|
48
|
+
.transaction('firebaseLocalStorage', 'readonly')
|
|
49
|
+
.objectStore('firebaseLocalStorage')
|
|
50
|
+
const all = store.getAll()
|
|
51
|
+
all.onerror = () => resolve(null)
|
|
52
|
+
all.onsuccess = () => {
|
|
53
|
+
for (const row of (all.result || [])) {
|
|
54
|
+
const m = row && row.value && row.value.stsTokenManager
|
|
55
|
+
if (m && m.accessToken) { resolve(m.accessToken); return }
|
|
56
|
+
}
|
|
57
|
+
resolve(null)
|
|
58
|
+
}
|
|
59
|
+
} catch (e) { resolve(null) }
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
} catch (e) { return null }
|
|
63
|
+
})()`
|
|
64
|
+
|
|
65
|
+
export interface CdpLoginResult {
|
|
66
|
+
token: string
|
|
67
|
+
/** True when we launched (and then closed) a dedicated Chrome just for
|
|
68
|
+
* login; false when we attached to an already-running browser and only
|
|
69
|
+
* closed the tab we opened. */
|
|
70
|
+
closedBrowser: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Open the dashboard login page in Chrome via CDP, wait for the developer to
|
|
75
|
+
* finish signing in, read the resulting Firebase ID token from page storage,
|
|
76
|
+
* then clean up. If we had to launch a dedicated Chrome for this, we close the
|
|
77
|
+
* whole browser afterwards; if we attached to a Chrome that was already up
|
|
78
|
+
* (e.g. the capture flow's), we only close the tab we opened so we don't
|
|
79
|
+
* disrupt it.
|
|
80
|
+
*/
|
|
81
|
+
export async function captureFirebaseTokenViaCdp(opts: {
|
|
82
|
+
loginUrl?: string
|
|
83
|
+
timeoutMs?: number
|
|
84
|
+
pollMs?: number
|
|
85
|
+
}): Promise<CdpLoginResult> {
|
|
86
|
+
const loginUrl =
|
|
87
|
+
opts.loginUrl || process.env.RECLAIM_OLD_LOGIN_URL || DEFAULT_LOGIN_URL
|
|
88
|
+
const timeoutMs = opts.timeoutMs ?? 180_000
|
|
89
|
+
const pollMs = opts.pollMs ?? 1000
|
|
90
|
+
|
|
91
|
+
const cfg = new ConfigStore().read()?.browser_agent
|
|
92
|
+
const mode = cfg?.attachMode ?? 'dedicated'
|
|
93
|
+
const port = cfg?.port ?? 9222
|
|
94
|
+
|
|
95
|
+
// Ensure something is answering CDP. In dedicated mode we may launch it.
|
|
96
|
+
let launchedByUs = false
|
|
97
|
+
try {
|
|
98
|
+
await waitForCdpReady(port, 500)
|
|
99
|
+
} catch{
|
|
100
|
+
if(mode !== 'dedicated') {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`attach mode: nothing answering CDP on port ${port}. Launch Chrome ` +
|
|
103
|
+
`with --remote-debugging-port=${port} AND a non-default ` +
|
|
104
|
+
'--user-data-dir, or switch to dedicated mode (attach_chrome).',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
launchChrome({ port, profileDir: defaultProfileDir() })
|
|
109
|
+
await waitForCdpReady(port, 10_000)
|
|
110
|
+
launchedByUs = true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handles = await connectCdp(port)
|
|
114
|
+
const { Target } = handles.browser
|
|
115
|
+
const { targetId } = await Target.createTarget({ url: loginUrl })
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const session = await handles.attachToTab(targetId)
|
|
119
|
+
const { Runtime } = session
|
|
120
|
+
const deadline = Date.now() + timeoutMs
|
|
121
|
+
while(Date.now() < deadline) {
|
|
122
|
+
const r = await Runtime.evaluate({
|
|
123
|
+
expression: EXTRACT_TOKEN_JS,
|
|
124
|
+
awaitPromise: true,
|
|
125
|
+
returnByValue: true,
|
|
126
|
+
})
|
|
127
|
+
const value = r.result?.value
|
|
128
|
+
if(typeof value === 'string' && value.length > 0) {
|
|
129
|
+
return { token: value, closedBrowser: launchedByUs }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw new Error(
|
|
136
|
+
'Timed out waiting for login. Finish signing in at the opened ' +
|
|
137
|
+
'page, or re-run reclaim_authenticate with a longer timeoutMs.',
|
|
138
|
+
)
|
|
139
|
+
} finally {
|
|
140
|
+
// Best-effort cleanup — never mask the real result/error.
|
|
141
|
+
try {
|
|
142
|
+
if(launchedByUs) {
|
|
143
|
+
await handles.browser.Browser.close()
|
|
144
|
+
} else {
|
|
145
|
+
await handles.browser.Target.closeTarget({ targetId })
|
|
146
|
+
}
|
|
147
|
+
} catch{
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await handles.close().catch(() => {})
|
|
152
|
+
}
|
|
153
|
+
}
|