@reclaimprotocol/agent 0.1.36 → 0.1.38

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.
Files changed (104) hide show
  1. package/README.md +8 -8
  2. package/lib/authoring/backend.d.ts +12 -1
  3. package/lib/authoring/backend.js +3 -0
  4. package/lib/authoring/tools.js +33 -5
  5. package/lib/authoring/util.d.ts +43 -0
  6. package/lib/authoring/util.js +54 -0
  7. package/lib/cdp/chrome-for-testing.d.ts +14 -0
  8. package/lib/cdp/chrome-for-testing.js +43 -0
  9. package/lib/cdp/geo.d.ts +3 -0
  10. package/lib/cdp/geo.js +27 -15
  11. package/lib/cdp/launcher.d.ts +79 -1
  12. package/lib/cdp/launcher.js +244 -24
  13. package/lib/logger.js +3 -1
  14. package/lib/mcp/instructions.js +3 -3
  15. package/lib/mcp/start.js +17 -2
  16. package/lib/mcp/tools/agent/attach.d.ts +6 -0
  17. package/lib/mcp/tools/agent/attach.js +58 -118
  18. package/lib/mcp/tools/agent/build.js +8 -0
  19. package/lib/mcp/tools/agent/capture.js +33 -10
  20. package/lib/mcp/tools/agent/check-attestor.js +15 -7
  21. package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
  22. package/lib/mcp/tools/agent/draft-tools.js +170 -0
  23. package/lib/mcp/tools/agent/example-app.d.ts +2 -0
  24. package/lib/mcp/tools/agent/example-app.js +167 -0
  25. package/lib/mcp/tools/agent/pod-proof.js +5 -1
  26. package/lib/mcp/tools/agent/proof.d.ts +28 -0
  27. package/lib/mcp/tools/agent/proof.js +72 -0
  28. package/lib/mcp/tools/agent/shared.d.ts +8 -1
  29. package/lib/mcp/tools/agent/shared.js +98 -19
  30. package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
  31. package/lib/mcp/tools/agent/start-integration.js +10 -6
  32. package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
  33. package/lib/mcp/tools/authenticate/authenticate.js +212 -116
  34. package/lib/mcp/tools/authenticate/build.d.ts +2 -2
  35. package/lib/mcp/tools/authenticate/build.js +6 -3
  36. package/lib/mcp/tools/authenticate/builder-access.js +5 -2
  37. package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
  38. package/lib/mcp/tools/authenticate/eth-key.js +29 -5
  39. package/lib/mcp/tools/authenticate/index.d.ts +1 -1
  40. package/lib/mcp/tools/authenticate/index.js +1 -1
  41. package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
  42. package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
  43. package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
  44. package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
  45. package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
  46. package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
  47. package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
  48. package/lib/mcp/tools/authenticate/session-cache.js +95 -4
  49. package/lib/mcp/tools/mode/mode.js +6 -6
  50. package/lib/old/login-cdp.js +2 -3
  51. package/lib/proof/attestor.js +2 -3
  52. package/lib/proof/check-attestor.d.ts +12 -2
  53. package/lib/proof/check-attestor.js +40 -10
  54. package/lib/proof/check-matchers.d.ts +36 -0
  55. package/lib/proof/check-matchers.js +161 -0
  56. package/lib/proof/zk-files.d.ts +50 -11
  57. package/lib/proof/zk-files.js +259 -63
  58. package/lib/provider/draft.d.ts +24 -1
  59. package/lib/provider/draft.js +135 -0
  60. package/lib/provider/matcher.js +171 -4
  61. package/package.json +6 -4
  62. package/skill/SKILL.builder.md +7 -2
  63. package/skill/SKILL.common.md +90 -70
  64. package/skill/SKILL.old.md +3 -3
  65. package/skill/builder-mode.md +35 -5
  66. package/skill/old-mode.md +18 -13
  67. package/skill/start-integration.md +50 -33
  68. package/skill/verification-client-integration.md +21 -6
  69. package/src/authoring/backend.ts +12 -1
  70. package/src/authoring/tools.ts +48 -7
  71. package/src/authoring/util.ts +64 -0
  72. package/src/cdp/chrome-for-testing.ts +66 -0
  73. package/src/cdp/geo.ts +36 -21
  74. package/src/cdp/launcher.ts +328 -24
  75. package/src/logger.ts +3 -1
  76. package/src/mcp/instructions.ts +3 -3
  77. package/src/mcp/start.ts +20 -1
  78. package/src/mcp/tools/agent/attach.ts +65 -128
  79. package/src/mcp/tools/agent/build.ts +9 -0
  80. package/src/mcp/tools/agent/capture.ts +54 -11
  81. package/src/mcp/tools/agent/check-attestor.ts +15 -7
  82. package/src/mcp/tools/agent/draft-tools.ts +234 -0
  83. package/src/mcp/tools/agent/example-app.ts +227 -0
  84. package/src/mcp/tools/agent/pod-proof.ts +5 -1
  85. package/src/mcp/tools/agent/proof.ts +110 -1
  86. package/src/mcp/tools/agent/shared.ts +130 -19
  87. package/src/mcp/tools/agent/start-integration.ts +10 -6
  88. package/src/mcp/tools/authenticate/authenticate.ts +276 -131
  89. package/src/mcp/tools/authenticate/build.ts +6 -3
  90. package/src/mcp/tools/authenticate/builder-access.ts +5 -2
  91. package/src/mcp/tools/authenticate/eth-key.ts +41 -7
  92. package/src/mcp/tools/authenticate/index.ts +1 -1
  93. package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
  94. package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
  95. package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
  96. package/src/mcp/tools/authenticate/session-cache.ts +125 -5
  97. package/src/mcp/tools/mode/mode.ts +6 -6
  98. package/src/old/login-cdp.ts +2 -3
  99. package/src/proof/attestor.ts +2 -3
  100. package/src/proof/check-attestor.ts +55 -12
  101. package/src/proof/check-matchers.ts +227 -0
  102. package/src/proof/zk-files.ts +357 -65
  103. package/src/provider/draft.ts +215 -7
  104. package/src/provider/matcher.ts +237 -4
