@reclaimprotocol/agent 0.1.33 → 0.1.35

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 (117) hide show
  1. package/README.md +3 -3
  2. package/lib/authoring/backend.d.ts +13 -0
  3. package/lib/authoring/index.d.ts +1 -1
  4. package/lib/authoring/tools.js +78 -42
  5. package/lib/config/store.d.ts +10 -0
  6. package/lib/mcp/api/operations.gen.d.ts +238 -4
  7. package/lib/mcp/api/operations.gen.js +6967 -672
  8. package/lib/mcp/api/tools.js +21 -8
  9. package/lib/mcp/instructions.js +12 -12
  10. package/lib/mcp/server.d.ts +1 -1
  11. package/lib/mcp/server.js +3 -0
  12. package/lib/mcp/start.js +8 -7
  13. package/lib/mcp/success-response.js +5 -1
  14. package/lib/mcp/tools/agent/analyze.js +5 -4
  15. package/lib/mcp/tools/agent/attach.js +11 -7
  16. package/lib/mcp/tools/agent/build.js +5 -1
  17. package/lib/mcp/tools/agent/capture.d.ts +2 -2
  18. package/lib/mcp/tools/agent/capture.js +15 -11
  19. package/lib/mcp/tools/agent/check-attestor.d.ts +5 -0
  20. package/lib/mcp/tools/agent/check-attestor.js +37 -0
  21. package/lib/mcp/tools/agent/eval.js +4 -3
  22. package/lib/mcp/tools/agent/find.js +9 -10
  23. package/lib/mcp/tools/agent/how-it-works.js +1 -0
  24. package/lib/mcp/tools/agent/live-view.js +49 -22
  25. package/lib/mcp/tools/agent/pod-proof.js +7 -11
  26. package/lib/mcp/tools/agent/proof.d.ts +12 -0
  27. package/lib/mcp/tools/agent/proof.js +32 -0
  28. package/lib/mcp/tools/agent/propose.js +4 -3
  29. package/lib/mcp/tools/agent/publish.d.ts +2 -1
  30. package/lib/mcp/tools/agent/publish.js +12 -6
  31. package/lib/mcp/tools/agent/replay.js +10 -7
  32. package/lib/mcp/tools/agent/session.js +16 -8
  33. package/lib/mcp/tools/agent/shared.d.ts +2 -1
  34. package/lib/mcp/tools/agent/shared.js +29 -24
  35. package/lib/mcp/tools/agent/start-integration.d.ts +15 -0
  36. package/lib/mcp/tools/agent/start-integration.js +62 -0
  37. package/lib/mcp/tools/authenticate/authenticate.js +9 -11
  38. package/lib/mcp/tools/authenticate/build.js +2 -0
  39. package/lib/mcp/tools/authenticate/builder-access.js +1 -0
  40. package/lib/mcp/tools/authenticate/eth-key.d.ts +8 -0
  41. package/lib/mcp/tools/authenticate/eth-key.js +27 -15
  42. package/lib/mcp/tools/authenticate/import.js +18 -31
  43. package/lib/mcp/tools/authenticate/issue-org-token.d.ts +8 -0
  44. package/lib/mcp/tools/authenticate/issue-org-token.js +60 -0
  45. package/lib/mcp/tools/authenticate/issue.js +9 -9
  46. package/lib/mcp/tools/authenticate/org-key-store.d.ts +10 -0
  47. package/lib/mcp/tools/authenticate/org-key-store.js +23 -0
  48. package/lib/mcp/tools/authenticate/resolve-owner-key.js +10 -12
  49. package/lib/mcp/tools/mode/mode.js +2 -0
  50. package/lib/mcp/tools/verification/build.d.ts +3 -1
  51. package/lib/mcp/tools/verification/build.js +10 -2
  52. package/lib/mcp/tools/verification/callback-listener.d.ts +51 -0
  53. package/lib/mcp/tools/verification/callback-listener.js +163 -0
  54. package/lib/mcp/tools/verification/index.d.ts +2 -0
  55. package/lib/mcp/tools/verification/index.js +2 -0
  56. package/lib/mcp/tools/verification/proxy-callback-events.d.ts +59 -0
  57. package/lib/mcp/tools/verification/proxy-callback-events.js +300 -0
  58. package/lib/mcp/tools/verification/read-callback-events.d.ts +3 -0
  59. package/lib/mcp/tools/verification/read-callback-events.js +135 -0
  60. package/lib/mcp/tools/verification/result.d.ts +6 -0
  61. package/lib/mcp/tools/verification/result.js +11 -8
  62. package/lib/proof/check-attestor.d.ts +67 -0
  63. package/lib/proof/check-attestor.js +156 -0
  64. package/lib/tools/registry.d.ts +12 -0
  65. package/lib/tools/registry.js +1 -0
  66. package/package.json +2 -2
  67. package/skill/SKILL.builder.md +84 -75
  68. package/skill/SKILL.common.md +23 -10
  69. package/skill/builder-mode.md +140 -138
  70. package/skill/start-integration.md +74 -0
  71. package/skill/user-script.md +64 -0
  72. package/skill/verification-client-integration.md +40 -28
  73. package/src/authoring/backend.ts +15 -0
  74. package/src/authoring/index.ts +2 -0
  75. package/src/authoring/tools.ts +86 -42
  76. package/src/config/store.ts +11 -0
  77. package/src/mcp/api/operations.gen.ts +7011 -704
  78. package/src/mcp/api/tools.ts +21 -8
  79. package/src/mcp/instructions.ts +12 -12
  80. package/src/mcp/server.ts +4 -0
  81. package/src/mcp/start.ts +9 -7
  82. package/src/mcp/success-response.ts +5 -1
  83. package/src/mcp/tools/agent/analyze.ts +5 -4
  84. package/src/mcp/tools/agent/attach.ts +11 -7
  85. package/src/mcp/tools/agent/build.ts +7 -1
  86. package/src/mcp/tools/agent/capture.ts +20 -12
  87. package/src/mcp/tools/agent/check-attestor.ts +49 -0
  88. package/src/mcp/tools/agent/eval.ts +4 -3
  89. package/src/mcp/tools/agent/find.ts +9 -10
  90. package/src/mcp/tools/agent/how-it-works.ts +1 -0
  91. package/src/mcp/tools/agent/live-view.ts +49 -22
  92. package/src/mcp/tools/agent/pod-proof.ts +7 -11
  93. package/src/mcp/tools/agent/proof.ts +55 -0
  94. package/src/mcp/tools/agent/propose.ts +4 -3
  95. package/src/mcp/tools/agent/publish.ts +12 -4
  96. package/src/mcp/tools/agent/replay.ts +10 -7
  97. package/src/mcp/tools/agent/session.ts +16 -8
  98. package/src/mcp/tools/agent/shared.ts +39 -24
  99. package/src/mcp/tools/agent/start-integration.ts +84 -0
  100. package/src/mcp/tools/authenticate/authenticate.ts +9 -11
  101. package/src/mcp/tools/authenticate/build.ts +2 -0
  102. package/src/mcp/tools/authenticate/builder-access.ts +1 -0
  103. package/src/mcp/tools/authenticate/eth-key.ts +48 -18
  104. package/src/mcp/tools/authenticate/import.ts +21 -36
  105. package/src/mcp/tools/authenticate/issue-org-token.ts +84 -0
  106. package/src/mcp/tools/authenticate/issue.ts +9 -9
  107. package/src/mcp/tools/authenticate/org-key-store.ts +25 -0
  108. package/src/mcp/tools/authenticate/resolve-owner-key.ts +10 -12
  109. package/src/mcp/tools/mode/mode.ts +2 -0
  110. package/src/mcp/tools/verification/build.ts +10 -2
  111. package/src/mcp/tools/verification/callback-listener.ts +203 -0
  112. package/src/mcp/tools/verification/index.ts +2 -0
  113. package/src/mcp/tools/verification/proxy-callback-events.ts +374 -0
  114. package/src/mcp/tools/verification/read-callback-events.ts +160 -0
  115. package/src/mcp/tools/verification/result.ts +11 -8
  116. package/src/proof/check-attestor.ts +209 -0
  117. package/src/tools/registry.ts +14 -0
