@reclaimprotocol/agent 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +8 -8
  2. package/lib/authoring/backend.d.ts +12 -1
  3. package/lib/authoring/backend.js +3 -0
  4. package/lib/authoring/tools.js +33 -5
  5. package/lib/authoring/util.d.ts +43 -0
  6. package/lib/authoring/util.js +54 -0
  7. package/lib/cdp/chrome-for-testing.d.ts +14 -0
  8. package/lib/cdp/chrome-for-testing.js +43 -0
  9. package/lib/cdp/geo.d.ts +3 -0
  10. package/lib/cdp/geo.js +27 -15
  11. package/lib/cdp/launcher.d.ts +79 -1
  12. package/lib/cdp/launcher.js +244 -24
  13. package/lib/logger.js +3 -1
  14. package/lib/mcp/api/operations.gen.js +1 -1
  15. package/lib/mcp/instructions.js +3 -3
  16. package/lib/mcp/start.js +17 -2
  17. package/lib/mcp/tools/agent/attach.d.ts +6 -0
  18. package/lib/mcp/tools/agent/attach.js +58 -118
  19. package/lib/mcp/tools/agent/build.js +6 -0
  20. package/lib/mcp/tools/agent/capture.js +33 -10
  21. package/lib/mcp/tools/agent/check-attestor.js +15 -7
  22. package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
  23. package/lib/mcp/tools/agent/draft-tools.js +170 -0
  24. package/lib/mcp/tools/agent/pod-proof.js +5 -1
  25. package/lib/mcp/tools/agent/proof.d.ts +28 -0
  26. package/lib/mcp/tools/agent/proof.js +72 -0
  27. package/lib/mcp/tools/agent/shared.d.ts +8 -1
  28. package/lib/mcp/tools/agent/shared.js +98 -19
  29. package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
  30. package/lib/mcp/tools/agent/start-integration.js +10 -6
  31. package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
  32. package/lib/mcp/tools/authenticate/authenticate.js +212 -116
  33. package/lib/mcp/tools/authenticate/build.d.ts +2 -2
  34. package/lib/mcp/tools/authenticate/build.js +6 -3
  35. package/lib/mcp/tools/authenticate/builder-access.js +5 -2
  36. package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
  37. package/lib/mcp/tools/authenticate/eth-key.js +29 -5
  38. package/lib/mcp/tools/authenticate/index.d.ts +1 -1
  39. package/lib/mcp/tools/authenticate/index.js +1 -1
  40. package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
  41. package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
  42. package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
  43. package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
  44. package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
  45. package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
  46. package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
  47. package/lib/mcp/tools/authenticate/session-cache.js +95 -4
  48. package/lib/mcp/tools/mode/mode.js +6 -6
  49. package/lib/mcp/tools/verification/proxy-callback-events.d.ts +21 -3
  50. package/lib/mcp/tools/verification/proxy-callback-events.js +128 -45
  51. package/lib/old/login-cdp.js +2 -3
  52. package/lib/proof/attestor.js +2 -3
  53. package/lib/proof/check-attestor.d.ts +12 -2
  54. package/lib/proof/check-attestor.js +40 -10
  55. package/lib/proof/check-matchers.d.ts +36 -0
  56. package/lib/proof/check-matchers.js +161 -0
  57. package/lib/proof/zk-files.d.ts +50 -11
  58. package/lib/proof/zk-files.js +259 -63
  59. package/lib/provider/draft.d.ts +24 -1
  60. package/lib/provider/draft.js +135 -0
  61. package/lib/provider/matcher.js +171 -4
  62. package/package.json +6 -4
  63. package/skill/SKILL.builder.md +7 -2
  64. package/skill/SKILL.common.md +90 -70
  65. package/skill/SKILL.old.md +3 -3
  66. package/skill/builder-mode.md +44 -6
  67. package/skill/old-mode.md +18 -13
  68. package/skill/start-integration.md +51 -35
  69. package/skill/verification-client-integration.md +87 -15
  70. package/src/authoring/backend.ts +12 -1
  71. package/src/authoring/tools.ts +48 -7
  72. package/src/authoring/util.ts +64 -0
  73. package/src/cdp/chrome-for-testing.ts +66 -0
  74. package/src/cdp/geo.ts +36 -21
  75. package/src/cdp/launcher.ts +328 -24
  76. package/src/logger.ts +3 -1
  77. package/src/mcp/api/operations.gen.ts +1 -1
  78. package/src/mcp/instructions.ts +3 -3
  79. package/src/mcp/start.ts +20 -1
  80. package/src/mcp/tools/agent/attach.ts +65 -128
  81. package/src/mcp/tools/agent/build.ts +7 -0
  82. package/src/mcp/tools/agent/capture.ts +54 -11
  83. package/src/mcp/tools/agent/check-attestor.ts +15 -7
  84. package/src/mcp/tools/agent/draft-tools.ts +234 -0
  85. package/src/mcp/tools/agent/pod-proof.ts +5 -1
  86. package/src/mcp/tools/agent/proof.ts +110 -1
  87. package/src/mcp/tools/agent/shared.ts +130 -19
  88. package/src/mcp/tools/agent/start-integration.ts +10 -6
  89. package/src/mcp/tools/authenticate/authenticate.ts +276 -131
  90. package/src/mcp/tools/authenticate/build.ts +6 -3
  91. package/src/mcp/tools/authenticate/builder-access.ts +5 -2
  92. package/src/mcp/tools/authenticate/eth-key.ts +41 -7
  93. package/src/mcp/tools/authenticate/index.ts +1 -1
  94. package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
  95. package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
  96. package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
  97. package/src/mcp/tools/authenticate/session-cache.ts +125 -5
  98. package/src/mcp/tools/mode/mode.ts +6 -6
  99. package/src/mcp/tools/verification/proxy-callback-events.ts +151 -41
  100. package/src/old/login-cdp.ts +2 -3
  101. package/src/proof/attestor.ts +2 -3
  102. package/src/proof/check-attestor.ts +55 -12
  103. package/src/proof/check-matchers.ts +227 -0
  104. package/src/proof/zk-files.ts +357 -65
  105. package/src/provider/draft.ts +215 -7
  106. package/src/provider/matcher.ts +237 -4