@@ -0,0 +1,82 @@
1
+ import { deriveFromPrivateKey, generateKeypair } from '@reclaimprotocol/client';
2
+ import assert from 'node:assert';
3
+ import { isAbsolute, relative, resolve } from 'node:path';
4
+ import { defineTool } from "../../server.js";
5
+ import { normalizeEthHex, readDotenvKey, upsertEnvVar } from "./eth-key.js";
6
+ /**
7
+ * Creates the organization's eth key pair for session binding and encrypted
8
+ * results: writes the private key to the app's server env file and registers
9
+ * only the public key with Builder, so the private key never passes through
10
+ * the agent's transcript. Reuses a key the env file already holds, so a
11
+ * repeat call (for example, to turn on encryption) keeps the same key.
12
+ */
13
+ export function issueOrgKeypairTool(client) {
14
+ return defineTool({
15
+ name: 'issue_org_keypair',
16
+ description: 'Sets up the organization\'s eth key pair, used for TEE session '
17
+ + 'binding (`sessions.create({ orgEthPrivateKey })`) and, with '
18
+ + '`encryptResults`, encrypted result delivery '
19
+ + '(`results.receive(body, { orgEthPrivateKey })`). Writes the '
20
+ + 'private key to a server env file in the project (reusing a key '
21
+ + 'already there) and registers only the public key. Returns the '
22
+ + 'address, file, and variable; never the key. Returns 409 when the '
23
+ + 'org already has a different key: ask the developer for that key '
24
+ + 'rather than replacing it.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ required: ['orgId', 'projectDir'],
28
+ properties: {
29
+ orgId: { type: 'string', format: 'uuid' },
30
+ projectDir: {
31
+ type: 'string',
32
+ description: 'Absolute path of the app\'s project directory.',
33
+ },
34
+ envFile: {
35
+ type: 'string',
36
+ description: 'Env file to write, relative to `projectDir`. Default `.env`.',
37
+ },
38
+ envVar: {
39
+ type: 'string',
40
+ pattern: '^[A-Za-z_][A-Za-z0-9_]*$',
41
+ description: 'Variable name to set. Default `RECLAIM_ORG_ETH_PRIVATE_KEY`.',
42
+ },
43
+ encryptResults: {
44
+ type: 'boolean',
45
+ description: 'Encrypt delivered results to this key. Default false.',
46
+ },
47
+ },
48
+ },
49
+ annotations: { openWorldHint: true, destructiveHint: false },
50
+ }, async (args) => {
51
+ assert(isAbsolute(args.projectDir), new Error('`projectDir` must be an absolute path.'));
52
+ const envFile = args.envFile || '.env';
53
+ const envVar = args.envVar || 'RECLAIM_ORG_ETH_PRIVATE_KEY';
54
+ const envPath = resolve(args.projectDir, envFile);
55
+ const inside = relative(args.projectDir, envPath);
56
+ assert(inside && !inside.startsWith('..') && !isAbsolute(inside), new Error('`envFile` must be inside `projectDir`.'));
57
+ const existing = readDotenvKey(envPath, envVar);
58
+ const keypair = existing
59
+ ? deriveFromPrivateKey(normalizeEthHex(existing))
60
+ : generateKeypair();
61
+ // Register first: a 409 (the org has another key) must not leave a
62
+ // key Builder doesn't know in the env file.
63
+ const canEncryptResult = args.encryptResults ?? false;
64
+ await client.call('SetOrgKeypair', {
65
+ params: { orgId: args.orgId },
66
+ body: {
67
+ publicKey: keypair.publicKey,
68
+ canEncryptResult,
69
+ replace: false,
70
+ },
71
+ });
72
+ const path = upsertEnvVar(args.projectDir, envFile, envVar, keypair.privateKey);
73
+ return {
74
+ orgId: args.orgId,
75
+ address: keypair.address,
76
+ canEncryptResult,
77
+ envFile: path,
78
+ envVar,
79
+ reused: Boolean(existing),
80
+ };
81
+ });
82
+ }
@@ -0,0 +1,31 @@
1
+ import * as oauth from 'openid-client';
2
+ import type { CachedSession } from './session-cache.ts';
3
+ /**
4
+ * `offline_access` makes Better Auth's `oauth-provider` issue a refresh token
5
+ * (it issues one only when the grant carries that scope), so the session can
6
+ * outlive the 1-hour access token.
7
+ */
8
+ export declare const SCOPE = "openid profile email offline_access";
9
+ /** Where Better Auth mounts its OAuth endpoints under the Builder origin. */
10
+ export declare function authBaseFor(baseUrl: string): string;
11
+ /**
12
+ * Request options every openid-client call against Builder's auth server
13
+ * needs. Pass them to `dynamicClientRegistration` or `discovery`; the
14
+ * returned `Configuration` keeps them for its later calls.
15
+ */
16
+ export declare function oauthRequestOptions(authBase: string): {
17
+ execute?: (typeof oauth.allowInsecureRequests)[] | undefined;
18
+ algorithm: "oauth2";
19
+ [customFetch]: typeof customFetchForOauth;
20
+ };
21
+ /**
22
+ * The session to cache for a token response. A refresh response may omit
23
+ * `refresh_token` (no rotation), so the caller passes the one it already has.
24
+ */
25
+ export declare function sessionFromTokens(tokens: oauth.TokenEndpointResponse, client: {
26
+ clientId: string;
27
+ authBase: string;
28
+ refreshToken?: string;
29
+ }): CachedSession;
30
+ declare function customFetchForOauth(url: string, options: oauth.CustomFetchOptions): Promise<Response>;
31
+ export {};
@@ -0,0 +1,67 @@
1
+ import * as oauth from 'openid-client';
2
+ /**
3
+ * `offline_access` makes Better Auth's `oauth-provider` issue a refresh token
4
+ * (it issues one only when the grant carries that scope), so the session can
5
+ * outlive the 1-hour access token.
6
+ */
7
+ export const SCOPE = 'openid profile email offline_access';
8
+ /** Where Better Auth mounts its OAuth endpoints under the Builder origin. */
9
+ export function authBaseFor(baseUrl) {
10
+ return `${baseUrl}/auth`;
11
+ }
12
+ /**
13
+ * Request options every openid-client call against Builder's auth server
14
+ * needs. Pass them to `dynamicClientRegistration` or `discovery`; the
15
+ * returned `Configuration` keeps them for its later calls.
16
+ */
17
+ export function oauthRequestOptions(authBase) {
18
+ // openid-client/oauth4webapi refuse any non-HTTPS issuer by default
19
+ // ("only requests to HTTPS are allowed"). Better Auth's own dev default
20
+ // (`canonicalOrigin()`, packages/app/src/consts.ts) is plain
21
+ // `http://localhost:4001` — the server side has no HTTPS requirement at
22
+ // all — so a local `RECLAIM_API_URL=http://localhost:...` would otherwise
23
+ // always fail here. Loosen it ONLY for loopback hosts, matching what the
24
+ // server already trusts (`authTrustedOrigins()` defaults to
25
+ // localhost:4000/4001 outside production); a non-loopback http:// origin
26
+ // still gets the strict check.
27
+ const isLoopbackAuthBase = ['localhost', '127.0.0.1', '[::1]'].includes(new URL(authBase).hostname);
28
+ return {
29
+ // Better Auth exposes its metadata at the RFC 8414 well-known path
30
+ // (`.well-known/oauth-authorization-server`), not the OIDC one.
31
+ algorithm: 'oauth2',
32
+ // RFC 7591 §3.2.1 requires 201 Created on a successful registration;
33
+ // openid-client enforces that strictly. Better Auth's `oauth-provider`
34
+ // plugin returns 200 with an otherwise well-formed body. `customFetch`
35
+ // persists onto the returned `Configuration` for ALL its future calls
36
+ // (token exchange included, which correctly returns 200), so the URL,
37
+ // not just method+status, gates the rewrite.
38
+ [oauth.customFetch]: customFetchForOauth,
39
+ ...(isLoopbackAuthBase ? { execute: [oauth.allowInsecureRequests] } : {}),
40
+ };
41
+ }
42
+ /**
43
+ * The session to cache for a token response. A refresh response may omit
44
+ * `refresh_token` (no rotation), so the caller passes the one it already has.
45
+ */
46
+ export function sessionFromTokens(tokens, client) {
47
+ return {
48
+ token: tokens.access_token,
49
+ expiresAt: new Date(Date.now() + (tokens.expires_in ?? 3600) * 1000).toISOString(),
50
+ refreshToken: tokens.refresh_token ?? client.refreshToken,
51
+ clientId: client.clientId,
52
+ authBase: client.authBase,
53
+ };
54
+ }
55
+ async function customFetchForOauth(url, options) {
56
+ // openid-client hands over a fetch-compatible options bag whose `body`
57
+ // type is narrower than, but assignable in practice to, `RequestInit`'s.
58
+ const res = await fetch(url, options);
59
+ if (options.method === 'POST' && res.status === 200
60
+ && new URL(url).pathname.endsWith('/oauth2/register')) {
61
+ return new Response(await res.text(), {
62
+ status: 201,
63
+ headers: res.headers,
64
+ });
65
+ }
66
+ return res;
67
+ }
@@ -1,13 +1,12 @@
1
1
  import { type RegisteredTool } from '../../server.ts';
