@reclaimprotocol/agent 0.1.26 → 0.1.28

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 (92) hide show
  1. package/README.md +5 -5
  2. package/lib/authoring/tools.js +2 -1
  3. package/lib/consts.d.ts +1 -1
  4. package/lib/consts.js +1 -1
  5. package/lib/live/tunnel.d.ts +10 -7
  6. package/lib/live/tunnel.js +78 -56
  7. package/lib/mcp/api/operations.gen.d.ts +12 -8
  8. package/lib/mcp/api/operations.gen.js +378 -580
  9. package/lib/mcp/api/tools.d.ts +1 -1
  10. package/lib/mcp/instructions.js +7 -3
  11. package/lib/mcp/remap_errors.js +9 -5
  12. package/lib/mcp/start.js +1 -1
  13. package/lib/mcp/tools/agent/attach.d.ts +2 -2
  14. package/lib/mcp/tools/agent/attach.js +4 -0
  15. package/lib/mcp/tools/agent/build.d.ts +1 -1
  16. package/lib/mcp/tools/agent/live-view-url.d.ts +2 -0
  17. package/lib/mcp/tools/agent/live-view-url.js +11 -0
  18. package/lib/mcp/tools/agent/live-view.d.ts +4 -2
  19. package/lib/mcp/tools/agent/live-view.js +40 -54
  20. package/lib/mcp/tools/agent/propose.js +10 -4
  21. package/lib/mcp/tools/agent/publish.d.ts +1 -1
  22. package/lib/mcp/tools/agent/publish.js +49 -26
  23. package/lib/mcp/tools/agent/shared.js +0 -9
  24. package/lib/mcp/tools/authenticate/authenticate.d.ts +1 -1
  25. package/lib/mcp/tools/authenticate/build.d.ts +1 -1
  26. package/lib/mcp/tools/authenticate/builder-access.d.ts +1 -1
  27. package/lib/mcp/tools/verification/result.js +12 -13
  28. package/lib/old/login-cdp.d.ts +17 -6
  29. package/lib/old/login-cdp.js +28 -11
  30. package/lib/old/to-register.d.ts +6 -0
  31. package/lib/old/to-register.js +35 -5
  32. package/lib/old/tools/authenticate.d.ts +5 -4
  33. package/lib/old/tools/authenticate.js +9 -5
  34. package/lib/old/tools/publish.d.ts +2 -2
  35. package/lib/old/tools/publish.js +47 -21
  36. package/lib/proof/attestor-auth.d.ts +13 -0
  37. package/lib/proof/attestor-auth.js +16 -0
  38. package/lib/proof/attestor.d.ts +7 -4
  39. package/lib/proof/attestor.js +7 -4
  40. package/lib/proof/index.d.ts +1 -0
  41. package/lib/proof/index.js +1 -0
  42. package/lib/proof/pod-prover.d.ts +2 -0
  43. package/lib/proof/pod-prover.js +5 -5
  44. package/lib/provider/injection-doc.d.ts +2 -0
  45. package/lib/provider/injection-doc.js +7 -1
  46. package/lib/provider/post-publish-browser.d.ts +12 -0
  47. package/lib/provider/post-publish-browser.js +40 -0
  48. package/lib/provider/to-version.d.ts +19 -7
  49. package/lib/provider/to-version.js +67 -25
  50. package/lib/tools/registry.d.ts +1 -1
  51. package/lib/tools/registry.js +7 -4
  52. package/package.json +2 -2
  53. package/skill/SKILL.builder.md +134 -13
  54. package/skill/SKILL.common.md +30 -19
  55. package/skill/SKILL.old.md +22 -12
  56. package/skill/builder-mode.md +19 -9
  57. package/skill/community-contributions.md +108 -0
  58. package/skill/hash-validation.md +8 -5
  59. package/skill/interception.md +35 -22
  60. package/skill/old-mode.md +11 -5
  61. package/skill/user-script.md +41 -10
  62. package/src/authoring/tools.ts +2 -1
  63. package/src/consts.ts +1 -2
  64. package/src/live/tunnel.ts +112 -66
  65. package/src/mcp/api/operations.gen.ts +384 -584
  66. package/src/mcp/api/tools.ts +1 -1
  67. package/src/mcp/instructions.ts +7 -3
  68. package/src/mcp/remap_errors.ts +9 -7
  69. package/src/mcp/start.ts +1 -1
  70. package/src/mcp/tools/agent/attach.ts +6 -2
  71. package/src/mcp/tools/agent/build.ts +1 -1
  72. package/src/mcp/tools/agent/live-view-url.ts +13 -0
  73. package/src/mcp/tools/agent/live-view.ts +42 -54
  74. package/src/mcp/tools/agent/propose.ts +10 -4
  75. package/src/mcp/tools/agent/publish.ts +63 -28
  76. package/src/mcp/tools/agent/shared.ts +5 -0
  77. package/src/mcp/tools/authenticate/authenticate.ts +1 -1
  78. package/src/mcp/tools/authenticate/build.ts +1 -1
  79. package/src/mcp/tools/authenticate/builder-access.ts +1 -1
  80. package/src/mcp/tools/verification/result.ts +11 -13
  81. package/src/old/login-cdp.ts +38 -11
  82. package/src/old/to-register.ts +56 -7
  83. package/src/old/tools/authenticate.ts +9 -5
  84. package/src/old/tools/publish.ts +64 -22
  85. package/src/proof/attestor-auth.ts +33 -0
  86. package/src/proof/attestor.ts +14 -8
  87. package/src/proof/index.ts +4 -0
  88. package/src/proof/pod-prover.ts +7 -5
  89. package/src/provider/injection-doc.ts +9 -1
  90. package/src/provider/post-publish-browser.ts +56 -0
  91. package/src/provider/to-version.ts +104 -27
  92. package/src/tools/registry.ts +12 -9
