@reclaimprotocol/agent 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +5 -5
  2. package/lib/authoring/backend.d.ts +71 -0
  3. package/lib/authoring/backend.js +24 -0
  4. package/lib/authoring/find.d.ts +3 -0
  5. package/lib/authoring/find.js +54 -0
  6. package/lib/authoring/index.d.ts +4 -0
  7. package/lib/authoring/index.js +6 -0
  8. package/lib/authoring/propose.d.ts +3 -0
  9. package/lib/authoring/propose.js +109 -0
  10. package/lib/authoring/replay.d.ts +10 -0
  11. package/lib/authoring/replay.js +34 -0
  12. package/lib/authoring/tools.d.ts +16 -0
  13. package/lib/authoring/tools.js +193 -0
  14. package/lib/authoring/util.d.ts +27 -0
  15. package/lib/authoring/util.js +21 -0
  16. package/lib/cdp/capture-engine.js +1 -1
  17. package/lib/consts.d.ts +6 -1
  18. package/lib/consts.js +9 -2
  19. package/lib/mcp/api/operations.gen.js +46 -18
  20. package/lib/mcp/server.d.ts +2 -25
  21. package/lib/mcp/server.js +4 -17
  22. package/lib/mcp/start.js +4 -2
  23. package/lib/mcp/tools/agent/analyze.js +3 -3
  24. package/lib/mcp/tools/agent/build.d.ts +4 -4
  25. package/lib/mcp/tools/agent/build.js +13 -15
  26. package/lib/mcp/tools/agent/capture.d.ts +13 -1
  27. package/lib/mcp/tools/agent/capture.js +18 -85
  28. package/lib/mcp/tools/agent/eval.d.ts +5 -2
  29. package/lib/mcp/tools/agent/eval.js +11 -121
  30. package/lib/mcp/tools/agent/find.d.ts +10 -3
  31. package/lib/mcp/tools/agent/find.js +25 -31
  32. package/lib/mcp/tools/agent/proof.d.ts +13 -3
  33. package/lib/mcp/tools/agent/proof.js +81 -132
  34. package/lib/mcp/tools/agent/propose.d.ts +10 -9
  35. package/lib/mcp/tools/agent/propose.js +87 -64
  36. package/lib/mcp/tools/agent/publish.js +2 -2
  37. package/lib/mcp/tools/agent/replay.d.ts +10 -3
  38. package/lib/mcp/tools/agent/replay.js +29 -36
  39. package/lib/mcp/tools/agent/session.d.ts +1 -3
  40. package/lib/mcp/tools/agent/session.js +5 -5
  41. package/lib/mcp/tools/agent/shared.d.ts +8 -0
  42. package/lib/mcp/tools/agent/shared.js +125 -0
  43. package/lib/old/client.d.ts +35 -0
  44. package/lib/old/client.js +18 -0
  45. package/lib/old/login-cdp.d.ts +24 -0
  46. package/lib/old/login-cdp.js +28 -3
  47. package/lib/old/sdk-client.d.ts +39 -0
  48. package/lib/old/sdk-client.js +85 -0
  49. package/lib/old/to-register.d.ts +69 -0
  50. package/lib/old/to-register.js +52 -2
  51. package/lib/old/tools/build.d.ts +6 -1
  52. package/lib/old/tools/build.js +11 -1
  53. package/lib/old/tools/get-provider-info.d.ts +16 -0
  54. package/lib/old/tools/get-provider-info.js +73 -0
  55. package/lib/old/tools/get-version-config.d.ts +17 -0
  56. package/lib/old/tools/get-version-config.js +47 -0
  57. package/lib/old/tools/publish.js +189 -2
  58. package/lib/old/tools/update-provider-metadata.d.ts +16 -0
  59. package/lib/old/tools/update-provider-metadata.js +61 -0
  60. package/lib/proof/attestor.d.ts +6 -3
  61. package/lib/proof/attestor.js +10 -1
  62. package/lib/proof/debug-response-dump.d.ts +4 -0
  63. package/lib/proof/debug-response-dump.js +55 -0
  64. package/lib/proof/patch-domhandler.d.ts +1 -0
  65. package/lib/proof/patch-domhandler.js +59 -0
  66. package/lib/proof/proof-core.d.ts +1 -1
  67. package/lib/proof/proof-core.js +1 -1
  68. package/lib/proof/zk-files.js +7 -6
  69. package/lib/provider/dom-xpath.d.ts +17 -0
  70. package/lib/provider/dom-xpath.js +187 -0
  71. package/lib/provider/draft.d.ts +30 -2
  72. package/lib/provider/draft.js +117 -28
  73. package/lib/provider/index.d.ts +4 -0
  74. package/lib/provider/index.js +7 -0
  75. package/lib/provider/injection-doc.js +6 -0
  76. package/lib/provider/matcher.d.ts +17 -20
  77. package/lib/provider/matcher.js +394 -280
  78. package/lib/provider/schema.d.ts +7 -5
  79. package/lib/tools/registry.d.ts +50 -0
  80. package/lib/tools/registry.js +49 -0
  81. package/package.json +9 -1
  82. package/skill/SKILL.builder.md +55 -32
  83. package/skill/SKILL.old.md +20 -12
  84. package/skill/builder-mode.md +1 -1
  85. package/skill/injection-api.md +117 -0
  86. package/src/authoring/backend.ts +98 -0
  87. package/src/authoring/index.ts +20 -0
  88. package/src/authoring/tools.ts +273 -0
  89. package/src/authoring/util.ts +42 -0
  90. package/src/cdp/capture-engine.ts +1 -1
  91. package/src/consts.ts +15 -2
  92. package/src/mcp/api/operations.gen.ts +46 -18
  93. package/src/mcp/server.ts +9 -39
  94. package/src/mcp/start.ts +4 -1
  95. package/src/mcp/tools/agent/analyze.ts +3 -3
  96. package/src/mcp/tools/agent/build.ts +13 -21
  97. package/src/mcp/tools/agent/capture.ts +31 -115
  98. package/src/mcp/tools/agent/eval.ts +19 -155
  99. package/src/mcp/tools/agent/find.ts +33 -36
  100. package/src/mcp/tools/agent/proof.ts +87 -160
  101. package/src/mcp/tools/agent/propose.ts +93 -85
  102. package/src/mcp/tools/agent/publish.ts +2 -2
  103. package/src/mcp/tools/agent/replay.ts +32 -42
  104. package/src/mcp/tools/agent/session.ts +4 -5
  105. package/src/mcp/tools/agent/shared.ts +152 -0
  106. package/src/old/client.ts +56 -0
  107. package/src/old/login-cdp.ts +28 -3
  108. package/src/old/sdk-client.ts +105 -0
  109. package/src/old/to-register.ts +98 -6
  110. package/src/old/tools/build.ts +12 -0
  111. package/src/old/tools/get-provider-info.ts +91 -0
  112. package/src/old/tools/get-version-config.ts +56 -0
  113. package/src/old/tools/publish.ts +244 -1
  114. package/src/old/tools/update-provider-metadata.ts +72 -0
  115. package/src/proof/attestor.ts +16 -4
  116. package/src/proof/debug-response-dump.ts +70 -0
  117. package/src/proof/patch-domhandler.ts +68 -0
  118. package/src/proof/proof-core.ts +1 -1
  119. package/src/proof/zk-files.ts +7 -6
  120. package/src/provider/dom-xpath.ts +225 -0
  121. package/src/provider/draft.ts +168 -35
  122. package/src/provider/index.ts +8 -0
  123. package/src/provider/injection-doc.ts +6 -0
  124. package/src/provider/matcher.ts +477 -357
  125. package/src/provider/schema.ts +7 -5
  126. package/src/tools/registry.ts +92 -0
