@reclaimprotocol/agent 0.1.5 → 0.1.6
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/authoring/backend.d.ts +0 -24
- package/lib/authoring/find.d.ts +3 -0
- package/lib/authoring/find.js +54 -0
- package/lib/authoring/index.d.ts +3 -3
- package/lib/authoring/index.js +4 -3
- package/lib/authoring/propose.d.ts +3 -0
- package/lib/authoring/propose.js +109 -0
- package/lib/authoring/replay.d.ts +10 -0
- package/lib/authoring/replay.js +34 -0
- package/lib/authoring/tools.d.ts +13 -6
- package/lib/authoring/tools.js +108 -293
- package/lib/authoring/util.d.ts +27 -0
- package/lib/authoring/util.js +21 -0
- package/lib/consts.d.ts +6 -1
- package/lib/consts.js +9 -2
- package/lib/mcp/server.d.ts +2 -41
- package/lib/mcp/server.js +6 -34
- package/lib/mcp/start.js +4 -2
- package/lib/mcp/tools/agent/analyze.js +3 -3
- package/lib/mcp/tools/agent/build.d.ts +4 -4
- package/lib/mcp/tools/agent/build.js +13 -15
- package/lib/mcp/tools/agent/capture.d.ts +13 -1
- package/lib/mcp/tools/agent/capture.js +18 -85
- package/lib/mcp/tools/agent/eval.d.ts +5 -2
- package/lib/mcp/tools/agent/eval.js +11 -121
- package/lib/mcp/tools/agent/find.d.ts +10 -3
- package/lib/mcp/tools/agent/find.js +25 -31
- package/lib/mcp/tools/agent/proof.d.ts +13 -3
- package/lib/mcp/tools/agent/proof.js +81 -132
- package/lib/mcp/tools/agent/propose.d.ts +10 -9
- package/lib/mcp/tools/agent/propose.js +87 -64
- package/lib/mcp/tools/agent/replay.d.ts +10 -3
- package/lib/mcp/tools/agent/replay.js +29 -36
- package/lib/mcp/tools/agent/session.d.ts +1 -3
- package/lib/mcp/tools/agent/session.js +5 -5
- package/lib/mcp/tools/agent/shared.d.ts +5 -3
- package/lib/mcp/tools/agent/shared.js +29 -19
- package/lib/old/client.d.ts +35 -0
- package/lib/old/client.js +18 -0
- package/lib/old/login-cdp.d.ts +24 -5
- package/lib/old/login-cdp.js +29 -15
- package/lib/old/sdk-client.d.ts +39 -0
- package/lib/old/sdk-client.js +85 -0
- package/lib/old/to-register.d.ts +69 -0
- package/lib/old/to-register.js +52 -2
- package/lib/old/tools/authenticate.js +3 -7
- package/lib/old/tools/build.d.ts +6 -1
- package/lib/old/tools/build.js +11 -1
- package/lib/old/tools/get-provider-info.d.ts +16 -0
- package/lib/old/tools/get-provider-info.js +73 -0
- package/lib/old/tools/get-version-config.d.ts +17 -0
- package/lib/old/tools/get-version-config.js +47 -0
- package/lib/old/tools/publish.js +189 -2
- package/lib/old/tools/update-provider-metadata.d.ts +16 -0
- package/lib/old/tools/update-provider-metadata.js +61 -0
- package/lib/proof/attestor.d.ts +6 -3
- package/lib/proof/attestor.js +10 -1
- package/lib/proof/debug-response-dump.d.ts +4 -0
- package/lib/proof/debug-response-dump.js +55 -0
- package/lib/proof/patch-domhandler.d.ts +1 -0
- package/lib/proof/patch-domhandler.js +59 -0
- package/lib/proof/zk-files.js +7 -6
- package/lib/provider/dom-xpath.d.ts +17 -0
- package/lib/provider/dom-xpath.js +187 -0
- package/lib/provider/draft.d.ts +30 -2
- package/lib/provider/draft.js +117 -28
- package/lib/provider/index.d.ts +4 -0
- package/lib/provider/index.js +7 -0
- package/lib/provider/injection-doc.js +6 -0
- package/lib/provider/matcher.d.ts +17 -20
- package/lib/provider/matcher.js +394 -280
- package/lib/provider/schema.d.ts +7 -5
- package/lib/tools/registry.d.ts +14 -0
- package/lib/tools/registry.js +32 -0
- package/package.json +9 -1
- package/skill/SKILL.builder.md +42 -23
- package/skill/SKILL.old.md +6 -2
- package/skill/builder-mode.md +1 -1
- package/skill/injection-api.md +117 -0
- package/src/authoring/backend.ts +98 -0
- package/src/authoring/index.ts +20 -0
- package/src/authoring/tools.ts +273 -0
- package/src/authoring/util.ts +42 -0
- package/src/consts.ts +15 -2
- package/src/mcp/server.ts +11 -78
- package/src/mcp/start.ts +4 -1
- package/src/mcp/tools/agent/analyze.ts +3 -3
- package/src/mcp/tools/agent/build.ts +13 -21
- package/src/mcp/tools/agent/capture.ts +31 -115
- package/src/mcp/tools/agent/eval.ts +19 -155
- package/src/mcp/tools/agent/find.ts +33 -36
- package/src/mcp/tools/agent/proof.ts +87 -160
- package/src/mcp/tools/agent/propose.ts +93 -85
- package/src/mcp/tools/agent/replay.ts +32 -42
- package/src/mcp/tools/agent/session.ts +4 -5
- package/src/mcp/tools/agent/shared.ts +152 -0
- package/src/old/client.ts +56 -0
- package/src/old/login-cdp.ts +29 -23
- package/src/old/sdk-client.ts +105 -0
- package/src/old/to-register.ts +98 -6
- package/src/old/tools/authenticate.ts +3 -11
- package/src/old/tools/build.ts +12 -0
- package/src/old/tools/get-provider-info.ts +91 -0
- package/src/old/tools/get-version-config.ts +56 -0
- package/src/old/tools/publish.ts +244 -1
- package/src/old/tools/update-provider-metadata.ts +72 -0
- package/src/proof/attestor.ts +16 -4
- package/src/proof/debug-response-dump.ts +70 -0
- package/src/proof/patch-domhandler.ts +68 -0
- package/src/proof/zk-files.ts +7 -6
- package/src/provider/dom-xpath.ts +225 -0
- package/src/provider/draft.ts +168 -35
- package/src/provider/index.ts +8 -0
- package/src/provider/injection-doc.ts +6 -0
- package/src/provider/matcher.ts +477 -357
- package/src/provider/schema.ts +7 -5
- package/src/tools/registry.ts +92 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the shared authoring tools for the MCP, bound to a per-capture LOCAL
|
|
3
|
+
* backend: a Chrome tab over CDP + the in-process attestor SDK. The tool logic
|
|
4
|
+
* (navigate/inspect/propose/replay/prove) is authored once in `../../../
|
|
5
|
+
* authoring` with the shared `defineTool` primitive; this only supplies the
|
|
6
|
+
* MCP host — resolve the backend from `captureId` (the session handle from
|
|
7
|
+
* `start_capture`, since one capture bundles the tab, its requests, and its
|
|
8
|
+
* drafts) and the rich, credential-aware `run_proof` schema.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
type AuthoringBackend,
|
|
12
|
+
authoringTools,
|
|
13
|
+
extendTool,
|
|
14
|
+
} from '../../../authoring/index.ts'
|
|
15
|
+
import { runAttestorProof } from '../../../proof/attestor.ts'
|
|
16
|
+
import { replayProvider } from '../../../proof/replay.ts'
|
|
17
|
+
import type { RegisteredTool } from '../../server.ts'
|
|
18
|
+
import type { CaptureEntry, CaptureStore } from './capture.ts'
|
|
19
|
+
import { shapeRichProofResult } from './proof.ts'
|
|
20
|
+
|
|
21
|
+
/** The local backend for one capture: drive its tab over CDP, read its
|
|
22
|
+
* captured requests, prove via the in-process attestor, replay by re-fetch. */
|
|
23
|
+
function localBackend(entry: CaptureEntry): AuthoringBackend {
|
|
24
|
+
const { tabClient, session, drafts } = entry
|
|
25
|
+
// The per-tab CDP client is strongly typed to `keyof Commands`; the shared
|
|
26
|
+
// tools drive it with dynamic method/event names, so view it as string-keyed.
|
|
27
|
+
const client = tabClient as unknown as {
|
|
28
|
+
send: (method: string, params: object) => Promise<unknown>
|
|
29
|
+
once: (e: string, cb: () => void) => void
|
|
30
|
+
removeListener: (e: string, cb: () => void) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
capture: session,
|
|
35
|
+
drafts,
|
|
36
|
+
sendCdp: (method, params) => client.send(method, params ?? {}),
|
|
37
|
+
waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
|
|
38
|
+
const onEvent = () => {
|
|
39
|
+
cleanup()
|
|
40
|
+
resolve('fired')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
cleanup()
|
|
45
|
+
resolve('timeout')
|
|
46
|
+
}, timeoutMs)
|
|
47
|
+
const cleanup = () => {
|
|
48
|
+
clearTimeout(timer)
|
|
49
|
+
client.removeListener(method, onEvent)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
client.once(method, onEvent)
|
|
53
|
+
}),
|
|
54
|
+
replay: async(draft, opts) => {
|
|
55
|
+
// Auth-bound check: re-fetch with the secrets dropped. replayProvider
|
|
56
|
+
// already does a live Node fetch, so empty secrets == anonymous.
|
|
57
|
+
const secrets = opts?.withoutSecrets ? {} : draft.secrets
|
|
58
|
+
const result = await replayProvider(draft.provider, secrets)
|
|
59
|
+
return {
|
|
60
|
+
status: result.status,
|
|
61
|
+
matchesOriginal: result.matchesOriginal,
|
|
62
|
+
bodySnippet: result.body.slice(0, 256),
|
|
63
|
+
...(result.hints !== undefined ? { hints: result.hints } : {}),
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
prove: async(draft, opts) => {
|
|
67
|
+
const result = await runAttestorProof(
|
|
68
|
+
draft.provider,
|
|
69
|
+
draft.secrets,
|
|
70
|
+
{
|
|
71
|
+
...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
|
|
72
|
+
...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
return shapeRichProofResult(result, {
|
|
76
|
+
provider: draft.provider,
|
|
77
|
+
hasSecrets: Object.keys(draft.secrets).length > 0,
|
|
78
|
+
...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The MCP's rich, credential-aware `run_proof` schema, merged onto the shared
|
|
85
|
+
* tool by the host below (the cloud's TEE signs in-enclave, so it needs none
|
|
86
|
+
* of this). */
|
|
87
|
+
const RUN_PROOF_EXTRAS = {
|
|
88
|
+
description:
|
|
89
|
+
'Run a full zkTLS proof of the drafted recipe through the Reclaim '
|
|
90
|
+
+ 'attestor SDK. Use only after replay_request looks right — attestor '
|
|
91
|
+
+ 'calls are expensive. Writes the full claim to '
|
|
92
|
+
+ '~/.reclaim/claims/<timestamp>.json and returns identifier + '
|
|
93
|
+
+ 'extractedParameters + claimPath + a prettified `claim` (request '
|
|
94
|
+
+ 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
|
|
95
|
+
+ 'writes a per-call diagnostic dump (input provider, sentParams, full '
|
|
96
|
+
+ 'attestor log stream, result) and returns it as `runLogPath`. '
|
|
97
|
+
+ '`attestorUrl` overrides the default only when the dev supplies a '
|
|
98
|
+
+ 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
|
|
99
|
+
+ '(the credential to sign with) is REQUIRED — call resolve_owner_key '
|
|
100
|
+
+ 'first. errorKind="missing-owner-key" means that address has no known '
|
|
101
|
+
+ 'private key; re-run resolve_owner_key or issue/import a credential.',
|
|
102
|
+
properties: {
|
|
103
|
+
ownerAddress: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
pattern: '^0x[0-9a-fA-F]{40}$',
|
|
106
|
+
description:
|
|
107
|
+
'ETH address of the credential to sign the claim with. Get it '
|
|
108
|
+
+ 'from resolve_owner_key first. The private key is resolved '
|
|
109
|
+
+ 'locally (cache / env / .env) — never passed here.',
|
|
110
|
+
},
|
|
111
|
+
attestorUrl: { type: 'string' },
|
|
112
|
+
},
|
|
113
|
+
required: ['ownerAddress'],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The shared authoring tools mapped to the MCP's shape: resolve each call's
|
|
117
|
+
* local backend from `args.captureId`, and widen the input schema — every tool
|
|
118
|
+
* takes `captureId`; `run_proof` also gets the credential-aware contract. The
|
|
119
|
+
* tools are authored plain in `../../../authoring`; this only wraps them with
|
|
120
|
+
* `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
|
|
121
|
+
export function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[] {
|
|
122
|
+
const resolveBackend = (args: Record<string, unknown>): AuthoringBackend => {
|
|
123
|
+
const captureId = String(args.captureId)
|
|
124
|
+
const entry = captures.sessions.get(captureId)
|
|
125
|
+
if(!entry) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`no capture ${captureId} — call start_capture first `
|
|
128
|
+
+ '(it has been reset or never started).',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return localBackend(entry)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return authoringTools(resolveBackend).map((tool) => {
|
|
136
|
+
if(tool.definition.name === 'run_proof') {
|
|
137
|
+
return extendTool(tool, {
|
|
138
|
+
description: RUN_PROOF_EXTRAS.description,
|
|
139
|
+
properties: {
|
|
140
|
+
captureId: { type: 'string' },
|
|
141
|
+
...RUN_PROOF_EXTRAS.properties,
|
|
142
|
+
},
|
|
143
|
+
required: ['captureId', ...RUN_PROOF_EXTRAS.required],
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return extendTool(tool, {
|
|
148
|
+
properties: { captureId: { type: 'string' } },
|
|
149
|
+
required: ['captureId'],
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
}
|
package/src/old/client.ts
CHANGED
|
@@ -34,6 +34,15 @@ export interface RegisterProviderBody {
|
|
|
34
34
|
useProxy?: boolean
|
|
35
35
|
providerType?: 'PRIVATE' | 'PUBLIC'
|
|
36
36
|
requestData: unknown[]
|
|
37
|
+
/** Page-injection script (old-devtools name for the builder's
|
|
38
|
+
* `webSettings.jsUserScripts`). Top-level on register; nested under
|
|
39
|
+
* `providerConfig` on the config (add-version) body instead. */
|
|
40
|
+
customInjection?: string
|
|
41
|
+
/** Accepted at register too (unlike most `providerConfig` extras, which
|
|
42
|
+
* the register controller only reads on config/add-version). */
|
|
43
|
+
userAgent?: { ios?: string, android?: string }
|
|
44
|
+
/** Accepted at register too. */
|
|
45
|
+
pageTitle?: string
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
export interface UpdateProviderConfigBody {
|
|
@@ -42,6 +51,19 @@ export interface UpdateProviderConfigBody {
|
|
|
42
51
|
versionInfo: string
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/** Body for `POST /api/providers/:providerId` (`updateProviderMetadata`) —
|
|
55
|
+
* a genuine PARTIAL update on the PROVIDER document itself (name/
|
|
56
|
+
* description/providerType/tags/isActive), distinct from both `register`
|
|
57
|
+
* (creates the provider) and `config` (adds an immutable new version). Only
|
|
58
|
+
* the fields present get changed; everything else is left untouched. */
|
|
59
|
+
export interface UpdateProviderMetadataBody {
|
|
60
|
+
name?: string
|
|
61
|
+
description?: string
|
|
62
|
+
providerType?: 'PRIVATE' | 'PUBLIC'
|
|
63
|
+
tags?: string[]
|
|
64
|
+
isActive?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
export interface MyProvidersQuery {
|
|
46
68
|
pageKey?: number
|
|
47
69
|
pageSize?: number
|
|
@@ -121,6 +143,40 @@ export class ReclaimOldClient {
|
|
|
121
143
|
)
|
|
122
144
|
}
|
|
123
145
|
|
|
146
|
+
/** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
|
|
147
|
+
* one version — the latest when `version` is omitted. Readable for ANY
|
|
148
|
+
* non-archived provider (not owner-restricted), same as the dashboard's
|
|
149
|
+
* edit-form prefill fetch. */
|
|
150
|
+
getProviderConfig(
|
|
151
|
+
providerId: string,
|
|
152
|
+
version?: string,
|
|
153
|
+
): Promise<unknown> {
|
|
154
|
+
return this.#request(
|
|
155
|
+
'GET',
|
|
156
|
+
`/providers/${encodeURIComponent(providerId)}/config`,
|
|
157
|
+
{
|
|
158
|
+
auth: true,
|
|
159
|
+
query: version ? { versionNumber: version } : undefined,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Partial update on the PROVIDER document itself — name/description/
|
|
165
|
+
* providerType/tags/isActive, after creation. The backend 403s if you
|
|
166
|
+
* don't own the provider (and aren't admin); other providerConfig-style
|
|
167
|
+
* fields sent here are simply ignored (this endpoint only reads the
|
|
168
|
+
* ones above from the body). */
|
|
169
|
+
updateProviderMetadata(
|
|
170
|
+
providerId: string,
|
|
171
|
+
body: UpdateProviderMetadataBody,
|
|
172
|
+
): Promise<unknown> {
|
|
173
|
+
return this.#request(
|
|
174
|
+
'POST',
|
|
175
|
+
`/providers/${encodeURIComponent(providerId)}`,
|
|
176
|
+
{ body, auth: true },
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
124
180
|
/** Analytics logs for a verification session, from the logs service (a
|
|
125
181
|
* DIFFERENT host than the devtools API — hence its own fetch, not
|
|
126
182
|
* `#request`). Attaches the caller identity when one is set; queried
|
package/src/old/login-cdp.ts
CHANGED
|
@@ -18,16 +18,39 @@ import { DEFAULT_LOGIN_URL, oldLoginUrl } from '../consts.ts'
|
|
|
18
18
|
* `firebaseLocalStorage`) in case a build switches persistence. Returns the
|
|
19
19
|
* token string, or null when the user has not finished logging in yet.
|
|
20
20
|
*
|
|
21
|
+
* The CDP-launched Chrome reuses a PERSISTENT profile dir across runs, so a
|
|
22
|
+
* stale `firebase:authUser:*` entry from an earlier (now-expired, ~1h TTL)
|
|
23
|
+
* session is often still sitting in storage the instant the page loads —
|
|
24
|
+
* before the developer has clicked anything. Returning that immediately
|
|
25
|
+
* makes the poll "succeed" on tick one, closing the tab before the developer
|
|
26
|
+
* can sign in, and the captured token then fails backend verification. So
|
|
27
|
+
* every candidate's JWT `exp` claim is checked here (in-page, where `atob` is
|
|
28
|
+
* available) and stale ones are skipped — the loop keeps polling until a
|
|
29
|
+
* genuinely fresh token shows up from an interactive sign-in.
|
|
30
|
+
*
|
|
21
31
|
* Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
|
|
22
32
|
*/
|
|
23
|
-
const EXTRACT_TOKEN_JS = `(async () => {
|
|
33
|
+
export const EXTRACT_TOKEN_JS = `(async () => {
|
|
34
|
+
const isFresh = (token) => {
|
|
35
|
+
try {
|
|
36
|
+
const payload = JSON.parse(
|
|
37
|
+
atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
|
|
38
|
+
)
|
|
39
|
+
// No exp claim to check — don't block a token we can't evaluate.
|
|
40
|
+
if (typeof payload.exp !== 'number') return true
|
|
41
|
+
// 60s buffer so a token that's about to expire isn't treated as fresh.
|
|
42
|
+
return payload.exp * 1000 > Date.now() + 60000
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
24
47
|
try {
|
|
25
48
|
for (let i = 0; i < localStorage.length; i++) {
|
|
26
49
|
const k = localStorage.key(i)
|
|
27
50
|
if (k && k.indexOf('firebase:authUser:') === 0) {
|
|
28
51
|
const v = JSON.parse(localStorage.getItem(k) || 'null')
|
|
29
52
|
const t = v && v.stsTokenManager && v.stsTokenManager.accessToken
|
|
30
|
-
if (t) return t
|
|
53
|
+
if (t && isFresh(t)) return t
|
|
31
54
|
}
|
|
32
55
|
}
|
|
33
56
|
} catch (e) {}
|
|
@@ -49,7 +72,9 @@ const EXTRACT_TOKEN_JS = `(async () => {
|
|
|
49
72
|
all.onsuccess = () => {
|
|
50
73
|
for (const row of (all.result || [])) {
|
|
51
74
|
const m = row && row.value && row.value.stsTokenManager
|
|
52
|
-
if (m && m.accessToken
|
|
75
|
+
if (m && m.accessToken && isFresh(m.accessToken)) {
|
|
76
|
+
resolve(m.accessToken); return
|
|
77
|
+
}
|
|
53
78
|
}
|
|
54
79
|
resolve(null)
|
|
55
80
|
}
|
|
@@ -79,18 +104,11 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
79
104
|
loginUrl?: string
|
|
80
105
|
timeoutMs?: number
|
|
81
106
|
pollMs?: number
|
|
82
|
-
/** Called every ~10s while waiting for the human to finish signing in, so
|
|
83
|
-
* MCP clients that track progress on this call can reset their own
|
|
84
|
-
* request timeout instead of giving up on a login that's still in
|
|
85
|
-
* flight (see `ToolContext` in `mcp/server.ts`). */
|
|
86
|
-
onProgress?: (message: string) => Promise<void>
|
|
87
107
|
}): Promise<CdpLoginResult> {
|
|
88
108
|
const loginUrl =
|
|
89
109
|
opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL
|
|
90
110
|
const timeoutMs = opts.timeoutMs ?? 180_000
|
|
91
111
|
const pollMs = opts.pollMs ?? 1000
|
|
92
|
-
const onProgress = opts.onProgress ?? (async() => {})
|
|
93
|
-
const progressIntervalMs = 10_000
|
|
94
112
|
|
|
95
113
|
const cfg = new ConfigStore().read()?.browser_agent
|
|
96
114
|
const mode = cfg?.attachMode ?? 'dedicated'
|
|
@@ -121,9 +139,7 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
121
139
|
try {
|
|
122
140
|
const session = await handles.attachToTab(targetId)
|
|
123
141
|
const { Runtime } = session
|
|
124
|
-
const
|
|
125
|
-
const deadline = startedAt + timeoutMs
|
|
126
|
-
let lastProgressAt = startedAt
|
|
142
|
+
const deadline = Date.now() + timeoutMs
|
|
127
143
|
while(Date.now() < deadline) {
|
|
128
144
|
const r = await Runtime.evaluate({
|
|
129
145
|
expression: EXTRACT_TOKEN_JS,
|
|
@@ -135,16 +151,6 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
135
151
|
return { token: value, closedBrowser: launchedByUs }
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
const now = Date.now()
|
|
139
|
-
if(now - lastProgressAt >= progressIntervalMs) {
|
|
140
|
-
lastProgressAt = now
|
|
141
|
-
const remainingSec = Math.round((deadline - now) / 1000)
|
|
142
|
-
await onProgress(
|
|
143
|
-
`Still waiting for sign-in at ${loginUrl} ` +
|
|
144
|
-
`(${remainingSec}s left before this call times out).`,
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
154
|
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { NAME, VERSION } from '../pkg.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default origin of the PUBLIC production read API that the Reclaim
|
|
6
|
+
* verification SDK/attestor itself calls at runtime to fetch a provider's
|
|
7
|
+
* recipe — a DIFFERENT service from the old-devtools dashboard backend
|
|
8
|
+
* (`ReclaimOldClient`, `devapi.reclaimprotocol.org`). Both endpoints below are
|
|
9
|
+
* unauthenticated reads (no `authenticateToken` middleware on this backend's
|
|
10
|
+
* routes), so this client needs no identity. Override with
|
|
11
|
+
* `RECLAIM_SDK_API_URL`.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_SDK_API_URL = 'https://api.reclaimprotocol.org'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal read-only client for the public provider-info endpoints on the SDK
|
|
17
|
+
* backend (`reclaim-sdk-backend`). Two distinct reads, on purpose:
|
|
18
|
+
*
|
|
19
|
+
* - `getProvider` (`GET /providers/:id`) — the full provider row merged
|
|
20
|
+
* with its version's WHOLE providerConfig, `customInjection` included.
|
|
21
|
+
* - `getProviderConfigs` (`GET /providers/:id/configs`) — the trimmed
|
|
22
|
+
* recipe the attestor actually consumes at verification time: a fixed
|
|
23
|
+
* field projection that explicitly OMITS `customInjection` and adds a
|
|
24
|
+
* computed `isScriptRequestingClaim` flag instead.
|
|
25
|
+
*
|
|
26
|
+
* Errors are thrown as `@hapi/boom` so `remapErrorAsResponse` renders them
|
|
27
|
+
* the same way as every other tool in this package.
|
|
28
|
+
*/
|
|
29
|
+
export class ReclaimSdkClient {
|
|
30
|
+
readonly baseUrl: string
|
|
31
|
+
#fetch: typeof fetch
|
|
32
|
+
|
|
33
|
+
constructor(opts: { baseUrl?: string, fetch?: typeof fetch } = {}) {
|
|
34
|
+
this.baseUrl = (opts.baseUrl || DEFAULT_SDK_API_URL).replace(/\/+$/, '')
|
|
35
|
+
this.#fetch = opts.fetch ?? fetch
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Full provider + providerConfig (includes `customInjection`). Latest
|
|
39
|
+
* version when `versionNumber` is omitted. */
|
|
40
|
+
getProvider(providerId: string, versionNumber?: string): Promise<unknown> {
|
|
41
|
+
return this.#get(
|
|
42
|
+
`/providers/${encodeURIComponent(providerId)}`,
|
|
43
|
+
versionNumber,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Trimmed runtime recipe (no `customInjection`; adds
|
|
48
|
+
* `isScriptRequestingClaim`). Latest version when `versionNumber` is
|
|
49
|
+
* omitted. */
|
|
50
|
+
getProviderConfigs(
|
|
51
|
+
providerId: string,
|
|
52
|
+
versionNumber?: string,
|
|
53
|
+
): Promise<unknown> {
|
|
54
|
+
return this.#get(
|
|
55
|
+
`/providers/${encodeURIComponent(providerId)}/configs`,
|
|
56
|
+
versionNumber,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async #get(path: string, versionNumber?: string): Promise<unknown> {
|
|
61
|
+
let url = `${this.baseUrl}/api${path}`
|
|
62
|
+
if(versionNumber) {
|
|
63
|
+
url += `?versionNumber=${encodeURIComponent(versionNumber)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const res = await this.#fetch(url, {
|
|
67
|
+
method: 'GET',
|
|
68
|
+
headers: {
|
|
69
|
+
Accept: 'application/json',
|
|
70
|
+
'User-Agent': `${NAME}/${VERSION}`,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
let json: unknown
|
|
75
|
+
try {
|
|
76
|
+
json = await res.json()
|
|
77
|
+
} catch{
|
|
78
|
+
json = undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const failed =
|
|
82
|
+
!res.ok ||
|
|
83
|
+
(json !== undefined &&
|
|
84
|
+
typeof json === 'object' &&
|
|
85
|
+
(json as { isSuccess?: unknown }).isSuccess === false)
|
|
86
|
+
if(failed) {
|
|
87
|
+
const message =
|
|
88
|
+
(json && typeof json === 'object' &&
|
|
89
|
+
typeof (json as { message?: unknown }).message === 'string'
|
|
90
|
+
? (json as { message: string }).message
|
|
91
|
+
: undefined) || res.statusText || 'SDK backend request failed'
|
|
92
|
+
throw new Boom(message, {
|
|
93
|
+
statusCode: res.ok ? 502 : res.status,
|
|
94
|
+
data: {
|
|
95
|
+
status: res.status,
|
|
96
|
+
title: message,
|
|
97
|
+
detail: message,
|
|
98
|
+
body: json,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return json
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/old/to-register.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReclaimProvider } from '../provider/schema.ts'
|
|
2
|
+
import type { AllowedJsRequest, DraftRequest } from '../provider/to-version.ts'
|
|
2
3
|
import type {
|
|
3
4
|
RegisterProviderBody,
|
|
4
5
|
UpdateProviderConfigBody,
|
|
@@ -34,6 +35,30 @@ export interface OldProviderOpts {
|
|
|
34
35
|
/** Defaults to `true` when omitted. Register only (Provider-level flag). */
|
|
35
36
|
useProxy?: boolean
|
|
36
37
|
providerType?: 'PRIVATE' | 'PUBLIC'
|
|
38
|
+
/** Page-injection script — old-devtools' name for the builder's
|
|
39
|
+
* `webSettings.jsUserScripts`. Top-level on register, nested under
|
|
40
|
+
* `providerConfig` on config (add-version). The backend rejects a few
|
|
41
|
+
* deprecated injection APIs (e.g. `window.payloadData`) with a 400. */
|
|
42
|
+
customInjection?: string
|
|
43
|
+
/** Accepted at register too (unlike the four fields below, which the
|
|
44
|
+
* backend only accepts on config/add-version). */
|
|
45
|
+
userAgent?: { ios?: string, android?: string }
|
|
46
|
+
/** Accepted at register too. */
|
|
47
|
+
pageTitle?: string
|
|
48
|
+
/** Config (add-version) ONLY — the register endpoint hardcodes this to
|
|
49
|
+
* `[]` and ignores whatever is sent. Already old-backend-shaped: build
|
|
50
|
+
* fresh entries with {@link injectedRequestDataFrom}; a carried-forward
|
|
51
|
+
* value (read back from an existing version) passes through unchanged. */
|
|
52
|
+
allowedInjectedRequestData?: OldInjectedRequestData[]
|
|
53
|
+
/** Config (add-version) ONLY — the register endpoint never reads this
|
|
54
|
+
* field from the request body. */
|
|
55
|
+
stepsToFollow?: string
|
|
56
|
+
/** Config (add-version) ONLY — the register endpoint never reads this
|
|
57
|
+
* field from the request body. */
|
|
58
|
+
useIncognitoWebview?: boolean
|
|
59
|
+
/** Config (add-version) ONLY — the register endpoint never reads this
|
|
60
|
+
* field from the request body. Free-form JSON. */
|
|
61
|
+
extensionConfig?: unknown
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
interface OldResponseMatch {
|
|
@@ -63,8 +88,18 @@ interface OldRequestData {
|
|
|
63
88
|
bodySniff?: { enabled: boolean, template: string }
|
|
64
89
|
}
|
|
65
90
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
91
|
+
/** One `allowedInjectedRequestData` entry — an `OldRequestData` plus the
|
|
92
|
+
* pagination/optionality flags the old backend stores alongside it. */
|
|
93
|
+
export interface OldInjectedRequestData extends OldRequestData {
|
|
94
|
+
required?: boolean
|
|
95
|
+
multiple?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build the single `requestData` entry shared by register + update. Takes
|
|
99
|
+
* the loose `DraftRequest` shape (not the full `ReclaimProvider`) so it can
|
|
100
|
+
* build BOTH the top-level `requestData` and `allowedInjectedRequestData`
|
|
101
|
+
* entries — a `ReclaimProvider` satisfies it structurally either way. */
|
|
102
|
+
function requestDataFromProvider(provider: DraftRequest): OldRequestData {
|
|
68
103
|
const method = String(provider.method).toUpperCase()
|
|
69
104
|
if(method !== 'GET' && method !== 'POST') {
|
|
70
105
|
throw new Error(
|
|
@@ -87,14 +122,13 @@ function requestDataFromProvider(provider: ReclaimProvider): OldRequestData {
|
|
|
87
122
|
)
|
|
88
123
|
}
|
|
89
124
|
|
|
90
|
-
const responseMatches: OldResponseMatch[] = provider.responseMatches
|
|
91
|
-
(m, i) => ({
|
|
125
|
+
const responseMatches: OldResponseMatch[] = (provider.responseMatches ?? [])
|
|
126
|
+
.map((m, i) => ({
|
|
92
127
|
value: m.value,
|
|
93
128
|
type: m.type ?? 'contains',
|
|
94
129
|
isOptional: false,
|
|
95
130
|
order: i,
|
|
96
|
-
})
|
|
97
|
-
)
|
|
131
|
+
}))
|
|
98
132
|
|
|
99
133
|
// Old devtools wants `responseMatches` and `responseRedactions` as PARALLEL
|
|
100
134
|
// arrays — same shape the builder uses, so pass them through directly. We do
|
|
@@ -157,6 +191,22 @@ function requestDataFrom(providers: ReclaimProvider[]): OldRequestData[] {
|
|
|
157
191
|
return providers.map(requestDataFromProvider)
|
|
158
192
|
}
|
|
159
193
|
|
|
194
|
+
/** Translate fresh `allowedInjectedRequestData` drafts (the author's input
|
|
195
|
+
* shape, `AllowedJsRequest[]` — same shape the builder translator uses for
|
|
196
|
+
* its own `allowedJsRequests`) into the old backend's shape. A
|
|
197
|
+
* carried-forward value read back from an existing version is ALREADY in
|
|
198
|
+
* this shape and should be passed straight through instead — don't run it
|
|
199
|
+
* through this a second time. */
|
|
200
|
+
export function injectedRequestDataFrom(
|
|
201
|
+
requests: AllowedJsRequest[],
|
|
202
|
+
): OldInjectedRequestData[] {
|
|
203
|
+
return requests.map((req) => ({
|
|
204
|
+
...requestDataFromProvider(req),
|
|
205
|
+
required: req.required ?? true,
|
|
206
|
+
multiple: req.multiple ?? true,
|
|
207
|
+
}))
|
|
208
|
+
}
|
|
209
|
+
|
|
160
210
|
/** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
|
|
161
211
|
* drafted request becomes a `requestData` entry; provider-level fields (name)
|
|
162
212
|
* come from the first draft. */
|
|
@@ -177,6 +227,21 @@ export function providerToRegisterBody(
|
|
|
177
227
|
body.description = opts.description
|
|
178
228
|
}
|
|
179
229
|
|
|
230
|
+
if(opts.customInjection !== undefined) {
|
|
231
|
+
body.customInjection = opts.customInjection
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Register also accepts these two (unlike allowedInjectedRequestData /
|
|
235
|
+
// stepsToFollow / useIncognitoWebview / extensionConfig, which the
|
|
236
|
+
// register controller never reads from the body at all).
|
|
237
|
+
if(opts.userAgent !== undefined) {
|
|
238
|
+
body.userAgent = opts.userAgent
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if(opts.pageTitle !== undefined) {
|
|
242
|
+
body.pageTitle = opts.pageTitle
|
|
243
|
+
}
|
|
244
|
+
|
|
180
245
|
return body
|
|
181
246
|
}
|
|
182
247
|
|
|
@@ -197,6 +262,33 @@ export function providerToConfigBody(
|
|
|
197
262
|
geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
|
|
198
263
|
requestData: requestDataFrom(providers),
|
|
199
264
|
}
|
|
265
|
+
if(opts.customInjection !== undefined) {
|
|
266
|
+
providerConfig.customInjection = opts.customInjection
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if(opts.userAgent !== undefined) {
|
|
270
|
+
providerConfig.userAgent = opts.userAgent
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if(opts.pageTitle !== undefined) {
|
|
274
|
+
providerConfig.pageTitle = opts.pageTitle
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if(opts.allowedInjectedRequestData !== undefined) {
|
|
278
|
+
providerConfig.allowedInjectedRequestData = opts.allowedInjectedRequestData
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if(opts.stepsToFollow !== undefined) {
|
|
282
|
+
providerConfig.stepsToFollow = opts.stepsToFollow
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if(opts.useIncognitoWebview !== undefined) {
|
|
286
|
+
providerConfig.useIncognitoWebview = opts.useIncognitoWebview
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if(opts.extensionConfig !== undefined) {
|
|
290
|
+
providerConfig.extensionConfig = opts.extensionConfig
|
|
291
|
+
}
|
|
200
292
|
|
|
201
293
|
return { providerConfig, version, versionInfo }
|
|
202
294
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { agentDisabled } from '../../consts.ts'
|
|
2
|
-
import {
|
|
3
|
-
defineTool,
|
|
4
|
-
type RegisteredTool,
|
|
5
|
-
type ToolContext,
|
|
6
|
-
} from '../../mcp/server.ts'
|
|
2
|
+
import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
7
3
|
import type { OldIdentity, ReclaimOldClient } from '../client.ts'
|
|
8
4
|
import { persistIdentity, toEthUid } from '../identity.ts'
|
|
9
5
|
import { captureFirebaseTokenViaCdp } from '../login-cdp.ts'
|
|
@@ -87,7 +83,7 @@ export function authenticateTool(client: ReclaimOldClient): RegisteredTool {
|
|
|
87
83
|
},
|
|
88
84
|
},
|
|
89
85
|
},
|
|
90
|
-
async({ token, ethAddress, loginUrl, timeoutMs, useBrowser }
|
|
86
|
+
async({ token, ethAddress, loginUrl, timeoutMs, useBrowser }) => {
|
|
91
87
|
if(token && ethAddress) {
|
|
92
88
|
throw new Error(
|
|
93
89
|
'Provide only ONE of `token` or `ethAddress`, not both.',
|
|
@@ -140,11 +136,7 @@ export function authenticateTool(client: ReclaimOldClient): RegisteredTool {
|
|
|
140
136
|
)
|
|
141
137
|
}
|
|
142
138
|
|
|
143
|
-
const result = await captureFirebaseTokenViaCdp({
|
|
144
|
-
loginUrl,
|
|
145
|
-
timeoutMs,
|
|
146
|
-
onProgress: ctx.reportProgress,
|
|
147
|
-
})
|
|
139
|
+
const result = await captureFirebaseTokenViaCdp({ loginUrl, timeoutMs })
|
|
148
140
|
const identity: OldIdentity = { kind: 'bearer', token: result.token }
|
|
149
141
|
await validate(identity)
|
|
150
142
|
persistIdentity(identity)
|
package/src/old/tools/build.ts
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
import type { RegisteredTool } from '../../mcp/server.ts'
|
|
2
2
|
import type { ReclaimOldClient } from '../client.ts'
|
|
3
|
+
import type { ReclaimSdkClient } from '../sdk-client.ts'
|
|
3
4
|
import { authenticateTool } from './authenticate.ts'
|
|
5
|
+
import { getProviderInfoTool } from './get-provider-info.ts'
|
|
6
|
+
import { getProviderVersionConfigTool } from './get-version-config.ts'
|
|
4
7
|
import { getMeProvidersTool } from './me-providers.ts'
|
|
5
8
|
import { createVersionFromCaptureTool } from './publish.ts'
|
|
6
9
|
import { sessionAnalyticsLogsTool } from './session-analytics-logs.ts'
|
|
10
|
+
import { updateProviderMetadataTool } from './update-provider-metadata.ts'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* The backend-facing tools for OLD-devtools mode. Mirrors
|
|
10
14
|
* `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
|
|
11
15
|
* list for `registerToolList`. These replace the builder credential + publish
|
|
12
16
|
* tools and the (disabled) generated provider tools.
|
|
17
|
+
*
|
|
18
|
+
* `sdkClient` is a SEPARATE, unauthenticated client for the public
|
|
19
|
+
* production SDK backend (`api.reclaimprotocol.org`) — a different host from
|
|
20
|
+
* `client`'s old-devtools dashboard backend (`devapi.reclaimprotocol.org`).
|
|
13
21
|
*/
|
|
14
22
|
export function buildOldBackendTools(
|
|
15
23
|
client: ReclaimOldClient,
|
|
24
|
+
sdkClient: ReclaimSdkClient,
|
|
16
25
|
): RegisteredTool[] {
|
|
17
26
|
return [
|
|
18
27
|
authenticateTool(client),
|
|
19
28
|
createVersionFromCaptureTool(client),
|
|
20
29
|
getMeProvidersTool(client),
|
|
30
|
+
getProviderVersionConfigTool(client),
|
|
31
|
+
getProviderInfoTool(sdkClient),
|
|
32
|
+
updateProviderMetadataTool(client),
|
|
21
33
|
sessionAnalyticsLogsTool(client),
|
|
22
34
|
]
|
|
23
35
|
}
|