@@ -1,14 +1,53 @@
1
+ import { type StdioOptions } from 'node:child_process';
2
+ export type ZkFilesState = {
3
+ status: 'idle';
4
+ } | {
5
+ status: 'downloading';
6
+ /** `stwo-files`: the ~1.5MB stwo files. `full-archive`: the ~350MB
7
+ * repository archive (fallback). */
8
+ method: 'stwo-files' | 'full-archive';
9
+ elapsedMs: number;
10
+ bytesDownloaded?: number;
11
+ totalBytes?: number;
12
+ } | {
13
+ status: 'ready';
14
+ } | {
15
+ status: 'failed';
16
+ reason: string;
17
+ };
18
+ /** Minimal child process surface `download-files.js` is driven through. */
19
+ export interface ChildLike {
20
+ once(event: 'exit', listener: (code: number | null) => void): unknown;
21
+ once(event: 'error', listener: (err: Error) => void): unknown;
22
+ }
23
+ export interface ZkFilesDeps {
24
+ /** The zk-symmetric-crypto package root, or undefined when not installed. */
25
+ resolvePackageDir?: () => string | undefined;
26
+ /** The pinned repository commit the package's resources come from. */
27
+ readCommit?: (pkgDir: string) => Promise<string>;
28
+ fetch?: typeof fetch;
29
+ spawn?: (command: string, args: string[], options: {
30
+ cwd: string;
31
+ stdio: StdioOptions;
32
+ }) => ChildLike;
33
+ /** Progress lines. Defaults to stderr: stdout carries MCP JSON-RPC. */
34
+ log?: (message: string) => void;
35
+ }
1
36
  /**
2
- * Ensure the zk circuit resources are on disk before a proof runs. Idempotent
3
- * and concurrency-safe: the download runs at most once per process, and a
4
- * failed attempt clears the latch so a later proof can retry.
5
- *
6
- * Resolves silently when the resources are already present (the warm npx cache,
7
- * or after a manual `npm run download:zk-files`) and when
8
- * `@reclaimprotocol/zk-symmetric-crypto` isn't installed at all (SDK-only
9
- * consumers have nothing to prove).
10
- *
11
- * All output goes to stderr (fd 2): the MCP server speaks JSON-RPC over stdout,
12
- * so a stray write to fd 1 mid-session would corrupt the protocol.
37
+ * Build a downloader with its own state. The module exports one default
38
+ * instance; tests build their own with injected `spawn` and `fetch`.
39
+ */
40
+ export declare function makeZkFiles(deps?: ZkFilesDeps): {
41
+ ensure: () => Promise<void>;
42
+ start: () => void;
43
+ state: () => ZkFilesState;
44
+ };
45
+ /**
46
+ * Ensure the zk circuit resources are on disk before a proof runs. Resolves
47
+ * at once when they're present or the package isn't installed.
13
48
  */