@@ -1,4 +1,4 @@
1
- import type { ReclaimClient } from '@reclaimprotocol/client';
1
+ import type { ReclaimClient } from '@reclaimprotocol/client/api';
2
2
  import type { CreateProviderVersionRequest, ProviderVersionRef } from '@reclaimprotocol/client/openapi';
3
3
  import type { DraftResponseMatch, HttpMethod, OldUrlType, ReclaimProvider, ResponseRedaction, WebCredentials } from './schema.ts';
4
4
  /** One entry of the version's `requests[]` (a `RequestSelection`), derived
@@ -50,6 +50,9 @@ export interface AllowedJsRequest extends DraftRequest {
50
50
  export interface CreateVersionOptions {
51
51
  notes?: string;
52
52
  version?: ProviderVersionRef;
53
+ /** Complete context schema for this new immutable version. When omitted,
54
+ * it is derived from `{{context.<name>}}` placeholders. */
55
+ requiredContext?: CreateProviderVersionRequest['requiredContext'];
53
56
  /** JS injected before every page load (`webSettings.jsUserScripts`). */
54
57
  jsUserScripts?: string;
55
58
  /** Requests the injection script may fire, as `RequestSelectionTemplate`s. */
@@ -74,7 +77,8 @@ export interface CreateVersionOptions {
74
77
  * injected requests and the injection script) — mechanical, so a published
75
78
  * recipe can never reference a context value the Builder doesn't require.
76
79
  * Returns undefined when nothing references context (the field is optional).
77
- * Authors can refine types/descriptions in the version editor.
80
+ * Authors can pass a complete schema when creating the new version to refine
81
+ * types or descriptions. Never edit a version in place after creation.
78
82
  *
79
83
  * A SECRET-named context param (see `classifyParamName`) is NOT rejected here
80
84
  * even when it's templated into the URL/geoLocation — write-redaction can