2
2
  /**
3
- * Decide which eth proof-owner key `run_proof` should sign with, BEFORE
4
- * calling it. The key is a raw 0x-hex Ethereum private key, resolved (no
5
- * `~/.reclaim` cache) from:
3
+ * Decide which eth proof-owner key `run_proof` signs with. The key is a raw
4
+ * 0x-hex Ethereum private key, resolved (no `~/.reclaim` cache) from:
6
5
  * 1. `RECLAIM_PRIVATE_KEY` in the MCP env
7
6
  * 2. a `RECLAIM_PRIVATE_KEY=0x…` line in the project `.env`
8
7
  * 3. a file at `RECLAIM_PRIVATE_KEY_FILE` holding the raw hex
9
- * 4. none → ask the dev to generate (issue_credentials) or provide one
10
- * (import_credentials), then call again.
8
+ * 4. none → a local test key is generated into the project `.env`. Nobody
9
+ * is asked; the agent tells the developer afterwards.
11
10
  * Never returns the private key — only the derived address.
12
11
  */
13
12
  export declare function resolveOwnerKeyTool(): RegisteredTool;
@@ -1,51 +1,55 @@
1
- import { join } from 'node:path';
2
1
  import { findProjectRoot } from "../../../paths.js";
3
2
  import { defineTool } from "../../server.js";