@@ -5,53 +5,40 @@ import { writeEthKey } from "./eth-key.js";
5
5
  export function importTool() {
6
6
  return defineTool({
7
7
  name: 'import_credentials',
8
- description: 'Import an eth proof-owner key the dev already holds and persist it ' +
9
- 'to the project `.env` as RECLAIM_PRIVATE_KEY=0x… (mode 0600; `.env` ' +
10
- 'added to .gitignore). Provide EITHER `privateKey` (raw 0x-hex, 64 ' +
11
- 'chars) OR `keyFile` (path to a file holding the raw 0x-hex key). ' +
12
- 'The address is derived locally; the private key is never ' +
13
- 'returned. This is a local signing key only (not registered ' +
14
- 'with any server, ' +
15
- 'unrelated to org auth or encryption).',
8
+ description: 'Imports an eth proof-owner key from a file the developer already '
9
+ + 'holds. Reads the raw 0x-hex key from `keyFile` and writes it to '
10
+ + 'the project `.env` as RECLAIM_PRIVATE_KEY (mode 0600; `.env` '
11
+ + 'added to .gitignore). Returns the derived address and the `.env` '
12
+ + 'path; never the key. The key is a local signing key only: it is '
13
+ + 'not registered with any server and is unrelated to org auth or '
14
+ + 'encryption. To create a new key instead, use `issue_credentials`.',
16
15
  inputSchema: {
17
16
  type: 'object',
17
+ required: ['keyFile'],
18
18
  properties: {
19
- privateKey: {
20
- type: 'string',
21
- pattern: '^0x?[0-9a-fA-F]{64}$',
22
- description: 'Raw Ethereum private key, 0x-prefixed hex (64 hex chars).',
23
- },
24
19
  keyFile: {
25
20
  type: 'string',
26
- description: 'Path to a file containing the raw 0x-hex private key ' +
27
- '(absolute or project-relative).',
21
+ description: 'Path to a file containing the raw 0x-hex private key '
22
+ + '(absolute or relative to the working directory).',
28
23
  },
29
24
  projectDir: {
30
25
  type: 'string',
31
- description: 'Project directory whose `.env` the key is written to. ' +
32
- 'Defaults to the closest ancestor of cwd containing .git or ' +
33
- 'package.json.',
26
+ description: 'Project directory whose `.env` the key is written to. '
27
+ + 'Defaults to the closest ancestor of cwd containing .git or '
28
+ + 'package.json.',
34
29
  },
35
30
  },
36
31
  },
32
+ annotations: { readOnlyHint: false, destructiveHint: false },
37
33
  }, async (args) => {
38
- if (!args.privateKey && !args.keyFile) {
39
- throw new Error('Provide either `privateKey` (0x-hex) or `keyFile`.');
40
- }
41
- if (args.privateKey && args.keyFile) {
42
- throw new Error('Provide only one of `privateKey` or `keyFile`.');
43
- }
44
34
  const projectDir = findProjectRoot(args.projectDir || process.cwd());
45
- const privateKeyHex = args.keyFile
46
- ? readFileSync(args.keyFile, 'utf8')
47
- : args.privateKey;
48
- const { path, address } = writeEthKey(privateKeyHex, projectDir);
35
+ const { path, address } = writeEthKey(readFileSync(args.keyFile, 'utf8'), projectDir);
49
36
  return {
50
37
  address,
51
38
  path,
52
- note: 'Eth proof-owner key written to .env as RECLAIM_PRIVATE_KEY ' +
53
- '(mode 0600). resolve_owner_key resolves it from .env. Keep .env ' +
54
- 'secret — it has been added to .gitignore.',
39
+ note: 'Eth proof-owner key written to .env as RECLAIM_PRIVATE_KEY '
40
+ + '(mode 0600). resolve_owner_key resolves it from .env. Keep .env '
41
+ + 'secret: it has been added to .gitignore.',
55
42
  };
56
43
  });
57
44
  }