@@ -1,85 +1,108 @@
1
- import { randomUUID } from 'node:crypto';
1
+ /**
2
+ * `propose_provider` — draft a provider (recipe + captured secrets) from ONE
3
+ * captured request, one match + redaction per target value. Authored once as a
4
+ * plain {@link RegisteredTool}; hosts pass their own backend resolver. The
5
+ * draft (recipe + real secret header values) is held server-side under an
6
+ * opaque `draftId`; the result carries only the secret-free provider.
7
+ */
8
+ import { objectSchema } from "../../../authoring/util.js";
2
9
  import { draftProvider } from "../../../provider/draft.js";
3
10
  import { OPRF_MAX_BYTES } from "../../../provider/matcher.js";
4
11
  import { verificationWarnings } from "../../../provider/verification-checks.js";
5
- import { defineTool } from "../../server.js";
6
- export function makeSecretStore() {
7
- const map = new Map();
8
- return {
9
- put: (values) => {
10
- const id = `sref_${randomUUID()}`;
11
- map.set(id, values);
12
- return id;
13
- },
14
- resolve: (ref) => map.get(ref),
15
- clear: () => map.clear(),
16
- };
17
- }
18
- export function proposeTool(captures, secrets) {
12
+ import { defineTool } from "../../../tools/registry.js";
13
+ export function proposeTool(resolveBackend) {
19
14
  return defineTool({
20
15
  name: 'propose_provider',
21
- description: 'Draft a Reclaim provider JSON from a captured request + ' +
22
- 'a target value. ' +
23
- 'Secret header values are stored server-side and replaced with an ' +
24
- 'opaque secretParamsRef ' +
25
- 'in the response — raw secret values never appear in tool output. ' +
26
- "`hash` is opt-in: when set, the captured value is OPRF'd so " +
27
- 'the attestor never sees ' +
28
- 'cleartext. Skill should ask the dev first. `oprf-raw` ' +
29
- 'is the only variant that runs ' +
30
- 'without external deps; `oprf` needs gnark (Linux/macOS), ' +
31
- '`oprf-mpc` needs a TEE attestor. ' +
32
- 'The response may include `verificationWarnings` — advisory ' +
33
- 'author-only decisions (e.g. a user-specific URL part needing ' +
34
- 'the {{context.<name>}}-vs-bare call). Surface each to the ' +
35
- 'dev verbatim and resolve before publishing.',
36
- inputSchema: {
37
- type: 'object',
38
- properties: {
39
- captureId: { type: 'string' },
40
- requestId: { type: 'string' },
41
- target: { type: 'string' },
42
- name: { type: 'string' },
43
- hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'] },
16
+ description: 'Draft a provider from ONE captured request. Pass `targets` (an '
17
+ + 'array): one entry per value to extract, each with its `value` '
18
+ + '(verbatim) and a distinct `name` (its param). The provider gets '
19
+ + 'one match + redaction per target. Returns a draftId + recipe '
20
+ + '(never secrets). Prefer a SINGLE request holding ALL values. '
21
+ + 'Single-value shorthand: `target` + `name`. Per-target `hash` '
22
+ + 'OPRF-hashes that value.',
23
+ inputSchema: objectSchema({
24
+ requestId: { type: 'string' },
25
+ name: { type: 'string' },
26
+ targets: {
27
+ type: 'array',
28
+ items: {
29
+ type: 'object',
30
+ properties: {
31
+ value: { type: 'string' },
32
+ name: { type: 'string' },
33
+ hash: {
34
+ type: 'string',
35
+ enum: ['oprf-raw', 'oprf', 'oprf-mpc'],
36
+ },
37
+ },
38
+ required: ['value', 'name'],
39
+ },
44
40
  },
45
- required: ['captureId', 'requestId', 'target', 'name'],
46
- },
47
- }, async ({ captureId, requestId, target, name, hash }) => {
48
- if (hash !== undefined && target.length > OPRF_MAX_BYTES) {
49
- throw new Error(`OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; ` +
50
- `got ${target.length}-byte target ` +
51
- `"${target}". Try a tighter target ` +
52
- '(e.g. just the relevant substring) or call ' +
53
- 'propose_provider without `hash`.');
41
+ target: { type: 'string' },
42
+ hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'] },
43
+ }, ['requestId', 'name']),
44
+ }, async (args) => {
45
+ const backend = resolveBackend(args);
46
+ const requestId = String(args.requestId);
47
+ const providerName = String(args.name);
48
+ const defaultHash = args.hash;
49
+ // Explicit multi-value `targets`, else the single `target` shorthand.
50
+ const raw = Array.isArray(args.targets) ? args.targets : [];
51
+ const targets = raw.length
52
+ ? raw.map((t) => {
53
+ const o = t;
54
+ return {
55
+ value: String(o.value),
56
+ name: String(o.name),
57
+ ...(typeof o.hash === 'string'
58
+ ? { hash: o.hash }
59
+ : {}),
60
+ };
61
+ })
62
+ : typeof args.target === 'string'
63
+ ? [{
64
+ value: args.target,
65
+ name: providerName,
66
+ ...(defaultHash ? { hash: defaultHash } : {}),
67
+ }]
68
+ : [];
69
+ if (targets.length === 0) {
70
+ throw new Error('propose_provider needs a `target` value or a non-empty '
71
+ + '`targets` array');
54
72
  }
55
- const session = captures.sessions.get(captureId);
56
- if (!session) {
57
- throw new Error(`no capture ${captureId}`);
73
+ for (const t of targets) {
74
+ if ((t.hash ?? defaultHash) !== undefined
75
+ && t.value.length > OPRF_MAX_BYTES) {
76
+ throw new Error(`OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; got `
77
+ + `${t.value.length}-byte "${t.value}". Use a tighter `
78
+ + 'target or drop `hash`.');
79
+ }
58
80
  }
59
- const r = session.requests.get(requestId);
60
- if (!r) {
61
- throw new Error(`no request ${requestId}`);
81
+ const captured = backend.capture.requests.get(requestId);
82
+ if (!captured) {
83
+ throw new Error(`no captured request ${requestId}`);
62
84
  }
63
- const body = session.bodies.get(requestId);
64
- if (!body.available) {
65
- throw new Error(`request body unavailable: ${body.reason}`);
85
+ if (!captured.responseBody) {
86
+ throw new Error(`request body unavailable for ${requestId}`);
66
87
  }
67
- const reqWithBody = { ...r, responseBody: body.body };
68
- const { provider, secretRefs } = draftProvider(reqWithBody, target, name, hash);
69
- // Stash real secret values for later replay/proof.
70
- const realSecrets = {};
88
+ const { provider, secretRefs, extractions } = draftProvider(captured, targets, providerName, defaultHash);
89
+ // Materialize the real secret header values for later replay/prove;
90
+ // they stay in the server-side draft, never in this result.
91
+ const secrets = {};
71
92
  for (const ref of secretRefs) {
72
- const real = r.requestHeaders[ref.name];
93
+ const real = captured.requestHeaders[ref.name];
73
94
  if (real !== undefined) {
74
- realSecrets[ref.name] = real;
95
+ secrets[ref.name] = real;
75
96
  }
76
97
  }
77
- const secretParamsRef = secrets.put(realSecrets);
98
+ const draftId = backend.drafts.put({
99
+ provider, secretRefs, secrets, requestId, extractions,
100
+ });
78
101
  const warnings = verificationWarnings(provider);
79
102
  return {
103
+ draftId,
80
104
  provider,
81
105
  secretRefs,
82
- secretParamsRef,
83
106
  ...(warnings.length ? { verificationWarnings: warnings } : {}),
84
107
  };
85
108
  });
@@ -89,7 +89,7 @@ const ALLOWED_JS_REQUEST_SCHEMA = {
89
89
  export function createVersionFromCaptureTool(client) {
90
90
  return defineTool({
91
91
  name: 'create_provider_version_from_capture',
92
- description: 'Persist drafted ReclaimProvider(s) as a verification version. ' +
92
+ description: 'Persist drafted ReclaimProvider(s) as a provider version. ' +
93
93
  'Call this after run_proof succeeds, passing the `provider` ' +
94
94
  'object(s) from propose_provider VERBATIM. This tool builds the ' +
95
95
  'body (the requests[] array, webSettings, etc.) for you — do NOT ' +
@@ -120,7 +120,7 @@ export function createVersionFromCaptureTool(client) {
120
120
  properties: {
121
121
  providerId: {
122
122
  type: 'string',
123
- description: 'UUID of the verification to publish the version on.',
123
+ description: 'UUID of the provider to publish the version on.',
124
124
  },
125
125
  provider: PROVIDER_SCHEMA,
126
126
  providers: {
@@ -1,3 +1,10 @@
1
- import { type RegisteredTool } from '../../server.ts';
2
- import type { SecretStore } from './propose.ts';
3
- export declare function replayTool(secrets: SecretStore): RegisteredTool;
1
+ /**
2
+ * `replay_request` — validate a drafted recipe before proving: confirm its
3
+ * selectors extract the target(s), and (with `withoutSecrets`) that the
4
+ * endpoint is auth-bound. Authored once as a plain {@link RegisteredTool}; the
5
+ * backend does the actual replay (MCP re-runs locally, cloud re-issues in the
6
+ * TEE pod).
7
+ */
8
+ import { type ResolveBackend } from '../../../authoring/util.ts';
9
+ import { type RegisteredTool } from '../../../tools/registry.ts';
10
+ export declare function replayTool(resolveBackend: ResolveBackend): RegisteredTool;
@@ -1,41 +1,34 @@
1
- import { replayProvider } from "../../../proof/replay.js";
2
- import { defineTool } from "../../server.js";
3
- export function replayTool(secrets) {
1
+ /**
2
+ * `replay_request` — validate a drafted recipe before proving: confirm its
3
+ * selectors extract the target(s), and (with `withoutSecrets`) that the
4
+ * endpoint is auth-bound. Authored once as a plain {@link RegisteredTool}; the
5
+ * backend does the actual replay (MCP re-runs locally, cloud re-issues in the
6
+ * TEE pod).
7
+ */
8
+ import { objectSchema } from "../../../authoring/util.js";
9
+ import { defineTool } from "../../../tools/registry.js";
10
+ export function replayTool(resolveBackend) {
4
11
  return defineTool({
5
12
  name: 'replay_request',
6
- description: 'Replay a drafted provider against the live endpoint ' +
7
- 'using Node fetch (no attestor). ' +
8
- 'Pre-flight gate before run_proof — ' +
9
- 'diagnoses CSRF / token-rotation / matcher-too-tight ' +
10
- 'issues with richer hints than the attestor would produce.',
11
- inputSchema: {
12
- type: 'object',
13
- properties: {
14
- provider: { type: 'object', properties: {} },
15
- secretParamsRef: { type: 'string' },
16
- },
17
- required: ['provider'],
18
- },
19
- }, async ({ provider, secretParamsRef }) => {
20
- let resolved = {};
21
- if (secretParamsRef !== undefined) {
22
- const r = secrets.resolve(secretParamsRef);
23
- if (!r) {
24
- throw new Error(`unknown secretParamsRef "${secretParamsRef}" — ` +
25
- 'the MCP server has restarted or reset_session was called. ' +
26
- 'Re-run propose_provider to mint a fresh ref.');
27
- }
28
- resolved = r;
13
+ description: 'Validate the drafted recipe before proving — confirm its selectors '
14
+ + 'extract the target value. Returns diagnostics; run before '
15
+ + 'proving. Set `withoutSecrets: true` for the AUTH-BOUND check: it '
16
+ + 're-issues the request with NO secrets/cookies (an anonymous '
17
+ + 'visitor). If the values still extract (valid:true), the endpoint '
18
+ + 'is PUBLIC and the proof would not bind to the user — reject it '
19
+ + 'and find an auth-bound request instead.',
20
+ inputSchema: objectSchema({
21
+ draftId: { type: 'string' },
22
+ withoutSecrets: { type: 'boolean' },
23
+ }, ['draftId']),
24
+ }, async (args) => {
25
+ const backend = resolveBackend(args);
26
+ const draft = backend.drafts.get(String(args.draftId));
27
+ if (!draft) {
28
+ throw new Error(`no draft ${String(args.draftId)}`);
29
29
  }
30
- const result = await replayProvider(provider, resolved);
31
- const out = {
32
- status: result.status,
33
- matchesOriginal: result.matchesOriginal,
34
- bodySnippet: result.body.slice(0, 256),
35
- };
36
- if (result.hints !== undefined) {
37
- out.hints = result.hints;
38
- }
39
- return out;
30
+ return backend.replay(draft, {
31
+ withoutSecrets: Boolean(args.withoutSecrets),
32
+ });
40
33
  });
41
34
  }
@@ -5,6 +5,4 @@ import type { CaptureStore } from './capture.ts';
5
5
  export declare function sessionTools(attachRef: {
6
6
  current?: AttachState;
7
7
  config: ConfigStore;
8
- }, captures: CaptureStore, secrets: {
9
- clear: () => void;
10
- }): RegisteredTool[];
8
+ }, captures: CaptureStore): RegisteredTool[];
@@ -1,5 +1,5 @@
1
1
  import { defineTool } from "../../server.js";
2
- export function sessionTools(attachRef, captures, secrets) {
2
+ export function sessionTools(attachRef, captures) {
3
3
  return [
4
4
  defineTool({
5
5
  name: 'get_session',
@@ -14,15 +14,15 @@ export function sessionTools(attachRef, captures, secrets) {
14
14
  })),
15
15
  defineTool({
16
16
  name: 'reset_session',
17
- description: 'Stop all captures, drop in-memory secrets, and clear capture state.',
17
+ description: 'Stop all captures and clear capture state — drops every '
18
+ + 'session\'s drafts and their in-memory secrets.',
18
19
  inputSchema: { type: 'object', properties: {} },
19
20
  }, async () => {
20
- for (const s of captures.sessions.values()) {
21
- await s.stop().catch(() => { });
21
+ for (const entry of captures.sessions.values()) {
22
+ await entry.session.stop().catch(() => { });
22
23
  }
23
24
  captures.sessions.clear();
24
25
  delete captures.activeTab;
25
- secrets.clear();
26
26
  return { reset: true };
27
27
  }),
28
28
  ];
@@ -0,0 +1,8 @@
1
+ import type { RegisteredTool } from '../../server.ts';
2
+ import type { CaptureStore } from './capture.ts';
3
+ /** The shared authoring tools mapped to the MCP's shape: resolve each call's
4
+ * local backend from `args.captureId`, and widen the input schema — every tool
5
+ * takes `captureId`; `run_proof` also gets the credential-aware contract. The
6
+ * tools are authored plain in `../../../authoring`; this only wraps them with
7
+ * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
8
+ export declare function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[];
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Build the shared authoring tools for the MCP, bound to a per-capture LOCAL
3
+ * backend: a Chrome tab over CDP + the in-process attestor SDK. The tool logic
4
+ * (navigate/inspect/propose/replay/prove) is authored once in `../../../
5
+ * authoring` with the shared `defineTool` primitive; this only supplies the
6
+ * MCP host — resolve the backend from `captureId` (the session handle from
7
+ * `start_capture`, since one capture bundles the tab, its requests, and its
8
+ * drafts) and the rich, credential-aware `run_proof` schema.
9
+ */
10
+ import { authoringTools, extendTool, } from "../../../authoring/index.js";
11
+ import { runAttestorProof } from "../../../proof/attestor.js";
12
+ import { replayProvider } from "../../../proof/replay.js";
13
+ import { shapeRichProofResult } from "./proof.js";
14
+ /** The local backend for one capture: drive its tab over CDP, read its
15
+ * captured requests, prove via the in-process attestor, replay by re-fetch. */
16
+ function localBackend(entry) {
17
+ const { tabClient, session, drafts } = entry;
18
+ // The per-tab CDP client is strongly typed to `keyof Commands`; the shared
19
+ // tools drive it with dynamic method/event names, so view it as string-keyed.
20
+ const client = tabClient;
21
+ return {
22
+ capture: session,
23
+ drafts,
24
+ sendCdp: (method, params) => client.send(method, params ?? {}),
25
+ waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
26
+ const onEvent = () => {
27
+ cleanup();
28
+ resolve('fired');
29
+ };
30
+ const timer = setTimeout(() => {
31
+ cleanup();
32
+ resolve('timeout');
33
+ }, timeoutMs);
34
+ const cleanup = () => {
35
+ clearTimeout(timer);
36
+ client.removeListener(method, onEvent);
37
+ };
38
+ client.once(method, onEvent);
39
+ }),
40
+ replay: async (draft, opts) => {
41
+ // Auth-bound check: re-fetch with the secrets dropped. replayProvider
42
+ // already does a live Node fetch, so empty secrets == anonymous.
43
+ const secrets = opts?.withoutSecrets ? {} : draft.secrets;
44
+ const result = await replayProvider(draft.provider, secrets);
45
+ return {
46
+ status: result.status,
47
+ matchesOriginal: result.matchesOriginal,
48
+ bodySnippet: result.body.slice(0, 256),
49
+ ...(result.hints !== undefined ? { hints: result.hints } : {}),
50
+ };
51
+ },
52
+ prove: async (draft, opts) => {
53
+ const result = await runAttestorProof(draft.provider, draft.secrets, {
54
+ ...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
55
+ ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
56
+ });
57
+ return shapeRichProofResult(result, {
58
+ provider: draft.provider,
59
+ hasSecrets: Object.keys(draft.secrets).length > 0,
60
+ ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
61
+ });
62
+ },
63
+ };
64
+ }
65
+ /** The MCP's rich, credential-aware `run_proof` schema, merged onto the shared
66
+ * tool by the host below (the cloud's TEE signs in-enclave, so it needs none
67
+ * of this). */
68
+ const RUN_PROOF_EXTRAS = {
69
+ description: 'Run a full zkTLS proof of the drafted recipe through the Reclaim '
70
+ + 'attestor SDK. Use only after replay_request looks right — attestor '
71
+ + 'calls are expensive. Writes the full claim to '
72
+ + '~/.reclaim/claims/<timestamp>.json and returns identifier + '
73
+ + 'extractedParameters + claimPath + a prettified `claim` (request '
74
+ + 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
75
+ + 'writes a per-call diagnostic dump (input provider, sentParams, full '
76
+ + 'attestor log stream, result) and returns it as `runLogPath`. '
77
+ + '`attestorUrl` overrides the default only when the dev supplies a '
78
+ + 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
79
+ + '(the credential to sign with) is REQUIRED — call resolve_owner_key '
80
+ + 'first. errorKind="missing-owner-key" means that address has no known '
81
+ + 'private key; re-run resolve_owner_key or issue/import a credential.',
82
+ properties: {
83
+ ownerAddress: {
84
+ type: 'string',
85
+ pattern: '^0x[0-9a-fA-F]{40}$',
86
+ description: 'ETH address of the credential to sign the claim with. Get it '
87
+ + 'from resolve_owner_key first. The private key is resolved '
88
+ + 'locally (cache / env / .env) — never passed here.',
89
+ },
90
+ attestorUrl: { type: 'string' },
91
+ },
92
+ required: ['ownerAddress'],
93
+ };
94
+ /** The shared authoring tools mapped to the MCP's shape: resolve each call's
95
+ * local backend from `args.captureId`, and widen the input schema — every tool
96
+ * takes `captureId`; `run_proof` also gets the credential-aware contract. The
97
+ * tools are authored plain in `../../../authoring`; this only wraps them with
98
+ * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
99
+ export function sharedAuthoringTools(captures) {
100
+ const resolveBackend = (args) => {
101
+ const captureId = String(args.captureId);
102
+ const entry = captures.sessions.get(captureId);
103
+ if (!entry) {
104
+ throw new Error(`no capture ${captureId} — call start_capture first `
105
+ + '(it has been reset or never started).');
106
+ }
107
+ return localBackend(entry);
108
+ };
109
+ return authoringTools(resolveBackend).map((tool) => {
110
+ if (tool.definition.name === 'run_proof') {
111
+ return extendTool(tool, {
112
+ description: RUN_PROOF_EXTRAS.description,
113
+ properties: {
114
+ captureId: { type: 'string' },
115
+ ...RUN_PROOF_EXTRAS.properties,
116
+ },
117
+ required: ['captureId', ...RUN_PROOF_EXTRAS.required],
118
+ });
119
+ }
120
+ return extendTool(tool, {
121
+ properties: { captureId: { type: 'string' } },
122
+ required: ['captureId'],
123
+ });
124
+ });
125
+ }
@@ -32,12 +32,36 @@ export interface RegisterProviderBody {
32
32
  useProxy?: boolean;
33
33
  providerType?: 'PRIVATE' | 'PUBLIC';
34
34
  requestData: unknown[];
35
+ /** Page-injection script (old-devtools name for the builder's
36
+ * `webSettings.jsUserScripts`). Top-level on register; nested under
37
+ * `providerConfig` on the config (add-version) body instead. */
38
+ customInjection?: string;
39
+ /** Accepted at register too (unlike most `providerConfig` extras, which
40
+ * the register controller only reads on config/add-version). */
41
+ userAgent?: {
42
+ ios?: string;
43
+ android?: string;
44
+ };
45
+ /** Accepted at register too. */
46
+ pageTitle?: string;
35
47
  }
36
48
  export interface UpdateProviderConfigBody {
37
49
  providerConfig: Record<string, unknown>;
38
50
  version: string;
39
51
  versionInfo: string;
40
52
  }
53
+ /** Body for `POST /api/providers/:providerId` (`updateProviderMetadata`) —
54
+ * a genuine PARTIAL update on the PROVIDER document itself (name/
55
+ * description/providerType/tags/isActive), distinct from both `register`
56
+ * (creates the provider) and `config` (adds an immutable new version). Only
57
+ * the fields present get changed; everything else is left untouched. */
58
+ export interface UpdateProviderMetadataBody {
59
+ name?: string;
60
+ description?: string;
61
+ providerType?: 'PRIVATE' | 'PUBLIC';
62
+ tags?: string[];
63
+ isActive?: boolean;
64
+ }
41
65
  export interface MyProvidersQuery {
42
66
  pageKey?: number;
43
67
  pageSize?: number;
@@ -71,6 +95,17 @@ export declare class ReclaimOldClient {
71
95
  registerProvider(body: RegisterProviderBody): Promise<unknown>;
72
96
  updateProviderConfig(providerId: string, body: UpdateProviderConfigBody): Promise<unknown>;
73
97
  listVersions(providerId: string): Promise<unknown>;
98
+ /** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
99
+ * one version — the latest when `version` is omitted. Readable for ANY
100
+ * non-archived provider (not owner-restricted), same as the dashboard's
101
+ * edit-form prefill fetch. */
102
+ getProviderConfig(providerId: string, version?: string): Promise<unknown>;
103
+ /** Partial update on the PROVIDER document itself — name/description/
104
+ * providerType/tags/isActive, after creation. The backend 403s if you
105
+ * don't own the provider (and aren't admin); other providerConfig-style
106
+ * fields sent here are simply ignored (this endpoint only reads the
107
+ * ones above from the body). */
108
+ updateProviderMetadata(providerId: string, body: UpdateProviderMetadataBody): Promise<unknown>;
74
109
  /** Analytics logs for a verification session, from the logs service (a
75
110
  * DIFFERENT host than the devtools API — hence its own fetch, not
76
111
  * `#request`). Attaches the caller identity when one is set; queried
package/lib/old/client.js CHANGED
@@ -49,6 +49,24 @@ export class ReclaimOldClient {
49
49
  listVersions(providerId) {
50
50
  return this.#request('GET', `/providers/${encodeURIComponent(providerId)}/versions`, { auth: true });
51
51
  }
52
+ /** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
53
+ * one version — the latest when `version` is omitted. Readable for ANY
54
+ * non-archived provider (not owner-restricted), same as the dashboard's
55
+ * edit-form prefill fetch. */
56
+ getProviderConfig(providerId, version) {
57
+ return this.#request('GET', `/providers/${encodeURIComponent(providerId)}/config`, {
58
+ auth: true,
59
+ query: version ? { versionNumber: version } : undefined,
60
+ });
61
+ }
62
+ /** Partial update on the PROVIDER document itself — name/description/
63
+ * providerType/tags/isActive, after creation. The backend 403s if you
64
+ * don't own the provider (and aren't admin); other providerConfig-style
65
+ * fields sent here are simply ignored (this endpoint only reads the
66
+ * ones above from the body). */
67
+ updateProviderMetadata(providerId, body) {
68
+ return this.#request('POST', `/providers/${encodeURIComponent(providerId)}`, { body, auth: true });
69
+ }
52
70
  /** Analytics logs for a verification session, from the logs service (a
53
71
  * DIFFERENT host than the devtools API — hence its own fetch, not
54
72
  * `#request`). Attaches the caller identity when one is set; queried
@@ -1,3 +1,27 @@
1
+ /**
2
+ * Read the Firebase ID token the dashboard persisted after login.
3
+ *
4
+ * The frontend uses `browserLocalPersistence`, which the Firebase JS SDK
5
+ * implements on `localStorage` under a `firebase:authUser:<apiKey>:[DEFAULT]`
6
+ * key whose JSON value carries `stsTokenManager.accessToken`. We scan for any
7
+ * such key (so we never hard-code the project apiKey) and fall back to the
8
+ * IndexedDB persistence layout (`firebaseLocalStorageDb` /
9
+ * `firebaseLocalStorage`) in case a build switches persistence. Returns the
10
+ * token string, or null when the user has not finished logging in yet.
11
+ *
12
+ * The CDP-launched Chrome reuses a PERSISTENT profile dir across runs, so a
13
+ * stale `firebase:authUser:*` entry from an earlier (now-expired, ~1h TTL)
14
+ * session is often still sitting in storage the instant the page loads —
15
+ * before the developer has clicked anything. Returning that immediately
16
+ * makes the poll "succeed" on tick one, closing the tab before the developer
17
+ * can sign in, and the captured token then fails backend verification. So
18
+ * every candidate's JWT `exp` claim is checked here (in-page, where `atob` is
19
+ * available) and stale ones are skipped — the loop keeps polling until a
20
+ * genuinely fresh token shows up from an interactive sign-in.
21
+ *
22
+ * Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
23
+ */
24
+ export declare const EXTRACT_TOKEN_JS = "(async () => {\n\tconst isFresh = (token) => {\n\t\ttry {\n\t\t\tconst payload = JSON.parse(\n\t\t\t\tatob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))\n\t\t\t)\n\t\t\t// No exp claim to check \u2014 don't block a token we can't evaluate.\n\t\t\tif (typeof payload.exp !== 'number') return true\n\t\t\t// 60s buffer so a token that's about to expire isn't treated as fresh.\n\t\t\treturn payload.exp * 1000 > Date.now() + 60000\n\t\t} catch (e) {\n\t\t\treturn true\n\t\t}\n\t}\n\ttry {\n\t\tfor (let i = 0; i < localStorage.length; i++) {\n\t\t\tconst k = localStorage.key(i)\n\t\t\tif (k && k.indexOf('firebase:authUser:') === 0) {\n\t\t\t\tconst v = JSON.parse(localStorage.getItem(k) || 'null')\n\t\t\t\tconst t = v && v.stsTokenManager && v.stsTokenManager.accessToken\n\t\t\t\tif (t && isFresh(t)) return t\n\t\t\t}\n\t\t}\n\t} catch (e) {}\n\ttry {\n\t\treturn await new Promise((resolve) => {\n\t\t\tconst req = indexedDB.open('firebaseLocalStorageDb')\n\t\t\treq.onerror = () => resolve(null)\n\t\t\treq.onsuccess = () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst db = req.result\n\t\t\t\t\tif (!db.objectStoreNames.contains('firebaseLocalStorage')) {\n\t\t\t\t\t\tresolve(null); return\n\t\t\t\t\t}\n\t\t\t\t\tconst store = db\n\t\t\t\t\t\t.transaction('firebaseLocalStorage', 'readonly')\n\t\t\t\t\t\t.objectStore('firebaseLocalStorage')\n\t\t\t\t\tconst all = store.getAll()\n\t\t\t\t\tall.onerror = () => resolve(null)\n\t\t\t\t\tall.onsuccess = () => {\n\t\t\t\t\t\tfor (const row of (all.result || [])) {\n\t\t\t\t\t\t\tconst m = row && row.value && row.value.stsTokenManager\n\t\t\t\t\t\t\tif (m && m.accessToken && isFresh(m.accessToken)) {\n\t\t\t\t\t\t\t\tresolve(m.accessToken); return\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve(null)\n\t\t\t\t\t}\n\t\t\t\t} catch (e) { resolve(null) }\n\t\t\t}\n\t\t})\n\t} catch (e) { return null }\n})()";
1
25
  export interface CdpLoginResult {
2
26
  token: string;
3
27
  /** True when we launched (and then closed) a dedicated Chrome just for
@@ -13,16 +13,39 @@ import { DEFAULT_LOGIN_URL, oldLoginUrl } from "../consts.js";
13
13
  * `firebaseLocalStorage`) in case a build switches persistence. Returns the
14
14
  * token string, or null when the user has not finished logging in yet.
15
15
  *
16
+ * The CDP-launched Chrome reuses a PERSISTENT profile dir across runs, so a
17
+ * stale `firebase:authUser:*` entry from an earlier (now-expired, ~1h TTL)
18
+ * session is often still sitting in storage the instant the page loads —
19
+ * before the developer has clicked anything. Returning that immediately
20
+ * makes the poll "succeed" on tick one, closing the tab before the developer
21
+ * can sign in, and the captured token then fails backend verification. So
22
+ * every candidate's JWT `exp` claim is checked here (in-page, where `atob` is
23
+ * available) and stale ones are skipped — the loop keeps polling until a
24
+ * genuinely fresh token shows up from an interactive sign-in.
25
+ *
16
26
  * Evaluated in-page with `awaitPromise` so the async IndexedDB branch resolves.
17
27
  */
18
- const EXTRACT_TOKEN_JS = `(async () => {
28
+ export const EXTRACT_TOKEN_JS = `(async () => {
29
+ const isFresh = (token) => {
30
+ try {
31
+ const payload = JSON.parse(
32
+ atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
33
+ )
34
+ // No exp claim to check — don't block a token we can't evaluate.
35
+ if (typeof payload.exp !== 'number') return true
36
+ // 60s buffer so a token that's about to expire isn't treated as fresh.
37
+ return payload.exp * 1000 > Date.now() + 60000
38
+ } catch (e) {
39
+ return true
40
+ }
41
+ }
19
42
  try {
20
43
  for (let i = 0; i < localStorage.length; i++) {
21
44
  const k = localStorage.key(i)
22
45
  if (k && k.indexOf('firebase:authUser:') === 0) {
23
46
  const v = JSON.parse(localStorage.getItem(k) || 'null')
24
47
  const t = v && v.stsTokenManager && v.stsTokenManager.accessToken
25
- if (t) return t
48
+ if (t && isFresh(t)) return t
26
49
  }
27
50
  }
28
51
  } catch (e) {}
@@ -44,7 +67,9 @@ const EXTRACT_TOKEN_JS = `(async () => {
44
67
  all.onsuccess = () => {
45
68
  for (const row of (all.result || [])) {
46
69
  const m = row && row.value && row.value.stsTokenManager
47
- if (m && m.accessToken) { resolve(m.accessToken); return }
70
+ if (m && m.accessToken && isFresh(m.accessToken)) {
71
+ resolve(m.accessToken); return
72
+ }
48
73
  }
49
74
  resolve(null)
50
75
  }