4
- import { ENV_FILE, resolveEthKey } from "./eth-key.js";
3
+ import { GENERATED_KEY_NOTE, resolveOrCreateEthKey } from "./eth-key.js";
5
4
  /**
6
- * Decide which eth proof-owner key `run_proof` should sign with, BEFORE
7
- * calling it. The key is a raw 0x-hex Ethereum private key, resolved (no
8
- * `~/.reclaim` cache) from:
5
+ * Decide which eth proof-owner key `run_proof` signs with. The key is a raw
6
+ * 0x-hex Ethereum private key, resolved (no `~/.reclaim` cache) from:
9
7
  * 1. `RECLAIM_PRIVATE_KEY` in the MCP env
10
8
  * 2. a `RECLAIM_PRIVATE_KEY=0x…` line in the project `.env`
11
9
  * 3. a file at `RECLAIM_PRIVATE_KEY_FILE` holding the raw hex
12
- * 4. none → ask the dev to generate (issue_credentials) or provide one
13
- * (import_credentials), then call again.
10
+ * 4. none → a local test key is generated into the project `.env`. Nobody
11
+ * is asked; the agent tells the developer afterwards.
14
12
  * Never returns the private key — only the derived address.
15
13
  */
16
14
  export function resolveOwnerKeyTool() {
17
15
  return defineTool({
18
16
  name: 'resolve_owner_key',
19
- description: 'Finds the eth proof-owner key `run_proof` signs with. Call it '
20
- + 'before `run_proof`. Checks RECLAIM_PRIVATE_KEY in the MCP env, a '
21
- + 'RECLAIM_PRIVATE_KEY line in the project .env, then the file at '
22
- + 'RECLAIM_PRIVATE_KEY_FILE. Returns { ready: true, address, source '
23
- + '}: pass `address` to `run_proof` as `ownerAddress`. Returns { '
24
- + 'ready: false } when none exists: ask the developer to create one '
25
- + 'with `issue_credentials` or import one with `import_credentials` '
26
- + '(a key file), then call it again. Never returns the key.',
27
- inputSchema: { type: 'object', properties: {} },
28
- annotations: { readOnlyHint: true, openWorldHint: false },
29
- }, async () => {
30
- const projectDir = findProjectRoot(process.cwd());
31
- const resolved = resolveEthKey(projectDir);
32
- if (resolved) {
33
- return {
34
- ready: true,
35
- address: resolved.address,
36
- source: resolved.source,
37
- ...(resolved.path ? { path: resolved.path } : {}),
38
- };
39
- }
17
+ description: 'Finds the eth proof-owner key `run_proof` signs with, creating a '
18
+ + 'local test key when none exists. `run_proof` does this itself, '
19
+ + 'so call it only to show the signing address. Checks '
20
+ + 'RECLAIM_PRIVATE_KEY in the MCP env, a RECLAIM_PRIVATE_KEY line in '
21
+ + 'the project .env, then the file at RECLAIM_PRIVATE_KEY_FILE. When '
22
+ + 'none is set, it writes a new key to the project .env (mode 0600, '
23
+ + 'added to .gitignore). Returns { ready: true, address, source, '
24
+ + 'path?, note? }; `source` is `generated` when it created the key, '
25
+ + 'and `note` then says to tell the developer once, after the fact. '
26
+ + 'Do not ask the developer for a key. Never returns the key.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ projectDir: {
31
+ type: 'string',
32
+ description: 'Project directory whose `.env` holds the key. Defaults to '
33
+ + 'the closest ancestor of cwd containing .git or '
34
+ + 'package.json.',
35
+ },
36
+ },
37
+ },
38
+ annotations: {
39
+ readOnlyHint: false,
40
+ destructiveHint: false,
41
+ idempotentHint: true,
42
+ openWorldHint: false,
43
+ },
44
+ }, async (args) => {
45
+ const projectDir = findProjectRoot(args.projectDir || process.cwd());
46
+ const key = resolveOrCreateEthKey(projectDir);
40
47
  return {
41
- ready: false,
42
- message: 'No eth proof-owner key available — no RECLAIM_PRIVATE_KEY in the ' +
43
- `env, no RECLAIM_PRIVATE_KEY in ${join(projectDir, ENV_FILE)}, and ` +
44
- 'no RECLAIM_PRIVATE_KEY_FILE. Ask the dev to generate one via ' +
45
- 'issue_credentials, or import one they already hold via ' +
46
- 'import_credentials (a key file). Then call ' +
47
- 'resolve_owner_key again and pass its address to run_proof as ' +
48
- 'ownerAddress.',
48
+ ready: true,
49
+ address: key.address,
50
+ source: key.source,
51
+ ...(key.path ? { path: key.path } : {}),
52
+ ...(key.source === 'generated' ? { note: GENERATED_KEY_NOTE } : {}),
49
53
  };
50
54
  });