@@ -0,0 +1,8 @@
1
+ import type { ReclaimClient } from '@reclaimprotocol/client/api';
2
+ import { type RegisteredTool } from '../../server.ts';
3
+ /**
4
+ * Issues an organization API key and stores it on disk, so the key never
5
+ * passes through the agent's transcript. Replaces the generated
6
+ * `issue_org_token` operation, which returned the key in its response.
7
+ */
8
+ export declare function issueOrgTokenTool(client: ReclaimClient): RegisteredTool;
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert';
2
+ import { isAbsolute, relative, resolve } from 'node:path';
3
+ import { defineTool } from "../../server.js";
4
+ import { upsertEnvVar } from "./eth-key.js";
5
+ import { saveOrgKey } from "./org-key-store.js";
6
+ /**
7
+ * Issues an organization API key and stores it on disk, so the key never
8
+ * passes through the agent's transcript. Replaces the generated
9
+ * `issue_org_token` operation, which returned the key in its response.
10
+ */
11
+ export function issueOrgTokenTool(client) {
12
+ return defineTool({
13
+ name: 'issue_org_token',
14
+ description: 'Issues an organization API key (owner only) and writes it to a '
15
+ + 'server env file in the project, so the app can read it at runtime. '
16
+ + 'Also saves it on this machine for the org-key tools. Returns the '
17
+ + 'org, file, variable, and last four characters; never the key. '
18
+ + 'Replaces an existing variable of the same name in the file. '
19
+ + 'Adds the file to .gitignore. Use when the app needs a key, or '
20
+ + 'before `create_verification_session`.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ required: ['orgId', 'projectDir'],
24
+ properties: {
25
+ orgId: { type: 'string', format: 'uuid' },
26
+ projectDir: {
27
+ type: 'string',
28
+ description: 'Absolute path of the app\'s project directory.',
29
+ },
30
+ envFile: {
31
+ type: 'string',
32
+ description: 'Env file to write, relative to `projectDir`. Default `.env`.',
33
+ },
34
+ envVar: {
35
+ type: 'string',
36
+ pattern: '^[A-Za-z_][A-Za-z0-9_]*$',
37
+ description: 'Variable name to set. Default `RECLAIM_ORG_SECRET`.',
38
+ },
39
+ },
40
+ },
41
+ annotations: { openWorldHint: true, destructiveHint: false },
42
+ }, async (args) => {
43
+ assert(isAbsolute(args.projectDir), new Error('`projectDir` must be an absolute path.'));
44
+ const envFile = args.envFile || '.env';
45
+ const envVar = args.envVar || 'RECLAIM_ORG_SECRET';
46
+ const inside = relative(args.projectDir, resolve(args.projectDir, envFile));
47
+ assert(inside && !inside.startsWith('..') && !isAbsolute(inside), new Error('`envFile` must be inside `projectDir`.'));
48
+ const { data } = await client.call('IssueOrgToken', {
49
+ params: { orgId: args.orgId },
50
+ });
51
+ const path = upsertEnvVar(args.projectDir, envFile, envVar, data.orgSecret);
52
+ saveOrgKey(args.orgId, data.orgSecret);
53
+ return {
54
+ orgId: args.orgId,
55
+ envFile: path,
56
+ envVar,
57
+ last4: data.last4,
58
+ };
59
+ });
60
+ }
@@ -5,15 +5,14 @@ import { writeEthKey } from "./eth-key.js";
5
5
  export function issueTool() {
6
6
  return defineTool({
7
7
  name: 'issue_credentials',
8
- description: 'Generate a fresh secp256k1 eth proof-owner key-pair (the key ' +
9
- 'that signs local claim creation) and write the raw private key to ' +
10
- 'the project `.env` as RECLAIM_PRIVATE_KEY=0x… (mode 0600; `.env` ' +
11
- 'added to .gitignore). This is a LOCAL signing key only — it is NOT ' +
12
- 'registered with any server and is unrelated to org auth (the org ' +
13
- 'secret) or encryption (the eth decrypt key). ALWAYS confirm with ' +
14
- 'the dev before generating a new key. After writing, run_proof picks ' +
15
- 'it up automatically via resolve_owner_key. The private key is never ' +
16
- 'returned in the response.',
8
+ description: 'Generates a new secp256k1 eth proof-owner key and writes the '
9
+ + 'private key to the project `.env` as RECLAIM_PRIVATE_KEY (mode '
10
+ + '0600; `.env` added to .gitignore). Returns the address and the '
11
+ + '`.env` path; never the key. The key is a local signing key only: '
12
+ + 'it is not registered with any server and is unrelated to org auth '
13
+ + 'or encryption. Confirm with the developer before replacing an '
14
+ + 'existing key. `run_proof` then picks it up through '
15
+ + '`resolve_owner_key`.',
17
16
  inputSchema: {
18
17
  type: 'object',
19
18
  properties: {
@@ -25,6 +24,7 @@ export function issueTool() {
25
24
  },
26
25
  },
27
26
  },
27
+ annotations: { destructiveHint: false },
28
28
  }, async (args) => {
29
29
  const projectDir = findProjectRoot(args.projectDir || process.cwd());
30
30
  const keypair = generateKeypair();
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Save an organization API key (`rorg_…`) in the local config (mode 0600), so
3
+ * the org-key tools can use it without the key passing through a transcript.
4
+ */
5
+ export declare function saveOrgKey(orgId: string, secret: string): void;
6
+ /**
7
+ * The saved key for `orgId`, or the most recently saved key when no org is
8
+ * given. Returns `undefined` when none is saved.
9
+ */
10
+ export declare function savedOrgKey(orgId?: string): string | undefined;
@@ -0,0 +1,23 @@
1
+ import { ConfigStore } from "../../../config/store.js";
2
+ /**
3
+ * Save an organization API key (`rorg_…`) in the local config (mode 0600), so
4
+ * the org-key tools can use it without the key passing through a transcript.
5
+ */
6
+ export function saveOrgKey(orgId, secret) {
7
+ const saved = new ConfigStore().read()?.org_keys;
8
+ new ConfigStore().write({
9
+ org_keys: {
10
+ byOrg: { ...saved?.byOrg, [orgId]: secret },
11
+ recent: orgId,
12
+ },
13
+ });
14
+ }
15
+ /**
16
+ * The saved key for `orgId`, or the most recently saved key when no org is
17
+ * given. Returns `undefined` when none is saved.
18
+ */
19
+ export function savedOrgKey(orgId) {
20
+ const saved = new ConfigStore().read()?.org_keys;
21
+ const id = orgId || saved?.recent;
22
+ return id ? saved?.byOrg[id] : undefined;
23
+ }
@@ -16,18 +16,16 @@ import { ENV_FILE, resolveEthKey } from "./eth-key.js";
16
16
  export function resolveOwnerKeyTool() {
17
17
  return defineTool({
18
18
  name: 'resolve_owner_key',
19
- description: 'Resolve which eth proof-owner key run_proof should sign with — ' +
20
- 'call this BEFORE run_proof. Checks, in order: RECLAIM_PRIVATE_KEY ' +
21
- 'in the MCP env, a RECLAIM_PRIVATE_KEY=0x… line in the project .env, ' +
22
- 'then a file at RECLAIM_PRIVATE_KEY_FILE holding the raw hex. ' +
23
- 'Returns { ready: true, address, source } when one is ' +
24
- 'available — pass `address` to run_proof as `ownerAddress`. ' +
25
- 'Returns { ready: false } ' +
26
- 'when none exists: then ask the dev to generate one ' +
27
- '(issue_credentials) or import one they already hold ' +
28
- '(import_credentials, accepts 0x-hex or a key-file path), and call ' +
29
- 'resolve_owner_key again. Never exposes the private 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.',
30
27
  inputSchema: { type: 'object', properties: {} },
28
+ annotations: { readOnlyHint: true, openWorldHint: false },
31
29
  }, async () => {
32
30
  const projectDir = findProjectRoot(process.cwd());
33
31
  const resolved = resolveEthKey(projectDir);
@@ -45,7 +43,7 @@ export function resolveOwnerKeyTool() {
45
43
  `env, no RECLAIM_PRIVATE_KEY in ${join(projectDir, ENV_FILE)}, and ` +
46
44
  'no RECLAIM_PRIVATE_KEY_FILE. Ask the dev to generate one via ' +
47
45
  'issue_credentials, or import one they already hold via ' +
48
- 'import_credentials (0x-hex or a key-file path). Then call ' +
46
+ 'import_credentials (a key file). Then call ' +
49
47
  'resolve_owner_key again and pass its address to run_proof as ' +
50
48
  'ownerAddress.',
51
49
  };
@@ -44,6 +44,7 @@ export function buildModeTools(config, currentOldMode) {
44
44
  },
45
45
  required: ['mode'],
46
46
  },
47
+ annotations: { openWorldHint: false, idempotentHint: true },
47
48
  }, async (parsed) => {
48
49
  config.write({ mode: parsed.mode });
49
50
  const restartRequired = parsed.mode !== current;
@@ -71,6 +72,7 @@ export function buildModeTools(config, currentOldMode) {
71
72
  + 'the staleness with an error naming the tool to re-run. Which '
72
73
  + 'login authorizes what: how_it_works({ topic: "auth" }).',
73
74
  inputSchema: { type: 'object', properties: {} },
75
+ annotations: { readOnlyHint: true, openWorldHint: false },
74
76
  }, async () => {
75
77
  const persisted = config.read()?.mode;
76
78
  const oldIdentity = config.read()?.old_devtools;
@@ -4,7 +4,9 @@ import type { RegisteredTool } from '../../server.ts';
4
4
  * generated API surface. `get_verification_result` authenticates with the org
5
5
  * token, checks status, pulls the stored payload, and decrypts it locally with
6
6
  * the optional eth key — a flow the generated raw-API tools can't express
7
- * (the decryption key never leaves the consumer). Diagnosis is left to the
7
+ * (the decryption key never leaves the consumer). `read_callback_events` and
8
+ * `proxy_callback_events` read and forward the org's pull-listener events.
9
+ * Diagnosis is left to the
8
10
  * agent over the generated tools (`get_verification_session`,
9
11
  * `list_verification_events`, `list_verification_deliveries`), whose
10
12
  * descriptions carry the lifecycle and delivery semantics.
@@ -1,14 +1,22 @@
1
+ import { proxyCallbackEventsTool } from "./proxy-callback-events.js";
2
+ import { readCallbackEventsTool } from "./read-callback-events.js";
1
3
  import { verificationResultTool } from "./result.js";
2
4
  /**
3
5
  * Builder-mode verification tools: higher-level orchestrations over the
4
6
  * generated API surface. `get_verification_result` authenticates with the org
5
7
  * token, checks status, pulls the stored payload, and decrypts it locally with
6
8
  * the optional eth key — a flow the generated raw-API tools can't express
7
- * (the decryption key never leaves the consumer). Diagnosis is left to the
9
+ * (the decryption key never leaves the consumer). `read_callback_events` and
10
+ * `proxy_callback_events` read and forward the org's pull-listener events.
11
+ * Diagnosis is left to the
8
12
  * agent over the generated tools (`get_verification_session`,
9
13
  * `list_verification_events`, `list_verification_deliveries`), whose
10
14
  * descriptions carry the lifecycle and delivery semantics.
11
15
  */
12
16
  export function buildVerificationTools() {
13
- return [verificationResultTool()];
17
+ return [
18
+ verificationResultTool(),
19
+ readCallbackEventsTool(),
20
+ proxyCallbackEventsTool(),
21
+ ];
14
22
  }
@@ -0,0 +1,51 @@
1
+ import { ReclaimClient } from '@reclaimprotocol/client/api';
2
+ import type { IOperationResponse } from '@reclaimprotocol/client/openapi';
3
+ /** Runtime seams. Production passes none; tests pass a stub `fetch`. */
4
+ export interface CallbackDeps {
5
+ fetch?: typeof globalThis.fetch;
6
+ /** Waits between heartbeats and polls. Resolves, without error, on abort. */
7
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
8
+ keeper?: ListenerKeeper;
9
+ }
10
+ /**
11
+ * Events the org's pull listener subscribes to: the five session-ending
12
+ * events (the first four carry the signed result; `session_expired` has no
13
+ * result) plus the lifecycle milestones an app developer checks while testing.
14
+ * High-volume diagnostics (`network_request_observed`, `provider_script_log`,
15
+ * `user_interaction_*`) are left out, so a forwarded session sends the app at
16
+ * most 11 requests instead of dozens.
17
+ */
18
+ export declare const LISTENER_EVENTS: readonly ["verification_success", "verification_rejected", "verification_error", "verification_cancelled", "session_expired", "session_pending", "verification_client_opened", "authenticated", "verification_proofs_completed", "verification_result_submitting", "verification_result_submission_failed"];
19
+ export type ListenerInfo = {
20
+ subscriptionId: string;
21
+ created: boolean;
22
+ /** When the lease lapses unless the heartbeat renews it. */
23
+ expiresAt: string;
24
+ };
25
+ export type CallbackEvent = IOperationResponse<'ListCallbackEvents'>['items'][number];
26
+ /**
27
+ * A client that presents the organization's API key. With a `signal`, every
28
+ * request aborts when it does.
29
+ */
30
+ export declare function orgClient(secret: string, deps?: CallbackDeps, signal?: AbortSignal): ReclaimClient;
31
+ /**
32
+ * Keeps each org's pull listener alive while this MCP process runs. Creating
33
+ * the subscription is the heartbeat: it renews a lease, and the listener
34
+ * stops receiving events when the lease lapses. {@link release} ends the
35
+ * lease at shutdown.
36
+ */
37
+ export declare class ListenerKeeper {
38
+ #private;
39
+ /** Creates or renews the listener and starts its heartbeat (once). */
40
+ ensure(secret: string, deps?: CallbackDeps): Promise<ListenerInfo>;
41
+ /** Ends every lease and stops the heartbeats, within a short timeout. */
42
+ release(): Promise<void>;
43
+ }
44
+ /** Releases every listener this process keeps alive. Runs at MCP shutdown. */
45
+ export declare function releaseAllListeners(): Promise<void>;
46
+ /**
47
+ * Create the org's pull listener and keep it alive until shutdown. An org has
48
+ * one, so a second call returns the existing subscription (HTTP 200).
49
+ */
50
+ export declare function ensureListener(secret: string, deps?: CallbackDeps): Promise<ListenerInfo>;
51
+ export declare function sleepFor(ms: number, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,163 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _ListenerKeeper_instances, _ListenerKeeper_entries, _ListenerKeeper_beat;
7
+ import { badGateway } from '@hapi/boom';
8
+ import { ReclaimClient } from '@reclaimprotocol/client/api';
9
+ import assert from 'node:assert';
10
+ import { setTimeout as delay } from 'node:timers/promises';
11
+ import { apiUrl } from "../../../consts.js";
12
+ import { LOGGER } from "../../../logger.js";
13
+ import { NAME, VERSION } from "../../../pkg.js";
14
+ /**
15
+ * Events the org's pull listener subscribes to: the five session-ending
16
+ * events (the first four carry the signed result; `session_expired` has no
17
+ * result) plus the lifecycle milestones an app developer checks while testing.
18
+ * High-volume diagnostics (`network_request_observed`, `provider_script_log`,
19
+ * `user_interaction_*`) are left out, so a forwarded session sends the app at
20
+ * most 11 requests instead of dozens.
21
+ */
22
+ export const LISTENER_EVENTS = [
23
+ 'verification_success',
24
+ 'verification_rejected',
25
+ 'verification_error',
26
+ 'verification_cancelled',
27
+ 'session_expired',
28
+ 'session_pending',
29
+ 'verification_client_opened',
30
+ 'authenticated',
31
+ 'verification_proofs_completed',
32
+ 'verification_result_submitting',
33
+ 'verification_result_submission_failed',
34
+ ];
35
+ /**
36
+ * A client that presents the organization's API key. With a `signal`, every
37
+ * request aborts when it does.
38
+ */
39
+ export function orgClient(secret, deps = {}, signal) {
40
+ const base = deps.fetch || globalThis.fetch;
41
+ return new ReclaimClient({
42
+ baseUrl: apiUrl(),
43
+ token: secret,
44
+ fetch: signal
45
+ ? (input, init) => base(input, { ...init, signal })
46
+ : deps.fetch,
47
+ headers: { 'User-Agent': `${NAME}/${VERSION}` },
48
+ });
49
+ }
50
+ /** Renewal interval. The backend lease lasts 90 seconds. */
51
+ const HEARTBEAT_MS = 30000;
52
+ /** Upper bound for releasing all listeners at shutdown. */
53
+ const RELEASE_TIMEOUT_MS = 3000;
54
+ /**
55
+ * Keeps each org's pull listener alive while this MCP process runs. Creating
56
+ * the subscription is the heartbeat: it renews a lease, and the listener
57
+ * stops receiving events when the lease lapses. {@link release} ends the
58
+ * lease at shutdown.
59
+ */
60
+ export class ListenerKeeper {
61
+ constructor() {
62
+ _ListenerKeeper_instances.add(this);
63
+ _ListenerKeeper_entries.set(this, new Map()
64
+ /** Creates or renews the listener and starts its heartbeat (once). */
65
+ );
66
+ }
67
+ /** Creates or renews the listener and starts its heartbeat (once). */
68
+ async ensure(secret, deps = {}) {
69
+ const existing = __classPrivateFieldGet(this, _ListenerKeeper_entries, "f").get(secret);
70
+ if (existing) {
71
+ const wasSettled = existing.settled;
72
+ const first = await existing.ready;
73
+ // Callers that arrived during creation share its result.
74
+ return wasSettled ? await renew(existing.client) : first;
75
+ }
76
+ const abort = new AbortController();
77
+ const client = orgClient(secret, deps, abort.signal);
78
+ const sleep = deps.sleep || sleepFor;
79
+ const entry = {
80
+ client,
81
+ releaseClient: orgClient(secret, deps),
82
+ abort,
83
+ settled: false,
84
+ ready: renew(client).then((info) => {
85
+ entry.settled = true;
86
+ void __classPrivateFieldGet(this, _ListenerKeeper_instances, "m", _ListenerKeeper_beat).call(this, entry, sleep);
87
+ return info;
88
+ }, (err) => {
89
+ if (__classPrivateFieldGet(this, _ListenerKeeper_entries, "f").get(secret) === entry) {
90
+ __classPrivateFieldGet(this, _ListenerKeeper_entries, "f").delete(secret);
91
+ }
92
+ abort.abort();
93
+ throw err;
94
+ }),
95
+ };
96
+ __classPrivateFieldGet(this, _ListenerKeeper_entries, "f").set(secret, entry);
97
+ return await entry.ready;
98
+ }
99
+ /** Ends every lease and stops the heartbeats, within a short timeout. */
100
+ async release() {
101
+ const entries = [...__classPrivateFieldGet(this, _ListenerKeeper_entries, "f").values()];
102
+ __classPrivateFieldGet(this, _ListenerKeeper_entries, "f").clear();
103
+ const timeout = AbortSignal.timeout(RELEASE_TIMEOUT_MS);
104
+ await Promise.all(entries.map(async ({ releaseClient, abort }) => {
105
+ abort.abort();
106
+ try {
107
+ await Promise.race([
108
+ releaseClient.call('ReleaseCallbackListener', {}),
109
+ new Promise((_, reject) => {
110
+ timeout.addEventListener('abort', () => reject(timeout.reason));
111
+ }),
112
+ ]);
113
+ }
114
+ catch (err) {
115
+ LOGGER.warn({ err }, 'releasing the callback listener failed');
116
+ }
117
+ }));
118
+ }
119
+ }
120
+ _ListenerKeeper_entries = new WeakMap(), _ListenerKeeper_instances = new WeakSet(), _ListenerKeeper_beat = async function _ListenerKeeper_beat(entry, sleep) {
121
+ const { signal } = entry.abort;
122
+ while (!signal.aborted) {
123
+ await sleep(HEARTBEAT_MS, signal);
124
+ if (signal.aborted) {
125
+ return;
126
+ }
127
+ try {
128
+ await renew(entry.client);
129
+ }
130
+ catch (err) {
131
+ if (signal.aborted) {
132
+ return;
133
+ }
134
+ LOGGER.warn({ err }, 'callback listener heartbeat failed; retrying');
135
+ }
136
+ }
137
+ };
138
+ const defaultKeeper = new ListenerKeeper();
139
+ /** Releases every listener this process keeps alive. Runs at MCP shutdown. */
140
+ export function releaseAllListeners() {
141
+ return defaultKeeper.release();
142
+ }
143
+ /**
144
+ * Create the org's pull listener and keep it alive until shutdown. An org has
145
+ * one, so a second call returns the existing subscription (HTTP 200).
146
+ */
147
+ export function ensureListener(secret, deps = {}) {
148
+ return (deps.keeper || defaultKeeper).ensure(secret, deps);
149
+ }
150
+ async function renew(client) {
151
+ const { data, response } = await client.call('CreateCallbackSubscription', {
152
+ body: { deliveryMode: 'pull', events: [...LISTENER_EVENTS] },
153
+ });
154
+ assert(data.listenerExpiresAt, badGateway('Builder returned a pull listener without a lease.'));
155
+ return {
156
+ subscriptionId: data.id,
157
+ created: response.status === 201,
158
+ expiresAt: data.listenerExpiresAt,
159
+ };
160
+ }
161
+ export async function sleepFor(ms, signal) {
162
+ await delay(ms, undefined, { signal }).catch(() => undefined);
163
+ }
@@ -1 +1,3 @@
1
1
  export { buildVerificationTools } from './build.ts';
2
+ export { stopAllForwarders } from './proxy-callback-events.ts';
3
+ export { releaseAllListeners } from './callback-listener.ts';
@@ -1,2 +1,4 @@
1
1
  // Barrel for the verification tool group (result retrieval + decryption).
2
2
  export { buildVerificationTools } from "./build.js";
3
+ export { stopAllForwarders } from "./proxy-callback-events.js";
4
+ export { releaseAllListeners } from "./callback-listener.js";
@@ -0,0 +1,59 @@
1
+ import type { ReclaimClient } from '@reclaimprotocol/client/api';
2
+ import { type RegisteredTool } from '../../server.ts';
3
+ import { type CallbackDeps, type ListenerInfo } from './callback-listener.ts';
4
+ interface Target {
5
+ port: number;
6
+ path: string;
7
+ }
8
+ type Recent = {
9
+ id: string;
10
+ sessionId: string;
11
+ event: string;
12
+ status: 'delivered' | 'failed';
13
+ httpStatus?: number;
14
+ at: string;
15
+ };
16
+ export interface ProxyDeps extends CallbackDeps {
17
+ /** Delivers an event to the app. Defaults to global `fetch`. */
18
+ forwardFetch?: typeof globalThis.fetch;
19
+ /** Waits between polls. Resolves early, without error, on abort. */
20
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
21
+ registry?: ForwarderRegistry;
22
+ }
23
+ /**
24
+ * Polls the org's pending pull events and POSTs each envelope to a local
25
+ * route, like `stripe listen --forward-to`. 2xx responses are acknowledged;
26
+ * anything else (redirects included) stays pending and is retried with
27
+ * exponential backoff. After a failure the batch stops, so events reach the
28
+ * app in order.
29
+ */
30
+ declare class Forwarder {
31
+ #private;
32
+ target: Target;
33
+ listener: ListenerInfo;
34
+ constructor(client: ReclaimClient, listener: ListenerInfo, target: Target, deps: ProxyDeps);
35
+ status(): {
36
+ recent: Recent[];
37
+ lastError?: string | undefined;
38
+ forwarding: boolean;
39
+ target: string;
40
+ listener: ListenerInfo;
41
+ forwarded: number;
42
+ failed: number;
43
+ };
44
+ stop(): Promise<void>;
45
+ }
46
+ /** The forwarders of one MCP process, one per org key. */
47
+ export declare class ForwarderRegistry {
48
+ #private;
49
+ get(secret: string): Forwarder | undefined;
50
+ set(secret: string, forwarder: Forwarder): void;
51
+ stop(secret: string): Promise<Forwarder | undefined>;
52
+ stopAll(): Promise<void>;
53
+ }
54
+ /** Stops every forwarder. Called when the MCP server shuts down. */
55
+ export declare function stopAllForwarders(): Promise<void>;
56
+ export declare function proxyCallbackEventsTool(deps?: ProxyDeps): RegisteredTool;
57
+ /** Rejects anything that is not a path on a loopback port. */
58
+ export declare function validateTarget(port: number, path: string): Target;
59
+ export {};