14
49
  export declare function ensureZkFiles(): Promise<void>;
50
+ /** Start the download in the background (MCP server launch). */
51
+ export declare function startZkFilesDownload(): void;
52
+ /** Where the download stands: idle, downloading, ready, or failed. */
53
+ export declare function zkFilesState(): ZkFilesState;
@@ -1,74 +1,270 @@
1
- // Lazy download of the @reclaimprotocol/zk-symmetric-crypto circuit resources
2
- // the proof engines need (the downloader fetches every engine — stwo, snarkjs,
3
- // gnark — as one bundle). These are ~280MB and are NOT fetched in a
4
- // `postinstall` (that blocked the MCP server's first launch past the connect
5
- // timeout — see the install notes). Instead we fetch them on the first proof,
6
- // once, so the server starts immediately and only `run_proof` pays the cost.
7
- import { spawn } from 'node:child_process';
8
- import { existsSync } from 'node:fs';
1
+ // Download of the @reclaimprotocol/zk-symmetric-crypto circuit resources the
2
+ // proof engines read from disk. Never a `postinstall`: that blocked the MCP
3
+ // server's first launch past the connect timeout. Instead the MCP server starts
4
+ // the download in the background at launch (`startZkFilesDownload`), and every
5
+ // proof awaits it (`ensureZkFiles`) as the safety net.
6
+ //
7
+ // Proofs default to the stwo engine. Its resources are ~1.5MB, so we fetch only
8
+ // `resources/stwo` file by file from the package's pinned commit and check each
9
+ // file's git blob hash against the listing for that commit. When that fails
10
+ // (GitHub API rate limit, a changed layout), we fall back to the package's own
11
+ // downloader, which fetches the whole repository archive (~350MB).
12
+ // (In zk-symmetric-crypto 5.1.x the stwo prover loads its WASM from the npm
13
+ // package itself; `resources/stwo` is kept as the readiness signal.)
14
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
15
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
16
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
17
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
18
+ });
19
+ }
20
+ return path;
21
+ };
22
+ import { badGateway, internal } from '@hapi/boom';
23
+ import assert from 'node:assert';
24
+ import { spawn as nodeSpawn } from 'node:child_process';
25
+ import { createHash } from 'node:crypto';
26
+ import { existsSync, statSync } from 'node:fs';
27
+ import { mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
9
28
  import { createRequire } from 'node:module';
10
29
  import { dirname, join } from 'node:path';
11
- const require = createRequire(import.meta.url);
12
- let inFlight;
30
+ import { pathToFileURL } from 'node:url';
31
+ const REPO = 'reclaimprotocol/zk-symmetric-crypto';
32
+ /** The stwo resource dir; its presence means the circuits are ready. */
33
+ const STWO_DIR = join('resources', 'stwo');
34
+ /** Where the package's downloader writes the archive, relative to its cwd. */
35
+ const ARCHIVE_PATH = join('zk-symmetric-crypto-download', 'repo.zip');
13
36
  /**
14
- * Ensure the zk circuit resources are on disk before a proof runs. Idempotent
15
- * and concurrency-safe: the download runs at most once per process, and a
16
- * failed attempt clears the latch so a later proof can retry.
17
- *
18
- * Resolves silently when the resources are already present (the warm npx cache,
19
- * or after a manual `npm run download:zk-files`) and when
20
- * `@reclaimprotocol/zk-symmetric-crypto` isn't installed at all (SDK-only
21
- * consumers have nothing to prove).
22
- *
23
- * All output goes to stderr (fd 2): the MCP server speaks JSON-RPC over stdout,
24
- * so a stray write to fd 1 mid-session would corrupt the protocol.
37
+ * Build a downloader with its own state. The module exports one default
38
+ * instance; tests build their own with injected `spawn` and `fetch`.
25
39
  */
26
- export function ensureZkFiles() {
27
- inFlight ?? (inFlight = download().catch((err) => {
28
- inFlight = undefined;
29
- throw err;
30
- }));
31
- return inFlight;
32
- }
33
- async function download() {
34
- let mainPath;
35
- try {
36
- mainPath = require.resolve('@reclaimprotocol/zk-symmetric-crypto');
37
- }
38
- catch {
39
- return;
40
+ export function makeZkFiles(deps = {}) {
41
+ const resolvePackageDir = deps.resolvePackageDir ?? resolveZkPackageDir;
42
+ const readCommit = deps.readCommit ?? readPinnedCommit;
43
+ const fetchFn = deps.fetch ?? fetch;
44
+ const spawn = deps.spawn ?? nodeSpawn;
45
+ const log = deps.log ?? ((message) => {
46
+ process.stderr.write(`[reclaim-agent] ${message}\n`);
47
+ });
48
+ let inFlight;
49
+ let failure;
50
+ let ready = false;
51
+ let progress;
52
+ return { ensure, start, state };
53
+ /**
54
+ * Ensure the circuit resources are on disk. Idempotent and
55
+ * concurrency-safe: one download runs at a time per process, and a failed
56
+ * attempt clears the latch so a later call retries.
57
+ */
58
+ function ensure() {
59
+ inFlight ?? (inFlight = download()
60
+ .then(() => {
61
+ ready = true;
62
+ failure = undefined;
63
+ }, (err) => {
64
+ inFlight = undefined;
65
+ failure = messageOf(err);
66
+ throw err;
67
+ })
68
+ .finally(() => {
69
+ progress = undefined;
70
+ }));
71
+ return inFlight;
40
72
  }
41
- // mainPath is <pkg>/lib/index.js → resources live at <pkg>/resources, the
42
- // downloader at <pkg>/lib/scripts/download-files.js. Proofs default to the
43
- // stwo (WASM) engine, so its resource dir being present is the signal that
44
- // the circuits are ready (the downloader fetches all engines together).
45
- const libDir = dirname(mainPath);
46
- const pkgDir = dirname(libDir);
47
- if (existsSync(join(pkgDir, 'resources', 'stwo'))) {
48
- return;
73
+ /** Start `ensure` in the background; `state` reports the outcome. */
74
+ function start() {
75
+ ensure().catch(() => { });
49
76
  }
50
- const downloadScript = join(libDir, 'scripts', 'download-files.js');
51
- if (!existsSync(downloadScript)) {
52
- throw new Error('zk circuit downloader not found in '
53
- + '@reclaimprotocol/zk-symmetric-crypto; '
54
- + 'run `npm run download:zk-files` and retry.');
77
+ function state() {
78
+ if (ready) {
79
+ return { status: 'ready' };
80
+ }
81
+ if (progress) {
82
+ const bytesDownloaded = progress.archivePath
83
+ ? fileSize(progress.archivePath)
84
+ : progress.bytesDownloaded;
85
+ return {
86
+ status: 'downloading',
87
+ method: progress.method,
88
+ elapsedMs: Date.now() - progress.startedAt,
89
+ ...(bytesDownloaded !== undefined ? { bytesDownloaded } : {}),
90
+ ...(progress.totalBytes !== undefined
91
+ ? { totalBytes: progress.totalBytes }
92
+ : {}),
93
+ };
94
+ }
95
+ if (failure !== undefined) {
96
+ return { status: 'failed', reason: failure };
97
+ }
98
+ return { status: 'idle' };
55
99
  }
56
- process.stderr.write('[reclaim-agent] downloading zkTLS proof circuits '
57
- + '(~280MB, one-time)…\n');
58
- // stdio fd 1→2: send the downloader's stdout to our stderr so its progress
59
- // never lands on the MCP server's JSON-RPC stdout.
60
- await new Promise((resolve, reject) => {
61
- const child = spawn(process.execPath, [downloadScript], {
62
- stdio: ['ignore', 2, 2],
63
- });
64
- child.on('error', reject);
65
- child.on('exit', (code) => {
66
- if (code === 0) {
67
- resolve();
100
+ async function download() {
101
+ const pkgDir = resolvePackageDir();
102
+ // SDK-only consumers don't install the package: nothing to prove.
103
+ if (!pkgDir || existsSync(join(pkgDir, STWO_DIR))) {
104
+ return;
105
+ }
106
+ try {
107
+ progress = { method: 'stwo-files', startedAt: Date.now() };
108
+ await fetchStwoFiles(pkgDir, progress);
109
+ }
110
+ catch (err) {
111
+ const stwoError = messageOf(err);
112
+ log(`fetching the stwo proof files failed (${stwoError}); `
113
+ + 'downloading the full circuit archive (~350MB, one-time)…');
114
+ progress = {
115
+ method: 'full-archive',
116
+ startedAt: Date.now(),
117
+ archivePath: join(pkgDir, ARCHIVE_PATH),
118
+ };
119
+ try {
120
+ await runArchiveDownloader(pkgDir);
121
+ }
122
+ catch (archiveErr) {
123
+ throw badGateway(`stwo files: ${stwoError}; full archive: ${messageOf(archiveErr)}`);
68
124
  }
69
- else {
70
- reject(new Error(`zk download exited ${code}`));
125
+ }
126
+ assert(existsSync(join(pkgDir, STWO_DIR)), internal(`zk circuit download finished but ${join(pkgDir, STWO_DIR)} `
127
+ + 'is missing; run `npm run download:zk-files` and retry.'));
128
+ }
129
+ /**
130
+ * Fetch `resources/stwo` at the pinned commit, file by file, into a temp
131
+ * dir inside the package, then rename it into place (same filesystem).
132
+ */
133
+ async function fetchStwoFiles(pkgDir, stats) {
134
+ const commit = await readCommit(pkgDir);
135
+ assert(/^[0-9a-f]{40}$/.test(commit), internal('invalid pinned commit'));
136
+ const files = await listStwoFiles(commit);
137
+ stats.bytesDownloaded = 0;
138
+ stats.totalBytes = files.reduce((sum, f) => sum + f.size, 0);
139
+ log(`downloading zkTLS stwo proof files (${stats.totalBytes} bytes, `
140
+ + 'one-time)…');
141
+ const tmp = await mkdtemp(join(pkgDir, '.stwo-download-'));
142
+ try {
143
+ for (const file of files) {
144
+ const res = await fetchFn(`https://raw.githubusercontent.com/${REPO}/${commit}/`
145
+ + `resources/stwo/${file.name}`);
146
+ assert(res.ok, badGateway(`stwo file ${file.name}: HTTP ${res.status}`));
147
+ const bytes = new Uint8Array(await res.arrayBuffer());
148
+ assert(gitBlobSha(bytes) === file.sha, badGateway(`stwo file ${file.name} failed its integrity check`));
149
+ await writeFile(join(tmp, file.name), bytes);
150
+ stats.bytesDownloaded = (stats.bytesDownloaded ?? 0) + bytes.length;
71
151
  }
152
+ const dest = join(pkgDir, STWO_DIR);
153
+ await mkdir(dirname(dest), { recursive: true });
154
+ // Another process may have finished first; its copy is as good.
155
+ await rename(tmp, dest).catch((err) => {
156
+ if (!existsSync(dest)) {
157
+ throw err;
158
+ }
159
+ });
160
+ }
161
+ finally {
162
+ await rm(tmp, { recursive: true, force: true });
163
+ }
164
+ }
165
+ /** The file listing of `resources/stwo` at `commit`, with blob hashes. */
166
+ async function listStwoFiles(commit) {
167
+ const res = await fetchFn(`https://api.github.com/repos/${REPO}/contents/resources/stwo`
168
+ + `?ref=${commit}`, { headers: { Accept: 'application/vnd.github+json' } });
169
+ assert(res.ok, badGateway(`stwo file listing: HTTP ${res.status}`));
170
+ const listing = await res.json();
171
+ assert(Array.isArray(listing) && listing.length > 0, badGateway('stwo file listing is empty'));
172
+ return listing.map((entry) => {
173
+ assert(typeof entry === 'object' && entry !== null
174
+ && 'type' in entry && entry.type === 'file'
175
+ && 'name' in entry && typeof entry.name === 'string'
176
+ && /^[\w.-]+$/.test(entry.name) && !entry.name.startsWith('.')
177
+ && 'size' in entry && typeof entry.size === 'number'
178
+ && 'sha' in entry && typeof entry.sha === 'string', badGateway('unexpected entry in the stwo file listing'));
179
+ return { name: entry.name, size: entry.size, sha: entry.sha };
72
180
  });
73
- });
181
+ }
182
+ /**
183
+ * Run the package's `download-files.js`. Its temp paths are relative, so
184
+ * cwd is the package dir: the archive and extracted tree then sit on the
185
+ * same filesystem as the final `rename()` (no EXDEV) and never land in the
186
+ * developer's project.
187
+ */
188
+ async function runArchiveDownloader(pkgDir) {
189
+ const script = join(pkgDir, 'lib', 'scripts', 'download-files.js');
190
+ assert(existsSync(script), internal('zk circuit downloader not found in '
191
+ + '@reclaimprotocol/zk-symmetric-crypto; '
192
+ + 'run `npm run download:zk-files` and retry.'));
193
+ await new Promise((resolve, reject) => {
194
+ // stdio fd 1→2: the downloader's progress must never reach the MCP
195
+ // server's JSON-RPC stdout.
196
+ const child = spawn(process.execPath, [script], {
197
+ cwd: pkgDir,
198
+ stdio: ['ignore', 2, 2],
199
+ });
200
+ child.once('error', reject);
201
+ child.once('exit', (code) => {
202
+ if (code === 0) {
203
+ resolve();
204
+ }
205
+ else {
206
+ reject(internal(`zk circuit download exited with code ${code}`));
207
+ }
208
+ });
209
+ });
210
+ }
211
+ }
212
+ const DEFAULT = makeZkFiles();
213
+ /**
214
+ * Ensure the zk circuit resources are on disk before a proof runs. Resolves
215
+ * at once when they're present or the package isn't installed.
216
+ */
217
+ export function ensureZkFiles() {
218
+ return DEFAULT.ensure();
219
+ }
220
+ /** Start the download in the background (MCP server launch). */
221
+ export function startZkFilesDownload() {
222
+ DEFAULT.start();
223
+ }
224
+ /** Where the download stands: idle, downloading, ready, or failed. */
225
+ export function zkFilesState() {
226
+ return DEFAULT.state();
227
+ }
228
+ /**
229
+ * The zk-symmetric-crypto copy attestor-core loads (it may be nested under
230
+ * attestor-core), else the hoisted one.
231
+ */
232
+ function resolveZkPackageDir() {
233
+ const require = createRequire(import.meta.url);
234
+ for (const from of [
235
+ () => createRequire(require.resolve('@reclaimprotocol/attestor-core')),
236
+ () => require,
237
+ ]) {
238
+ try {
239
+ // <pkg>/lib/index.js → <pkg>
240
+ return dirname(dirname(from().resolve('@reclaimprotocol/zk-symmetric-crypto')));
241
+ }
242
+ catch {
243
+ // try the next resolution root
244
+ }
245
+ }
246
+ return undefined;
247
+ }
248
+ async function readPinnedCommit(pkgDir) {
249
+ const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(join(pkgDir, 'lib', 'index.js')).href));
250
+ assert(typeof mod.GIT_COMMIT_HASH === 'string', internal('zk-symmetric-crypto exports no GIT_COMMIT_HASH'));
251
+ return mod.GIT_COMMIT_HASH;
252
+ }
253
+ /** Git's blob object id: sha1 of `blob <size>\0<content>`. */
254
+ function gitBlobSha(bytes) {
255
+ return createHash('sha1')
256
+ .update(`blob ${bytes.length}\0`)
257
+ .update(bytes)
258
+ .digest('hex');
259
+ }
260
+ function fileSize(path) {
261
+ try {
262
+ return statSync(path).size;
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ }
268
+ function messageOf(err) {
269
+ return err instanceof Error ? err.message : String(err);
74
270
  }
@@ -1,4 +1,4 @@
1
- import type { CapturedRequest, OprfMode, ReclaimProvider, ResponseMatch, ResponseRedaction, SecretParamRef } from './schema.ts';
1
+ import { type CapturedRequest, type OprfMode, type ReclaimProvider, type ResponseMatch, type ResponseRedaction, type SecretParamRef } from './schema.ts';
2
2
  /** OPRF mode to apply when the caller passed none: the anti-leak floor —
3
3
  * force-hash a value whose SHAPE is unambiguous PII, but ONLY when it's
4
4
  * OPRF-hashable AND within the 62-byte MPC-circuit cap (UTF-8). Undefined =
@@ -63,3 +63,26 @@ export declare function assertNoSecretUrlConflict(url: string, secrets: Record<s
63
63
  export declare function draftProvider(req: CapturedRequest, targets: DraftTarget[], name: string, hash?: OprfMode, opts?: {
64
64
  shortenNames?: boolean;
65
65
  }): DraftResult;
66
+ /** Fields `update_draft` may replace on a drafted provider. */
67
+ export interface DraftEdit {
68
+ responseMatches?: ResponseMatch[];
69
+ responseRedactions?: ResponseRedaction[];
70
+ paramValues?: Record<string, string>;
71
+ /** ISO country the request goes out from, or `{{DYNAMIC_GEO}}` /
72
+ * `{{DYNAMIC_GEO_SECRET}}` for the verifying user's own country. */
73
+ geoLocation?: string;
74
+ }
75
+ /**
76
+ * Apply an agent's edit to a drafted provider: validate the new matchers the
77
+ * way the attestor reads them (regexes compile with its `sgi` flags, every
78
+ * redaction has a selector, a hashed redaction has exactly one named group),
79
+ * then run the same portability gate as {@link draftProvider} so an edit can't
80
+ * bake a captured value in as a literal. Returns the updated provider, its
81
+ * extractions (re-paired with the redaction that names each param), and
82
+ * advisory warnings (for example, a `{{param}}` nothing resolves).
83
+ */
84
+ export declare function editDraftProvider(provider: ReclaimProvider, extractions: Extraction[], edit: DraftEdit): {
85
+ provider: ReclaimProvider;
86
+ extractions: Extraction[];
87
+ warnings: string[];
88
+ };
@@ -1,5 +1,8 @@
1
+ import { badRequest } from '@hapi/boom';
2
+ import assert from 'node:assert';
1
3
  import { isOprfHashable, OPRF_MAX_BYTES, synthesizeMatcher } from "./matcher.js";
2
4
  import { normalizeCandidates } from "./normalize.js";
5
+ import { DYNAMIC_GEO, } from "./schema.js";
3
6
  import { secretUrlCharBudget, splitSecrets, stripTransportHeaders, } from "./secrets.js";
4
7
  /** OPRF mode for the auto-hash floor — TEE-native (attestor rejects oprf-raw,
5
8
  * isn't set up for gnark oprf). */
@@ -312,3 +315,135 @@ export function draftProvider(req, targets, name, hash, opts) {
312
315
  }
313
316
  return { provider, secretRefs, extractions };
314
317
  }
318
+ const GEO_TEMPLATES = new Set([DYNAMIC_GEO, '{{DYNAMIC_GEO_SECRET}}']);
319
+ const MATCH_TYPES = new Set(['contains', 'regex']);
320
+ const OPRF_MODES = new Set(['oprf', 'oprf-raw', 'oprf-mpc']);
321
+ /**
322
+ * Apply an agent's edit to a drafted provider: validate the new matchers the
323
+ * way the attestor reads them (regexes compile with its `sgi` flags, every
324
+ * redaction has a selector, a hashed redaction has exactly one named group),
325
+ * then run the same portability gate as {@link draftProvider} so an edit can't
326
+ * bake a captured value in as a literal. Returns the updated provider, its
327
+ * extractions (re-paired with the redaction that names each param), and
328
+ * advisory warnings (for example, a `{{param}}` nothing resolves).
329
+ */
330
+ export function editDraftProvider(provider, extractions, edit) {
331
+ const matches = edit.responseMatches ?? provider.responseMatches;
332
+ const redactions = edit.responseRedactions
333
+ ?? provider.responseRedactions ?? [];
334
+ const paramValues = edit.paramValues ?? provider.paramValues ?? {};
335
+ const geoLocation = GEO_TEMPLATES.has(edit.geoLocation ?? '')
336
+ ? edit.geoLocation
337
+ : edit.geoLocation?.toUpperCase();
338
+ assert(geoLocation === undefined || GEO_TEMPLATES.has(geoLocation)
339
+ || /^[A-Z]{2}$/.test(geoLocation), badRequest('geoLocation must be a 2-letter ISO country code, {{DYNAMIC_GEO}}, '
340
+ + 'or {{DYNAMIC_GEO_SECRET}}'));
341
+ assert(matches.length > 0, badRequest('responseMatches must have at least one entry'));
342
+ for (const [i, m] of matches.entries()) {
343
+ assert(MATCH_TYPES.has(m.type) && typeof m.value === 'string' && m.value, badRequest(`responseMatches[${i}] needs type "contains" or "regex" and a `
344
+ + 'non-empty value'));
345
+ if (m.type === 'regex') {
346
+ assertRegex(m.value, `responseMatches[${i}].value`);
347
+ }
348
+ }
349
+ for (const [i, r] of redactions.entries()) {
350
+ assert(r.xPath || r.jsonPath || r.regex, badRequest(`responseRedactions[${i}] needs an xPath, jsonPath, or regex`));
351
+ if (r.regex) {
352
+ assertRegex(r.regex, `responseRedactions[${i}].regex`);
353
+ }
354
+ assert(!r.hash || (OPRF_MODES.has(r.hash) && groupNames(r.regex).length === 1), badRequest(`responseRedactions[${i}] is hashed (${r.hash}), so it needs a hash `
355
+ + 'of oprf, oprf-raw, or oprf-mpc and a regex with exactly one named '
356
+ + 'capture group'));
357
+ }
358
+ // An edit can't lower privacy: a draft that redacts keeps redacting, and
359
+ // every group it hashed stays hashed with the same mode.
360
+ const original = provider.responseRedactions ?? [];
361
+ assert(!original.length || redactions.length, badRequest('responseRedactions can\'t be emptied: the attestor would see the '
362
+ + 'whole response'));
363
+ for (const r of original.filter((o) => o.hash)) {
364
+ for (const name of groupNames(r.regex)) {
365
+ const kept = redactions.find((e) => groupNames(e.regex).includes(name));
366
+ assert(kept?.hash === r.hash, badRequest(`capture group ${name} is ${r.hash}-hashed in the draft; keep a `
367
+ + 'redaction for it with the same hash'));
368
+ }
369
+ }
370
+ const groups = matcherGroups(matches, redactions);
371
+ // A group the draft never extracted has a value nothing below checks, and
372
+ // no extraction would track it: new data points need a fresh draft.
373
+ const known = new Set([
374
+ ...extractions.map((e) => e.name),
375
+ ...matcherGroups(provider.responseMatches, provider.responseRedactions ?? []),
376
+ ]);
377
+ const unknown = [...groups].filter((name) => !known.has(name));
378
+ assert(!unknown.length, badRequest(`capture group(s) ${unknown.join(', ')} are not drafted extractions; `
379
+ + 'propose a new draft to add a data point'));
380
+ // Always the captured values, so editing a paramValues entry can't hide
381
+ // one from the portability gate; edited values are checked as well.
382
+ const targets = [
383
+ ...extractions.map(({ name, value }) => ({ name, value })),
384
+ ...Object.entries(paramValues)
385
+ .filter(([name, value]) => !extractions
386
+ .some((e) => e.name === name && e.value === value))
387
+ .map(([name, value]) => ({ name, value })),
388
+ ];
389
+ assertPortableMatches(matches, redactions, targets);
390
+ // Same OPRF gate as draftProvider, against the URL as the attestor signs it.
391
+ const hashed = redactions.flatMap(({ regex, hash }) => hash
392
+ ? groupNames(regex).flatMap((name) => targets
393
+ .filter((t) => t.name === name)
394
+ .map((t) => ({ ...t, hash })))
395
+ : []);
396
+ const url = provider.url
397
+ .replace(/\{\{([^{}]+)\}\}/g, (t, name) => paramValues[name] ?? t);
398
+ assertNoOprfUrlConflict(url, hashed, hashed.map((t) => t.hash));
399
+ const { responseRedactions: _r, paramValues: _p, ...rest } = provider;
400
+ void _r;
401
+ void _p;
402
+ const next = {
403
+ ...rest,
404
+ responseMatches: matches,
405
+ ...(redactions.length ? { responseRedactions: redactions } : {}),
406
+ ...(Object.keys(paramValues).length ? { paramValues } : {}),
407
+ ...(geoLocation ? { geoLocation } : {}),
408
+ };
409
+ const warnings = matches
410
+ .flatMap((m) => [...m.value.matchAll(/\{\{([^{}]+)\}\}/g)].map((t) => t[1]))
411
+ .filter((name) => !(name in paramValues) && !groups.has(name))
412
+ .map((name) => `{{${name}}} has no paramValues entry or named capture `
413
+ + 'group, so the attestor would look for it literally.');
414
+ const nextExtractions = extractions.map((e) => {
415
+ const redaction = redactions
416
+ .find((r) => groupNames(r.regex).includes(e.name));
417
+ // The captured value, never an edited one, so a later edit's gate still
418
+ // sees it.
419
+ return {
420
+ name: e.name,
421
+ value: e.value,
422
+ ...(redaction ? { redaction } : {}),
423
+ };
424
+ });
425
+ return { provider: next, extractions: nextExtractions, warnings };
426
+ }
427
+ /** Named capture groups across regex matches and redactions. */
428
+ function matcherGroups(matches, redactions) {
429
+ return new Set([
430
+ ...matches.flatMap((m) => m.type === 'regex' ? groupNames(m.value) : []),
431
+ ...redactions.flatMap((r) => groupNames(r.regex)),
432
+ ]);
433
+ }
434
+ /** Named capture groups in `regex` (none when absent). */
435
+ function groupNames(regex) {
436
+ return [...(regex ?? '').matchAll(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g)]
437
+ .map((m) => m[1]);
438
+ }
439
+ /** Throw a 400 unless `regex` compiles the way the attestor compiles it. */
440
+ function assertRegex(regex, where) {
441
+ let error;
442
+ try {
443
+ new RegExp(regex, 'sgi');
444
+ }
445
+ catch (err) {
446
+ error = err instanceof Error ? err.message : String(err);
447
+ }
448
+ assert(!error, badRequest(`${where} is not a valid regex: ${error}`));
449
+ }