@@ -102,10 +106,18 @@ export declare function deriveRequiredContext(providers: ReclaimProvider[], inje
102
106
  export declare function providerToRequestSelection(provider: DraftRequest): RequestSelection;
103
107
  export declare function providerToCreateVersionRequest(providers: ReclaimProvider[], initialUrl: string, opts?: CreateVersionOptions): CreateProviderVersionRequest;
104
108
  /**
105
- * Fetch the current versions for a provider and return the next version above
106
- * the highest existing one (bumping the patch number), or 1.0.0 when the
107
- * provider has no versions yet. A failed lookup propagates — silently
108
- * resetting to 1.0.0 would re-publish a v3 provider as v1.0.0.
109
+ * Fetch every current version and resolve a strictly newer immutable version.
110
+ * The default bump is patch. Callers can request major/minor/patch or provide
111
+ * an exact version; an exact version must be higher than every existing one.
112
+ * A failed lookup propagates — silently resetting to 1.0.0 could collide with
113
+ * an existing version.
109
114
  */
110
- export declare function nextVersion(client: ReclaimClient, providerId: string): Promise<ProviderVersionRef>;
115
+ export declare function nextVersion(client: ReclaimClient, providerId: string, bump?: VersionBump): Promise<ProviderVersionRef>;
116
+ export type VersionBump = 'major' | 'minor' | 'patch';
117
+ /** Resolve either an explicit version or a requested semantic bump. */
118
+ export declare function resolveNewVersion(client: ReclaimClient, providerId: string, input?: {
119
+ version?: ProviderVersionRef;
120
+ bump?: VersionBump;
121
+ }): Promise<ProviderVersionRef>;
122
+ export declare function bumpVersion(versions: ProviderVersionRef[], bump?: VersionBump): ProviderVersionRef;
111
123
  export {};
@@ -1,3 +1,5 @@
1
+ import { badRequest, conflict } from '@hapi/boom';
2
+ import assert from 'node:assert';
1
3
  import { DYNAMIC_GEO, normalizeGeoLocation } from "./schema.js";
2
4
  /**
3
5
  * Translate a drafted ReclaimProvider into the body for
@@ -21,7 +23,8 @@ const CONTEXT_PARAM_RE = /\{\{context\.([A-Za-z_][A-Za-z0-9_]*)\}\}/g;
21
23
  * injected requests and the injection script) — mechanical, so a published
22
24
  * recipe can never reference a context value the Builder doesn't require.
23
25
  * Returns undefined when nothing references context (the field is optional).
24
- * Authors can refine types/descriptions in the version editor.
26
+ * Authors can pass a complete schema when creating the new version to refine
27
+ * types or descriptions. Never edit a version in place after creation.
25
28
  *
26
29
  * A SECRET-named context param (see `classifyParamName`) is NOT rejected here
27
30
  * even when it's templated into the URL/geoLocation — write-redaction can
@@ -101,11 +104,8 @@ function pickGeoLocation(providers) {
101
104
  return DYNAMIC_GEO;
102
105
  }
103
106
  export function providerToCreateVersionRequest(providers, initialUrl, opts = {}) {
104
- const { notes, version, jsUserScripts, allowedJsRequests, interceptorOptions, } = opts;
105
- const requiredContext = deriveRequiredContext(providers, {
106
- allowedJsRequests,
107
- jsUserScripts,
108
- });
107
+ const { notes, version, requiredContext: requestedContext, jsUserScripts, allowedJsRequests, interceptorOptions, } = opts;
108
+ const requiredContext = requestedContext ?? deriveRequiredContext(providers, { allowedJsRequests, jsUserScripts });
109
109
  // Each injected request carries its template flags (default true/true per
110
110
  // RequestSelectionTemplate) on top of the shared request shape.
111
111
  const allowedJs = (allowedJsRequests ?? []).map((req) => ({
@@ -166,28 +166,70 @@ export function providerToCreateVersionRequest(providers, initialUrl, opts = {})
166
166
  };
167
167
  }
168
168
  /**
169
- * Fetch the current versions for a provider and return the next version above
170
- * the highest existing one (bumping the patch number), or 1.0.0 when the
171
- * provider has no versions yet. A failed lookup propagates — silently
172
- * resetting to 1.0.0 would re-publish a v3 provider as v1.0.0.
169
+ * Fetch every current version and resolve a strictly newer immutable version.
170
+ * The default bump is patch. Callers can request major/minor/patch or provide
171
+ * an exact version; an exact version must be higher than every existing one.
172
+ * A failed lookup propagates — silently resetting to 1.0.0 could collide with
173
+ * an existing version.
173
174
  */
174
- export async function nextVersion(client, providerId) {
175
- const list = (await client.call('ListProviderVersions', {
176
- params: { providerId },
177
- query: {},
178
- }));
179
- if (!list.items?.length) {
175
+ export async function nextVersion(client, providerId, bump = 'patch') {
176
+ return bumpVersion(await listVersions(client, providerId), bump);
177
+ }
178
+ /** Resolve either an explicit version or a requested semantic bump. */
179
+ export async function resolveNewVersion(client, providerId, input = {}) {
180
+ assert(!(input.version && input.bump), badRequest('Pass either `version` or `bump`, not both.'));
181
+ const versions = await listVersions(client, providerId);
182
+ if (!input.version) {
183
+ return bumpVersion(versions, input.bump ?? 'patch');
184
+ }
185
+ const highest = highestVersion(versions);
186
+ assert(!highest || compareVersions(input.version, highest) > 0, conflict(`Version ${formatVersion(input.version)} must be higher than the ` +
187
+ `current highest version ${highest ? formatVersion(highest) : 'none'}.`));
188
+ return input.version;
189
+ }
190
+ export function bumpVersion(versions, bump = 'patch') {
191
+ const highest = highestVersion(versions);
192
+ if (!highest) {
180
193
  return { major: 1, minor: 0, patch: 0 };
181
194
  }
182
- // Find the highest version and bump its patch
183
- let hi = list.items[0].version;
184
- for (const item of list.items) {
185
- const v = item.version;
186
- if (v.major > hi.major ||
187
- (v.major === hi.major && v.minor > hi.minor) ||
188
- (v.major === hi.major && v.minor === hi.minor && v.patch > hi.patch)) {
189
- hi = v;
195
+ switch (bump) {
196
+ case 'major':
197
+ return { major: highest.major + 1, minor: 0, patch: 0 };
198
+ case 'minor':
199
+ return { major: highest.major, minor: highest.minor + 1, patch: 0 };
200
+ case 'patch':
201
+ return {
202
+ major: highest.major,
203
+ minor: highest.minor,
204
+ patch: highest.patch + 1,
205
+ };
206
+ }
207
+ }
208
+ async function listVersions(client, providerId) {
209
+ const versions = [];
210
+ let cursor;
211
+ do {
212
+ const { data } = await client.call('ListProviderVersions', {
213
+ params: { providerId },
214
+ query: { cursor },
215
+ });
216
+ versions.push(...data.items.map((item) => item.version));
217
+ cursor = data.nextPageCursor;
218
+ } while (cursor);
219
+ return versions;
220
+ }
221
+ function highestVersion(versions) {
222
+ let highest;
223
+ for (const version of versions) {
224
+ if (!highest || compareVersions(version, highest) > 0) {
225
+ highest = version;
190
226
  }
191
227
  }
192
- return { major: hi.major, minor: hi.minor, patch: hi.patch + 1 };
228
+ return highest;
229
+ }
230
+ function compareVersions(a, b) {
231
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
232
+ }
233
+ function formatVersion(version) {
234
+ return `${version.major}.${version.minor}.${version.patch}`;
193
235
  }
@@ -38,6 +38,6 @@ export declare function errorAsToolText(err: unknown): string;
38
38
  */
39
39
  export declare function extendTool(tool: RegisteredTool, extra: {
40
40
  description?: string;
41
- properties?: Record<string, unknown>;
41
+ properties?: JSONSchema.ObjectSchema['properties'];
42
42
  required?: string[];
43
43
  }): RegisteredTool;
@@ -12,6 +12,7 @@
12
12
  * re-exports this primitive for the MCP-side tool files.
13
13
  */
14
14
  import { isBoom } from '@hapi/boom';
15
+ import { isProblemDetails, isProblemError } from '@reclaimprotocol/client/api';
15
16
  import { toZod } from "../mcp/api/to-zod.js";
16
17
  /**
17
18
  * Define a tool with a single inputSchema (JSON Schema) as the source of truth.
@@ -35,8 +36,12 @@ export function defineTool(definition, handler) {
35
36
  * else falls back to the plain message.
36
37
  */
37
38
  export function errorAsToolText(err) {
39
+ if (isProblemError(err)) {
40
+ const detail = err.data.detail;
41
+ return detail ? `${err.message}: ${detail}` : err.message;
42
+ }
38
43
  if (isBoom(err) && err.output?.statusCode) {
39
- const detail = err.data?.detail;
44
+ const detail = isProblemDetails(err.data) ? err.data.detail : undefined;
40
45
  return detail ? `${err.message}: ${detail}` : err.message;
41
46
  }
42
47
  return err instanceof Error ? err.message : String(err);
@@ -56,9 +61,7 @@ export function extendTool(tool, extra) {
56
61
  ...(base.inputSchema.required ?? []),
57
62
  ...(extra.required ?? []),
58
63
  ];
59
- // The merged `properties` spread carries the base's untyped property values,
60
- // which don't structurally match ObjectSchema's strict per-property schema
61
- // type — assert the merged shape.
64
+ // Keep the merged schema checked against Zod's JSON Schema representation.
62
65
  const inputSchema = {
63
66
  ...base.inputSchema,
64
67
  properties: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.26",
4
+ "version": "0.1.28",
5
5
  "description": "Reclaim Protocol tools for AI coding agents. Reclaim lets your app verify data points about its users, such as a follower count, an account balance, or an order history. This package is the Reclaim MCP server: it gives your agent the tools to find, build, test, and fix data verification providers by capturing and replaying browser requests, drafting provider recipes, running zkTLS proofs, and publishing provider versions.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -83,7 +83,7 @@
83
83
  "@modelcontextprotocol/sdk": "^1.29.0",
84
84
  "@noble/hashes": "^2.2.0",
85
85
  "@reclaimprotocol/attestor-core": "^5.0.8",
86
- "@reclaimprotocol/client": "0.1.26",
86
+ "@reclaimprotocol/client": "0.1.28",
87
87
  "@reclaimprotocol/tls": "^0.1.3",
88
88
  "chrome-remote-interface": "^0.33.0",
89
89
  "domhandler": "^6.0.0",
@@ -1,6 +1,20 @@
1
1
  <!-- topic: overview -->
2
2
  # Authoring a Reclaim provider (Builder backend)
3
3
 
4
+ For claimant-facing Builder-client integration, result verification, or optional
5
+ TEE binding, read topic `integration`. Keep URLs without `api=2` on the
6
+ existing legacy path.
7
+
8
+ <!-- topic: contributions -->
9
+ ## Public provider contributions
10
+
11
+ For an asynchronous volunteer contribution, read
12
+ [community-contributions.md](community-contributions.md) before using Builder.
13
+ It defines the history-first workflow, approved-field allowlist, fixed-US
14
+ egress, privacy boundaries, generalized recipe checks, and candidate review.
15
+ Never modify a published version in place; create a candidate and submit it
16
+ for review.
17
+
4
18
  <!-- common: terminology -->
5
19
 
6
20
  ## The loop, at a glance
@@ -49,6 +63,22 @@ immediately with a `code` and `attachUrl` — it does NOT poll.
49
63
  The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs
50
64
  (`RECLAIM_API_TOKEN` in the server env overrides it).
51
65
 
66
+ <!-- topic: devtools-migration -->
67
+ ## Import providers from devtools
68
+
69
+ Use these operations only when the developer is migrating an existing legacy
70
+ integration. Prefer Builder for new integrations.
71
+
72
+ ### Import from devtools
73
+
74
+ 1. Get the target `orgId` with `list_orgs`.
75
+ 2. Ask the developer for the legacy provider UUID.
76
+ 3. Call `import_devtools_provider(orgId, providerId)`.
77
+ 4. Use the returned provider UUID for all Builder operations. Import preserves
78
+ the UUID and latest legacy semantic version. It claims an anonymous catalog
79
+ seed idempotently, but never moves a provider from another Builder org. The
80
+ server reads the public devtools API; no legacy token is required.
81
+
52
82
  <!-- topic: browser -->
53
83
  <!-- common: browser -->
54
84
 
@@ -92,7 +122,7 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs
92
122
  under that `draftId`; the secrets never enter the transcript, so you carry the
93
123
  `draftId`, not the provider JSON, into replay/prove.
94
124
 
95
- **Extracting MORE THAN ONE value?** Prefer a **single request whose response
125
+ **Extracting more than one value?** Prefer a **single request whose response
96
126
  contains all of them** and draft them together — one `responseMatches` +
97
127
  `responseRedactions` pair per value. Find that request by running
98
128
  `find_requests_containing` for each value and picking a `requestId` common to
@@ -155,10 +185,10 @@ changes.
155
185
  ## Parameters: extracted, consumer-supplied, and secret
156
186
 
157
187
  **Parameterize user-specific URL segments** so the provider isn't pinned to one
158
- user. The matcher already puts the *captured* value in `paramValues`. If the URL
159
- *also* carries user-identifying segments, those are **consumer-supplied context
160
- params**: at verification time their values come from the consumer's session
161
- `context`, not from extraction.
188
+ user. The matcher already puts the *captured* value in `paramValues`. If the
189
+ consumer knows a URL value before verification and the proof must be pinned to
190
+ it, make it a **consumer-supplied context param**. At verification time, its
191
+ value comes from the consumer's session `context`, not from extraction.
162
192
 
163
193
  Replace them with `{{context.<name>}}`, for example
164
194
  `https://github.com/Scratch-net/repos` →
@@ -171,6 +201,12 @@ placeholder exactly, dot included).
171
201
  - No user-identifying segments (for example, `/settings/profile`) → leave it
172
202
  alone.
173
203
 
204
+ Use a bare `{{name}}` only when the value can be observed in the same live
205
+ request. A multi-request recipe does not bind a value extracted from one
206
+ response to a later request. If proving that relationship is required, use a
207
+ request that exposes both values, require the value as consumer context, or
208
+ stop and explain that the current recipe format cannot prove the dependency.
209
+
174
210
  Declare them in the version's `requiredContext` at publish time (topic
175
211
  `publish`).
176
212
 
@@ -202,6 +238,14 @@ extracted-from-traffic). The attestor itself only looks up `{{whatever}}` in
202
238
  proof should be **pinned** to it; bare when the value should come from
203
239
  whatever the logged-in user's traffic shows.
204
240
 
241
+ Builder-mode Verification Clients include one migration fallback for old
242
+ DevTools providers: when a recipe uses bare `{{name}}` instead of
243
+ `{{context.name}}`, the client seeds a missing `parameters.name` from scalar
244
+ session `context.name`. An explicit parameter wins. Do not rely on this
245
+ fallback for new providers; keep using `{{context.<name>}}` so
246
+ `requiredContext` is derived and validated before the session starts.
247
+ Builder-owned `reclaimSessionId` and `attestationNonce` remain context-only.
248
+
205
249
  `propose_provider` returns `verificationWarnings` for calls only an author can
206
250
  make (currently: a captured value baked literally into the URL, needing the
207
251
  `{{context.<name>}}`-vs-bare-vs-recapture decision). Advisory, never blocking —
@@ -268,13 +312,18 @@ duplicates in "My providers".
268
312
  **2. Persist the version.**
269
313
  `create_provider_version_from_capture(providerId, provider, initialUrl, notes)`
270
314
  — `notes` such as `"Initial draft from browser capture"`. It lands on the
271
- `draft` branch.
315
+ `draft` branch. This always creates a new immutable semantic version. Omit
316
+ `version` and `bump` for the default patch bump; set `bump` to `major`, `minor`,
317
+ or `patch`, or pass the exact higher `version` requested by the user. Never edit
318
+ or archive an existing version to apply provider changes.
272
319
 
273
320
  - **Multi-request provider** (landing call + details call, a
274
321
  CSRF-preflight/refresh-token chain, …): run capture→draft→replay→prove ONCE
275
322
  PER REQUEST, then pass `providers: [draftA, draftB, …]` instead of the single
276
323
  `provider`. Each becomes its own request on the version and **all** are
277
- required at verification time.
324
+ required at verification time. This proves that every request matched; it
325
+ does not automatically bind a value extracted from one response to another
326
+ request.
278
327
  - **Login/navigation-gated data:** supply `jsUserScripts` (plain page JS that
279
328
  drives login/waits/navigation so the target request fires), or
280
329
  `allowedJsRequests` (extra requests the script may fire, for example,
@@ -287,8 +336,9 @@ url/body for `{{context.<name>}}` placeholders and emits a JSON Schema requiring
287
336
  each — the Builder then validates every session-create's `context` against it (a
288
337
  clear 400 instead of a late attestor failure). Extra context fields are always
289
338
  allowed; it's optional when the recipe has no `{{context.*}}` placeholders.
290
- Derived properties are plain strings; refine types or add descriptions with
291
- `update_provider_version` while the version is still on `draft`:
339
+ Derived properties are plain strings. To refine types or add descriptions,
340
+ pass the complete `requiredContext` in the same
341
+ `create_provider_version_from_capture` call:
292
342
 
293
343
  ```json
294
344
  { "requiredContext": { "type": "object", "required": ["username"], "properties": { "username": { "type": "string", "description": "GitHub username to verify" } } } }
@@ -304,10 +354,15 @@ context params — for example, `context: { "username": "…" }` in
304
354
  version verbatim — a session missing those fields gets a 400, and verification
305
355
  matches the supplied values exactly.
306
356
 
307
- **6. The browser is still open.** The response's `_notes` says so. Surface it
308
- and ask whether the developer wants to keep the session for further edits — call
309
- `dispose_browser` only once they confirm. Never close it unprompted; a
310
- `builder`-mode session is quota-accounted, so don't leave it idle either.
357
+ **6. If the response includes `_notes` for an attached browser, follow them for
358
+ an optional authenticated-state retest.** Only when its profile is still signed
359
+ in, navigate to `initialUrl`; if a user script is present, run
360
+ `test_user_script` with that script and `initialUrl`. When the existing capture
361
+ has enough data, replay and prove every required request. Do not clear
362
+ cookies or force another sign-in. Treat an unavailable or inconclusive retest as
363
+ advisory and do not block publication. Ask whether the developer wants to keep
364
+ the session for further edits; call `dispose_browser` only after they confirm.
365
+ A `builder` session is quota-accounted, so don't leave it idle.
311
366
 
312
367
  <!-- topic: discover -->
313
368
  ## Discover and resolve providers for the developer's app
@@ -328,6 +383,12 @@ use, run the same flow the Discover UI does:
328
383
  `merge.mergeable` only signals the set *could* collapse into one provider — you
329
384
  still pass them all; the Builder runs them in one session.
330
385
 
386
+ For client integration, result verification, or TEE binding, read topic
387
+ `integration`.
388
+
389
+ <!-- topic: integration -->
390
+ ## Integrate a Builder Verification Client
391
+
331
392
  **Verification sessions.** Consumers authenticate with a per-org token
332
393
  (`rorg_…`, issued in the dashboard via `POST /orgs/{orgId}/token`) and create
333
394
  sessions via `POST /verifications/sessions`. Results are ECIES-encrypted
@@ -337,6 +398,66 @@ callback subscriptions; plaintext otherwise. Troubleshoot with
337
398
  `get_verification_session`, `list_verification_events`,
338
399
  `list_org_verification_sessions`.
339
400
 
401
+ For a claimant-facing Builder-mode integration, share the returned
402
+ `verificationUrl` (it contains the session id and `api=2`). Append client-owned
403
+ redirect parameters only after session creation. Append `diag=1` only for a
404
+ deliberate troubleshooting launch; it can enable logs containing personal data.
405
+ The registered client resolves its own `x-reclaim-vc-id`; do not invent a
406
+ client token or put callback/redirect fields in the session request. Consumers
407
+ should verify terminal deliveries with `verifyResultFull` and the session's
408
+ trusted issuer.
409
+
410
+ An optional `themeId` on session creation selects an active theme owned by the
411
+ organization. Omit it to snapshot the organization's most recently updated
412
+ active theme. A theme's localized `consent` block makes supported clients show
413
+ consent before the first provider; no block means no consent screen. Theme and
414
+ branding data never affect proof trust.
415
+
416
+ **Browser extension.** For the exact `api=2` URL, use
417
+ `reclaimExtensionSDK.fromVerificationUrl` or `initBuilder` with the session's
418
+ `verificationClientId`, then call `startVerification()`. The extension
419
+ generates and persists `claimantClientId` when omitted, resolves recipes from
420
+ bootstrap, runs them sequentially, and sends `x-reclaim-vc-id` to every bridge
421
+ route, including session-bound `attestor-auth`. It forwards raw legacy proofs
422
+ inside the Builder result and does not perform client-side `allowedJsRequests`
423
+ validation. Builder recipes support dotted `{{context.key}}` templates; the
424
+ extension forwards matching session context to claim creation. Builder mode
425
+ skips legacy offscreen session-status calls and uses the Builder bridge for
426
+ session lifecycle updates. It emits `verification_result_submitting` and
427
+ `verification_result_submission_failed` when applicable. It does not send
428
+ terminal success or error as a separate best-effort event; the results request
429
+ makes the bridge record the matching terminal event strongly before signing
430
+ and storing result deliveries. Missing or unknown `api` values stay on the
431
+ existing `init` / `fromJsonString` / direct-callback path.
432
+
433
+ **Builder client bridge.** In `api=2` mode, the registered client UUID is sent
434
+ in `x-reclaim-vc-id` to these routes:
435
+
436
+ | Route | Purpose |
437
+ | --- | --- |
438
+ | `GET /api/sdk/builder/v2/sessions/{sessionId}/bootstrap` | Load the session and resolved recipe. |
439
+ | `PATCH /api/sdk/builder/v2/sessions/{sessionId}/claimant` | Update claimant diagnostics. |
440
+ | `POST /api/sdk/builder/v2/sessions/{sessionId}/events` | Report canonical lower-snake-case events. |
441
+ | `POST /api/sdk/builder/v2/sessions/{sessionId}/attestor-auth` | Request session-bound attestor authentication. Do not send a legacy app secret or provider signature. |
442
+ | `POST /api/sdk/builder/v2/sessions/{sessionId}/results` | Submit exact legacy proof objects; the bridge records the matching terminal event strongly before signing and storing result deliveries. |
443
+
444
+ The bridge preserves each legacy proof and signs only the outer Verification
445
+ Client JWS. `verifyResultFull` validates the issuer, JWS, session, audience,
446
+ and expiry, then passes each exact proof to `verifyProof`. Keep TEE settings in
447
+ the legacy verifier configuration; do not add verification hooks. Treat outer
448
+ `extracted_parameters` and `content_hash` as diagnostics, not trust inputs.
449
+
450
+ **Optional TEE binding.** A consumer can pass
451
+ `teeAttestation: { appSecret }` to `sessions.create`. The SDK derives the
452
+ hash nonce locally with the pinned `@reclaimprotocol/js-sdk` formula, signs
453
+ the binding payload with EIP-191, and registers it with
454
+ `PUT /verifications/sessions/{sessionId}/tee-nonce` using the organization
455
+ token. The private key stays in the consumer process. Bind before proof
456
+ generation. The binding is idempotent for the same values and immutable
457
+ afterward; this helper registers only `RECLAIM_TEE_NONCE_V1` (`v3`). Legacy
458
+ `verifyProof` accepts the older signature nonce, but this helper does not
459
+ generate or register it.
460
+
340
461
  <!-- topic: never-do -->
341
462
  ## Things to never do
342
463
 
@@ -22,11 +22,6 @@ already names it.
22
22
  <!-- section: browser -->
23
23
  ## Choosing a browser
24
24
 
25
- - Choosing one — ask who signs in, then offer the matching pair
26
- - Sharing the container's live view with someone remote
27
- - What happens when the session ends
28
- - When the container will not start
29
-
30
25
  **Ask one question first, before you list anything:**
31
26
 
32
27
  > Will you be doing this yourself, or does someone else need to sign in — the
@@ -51,7 +46,7 @@ are the only modes that produce a link another person can open:
51
46
 
52
47
  | mode | what it is | needs | costs |
53
48
  | --- | --- | --- | --- |
54
- | `container` | The Reclaim runtime in Docker, on their machine. `share_browser_view` turns its viewer into a public link. | Docker running | free — their Docker, their bandwidth. 432 MB the first time, about 2 GB of RAM, 7 to 17 seconds to start |
49
+ | `container` | The Reclaim runtime in Docker, on their machine. `share_browser_view` turns its viewer into a public link. | Docker running | free — their Docker and bandwidth |
55
50
  | `builder` | A browser in Reclaim's cloud. The link comes back immediately, with nothing to install. Also the only way to browse from a **specific country**. | Builder auth, `orgId` | **chargeable** — billed against the org's quota |
56
51
 
57
52
  **Say which one costs money.** They are otherwise interchangeable from the
@@ -62,7 +57,7 @@ decision. Do not quietly pick the billed one.
62
57
  was handed an endpoint and knows why. Don't offer it unprompted; it cannot be
63
58
  shared either.
64
59
 
65
- **`attach` gotcha (Chrome 136+, May 2025):** the developer's Chrome must be
60
+ **`attach` gotcha (Chrome 136 and later):** the developer's Chrome must be
66
61
  started with BOTH `--remote-debugging-port=9222` AND
67
62
  `--user-data-dir=<non-default-path>`. The debug flag is silently ignored on the
68
63
  default profile. If `attach` fails, say this verbatim and offer `dedicated`.
@@ -109,8 +104,8 @@ Ask plainly:
109
104
  > whoever holds the login — they can watch and click in it, including from a
110
105
  > phone.
111
106
 
112
- If they say yes, call `share_browser_view` and **relay the full URL verbatim, on
113
- its own line.**
107
+ If they say yes, call `share_browser_view` and **relay the full returned URL
108
+ verbatim, on its own line.** It includes `liveview.html?magnify=1`.
114
109
 
115
110
  - **`container` is the only mode this applies to.** `builder` returns a hosted
116
111
  `liveViewUrl` from `attach_browser` — relay that instead, full and verbatim.
@@ -131,16 +126,15 @@ its own line.**
131
126
  - **Call `stop_browser_view` as soon as they're done.** That closes the tunnel
132
127
  only — the browser keeps running. `dispose_browser` and `reset_session` stop
133
128
  the container itself.
134
- - **If the URL will not load**, the tunnel may not have registered with
135
- Cloudflare — the hostname exists before the route does. Have them reload after
136
- 10 seconds; if it still fails, `stop_browser_view` then `share_browser_view`
137
- again, since a quick tunnel does occasionally fail to come up. A
138
- `propagating: true` in the result is that case, and `cloudflaredLog` carries
139
- the reason.
129
+ - **If the URL will not load**, check `propagating`. The hostname can exist
130
+ before Cloudflare finishes routing it. Keep the returned share open, wait a
131
+ few seconds, and reload; don't start a second tunnel while it propagates.
132
+ `cloudflaredLog` contains bounded diagnostic output. If it still fails after
133
+ 60 seconds, call `stop_browser_view`, then retry `share_browser_view` once.
140
134
  - **cloudflared is handled for you.** An existing install on PATH wins;
141
135
  otherwise it's fetched once into `~/.reclaim/bin` on the first share, never at
142
- install time. Say so when it happens — it's a 37 MB download onto their
143
- machine. `RECLAIM_CLOUDFLARED_PATH` points at their own copy instead.
136
+ install time. Tell the developer when the download starts.
137
+ `RECLAIM_CLOUDFLARED_PATH` points at their own copy instead.
144
138
 
145
139
  ### How containers are shared, and when they close
146
140
 
@@ -169,8 +163,8 @@ its own line.**
169
163
  runs translated. OrbStack does this itself; Docker Desktop needs "Use Rosetta
170
164
  for x86/amd64 emulation" turned on. Under QEMU instead of Rosetta, Chromium
171
165
  crashes on launch.
172
- - **First run looks stuck.** It is downloading 432 MB. Say that rather than
173
- letting it look hung.
166
+ - **First run looks stuck.** It is downloading the browser image. Say that
167
+ rather than letting it look hung.
174
168
  - A container left over from a crashed run is **reused if it is still serving**,
175
169
  and replaced if it is not — a second attach does not need a different port.
176
170
 
@@ -329,6 +323,23 @@ captured secret *header* whose value also sits in the URL, which is already
329
323
  known and so gets checked at draft time (`assertNoSecretUrlConflict`).
330
324
 
331
325
  <!-- section: no-rename-param -->
326
+ **Keep internal claim-construction fields out of claimant-facing UI.** When a
327
+ named regex group or `{{parameter}}` only helps construct another claim request,
328
+ consider an internal prefix:
329
+
330
+ - `REQ_` for request/body/header or other intermediate values.
331
+ - `URL_` for URL path/query values.
332
+
333
+ Builder data-point and SEO/catalog surfaces, and compatible claimant UIs, omit
334
+ these names case-insensitively. The prefix is presentation metadata, not a
335
+ security boundary: the exact parameter remains in the recipe, proof, and
336
+ proof-time substitution maps. Keep the same full name everywhere it is
337
+ referenced: the named group, template, and `paramValues`; for an
338
+ `allowedJsRequests` template, also use it in `templateParams`. With
339
+ `propose_provider`, pass the prefixed target name and `shortenNames: false`;
340
+ otherwise name shortening drops the prefix. Don't prefix an actual
341
+ consumer-facing result merely to conceal it.
342
+
332
343
  **Do not rename the match-template param.** The synthesizer's `{{name}}` in
333
344
  `responseMatches[].value` is an _extraction_ template — it stays bare and never
334
345
  takes the `%CTX%` prefix. Only URL/body placeholders whose values the consumer
@@ -45,10 +45,12 @@ emitted. Read both from topic `troubleshooting`.
45
45
  ## Getting authenticated
46
46
 
47
47
  Call `reclaim_authenticate({})` with no arguments. It opens the Reclaim
48
- dashboard in Chrome, waits for the developer to sign in, reads the token from
49
- the page and closes the tab — a still-valid cached identity is reused first, so
50
- it's often instant. Tell the developer once: _"A browser window is opening the
51
- Reclaim dashboard — please sign in and I'll continue automatically."_
48
+ dashboard only in a local Chrome over loopback CDP, waits for the developer to
49
+ sign in, reads the token from the page, and closes the tab. It never uses a
50
+ Builder, container, custom-CDP, or shared browser. A still-valid cached identity
51
+ is reused first, so it's often instant. Tell the developer once: _"A local
52
+ Chrome window is opening the Reclaim dashboard — please sign in and I'll
53
+ continue automatically."_
52
54
 
53
55
  - Force a fresh login: `reclaim_authenticate({ useBrowser: true })`.
54
56
  - Skip the browser: `reclaim_authenticate({ token: "<firebase-id-token>" })`, or
@@ -64,7 +66,7 @@ and are NOT interchangeable.
64
66
  - **`reclaim_authenticate`** authorizes everything in this mode:
65
67
  `create_provider_version_from_capture`, `get_me_providers`, and the rest.
66
68
  - **`authenticate_builder`** covers exactly two Builder-exclusive features:
67
- allocating a Popcorn remote browser (`attach_browser mode="builder"`) and
69
+ allocating a Builder remote browser (`attach_browser mode="builder"`) and
68
70
  `list_builder_organizations`. It authorizes nothing else.
69
71
 
70
72
  `get_devtools_mode` reports the presence of both. If a publish or
@@ -115,7 +117,7 @@ it. Don't assume `authenticate_builder` fixes it.
115
117
  under that `draftId`; the secrets never enter the transcript, so you carry the
116
118
  `draftId`, not the provider JSON, into replay/prove.
117
119
 
118
- **Extracting MORE THAN ONE value?** Prefer a **single request whose response
120
+ **Extracting more than one value?** Prefer a **single request whose response
119
121
  contains all of them** and draft them together — one `responseMatches` +
120
122
  `responseRedactions` pair per value. Find that request by running
121
123
  `find_requests_containing` for each value and picking a `requestId` common to
@@ -256,8 +258,11 @@ Call `create_provider_version_from_capture`:
256
258
  `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
257
259
  ⚠️ Re-running without `providerId` registers ANOTHER provider — that's how
258
260
  duplicates appear. Check `get_me_providers` first.
259
- - **New version of an existing provider:** pass its `providerId`; the patch is
260
- auto-bumped above the highest existing version. Pass `notes` (≥10 chars).
261
+ - **New version of an existing provider:** pass its `providerId`. Omit
262
+ `version` and `bump` for the default patch bump; set `bump` to `major`,
263
+ `minor`, or `patch`, or pass the exact higher `version` requested by the
264
+ user. Every change creates a new immutable version; never edit an existing
265
+ version. Pass `notes` (≥10 chars).
261
266
  - **Multi-request provider** (landing call + details call, a
262
267
  CSRF-preflight/refresh-token chain, …): run capture→draft→replay→prove ONCE
263
268
  PER REQUEST, then pass `providers: [draftA, draftB, …]` instead of the single
@@ -304,10 +309,15 @@ There's no create-then-version step and no org / public-review workflow here.
304
309
  When done, tell the developer the version is live and that they can integrate
305
310
  using the `providerId`.
306
311
 
307
- **The browser is still open.** The response's `_notes` says so. Surface it and
308
- ask whether the developer wants to keep the session for further edits — call
309
- `dispose_browser` only once they confirm. Never close it unprompted; a
310
- `builder`-mode session is quota-accounted.
312
+ **If the response includes `_notes` for an attached browser, follow them for an
313
+ optional authenticated-state retest.** Only when its profile is still signed in,
314
+ navigate to `initialUrl`; if `customInjection` is present, run
315
+ `test_user_script` with that script and `initialUrl`. When the existing capture
316
+ has enough data, replay and prove every required request. Do not clear
317
+ cookies or force another sign-in. Treat an unavailable or inconclusive retest as
318
+ advisory and do not block the completed provider. Ask whether the developer
319
+ wants to keep the session for further edits; call `dispose_browser` only after
320
+ they confirm.
311
321
 
312
322
  <!-- topic: never-do -->
313
323
  ## Things to never do