51
55
  }
@@ -1,9 +1,15 @@
1
1
  export type CachedSession = {
2
2
  token: string;
3
- /** ISO timestamp when the token expires (2 days from issue). */
3
+ /** ISO timestamp when the access token expires. */
4
4
  expiresAt: string;
5
5
  /** Email of the user who authorized (if available). */
6
6
  email?: string;
7
+ /** OAuth refresh token; trades for a new access token near expiry. */
8
+ refreshToken?: string;
9
+ /** Public OAuth client the tokens were issued to (refresh needs it). */
10
+ clientId?: string;
11
+ /** Auth server base URL that issued the tokens. */
12
+ authBase?: string;
7
13
  };
8
14
  /** Persist the auth token to the `session` section of the unified config. */
9
15
  export declare function writeSession(session: CachedSession): void;
@@ -12,3 +18,21 @@ export declare function writeSession(session: CachedSession): void;
12
18
  * valid (not yet expired). Returns undefined on any error.
13
19
  */
14
20
  export declare function readSession(): CachedSession | undefined;
21
+ /** The cached session even when its access token has expired. */
22
+ export declare function readStoredSession(): CachedSession | undefined;
23
+ /**
24
+ * Wraps `fetch` for the Builder client so a cached sign-in keeps working past
25
+ * the access token's lifetime. Each request uses the cached session's token,
26
+ * refreshed via the OAuth refresh grant when it's about to expire or when
27
+ * Builder answers 401. If no refresh is possible, the 401 tells the agent to
28
+ * sign in again with `signInTool`. Without a cached session, requests pass
29
+ * through untouched. Don't install it when `RECLAIM_API_TOKEN` is set: that
30
+ * token takes precedence and is never refreshed.
31
+ */
32
+ export declare function sessionRefreshingFetch(signInTool: string, baseFetch?: typeof fetch): typeof fetch;
33
+ /**
34
+ * Trade the session's refresh token for a new access token and cache the
35
+ * result. Returns undefined when the session can't be refreshed; a refresh
36
+ * token the server rejects is dropped so later calls stop retrying it.
37
+ */
38
+ export declare function refreshSession(session: CachedSession): Promise<CachedSession | undefined>;
@@ -1,4 +1,10 @@
1
+ import { PROBLEM_CONTENT_TYPE } from '@reclaimprotocol/client/api';
2
+ import * as oauth from 'openid-client';
1
3
  import { ConfigStore } from "../../../config/store.js";
