@reclaimprotocol/agent 0.1.35 → 0.1.37
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 +8 -8
- package/lib/authoring/backend.d.ts +12 -1
- package/lib/authoring/backend.js +3 -0
- package/lib/authoring/tools.js +33 -5
- package/lib/authoring/util.d.ts +43 -0
- package/lib/authoring/util.js +54 -0
- package/lib/cdp/chrome-for-testing.d.ts +14 -0
- package/lib/cdp/chrome-for-testing.js +43 -0
- package/lib/cdp/geo.d.ts +3 -0
- package/lib/cdp/geo.js +27 -15
- package/lib/cdp/launcher.d.ts +79 -1
- package/lib/cdp/launcher.js +244 -24
- package/lib/logger.js +3 -1
- package/lib/mcp/api/operations.gen.js +1 -1
- package/lib/mcp/instructions.js +3 -3
- package/lib/mcp/start.js +17 -2
- package/lib/mcp/tools/agent/attach.d.ts +6 -0
- package/lib/mcp/tools/agent/attach.js +58 -118
- package/lib/mcp/tools/agent/build.js +6 -0
- package/lib/mcp/tools/agent/capture.js +33 -10
- package/lib/mcp/tools/agent/check-attestor.js +15 -7
- package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
- package/lib/mcp/tools/agent/draft-tools.js +170 -0
- package/lib/mcp/tools/agent/pod-proof.js +5 -1
- package/lib/mcp/tools/agent/proof.d.ts +28 -0
- package/lib/mcp/tools/agent/proof.js +72 -0
- package/lib/mcp/tools/agent/shared.d.ts +8 -1
- package/lib/mcp/tools/agent/shared.js +98 -19
- package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
- package/lib/mcp/tools/agent/start-integration.js +10 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
- package/lib/mcp/tools/authenticate/authenticate.js +212 -116
- package/lib/mcp/tools/authenticate/build.d.ts +2 -2
- package/lib/mcp/tools/authenticate/build.js +6 -3
- package/lib/mcp/tools/authenticate/builder-access.js +5 -2
- package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
- package/lib/mcp/tools/authenticate/eth-key.js +29 -5
- package/lib/mcp/tools/authenticate/index.d.ts +1 -1
- package/lib/mcp/tools/authenticate/index.js +1 -1
- package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
- package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
- package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
- package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
- package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
- package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
- package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
- package/lib/mcp/tools/authenticate/session-cache.js +95 -4
- package/lib/mcp/tools/mode/mode.js +6 -6
- package/lib/mcp/tools/verification/proxy-callback-events.d.ts +21 -3
- package/lib/mcp/tools/verification/proxy-callback-events.js +128 -45
- package/lib/old/login-cdp.js +2 -3
- package/lib/proof/attestor.js +2 -3
- package/lib/proof/check-attestor.d.ts +12 -2
- package/lib/proof/check-attestor.js +40 -10
- package/lib/proof/check-matchers.d.ts +36 -0
- package/lib/proof/check-matchers.js +161 -0
- package/lib/proof/zk-files.d.ts +50 -11
- package/lib/proof/zk-files.js +259 -63
- package/lib/provider/draft.d.ts +24 -1
- package/lib/provider/draft.js +135 -0
- package/lib/provider/matcher.js +171 -4
- package/package.json +6 -4
- package/skill/SKILL.builder.md +7 -2
- package/skill/SKILL.common.md +90 -70
- package/skill/SKILL.old.md +3 -3
- package/skill/builder-mode.md +44 -6
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +51 -35
- package/skill/verification-client-integration.md +87 -15
- package/src/authoring/backend.ts +12 -1
- package/src/authoring/tools.ts +48 -7
- package/src/authoring/util.ts +64 -0
- package/src/cdp/chrome-for-testing.ts +66 -0
- package/src/cdp/geo.ts +36 -21
- package/src/cdp/launcher.ts +328 -24
- package/src/logger.ts +3 -1
- package/src/mcp/api/operations.gen.ts +1 -1
- package/src/mcp/instructions.ts +3 -3
- package/src/mcp/start.ts +20 -1
- package/src/mcp/tools/agent/attach.ts +65 -128
- package/src/mcp/tools/agent/build.ts +7 -0
- package/src/mcp/tools/agent/capture.ts +54 -11
- package/src/mcp/tools/agent/check-attestor.ts +15 -7
- package/src/mcp/tools/agent/draft-tools.ts +234 -0
- package/src/mcp/tools/agent/pod-proof.ts +5 -1
- package/src/mcp/tools/agent/proof.ts +110 -1
- package/src/mcp/tools/agent/shared.ts +130 -19
- package/src/mcp/tools/agent/start-integration.ts +10 -6
- package/src/mcp/tools/authenticate/authenticate.ts +276 -131
- package/src/mcp/tools/authenticate/build.ts +6 -3
- package/src/mcp/tools/authenticate/builder-access.ts +5 -2
- package/src/mcp/tools/authenticate/eth-key.ts +41 -7
- package/src/mcp/tools/authenticate/index.ts +1 -1
- package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
- package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
- package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
- package/src/mcp/tools/authenticate/session-cache.ts +125 -5
- package/src/mcp/tools/mode/mode.ts +6 -6
- package/src/mcp/tools/verification/proxy-callback-events.ts +151 -41
- package/src/old/login-cdp.ts +2 -3
- package/src/proof/attestor.ts +2 -3
- package/src/proof/check-attestor.ts +55 -12
- package/src/proof/check-matchers.ts +227 -0
- package/src/proof/zk-files.ts +357 -65
- package/src/provider/draft.ts +215 -7
- package/src/provider/matcher.ts +237 -4
|
@@ -2,9 +2,18 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import * as oauth from 'openid-client';
|
|
4
4
|
import { defineTool } from "../../server.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import { authBaseFor, oauthRequestOptions, SCOPE, sessionFromTokens, } from "./oauth-config.js";
|
|
6
|
+
import { readSession, writeSession } from "./session-cache.js";
|
|
7
|
+
/** How long a sign-in link stays valid (the loopback listener's lifetime). */
|
|
7
8
|
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9
|
+
/** Longest single `wait_for_sign_in` call; stays under common 60 s tool
|
|
10
|
+
* timeouts. */
|
|
11
|
+
const MAX_WAIT_SECONDS = 55;
|
|
12
|
+
const DEFAULT_WAIT_SECONDS = 50;
|
|
13
|
+
/** How long a browser launcher gets to fail before it counts as opened. */
|
|
14
|
+
const BROWSER_GRACE_MS = 1500;
|
|
15
|
+
/** The sign-in in progress (or last finished) for each Builder client. */
|
|
16
|
+
const FLOWS = new WeakMap();
|
|
8
17
|
export function authenticateTool(client, name = 'reclaim_authenticate') {
|
|
9
18
|
return defineTool({
|
|
10
19
|
name,
|
|
@@ -16,29 +25,98 @@ export function authenticateTool(client, name = 'reclaim_authenticate') {
|
|
|
16
25
|
+ 'get_me_providers; those need the separate old-devtools login, '
|
|
17
26
|
+ 'reclaim_authenticate. Check both with get_devtools_mode. '
|
|
18
27
|
: '')
|
|
19
|
-
+ '
|
|
20
|
-
+ '
|
|
21
|
-
+ '
|
|
22
|
-
+ '
|
|
23
|
-
+ '
|
|
24
|
-
+ '
|
|
25
|
-
+ '
|
|
26
|
-
+ '
|
|
28
|
+
+ 'Starts signing the developer in to Builder (OAuth 2.0 '
|
|
29
|
+
+ 'authorization code with PKCE, redirecting to a localhost port '
|
|
30
|
+
+ 'this server opens) and returns at once without waiting for them: '
|
|
31
|
+
+ '`{ status: "PENDING", signInUrl, browserOpened, reason?, '
|
|
32
|
+
+ 'expiresAt }`. It tries to open signInUrl in their browser; '
|
|
33
|
+
+ '`browserOpened` says whether that worked and `reason` why not. '
|
|
34
|
+
+ 'Always give the developer signInUrl and ask them to open it in a '
|
|
35
|
+
+ 'browser on this machine. When browserOpened is false, end your '
|
|
36
|
+
+ 'turn after sharing it — nothing happens until they open it. Then '
|
|
37
|
+
+ 'call `wait_for_sign_in` to confirm. The link expires after 5 '
|
|
38
|
+
+ 'minutes; calling again while one is pending returns the same '
|
|
39
|
+
+ 'link. On success the token is saved in ~/.reclaim/config.json '
|
|
40
|
+
+ 'with mode 0600, refreshed automatically, and never returned. It '
|
|
41
|
+
+ 'authorizes organization-management tools such as `list_orgs`. '
|
|
42
|
+
+ 'Programmatic verification tools use a separate organization key; '
|
|
43
|
+
+ 'see `issue_org_token`. Call with no arguments.',
|
|
27
44
|
inputSchema: { type: 'object', properties: {} },
|
|
28
45
|
annotations: { openWorldHint: true, idempotentHint: true },
|
|
29
|
-
}, async () =>
|
|
46
|
+
}, async () => startSignIn(client));
|
|
30
47
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
status
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
export function waitForSignInTool(client, authToolName = 'reclaim_authenticate') {
|
|
49
|
+
return defineTool({
|
|
50
|
+
name: 'wait_for_sign_in',
|
|
51
|
+
description: `Waits for the Builder sign-in started by \`${authToolName}\` to `
|
|
52
|
+
+ `finish, for up to timeoutSeconds (default ${DEFAULT_WAIT_SECONDS}`
|
|
53
|
+
+ `, max ${MAX_WAIT_SECONDS}). Returns status AUTHENTICATED (with `
|
|
54
|
+
+ 'expiresAt); PENDING (not finished yet: repeats signInUrl — call '
|
|
55
|
+
+ 'again, or remind the developer to open the link); FAILED (with '
|
|
56
|
+
+ `reason: call \`${authToolName}\` for a new link); or NOT_STARTED `
|
|
57
|
+
+ '(no sign-in in progress and no valid session: call '
|
|
58
|
+
+ `\`${authToolName}\`).`,
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
timeoutSeconds: {
|
|
63
|
+
type: 'integer',
|
|
64
|
+
minimum: 0,
|
|
65
|
+
maximum: MAX_WAIT_SECONDS,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
70
|
+
}, async ({ timeoutSeconds }) => waitForSignIn(client, (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1000));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Start a sign-in and return its link without waiting for the developer. A
|
|
74
|
+
* sign-in already pending for `client` is returned as is rather than
|
|
75
|
+
* replaced. The flow finishes in the background: the loopback server keeps
|
|
76
|
+
* listening until the browser redirects back or the link expires, and on
|
|
77
|
+
* success caches the session and activates it on `client`.
|
|
78
|
+
*/
|
|
79
|
+
export async function startSignIn(client,
|
|
80
|
+
// Overridable only so tests can intercept the authorize URL instead of
|
|
81
|
+
// actually spawning a browser; every real caller uses the default.
|
|
82
|
+
openBrowserFn = openBrowser) {
|
|
83
|
+
const existing = FLOWS.get(client);
|
|
84
|
+
if (existing && !existing.outcome) {
|
|
85
|
+
return existing.started;
|
|
86
|
+
}
|
|
87
|
+
const begun = beginSignIn(client, openBrowserFn);
|
|
88
|
+
const flow = {
|
|
89
|
+
started: begun.then(({ pending }) => pending),
|
|
90
|
+
completion: begun.then(({ completion }) => completion, (err) => ({ status: 'FAILED', reason: failureReason(err) })),
|
|
91
|
+
};
|
|
92
|
+
void flow.completion.then((outcome) => {
|
|
93
|
+
flow.outcome = outcome;
|
|
94
|
+
});
|
|
95
|
+
FLOWS.set(client, flow);
|
|
96
|
+
return flow.started;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Wait up to `timeoutMs` for `client`'s sign-in to finish. Reports PENDING
|
|
100
|
+
* (with the link again) if it's still open when the wait runs out.
|
|
101
|
+
*/
|
|
102
|
+
export async function waitForSignIn(client, timeoutMs) {
|
|
103
|
+
const flow = FLOWS.get(client);
|
|
104
|
+
if (!flow) {
|
|
105
|
+
const session = readSession();
|
|
106
|
+
return session
|
|
107
|
+
? { status: 'AUTHENTICATED', expiresAt: session.expiresAt }
|
|
108
|
+
: { status: 'NOT_STARTED' };
|
|
39
109
|
}
|
|
40
|
-
|
|
41
|
-
|
|
110
|
+
let timer;
|
|
111
|
+
const outcome = await Promise.race([
|
|
112
|
+
flow.completion,
|
|
113
|
+
new Promise((resolve) => {
|
|
114
|
+
timer = setTimeout(() => resolve(undefined), timeoutMs);
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
return outcome ?? flow.started;
|
|
119
|
+
}
|
|
42
120
|
/**
|
|
43
121
|
* Standard OAuth 2.0 Authorization Code + PKCE, native-app style (RFC 8252):
|
|
44
122
|
* dynamically register a public client (RFC 7591 — Better Auth's
|
|
@@ -48,108 +126,92 @@ const _customFetchForOauth = async (url, options) => {
|
|
|
48
126
|
* validation (including a denied/failed login surfacing as
|
|
49
127
|
* `AuthorizationResponseError`) — this function only wires those calls
|
|
50
128
|
* together and owns the loopback server (no library spins that up for you).
|
|
129
|
+
* Resolves once the link is ready; `completion` settles when the flow ends
|
|
130
|
+
* and never rejects.
|
|
51
131
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// actually spawning a browser; every real caller uses the default.
|
|
55
|
-
openBrowserFn = openBrowser) {
|
|
56
|
-
const authBase = `${client.baseUrl}/auth`;
|
|
132
|
+
async function beginSignIn(client, openBrowserFn) {
|
|
133
|
+
const authBase = authBaseFor(client.baseUrl);
|
|
57
134
|
const codeVerifier = oauth.randomPKCECodeVerifier();
|
|
58
135
|
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
|
|
59
136
|
const state = oauth.randomState();
|
|
60
|
-
// openid-client/oauth4webapi refuse any non-HTTPS issuer by default
|
|
61
|
-
// ("only requests to HTTPS are allowed"). Better Auth's own dev default
|
|
62
|
-
// (`canonicalOrigin()`, packages/app/src/consts.ts) is plain
|
|
63
|
-
// `http://localhost:4001` — the server side has no HTTPS requirement at
|
|
64
|
-
// all — so a local `RECLAIM_API_URL=http://localhost:...` would otherwise
|
|
65
|
-
// always fail here. Loosen it ONLY for loopback hosts, matching what the
|
|
66
|
-
// server already trusts (`authTrustedOrigins()` defaults to
|
|
67
|
-
// localhost:4000/4001 outside production); a non-loopback http:// origin
|
|
68
|
-
// still gets the strict check.
|
|
69
|
-
const isLoopbackAuthBase = ['localhost', '127.0.0.1', '[::1]'].includes(new URL(authBase).hostname);
|
|
70
137
|
// Bind the callback port first: the client is registered for this exact
|
|
71
138
|
// loopback redirect.
|
|
72
139
|
const callback = await listenForCallback();
|
|
73
|
-
let
|
|
140
|
+
let config;
|
|
74
141
|
try {
|
|
75
|
-
|
|
142
|
+
config = await oauth.dynamicClientRegistration(new URL(authBase), {
|
|
76
143
|
client_name: 'Reclaim CLI',
|
|
77
144
|
redirect_uris: [callback.redirectUri],
|
|
78
145
|
token_endpoint_auth_method: 'none',
|
|
79
146
|
grant_types: ['authorization_code', 'refresh_token'],
|
|
80
147
|
response_types: ['code'],
|
|
81
148
|
scope: SCOPE,
|
|
82
|
-
}, undefined,
|
|
83
|
-
// Better Auth exposes its metadata at the RFC 8414 well-known path
|
|
84
|
-
// (`.well-known/oauth-authorization-server`), not the OIDC one.
|
|
85
|
-
algorithm: 'oauth2',
|
|
86
|
-
// RFC 7591 §3.2.1 requires 201 Created on a successful registration;
|
|
87
|
-
// openid-client enforces that strictly. Better Auth's
|
|
88
|
-
// `oauth-provider` plugin doesn't conform — confirmed live against
|
|
89
|
-
// the real server: `POST .../oauth2/register` returns 200 with an
|
|
90
|
-
// otherwise well-formed body, which openid-client rejects outright
|
|
91
|
-
// ("unexpected HTTP response status code") before ever looking at
|
|
92
|
-
// it. `customFetch` set here persists onto the returned
|
|
93
|
-
// `Configuration` for ALL its future calls too (token exchange
|
|
94
|
-
// included, which correctly returns 200) — so the URL itself, not
|
|
95
|
-
// just method+status, must gate the rewrite, or a genuine 200 from
|
|
96
|
-
// the token endpoint gets corrupted into a 201 and
|
|
97
|
-
// `authorizationCodeGrant` fails instead (confirmed by hitting
|
|
98
|
-
// exactly that when this only checked method === 'POST').
|
|
99
|
-
[oauth.customFetch]: _customFetchForOauth,
|
|
100
|
-
// Applies to the `Configuration` this call returns, so the later
|
|
101
|
-
// `buildAuthorizationUrl`/`authorizationCodeGrant` calls below (which
|
|
102
|
-
// reuse that same `config`) stop enforcing HTTPS too — no separate
|
|
103
|
-
// opt-in needed for them.
|
|
104
|
-
...(isLoopbackAuthBase
|
|
105
|
-
? { execute: [oauth.allowInsecureRequests] }
|
|
106
|
-
: {}),
|
|
107
|
-
});
|
|
108
|
-
openBrowserFn(oauth.buildAuthorizationUrl(config, {
|
|
109
|
-
redirect_uri: callback.redirectUri,
|
|
110
|
-
scope: SCOPE,
|
|
111
|
-
state,
|
|
112
|
-
code_challenge: codeChallenge,
|
|
113
|
-
code_challenge_method: 'S256',
|
|
114
|
-
}).href);
|
|
115
|
-
const callbackUrl = await callback.url;
|
|
116
|
-
tokens = await oauth.authorizationCodeGrant(config, callbackUrl, {
|
|
117
|
-
pkceCodeVerifier: codeVerifier,
|
|
118
|
-
expectedState: state,
|
|
119
|
-
});
|
|
149
|
+
}, undefined, oauthRequestOptions(authBase));
|
|
120
150
|
}
|
|
121
151
|
catch (err) {
|
|
122
|
-
// A denied or failed authorization surfaces as this error class with
|
|
123
|
-
// the raw callback params on `.cause` — reword it like the previous
|
|
124
|
-
// hand-rolled check did (RFC 6749 §4.1.2.1's error/error_description).
|
|
125
|
-
if (err instanceof oauth.AuthorizationResponseError) {
|
|
126
|
-
throw new Error(err.cause.get('error_description')
|
|
127
|
-
?? err.cause.get('error')
|
|
128
|
-
?? 'Sign-in failed or was cancelled.');
|
|
129
|
-
}
|
|
130
|
-
throw err;
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
152
|
callback.close();
|
|
153
|
+
throw err;
|
|
134
154
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
155
|
+
const signInUrl = oauth.buildAuthorizationUrl(config, {
|
|
156
|
+
redirect_uri: callback.redirectUri,
|
|
157
|
+
scope: SCOPE,
|
|
158
|
+
state,
|
|
159
|
+
code_challenge: codeChallenge,
|
|
160
|
+
code_challenge_method: 'S256',
|
|
161
|
+
}).href;
|
|
162
|
+
const completion = finish();
|
|
163
|
+
const launch = await openBrowserFn(signInUrl).catch((err) => ({
|
|
164
|
+
opened: false,
|
|
165
|
+
reason: failureReason(err),
|
|
166
|
+
}));
|
|
167
|
+
const pending = {
|
|
168
|
+
status: 'PENDING',
|
|
169
|
+
signInUrl,
|
|
170
|
+
browserOpened: launch.opened,
|
|
171
|
+
...(launch.reason ? { reason: launch.reason } : {}),
|
|
172
|
+
expiresAt: callback.expiresAt,
|
|
142
173
|
};
|
|
174
|
+
return { pending, completion };
|
|
175
|
+
async function finish() {
|
|
176
|
+
try {
|
|
177
|
+
const tokens = await oauth.authorizationCodeGrant(config, await callback.url, { pkceCodeVerifier: codeVerifier, expectedState: state });
|
|
178
|
+
const session = sessionFromTokens(tokens, {
|
|
179
|
+
clientId: config.clientMetadata().client_id,
|
|
180
|
+
authBase,
|
|
181
|
+
});
|
|
182
|
+
writeSession(session);
|
|
183
|
+
client.setToken(session.token);
|
|
184
|
+
return { status: 'AUTHENTICATED', expiresAt: session.expiresAt };
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return { status: 'FAILED', reason: failureReason(err) };
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
callback.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function failureReason(err) {
|
|
195
|
+
// A denied or failed authorization surfaces as this error class with the
|
|
196
|
+
// raw callback params on `.cause` (RFC 6749 §4.1.2.1's
|
|
197
|
+
// error/error_description).
|
|
198
|
+
if (err instanceof oauth.AuthorizationResponseError) {
|
|
199
|
+
return err.cause.get('error_description')
|
|
200
|
+
?? err.cause.get('error')
|
|
201
|
+
?? 'Sign-in failed or was cancelled.';
|
|
202
|
+
}
|
|
203
|
+
return err instanceof Error ? err.message : String(err);
|
|
143
204
|
}
|
|
144
205
|
/**
|
|
145
206
|
* Bind an ephemeral loopback port and start listening for the provider's
|
|
146
207
|
* redirect. Resolves as soon as the port is known — the `url` promise settles
|
|
147
|
-
* later, when the browser hits `/callback
|
|
148
|
-
* client for this exact redirect URI before
|
|
149
|
-
* The ephemeral port lets any number of
|
|
150
|
-
* RAW callback URL rather than picking
|
|
151
|
-
* `oauth.authorizationCodeGrant` does that
|
|
152
|
-
*
|
|
208
|
+
* later, when the browser hits `/callback`, or rejects when the link expires —
|
|
209
|
+
* so the caller can register a client for this exact redirect URI before
|
|
210
|
+
* sending the user off to authorize. The ephemeral port lets any number of
|
|
211
|
+
* CLIs run concurrently. Hands back the RAW callback URL rather than picking
|
|
212
|
+
* `code`/`state` apart itself — `oauth.authorizationCodeGrant` does that
|
|
213
|
+
* validation. The server and timer are unref'd: a pending sign-in alone
|
|
214
|
+
* doesn't keep the process alive.
|
|
153
215
|
*/
|
|
154
216
|
async function listenForCallback() {
|
|
155
217
|
let resolveUrl;
|
|
@@ -159,7 +221,9 @@ async function listenForCallback() {
|
|
|
159
221
|
rejectUrl = reject;
|
|
160
222
|
});
|
|
161
223
|
const server = createServer();
|
|
162
|
-
const
|
|
224
|
+
const expiresAt = new Date(Date.now() + CALLBACK_TIMEOUT_MS).toISOString();
|
|
225
|
+
const timer = setTimeout(() => rejectUrl(new Error('Sign-in link expired (5 min). Call the sign-in tool again for a new '
|
|
226
|
+
+ 'link.')), CALLBACK_TIMEOUT_MS).unref();
|
|
163
227
|
const port = await new Promise((resolve, reject) => {
|
|
164
228
|
server.once('error', reject);
|
|
165
229
|
server.listen(0, '127.0.0.1', () => {
|
|
@@ -173,11 +237,11 @@ async function listenForCallback() {
|
|
|
173
237
|
}).catch((err) => {
|
|
174
238
|
// Bind failed before `close()` (returned below) is ever reachable —
|
|
175
239
|
// clear the timer ourselves, or it fires 5 minutes later and rejects
|
|
176
|
-
// the now-unobserved `url` promise (unhandled rejection)
|
|
177
|
-
// the process alive in the meantime.
|
|
240
|
+
// the now-unobserved `url` promise (unhandled rejection).
|
|
178
241
|
clearTimeout(timer);
|
|
179
242
|
throw err;
|
|
180
243
|
});
|
|
244
|
+
server.unref();
|
|
181
245
|
// Past bind, a socket error fails the pending wait rather than throwing.
|
|
182
246
|
server.on('error', (err) => rejectUrl(err));
|
|
183
247
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
@@ -196,24 +260,56 @@ async function listenForCallback() {
|
|
|
196
260
|
return {
|
|
197
261
|
redirectUri,
|
|
198
262
|
url,
|
|
263
|
+
expiresAt,
|
|
199
264
|
close: () => {
|
|
200
265
|
clearTimeout(timer);
|
|
201
266
|
server.close();
|
|
202
267
|
},
|
|
203
268
|
};
|
|
204
269
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
// Non-fatal: the URL is still logged for the user to open manually.
|
|
217
|
-
}
|
|
270
|
+
/**
|
|
271
|
+
* Try to open `url` in the system browser. Never writes to stdout (it carries
|
|
272
|
+
* MCP JSON-RPC); the URL goes to stderr for a developer watching the logs.
|
|
273
|
+
* On Linux without a display server it doesn't try. Otherwise a launcher
|
|
274
|
+
* that errors or exits non-zero within a short grace window counts as not
|
|
275
|
+
* opened.
|
|
276
|
+
*/
|
|
277
|
+
export async function openBrowser(url, { env = process.env, platform = process.platform } = {}) {
|
|
218
278
|
process.stderr.write(`\nOpen this URL to sign in:\n${url}\n\n`);
|
|
279
|
+
const hasDisplay = platform === 'darwin' || platform === 'win32'
|
|
280
|
+
|| !!env.WAYLAND_DISPLAY || !!env.DISPLAY;
|
|
281
|
+
if (!hasDisplay) {
|
|
282
|
+
return {
|
|
283
|
+
opened: false,
|
|
284
|
+
reason: 'no display (WAYLAND_DISPLAY/DISPLAY not set)',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const cmd = platform === 'darwin'
|
|
288
|
+
? 'open'
|
|
289
|
+
: platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
290
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
291
|
+
return new Promise((resolve) => {
|
|
292
|
+
const timer = setTimeout(() => resolve({ opened: true }), BROWSER_GRACE_MS);
|
|
293
|
+
try {
|
|
294
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true, env });
|
|
295
|
+
child.once('error', (err) => done({
|
|
296
|
+
opened: false,
|
|
297
|
+
reason: `${cmd} failed: ${err.message}`,
|
|
298
|
+
}));
|
|
299
|
+
child.once('exit', (code, signal) => done(code === 0
|
|
300
|
+
? { opened: true }
|
|
301
|
+
: {
|
|
302
|
+
opened: false,
|
|
303
|
+
reason: `${cmd} exited with ${code ?? signal}`,
|
|
304
|
+
}));
|
|
305
|
+
child.unref();
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
done({ opened: false, reason: `${cmd} failed: ${failureReason(err)}` });
|
|
309
|
+
}
|
|
310
|
+
function done(launch) {
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
resolve(launch);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
219
315
|
}
|
|
@@ -9,7 +9,7 @@ import type { RegisteredTool } from '../../server.ts';
|
|
|
9
9
|
export declare function buildLocalCredentialTools(): RegisteredTool[];
|
|
10
10
|
/**
|
|
11
11
|
* Full builder-mode credential set: the local key tools plus the backend
|
|
12
|
-
* `reclaim_authenticate`
|
|
13
|
-
* authenticate tool — it has its own.
|
|
12
|
+
* `reclaim_authenticate` sign-in flow and its `wait_for_sign_in` companion.
|
|
13
|
+
* Old mode does NOT use the authenticate tool — it has its own.
|
|
14
14
|
*/
|
|
15
15
|
export declare function buildCredentialTools(client: ReclaimClient): RegisteredTool[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { authenticateTool } from "./authenticate.js";
|
|
1
|
+
import { authenticateTool, waitForSignInTool } from "./authenticate.js";
|
|
2
2
|
import { importTool } from "./import.js";
|
|
3
3
|
import { issueTool } from "./issue.js";
|
|
4
|
+
import { issueOrgKeypairTool } from "./issue-org-keypair.js";
|
|
4
5
|
import { issueOrgTokenTool } from "./issue-org-token.js";
|
|
5
6
|
import { resolveOwnerKeyTool } from "./resolve-owner-key.js";
|
|
6
7
|
/**
|
|
@@ -18,13 +19,15 @@ export function buildLocalCredentialTools() {
|
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Full builder-mode credential set: the local key tools plus the backend
|
|
21
|
-
* `reclaim_authenticate`
|
|
22
|
-
* authenticate tool — it has its own.
|
|
22
|
+
* `reclaim_authenticate` sign-in flow and its `wait_for_sign_in` companion.
|
|
23
|
+
* Old mode does NOT use the authenticate tool — it has its own.
|
|
23
24
|
*/
|
|
24
25
|
export function buildCredentialTools(client) {
|
|
25
26
|
return [
|
|
26
27
|
authenticateTool(client),
|
|
28
|
+
waitForSignInTool(client),
|
|
27
29
|
issueOrgTokenTool(client),
|
|
30
|
+
issueOrgKeypairTool(client),
|
|
28
31
|
...buildLocalCredentialTools(),
|
|
29
32
|
];
|
|
30
33
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { defineTool } from "../../server.js";
|
|
2
|
-
import { authenticateTool } from "./authenticate.js";
|
|
2
|
+
import { authenticateTool, waitForSignInTool } from "./authenticate.js";
|
|
3
3
|
/** Legacy-devtools mode needs a distinct Builder login for its two
|
|
4
4
|
* Builder-exclusive features: allocating quota-accounted Popcorn sessions
|
|
5
5
|
* (see `buildBuilderOrganizationTools` below) and listing Builder
|
|
6
6
|
* organizations. Builder mode needs neither — its native auth tool alone
|
|
7
7
|
* covers both, org listing included (via its generated `list_orgs`). */
|
|
8
8
|
export function buildBuilderAuthenticationTools(client) {
|
|
9
|
-
return [
|
|
9
|
+
return [
|
|
10
|
+
authenticateTool(client, 'authenticate_builder'),
|
|
11
|
+
waitForSignInTool(client, 'authenticate_builder'),
|
|
12
|
+
];
|
|
10
13
|
}
|
|
11
14
|
/** Legacy-devtools mode lacks generated Builder API tools, so it needs this
|
|
12
15
|
* narrow organization-discovery bridge — an org id is required to allocate a
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** The dotenv file the agent reads from / writes the key to. */
|
|
2
2
|
export declare const ENV_FILE = ".env";
|
|
3
|
-
/** Where the resolved eth key came from.
|
|
4
|
-
|
|
3
|
+
/** Where the resolved eth key came from. `generated`: created just now
|
|
4
|
+
* because none existed. */
|
|
5
|
+
export type EthKeySource = 'env' | 'dotenv' | 'file' | 'generated';
|
|
6
|
+
/** Tells the agent to inform the developer once, after the fact. */
|
|
7
|
+
export declare const GENERATED_KEY_NOTE: string;
|
|
5
8
|
export type ResolvedEthKey = {
|
|
6
9
|
/** Raw 0x-hex 32-byte scalar. */
|
|
7
10
|
privateKey: string;
|
|
@@ -13,12 +16,20 @@ export type ResolvedEthKey = {
|
|
|
13
16
|
};
|
|
14
17
|
/** Validate + normalize a raw Ethereum private key to lowercase 0x-hex. */
|
|
15
18
|
export declare function normalizeEthHex(value: string): string;
|
|
19
|
+
/** Read `name` (default `RECLAIM_PRIVATE_KEY`) from a dotenv file (simple
|
|
20
|
+
* KEY=VALUE parse). */
|
|
21
|
+
export declare function readDotenvKey(path: string, name?: string): string | undefined;
|
|
16
22
|
/**
|
|
17
23
|
* Resolve the eth proof-owner key without prompting. Returns `undefined` when
|
|
18
24
|
* no key is configured anywhere — the caller decides whether to generate or
|
|
19
25
|
* ask for one (see `resolve_owner_key`).
|
|
20
26
|
*/
|
|
21
27
|
export declare function resolveEthKey(projectDir?: string): ResolvedEthKey | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the eth proof-owner key, or generate a local test key and write it
|
|
30
|
+
* to the project `.env` when none is configured. Never prompts.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveOrCreateEthKey(projectDir?: string): ResolvedEthKey;
|
|
22
33
|
/** Append `entry` to the project `.gitignore` if not already present. */
|
|
23
34
|
export declare function ensureGitignored(projectDir: string, entry: string): void;
|
|
24
35
|
/**
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* 2. A `.env` file in the project root — its `RECLAIM_PRIVATE_KEY=0x…` line.
|
|
8
8
|
* 3. A key file holding the raw 0x-hex scalar, at `RECLAIM_PRIVATE_KEY_FILE`
|
|
9
9
|
* (absolute or project-relative).
|
|
10
|
-
* 4. Absent —
|
|
10
|
+
* 4. Absent — `resolveOrCreateEthKey` generates a local test key; nobody is
|
|
11
|
+
* asked.
|
|
11
12
|
*
|
|
12
13
|
* Generated/imported keys are written to the project `.env` (mode 0600, and the
|
|
13
14
|
* file is added to `.gitignore`). NO `~/.reclaim` caching. This is a LOCAL
|
|
14
15
|
* signing key only — unrelated to org auth (the org secret) or encryption
|
|
15
16
|
* (the eth decrypt key).
|
|
16
17
|
*/
|
|
17
|
-
import { deriveFromPrivateKey } from '@reclaimprotocol/client';
|
|
18
|
+
import { deriveFromPrivateKey, generateKeypair } from '@reclaimprotocol/client';
|
|
18
19
|
import assert from 'node:assert';
|
|
19
20
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
20
21
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
@@ -23,19 +24,28 @@ import { findProjectRoot } from "../../../paths.js";
|
|
|
23
24
|
const ENV_VAR = 'RECLAIM_PRIVATE_KEY';
|
|
24
25
|
/** The dotenv file the agent reads from / writes the key to. */
|
|
25
26
|
export const ENV_FILE = '.env';
|
|
27
|
+
/** Tells the agent to inform the developer once, after the fact. */
|
|
28
|
+
export const GENERATED_KEY_NOTE = 'No proof signing key existed, so a local test signing key was created and '
|
|
29
|
+
+ 'saved to the project .env as RECLAIM_PRIVATE_KEY (mode 0600, '
|
|
30
|
+
+ 'gitignored). Tell the developer once, in one sentence, that you created '
|
|
31
|
+
+ 'it; do not ask for approval. It is a local signing key only: not '
|
|
32
|
+
+ 'registered anywhere and unrelated to org auth. A developer who wants '
|
|
33
|
+
+ 'their own key can use import_credentials.';
|
|
26
34
|
/** Validate + normalize a raw Ethereum private key to lowercase 0x-hex. */
|
|
27
35
|
export function normalizeEthHex(value) {
|
|
28
36
|
const v = value.trim().replace(/^['"]|['"]$/g, '');
|
|
29
37
|
assert(/^(0x)?[0-9a-fA-F]{64}$/.test(v), new Error('not a valid Ethereum private key (expected a 32-byte / 64-hex scalar)'));
|
|
30
38
|
return '0x' + v.replace(/^0x/, '').toLowerCase();
|
|
31
39
|
}
|
|
32
|
-
/** Read `RECLAIM_PRIVATE_KEY` from a dotenv file (simple
|
|
33
|
-
|
|
40
|
+
/** Read `name` (default `RECLAIM_PRIVATE_KEY`) from a dotenv file (simple
|
|
41
|
+
* KEY=VALUE parse). */
|
|
42
|
+
export function readDotenvKey(path, name = ENV_VAR) {
|
|
34
43
|
if (!existsSync(path)) {
|
|
35
44
|
return undefined;
|
|
36
45
|
}
|
|
46
|
+
const re = new RegExp(`^\\s*${name}\\s*=\\s*(.+?)\\s*$`);
|
|
37
47
|
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
38
|
-
const m = line.match(
|
|
48
|
+
const m = line.match(re);
|
|
39
49
|
if (m) {
|
|
40
50
|
return m[1];
|
|
41
51
|
}
|
|
@@ -79,6 +89,20 @@ export function resolveEthKey(projectDir = findProjectRoot(process.cwd())) {
|
|
|
79
89
|
}
|
|
80
90
|
return undefined;
|
|
81
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the eth proof-owner key, or generate a local test key and write it
|
|
94
|
+
* to the project `.env` when none is configured. Never prompts.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveOrCreateEthKey(projectDir = findProjectRoot(process.cwd())) {
|
|
97
|
+
const existing = resolveEthKey(projectDir);
|
|
98
|
+
if (existing) {
|
|
99
|
+
return existing;
|
|
100
|
+
}
|
|
101
|
+
const { path } = writeEthKey(generateKeypair().privateKey, projectDir);
|
|
102
|
+
const created = resolveEthKey(projectDir);
|
|
103
|
+
assert(created, `generated key not readable from ${path}`);
|
|
104
|
+
return { ...created, source: 'generated' };
|
|
105
|
+
}
|
|
82
106
|
/** Append `entry` to the project `.gitignore` if not already present. */
|
|
83
107
|
export function ensureGitignored(projectDir, entry) {
|
|
84
108
|
const path = join(projectDir, '.gitignore');
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { buildBuilderAuthenticationTools, buildBuilderOrganizationTools, } from './builder-access.ts';
|
|
2
2
|
export { buildCredentialTools, buildLocalCredentialTools } from './build.ts';
|
|
3
|
-
export { readSession } from './session-cache.ts';
|
|
3
|
+
export { readSession, sessionRefreshingFetch } from './session-cache.ts';
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// and the org-token-authenticated builder tools. Import from here.
|
|
3
3
|
export { buildBuilderAuthenticationTools, buildBuilderOrganizationTools, } from "./builder-access.js";
|
|
4
4
|
export { buildCredentialTools, buildLocalCredentialTools } from "./build.js";
|
|
5
|
-
export { readSession } from "./session-cache.js";
|
|
5
|
+
export { readSession, sessionRefreshingFetch } from "./session-cache.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReclaimClient } from '@reclaimprotocol/client/api';
|
|
2
|
+
import { type RegisteredTool } from '../../server.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the organization's eth key pair for session binding and encrypted
|
|
5
|
+
* results: writes the private key to the app's server env file and registers
|
|
6
|
+
* only the public key with Builder, so the private key never passes through
|
|
7
|
+
* the agent's transcript. Reuses a key the env file already holds, so a
|
|
8
|
+
* repeat call (for example, to turn on encryption) keeps the same key.
|
|
9
|
+
*/
|
|
10
|
+
export declare function issueOrgKeypairTool(client: ReclaimClient): RegisteredTool;
|