4
+ import { LOGGER } from "../../../logger.js";
5
+ import { oauthRequestOptions, sessionFromTokens } from "./oauth-config.js";
6
+ /** Refresh this long before the access token actually expires. */
7
+ const EXPIRY_MARGIN_MS = 60 * 1000;
2
8
  /** Persist the auth token to the `session` section of the unified config. */
3
9
  export function writeSession(session) {
4
10
  new ConfigStore().write({ session });
@@ -8,12 +14,97 @@ export function writeSession(session) {
8
14
  * valid (not yet expired). Returns undefined on any error.
9
15
  */
10
16
  export function readSession() {
17
+ const s = readStoredSession();
18
+ if (!s || new Date(s.expiresAt) <= new Date()) {
19
+ return undefined;
20
+ }
21
+ return s;
22
+ }
23
+ /** The cached session even when its access token has expired. */
24
+ export function readStoredSession() {
11
25
  const s = new ConfigStore().read()?.session;
12
- if (!s?.token || !s.expiresAt) {
26
+ return s?.token && s.expiresAt ? s : undefined;
27
+ }
28
+ /**
29
+ * Wraps `fetch` for the Builder client so a cached sign-in keeps working past
30
+ * the access token's lifetime. Each request uses the cached session's token,
31
+ * refreshed via the OAuth refresh grant when it's about to expire or when
32
+ * Builder answers 401. If no refresh is possible, the 401 tells the agent to
33
+ * sign in again with `signInTool`. Without a cached session, requests pass
34
+ * through untouched. Don't install it when `RECLAIM_API_TOKEN` is set: that
35
+ * token takes precedence and is never refreshed.
36
+ */
37
+ export function sessionRefreshingFetch(signInTool, baseFetch = fetch) {
38
+ // One refresh at a time: Better Auth rotates refresh tokens and revokes
39
+ // the whole family when a used one is replayed.
40
+ let refreshing;
41
+ return async (input, init) => {
42
+ let session = readStoredSession();
43
+ if (!session) {
44
+ return baseFetch(input, init);
45
+ }
46
+ const expiring = new Date(session.expiresAt).getTime() - Date.now() < EXPIRY_MARGIN_MS;
47
+ const triedRefresh = expiring && !!session.refreshToken;
48
+ if (triedRefresh) {
49
+ session = await refreshOnce(session) ?? session;
50
+ }
51
+ const res = await baseFetch(input, withBearer(init, session.token));
52
+ if (res.status !== 401) {
53
+ return res;
54
+ }
55
+ const next = triedRefresh ? undefined : await refreshOnce(session);
56
+ if (next) {
57
+ return baseFetch(input, withBearer(init, next.token));
58
+ }
59
+ return sessionExpiredResponse(signInTool);
60
+ };
61
+ function refreshOnce(session) {
62
+ refreshing ?? (refreshing = refreshSession(session)
63
+ .finally(() => {
64
+ refreshing = undefined;
65
+ }));
66
+ return refreshing;
67
+ }
68
+ }
69
+ /**
70
+ * Trade the session's refresh token for a new access token and cache the
71
+ * result. Returns undefined when the session can't be refreshed; a refresh
72
+ * token the server rejects is dropped so later calls stop retrying it.
73
+ */
74
+ export async function refreshSession(session) {
75
+ const { refreshToken, clientId, authBase } = session;
76
+ if (!refreshToken || !clientId || !authBase) {
13
77
  return undefined;
14
78
  }
15
- if (new Date(s.expiresAt) <= new Date()) {
16
- return undefined; // expired
79
+ try {
80
+ const config = await oauth.discovery(new URL(authBase), clientId, undefined, oauth.None(), oauthRequestOptions(authBase));
81
+ const tokens = await oauth.refreshTokenGrant(config, refreshToken);
82
+ const next = {
83
+ ...sessionFromTokens(tokens, { clientId, authBase, refreshToken }),
84
+ email: session.email,
85
+ };
86
+ writeSession(next);
87
+ return next;
17
88
  }
18
- return s;
89
+ catch (err) {
90
+ LOGGER.warn({ err }, 'builder session refresh failed');
91
+ if (err instanceof oauth.ResponseBodyError) {
92
+ writeSession({ ...session, refreshToken: undefined });
93
+ }
94
+ return undefined;
95
+ }
96
+ }
97
+ function withBearer(init, token) {
98
+ const headers = new Headers(init?.headers);
99
+ headers.set('Authorization', `Bearer ${token}`);
100
+ return { ...init, headers };
101
+ }
102
+ function sessionExpiredResponse(signInTool) {
103
+ return new Response(JSON.stringify({
104
+ type: 'about:blank',
105
+ title: 'Unauthorized',
106
+ status: 401,
107
+ detail: 'Your Builder sign-in expired and could not be refreshed. '
108
+ + `Call ${signInTool} to sign in again, then retry.`,
109
+ }), { status: 401, headers: { 'content-type': PROBLEM_CONTENT_TYPE } });
19
110
  }
@@ -1,17 +1,17 @@
1
1
  import { defaultConfigPath } from "../../../config/store.js";
2
2
  import { oldModeSource } from "../../../consts.js";
3
3
  import { defineTool } from "../../server.js";
4
- /** Whether `config`'s cached builder session token is present and not yet
5
- * expired. Mirrors `readSession`'s check, but against the CALLER's
6
- * `ConfigStore` instance (not always the default `~/.reclaim/config.json`
7
- * path) so this stays test-isolatable and consistent with the rest of this
8
- * tool's config reads. */
4
+ /** Whether `config`'s cached builder session token is present and either
5
+ * not yet expired or refreshable. Mirrors `readSession`'s check, but against
6
+ * the CALLER's `ConfigStore` instance (not always the default
7
+ * `~/.reclaim/config.json` path) so this stays test-isolatable and
8
+ * consistent with the rest of this tool's config reads. */
9
9
  function hasValidBuilderSession(config) {
10
10
  const session = config.read()?.session;
11
11
  if (!session?.token || !session.expiresAt) {
12
12
  return false;
13
13
  }
14
- return new Date(session.expiresAt) > new Date();
14
+ return !!session.refreshToken || new Date(session.expiresAt) > new Date();
15
15
  }
16
16
  /**
17
17
  * The `set_devtools_mode` / `get_devtools_mode` tools: persist which backend
@@ -1,5 +1,5 @@
1
1
  import { connectCdp } from "../cdp/client.js";
2
- import { defaultProfileDir, launchChrome, waitForCdpReady, } from "../cdp/launcher.js";
2
+ import { defaultProfileDir, launchLocalBrowser, waitForCdpReady, } from "../cdp/launcher.js";
3
3
  import { ConfigStore } from "../config/store.js";
4
4
  import { DEFAULT_LOGIN_URL, oldLoginUrl } from "../consts.js";
5
5
  /**
@@ -127,8 +127,7 @@ export async function captureFirebaseTokenViaCdp(opts) {
127
127
  `with --remote-debugging-port=${port} AND a non-default ` +
128
128
  '--user-data-dir, or use the dedicated local login mode.');
129
129
  }
130
- launchChrome({ port, profileDir: defaultProfileDir() });
131
- await waitForCdpReady(port, 10000);
130
+ await launchLocalBrowser({ port, profileDir: defaultProfileDir() });
132
131
  launchedByUs = true;
133
132
  }
134
133
  const handles = await connectCdp(port);
@@ -108,9 +108,8 @@ export async function runAttestorProof(provider, secrets, opts = {}) {
108
108
  extractedParameters: {},
109
109
  verified: false,
110
110
  error: 'No eth proof-owner key available (no RECLAIM_PRIVATE_KEY in env, ' +
111
- '.env, or RECLAIM_PRIVATE_KEY_FILE). Call resolve_owner_key, then ' +
112
- 'issue_credentials (generate) or import_credentials (provide one), ' +
113
- 'and retry run_proof.',
111
+ '.env, or RECLAIM_PRIVATE_KEY_FILE). Call resolve_owner_key, ' +
112
+ 'which creates a local test key, and retry run_proof.',
114
113
  errorKind: 'missing-owner-key',
115
114
  };
116
115
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { createClaimOnAttestor } from '@reclaimprotocol/attestor-core';
7
7
  import './patch-domhandler.ts';
8
+ import { type ZkFilesState } from './zk-files.ts';
8
9
  /**
9
10
  * Default time budget for one check. A live run takes about 8 s; a longer
10
11
  * budget risks the MCP client's own tool-call timeout.
@@ -37,11 +38,16 @@ export declare const COIN_GECKO_PARAMS: {
37
38
  additionalClientOptions: {};
38
39
  };
39
40
  export type AttestorCheckErrorKind =
40
- /** Socket, connection, DNS, TLS, or timeout failure. */
41
+ /** Socket, connection, DNS, TLS, or timeout failure, with the proof files
42
+ * ready: the attestor itself didn't answer. */
41
43
  'attestor-unreachable'
42
44
  /** The attestor answered and rejected or failed the claim. */
43
45
  | 'attestor-error'
44
- /** Local setup (ZK files) failed before the attestor was contacted. */
46
+ /** The proof files are still downloading; the attestor wasn't contacted. */
47
+ | 'proof-files-downloading'
48
+ /** The proof file download failed; the attestor wasn't contacted. */
49
+ | 'proof-files-failed'
50
+ /** Other local setup failed before the attestor was contacted. */
45
51
  | 'setup-failed';
46
52
  export interface AttestorCheckResult {
47
53
  ok: boolean;
@@ -50,6 +56,8 @@ export interface AttestorCheckResult {
50
56
  extractedParameters?: Record<string, string>;
51
57
  error?: string;
52
58
  errorKind?: AttestorCheckErrorKind;
59
+ /** The proof file download state when the check finished. */
60
+ proofFiles?: ZkFilesState;
53
61
  }
54
62
  /** Creates the claim on the attestor (`createClaimOnAttestor` by default). */
55
63
  export type CreateClaim = (args: Parameters<typeof createClaimOnAttestor>[0]) => Promise<unknown>;
@@ -60,6 +68,8 @@ export interface CheckAttestorOptions {
60
68
  createClaim?: CreateClaim;
61
69
  /** Replaces the ZK file download in tests. */
62
70
  prepare?: () => Promise<void>;
71
+ /** Reports the ZK file download state (`zkFilesState` by default). */
72
+ proofFilesState?: () => ZkFilesState;
63
73
  }
64
74
  /** True when `message` looks like a network failure rather than a rejection. */
65
75
  export declare function isNetworkError(message: string | undefined): boolean;
@@ -7,7 +7,7 @@ import { createClaimOnAttestor } from '@reclaimprotocol/attestor-core';
7
7
  import { generateKeypair } from '@reclaimprotocol/client';
8
8
  import "./patch-domhandler.js";
9
9
  import { claimRejection, ensureAttestorCrypto, parseClaim, resolveAttestorUrl, } from "./proof-core.js";
10
- import { ensureZkFiles } from "./zk-files.js";
10
+ import { ensureZkFiles, zkFilesState } from "./zk-files.js";
11
11
  /**
12
12
  * Default time budget for one check. A live run takes about 8 s; a longer
13
13
  * budget risks the MCP client's own tool-call timeout.
@@ -87,9 +87,15 @@ export async function checkAttestor(opts = {}) {
87
87
  ensureAttestorCrypto();
88
88
  await ensureZkFiles();
89
89
  });
90
+ const proofFilesState = opts.proofFilesState ?? zkFilesState;
90
91
  const startedAt = Date.now();
91
92
  const finish = (outcome) => {
92
- return { ...outcome, attestorUrl, durationMs: Date.now() - startedAt };
93
+ return {
94
+ ...outcome,
95
+ attestorUrl,
96
+ durationMs: Date.now() - startedAt,
97
+ proofFiles: proofFilesState(),
98
+ };
93
99
  };
94
100
  let timer;
95
101
  const timeout = new Promise((_, reject) => {
@@ -102,14 +108,7 @@ export async function checkAttestor(opts = {}) {
102
108
  await Promise.race([prepare(), timeout]);
103
109
  }
104
110
  catch (err) {
105
- const error = messageOf(err);
106
- return finish({
107
- ok: false,
108
- error,
109
- errorKind: error.startsWith(CHECK_TIMEOUT_MESSAGE)
110
- ? 'attestor-unreachable'
111
- : 'setup-failed',
112
- });
111
+ return finish({ ok: false, ...setupFailure(messageOf(err)) });
113
112
  }
114
113
  const claim = await Promise.race([
115
114
  createClaim({
@@ -150,6 +149,37 @@ export async function checkAttestor(opts = {}) {
150
149
  finally {
151
150
  clearTimeout(timer);
152
151
  }
152
+ /** Classify a failure before the attestor was contacted by the proof
153
+ * file download state, so a slow or failed download never reads as an
154
+ * unreachable attestor. */
155
+ function setupFailure(error) {
156
+ const files = proofFilesState();
157
+ if (files.status === 'downloading') {
158
+ const done = files.bytesDownloaded !== undefined
159
+ ? `, ${files.bytesDownloaded}`
160
+ + (files.totalBytes !== undefined ? `/${files.totalBytes}` : '')
161
+ + ' bytes'
162
+ : '';
163
+ return {
164
+ error: `proof files are still downloading (${files.method}, `
165
+ + `${Math.round(files.elapsedMs / 1000)}s${done}); the attestor `
166
+ + 'was not contacted. Run check_attestor again shortly.',
167
+ errorKind: 'proof-files-downloading',
168
+ };
169
+ }
170
+ if (files.status === 'failed') {
171
+ return {
172
+ error: `proof file download failed: ${files.reason}`,
173
+ errorKind: 'proof-files-failed',
174
+ };
175
+ }
176
+ return {
177
+ error,
178
+ errorKind: error.startsWith(CHECK_TIMEOUT_MESSAGE)
179
+ ? 'attestor-unreachable'
180
+ : 'setup-failed',
181
+ };
182
+ }
153
183
  }
154
184
  function messageOf(err) {
155
185
  return err instanceof Error ? err.message : String(err);