@reclaimprotocol/agent 0.1.10 → 0.1.11
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.
- package/lib/authoring/tools.js +67 -0
- package/lib/mcp/tools/agent/find.js +41 -16
- package/lib/mcp/tools/agent/propose.js +4 -4
- package/lib/old/client.d.ts +0 -5
- package/lib/old/client.js +0 -10
- package/lib/old/to-register.d.ts +2 -0
- package/lib/old/to-register.js +9 -5
- package/lib/old/tools/build.js +0 -2
- package/lib/old/tools/get-provider-info.d.ts +10 -7
- package/lib/old/tools/get-provider-info.js +21 -15
- package/lib/old/tools/publish.js +64 -121
- package/lib/provider/dom-xpath.js +178 -25
- package/lib/provider/draft.d.ts +6 -0
- package/lib/provider/draft.js +54 -5
- package/lib/provider/matcher.d.ts +4 -0
- package/lib/provider/matcher.js +6 -0
- package/lib/provider/to-version.d.ts +13 -0
- package/package.json +1 -1
- package/src/authoring/tools.ts +76 -0
- package/src/mcp/tools/agent/find.ts +44 -16
- package/src/mcp/tools/agent/propose.ts +4 -4
- package/src/old/client.ts +0 -18
- package/src/old/to-register.ts +11 -5
- package/src/old/tools/build.ts +0 -2
- package/src/old/tools/get-provider-info.ts +21 -15
- package/src/old/tools/publish.ts +70 -154
- package/src/provider/dom-xpath.ts +227 -27
- package/src/provider/draft.ts +59 -5
- package/src/provider/matcher.ts +7 -0
- package/src/provider/to-version.ts +13 -0
- package/src/old/tools/get-version-config.ts +0 -56
package/lib/authoring/tools.js
CHANGED
|
@@ -135,6 +135,73 @@ export function authoringTools(resolveBackend) {
|
|
|
135
135
|
...(c.httpOnly ? { httpOnly: true } : {}),
|
|
136
136
|
}));
|
|
137
137
|
}),
|
|
138
|
+
defineTool({
|
|
139
|
+
name: 'wait_for_login',
|
|
140
|
+
description: 'Wait for the user to finish logging in. Polls the browser\'s '
|
|
141
|
+
+ 'REAL cookies over CDP (httpOnly INCLUDED) until a NEW session '
|
|
142
|
+
+ 'cookie appears, ignoring analytics/bot cookies. The ONLY '
|
|
143
|
+
+ 'correct login wait — NEVER use wait_for_page for it: page JS '
|
|
144
|
+
+ 'can\'t see the httpOnly session cookie (hangs the whole '
|
|
145
|
+
+ 'timeout), yet DOES see pre-login tracking cookies (a '
|
|
146
|
+
+ 'length/substring check fires instantly, before the user '
|
|
147
|
+
+ 'types). Snapshots the baseline itself — call right after '
|
|
148
|
+
+ 'navigating to the login page. Does NOT navigate (never yanks '
|
|
149
|
+
+ 'the user off login/2FA). Returns { loggedIn, newCookies }.',
|
|
150
|
+
inputSchema: objectSchema({ timeoutMs: { type: 'number' } }),
|
|
151
|
+
}, async (args) => {
|
|
152
|
+
const backend = resolveBackend(args);
|
|
153
|
+
const deadline = Date.now() + (Number(args.timeoutMs) || 180000);
|
|
154
|
+
const cookies = async () => {
|
|
155
|
+
const res = (await backend.sendCdp('Network.getCookies'));
|
|
156
|
+
return res.cookies ?? [];
|
|
157
|
+
};
|
|
158
|
+
// Analytics / bot-protection / pre-auth infra cookies appear (and
|
|
159
|
+
// rotate) without any login — never the signal, even when new.
|
|
160
|
+
const JUNK = new RegExp('^(' + [
|
|
161
|
+
'_ga', '_gid', '_gat', '_ym', '__ddg', '__gads', '__gpi', '__eoi',
|
|
162
|
+
'__utm', '_fbp', 'fbp', 'fr', 'FCCDCF', 'pinLogger',
|
|
163
|
+
'userFirstVisit', 'cf_', '_dd_', 'bcookie', 'bscookie', 'lidc',
|
|
164
|
+
].join('|') + ')', 'i');
|
|
165
|
+
// A real session/auth cookie name. Deliberately NOT bare `token` — a
|
|
166
|
+
// challenge/CSRF token (e.g. LinkedIn `chp_token`) contains it and is
|
|
167
|
+
// set BEFORE auth completes. Any new httpOnly cookie is also a
|
|
168
|
+
// candidate (server-set session cookies are usually httpOnly).
|
|
169
|
+
const SESSION = /sess|sid|jwt|_session|auth|logged.?in|remember|li_at/i;
|
|
170
|
+
// Auth/challenge URL paths — while the page is still on one, login is
|
|
171
|
+
// NOT done (a mid-flow cookie must not count). Cross-site-stable, far
|
|
172
|
+
// more so than cookie names.
|
|
173
|
+
const AUTH_URL = new RegExp([
|
|
174
|
+
'login', 'signin', 'sign-in', 'challenge', 'checkpoint', '\\buas\\b',
|
|
175
|
+
'authorize', '\\bsso\\b', 'mfa', '2fa', 'two-factor', 'otp', 'verify',
|
|
176
|
+
'password',
|
|
177
|
+
].join('|'), 'i');
|
|
178
|
+
const offAuthUrl = async () => {
|
|
179
|
+
const { value } = await evalInPage(backend, 'location.href');
|
|
180
|
+
const href = typeof value === 'string' ? value : '';
|
|
181
|
+
try {
|
|
182
|
+
const u = new URL(href);
|
|
183
|
+
return !AUTH_URL.test(u.pathname + u.search);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const initial = await cookies();
|
|
190
|
+
const baseline = new Set(initial.map((c) => c.name));
|
|
191
|
+
while (Date.now() < deadline) {
|
|
192
|
+
const current = await cookies();
|
|
193
|
+
const fresh = current
|
|
194
|
+
.filter((c) => !baseline.has(c.name) && !JUNK.test(c.name));
|
|
195
|
+
const hit = fresh.filter((c) => c.httpOnly || SESSION.test(c.name));
|
|
196
|
+
// BOTH required: a new session cookie AND the page has left the auth
|
|
197
|
+
// flow. Either alone false-positives (mid-flow token; stray nav).
|
|
198
|
+
if (hit.length && await offAuthUrl()) {
|
|
199
|
+
return { loggedIn: true, newCookies: hit.map((c) => c.name) };
|
|
200
|
+
}
|
|
201
|
+
await sleep(1000);
|
|
202
|
+
}
|
|
203
|
+
return { loggedIn: false, newCookies: [] };
|
|
204
|
+
}),
|
|
138
205
|
defineTool({
|
|
139
206
|
name: 'list_requests',
|
|
140
207
|
description: 'List captured network requests (method/url/status/size), newest '
|
|
@@ -14,38 +14,63 @@ export function findTool(resolveBackend) {
|
|
|
14
14
|
description: 'Search captured responses for any encoding of `value` (raw, '
|
|
15
15
|
+ 'URL/JSON/HTML-escaped, …). Returns a ranked candidate list with '
|
|
16
16
|
+ 'the matched variant, score, and a short snippet — used to locate '
|
|
17
|
-
+ 'the request that returns a target.
|
|
17
|
+
+ 'the request that returns a target. Also finds SPLIT matches: when '
|
|
18
|
+
+ '`value` is multi-word (e.g. a full name), a response that carries '
|
|
19
|
+
+ 'every word SEPARATELY (a JSON profile API with first/last name in '
|
|
20
|
+
+ 'distinct fields) is returned with `splitMatch:true` — prove those '
|
|
21
|
+
+ 'per-field (a redaction per part) rather than as one string. '
|
|
22
|
+
+ 'Server-side; bodies stay here.',
|
|
18
23
|
inputSchema: objectSchema({ value: { type: 'string' }, limit: { type: 'number' } }, ['value']),
|
|
19
24
|
}, async (args) => {
|
|
20
25
|
const backend = resolveBackend(args);
|
|
21
|
-
const
|
|
26
|
+
const value = String(args.value);
|
|
27
|
+
const variants = normalizeCandidates(value);
|
|
28
|
+
// Name-part tokens (≥3 chars) for split-field detection — a JSON API
|
|
29
|
+
// often carries a full name as separate first/last fields, so the whole
|
|
30
|
+
// string never matches even though the value IS there.
|
|
31
|
+
const tokens = value.split(/\s+/).filter((t) => t.length >= 3);
|
|
32
|
+
const snip = (body, idx, len) => {
|
|
33
|
+
const s = Math.max(0, idx - 40);
|
|
34
|
+
return body.slice(s, Math.min(body.length, idx + len + 40));
|
|
35
|
+
};
|
|
22
36
|
const ranked = [];
|
|
23
37
|
for (const r of backend.capture.requests.values()) {
|
|
24
38
|
if (!r.responseBody) {
|
|
25
39
|
continue;
|
|
26
40
|
}
|
|
41
|
+
const body = r.responseBody;
|
|
42
|
+
// Prefer JSON responses + smaller bodies (likely API endpoints).
|
|
43
|
+
const base = (r.contentType.includes('json') ? 10 : 0)
|
|
44
|
+
+ Math.max(0, 5 - Math.log10(Math.max(r.size, 1)));
|
|
27
45
|
let hit;
|
|
28
46
|
for (const v of variants) {
|
|
29
|
-
const idx =
|
|
47
|
+
const idx = body.indexOf(v);
|
|
30
48
|
if (idx >= 0) {
|
|
31
|
-
|
|
32
|
-
const end = Math.min(r.responseBody.length, idx + v.length + 40);
|
|
33
|
-
hit = { snippet: r.responseBody.slice(start, end), variant: v };
|
|
49
|
+
hit = { snippet: snip(body, idx, v.length), variant: v };
|
|
34
50
|
break;
|
|
35
51
|
}
|
|
36
52
|
}
|
|
37
|
-
if (
|
|
53
|
+
if (hit) {
|
|
54
|
+
// Whole-value match — ranked above split matches (+5).
|
|
55
|
+
ranked.push({
|
|
56
|
+
...publicRequest(r),
|
|
57
|
+
matchedVariant: hit.variant,
|
|
58
|
+
snippet: hit.snippet,
|
|
59
|
+
score: base + 5,
|
|
60
|
+
});
|
|
38
61
|
continue;
|
|
39
62
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
// Split match: every word present separately (e.g. first/last name
|
|
64
|
+
// in distinct JSON fields). Requires ≥2 tokens so it's specific.
|
|
65
|
+
if (tokens.length >= 2 && tokens.every((t) => body.includes(t))) {
|
|
66
|
+
ranked.push({
|
|
67
|
+
...publicRequest(r),
|
|
68
|
+
matchedVariant: `split:${tokens.join('+')}`,
|
|
69
|
+
splitMatch: true,
|
|
70
|
+
snippet: snip(body, body.indexOf(tokens[0]), tokens[0].length),
|
|
71
|
+
score: base,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
49
74
|
}
|
|
50
75
|
ranked.sort((a, b) => b.score - a.score);
|
|
51
76
|
const limit = Number(args.limit) || 10;
|
|
@@ -71,11 +71,11 @@ export function proposeTool(resolveBackend) {
|
|
|
71
71
|
+ '`targets` array');
|
|
72
72
|
}
|
|
73
73
|
for (const t of targets) {
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const bytes = Buffer.byteLength(t.value, 'utf8');
|
|
75
|
+
if ((t.hash ?? defaultHash) !== undefined && bytes > OPRF_MAX_BYTES) {
|
|
76
76
|
throw new Error(`OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; got `
|
|
77
|
-
+ `${
|
|
78
|
-
+ '
|
|
77
|
+
+ `${bytes}-byte "${t.value}". Use a tighter target or drop `
|
|
78
|
+
+ '`hash`.');
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
const captured = backend.capture.requests.get(requestId);
|
package/lib/old/client.d.ts
CHANGED
|
@@ -99,11 +99,6 @@ export declare class ReclaimOldClient {
|
|
|
99
99
|
registerProvider(body: RegisterProviderBody): Promise<unknown>;
|
|
100
100
|
updateProviderConfig(providerId: string, body: UpdateProviderConfigBody): Promise<unknown>;
|
|
101
101
|
listVersions(providerId: string): Promise<unknown>;
|
|
102
|
-
/** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
|
|
103
|
-
* one version — the latest when `version` is omitted. Readable for ANY
|
|
104
|
-
* non-archived provider (not owner-restricted), same as the dashboard's
|
|
105
|
-
* edit-form prefill fetch. */
|
|
106
|
-
getProviderConfig(providerId: string, version?: string): Promise<unknown>;
|
|
107
102
|
/** Partial update on the PROVIDER document itself — name/description/
|
|
108
103
|
* providerType/tags/isActive, after creation. The backend 403s if you
|
|
109
104
|
* don't own the provider (and aren't admin); other providerConfig-style
|
package/lib/old/client.js
CHANGED
|
@@ -60,16 +60,6 @@ export class ReclaimOldClient {
|
|
|
60
60
|
listVersions(providerId) {
|
|
61
61
|
return __classPrivateFieldGet(this, _ReclaimOldClient_instances, "m", _ReclaimOldClient_request).call(this, 'GET', `/providers/${encodeURIComponent(providerId)}/versions`, { auth: true });
|
|
62
62
|
}
|
|
63
|
-
/** Full `providerConfig` (loginUrl, customInjection, requestData, ...) for
|
|
64
|
-
* one version — the latest when `version` is omitted. Readable for ANY
|
|
65
|
-
* non-archived provider (not owner-restricted), same as the dashboard's
|
|
66
|
-
* edit-form prefill fetch. */
|
|
67
|
-
getProviderConfig(providerId, version) {
|
|
68
|
-
return __classPrivateFieldGet(this, _ReclaimOldClient_instances, "m", _ReclaimOldClient_request).call(this, 'GET', `/providers/${encodeURIComponent(providerId)}/config`, {
|
|
69
|
-
auth: true,
|
|
70
|
-
query: version ? { versionNumber: version } : undefined,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
63
|
/** Partial update on the PROVIDER document itself — name/description/
|
|
74
64
|
* providerType/tags/isActive, after creation. The backend 403s if you
|
|
75
65
|
* don't own the provider (and aren't admin); other providerConfig-style
|
package/lib/old/to-register.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ interface OldRequestData {
|
|
|
83
83
|
export interface OldInjectedRequestData extends OldRequestData {
|
|
84
84
|
required?: boolean;
|
|
85
85
|
multiple?: boolean;
|
|
86
|
+
templateParams?: string[];
|
|
87
|
+
templateParamsMode?: 'separate' | 'merge';
|
|
86
88
|
}
|
|
87
89
|
/** Translate fresh `allowedInjectedRequestData` drafts (the author's input
|
|
88
90
|
* shape, `AllowedJsRequest[]` — same shape the builder translator uses for
|
package/lib/old/to-register.js
CHANGED
|
@@ -89,11 +89,15 @@ function requestDataFrom(providers) {
|
|
|
89
89
|
* this shape and should be passed straight through instead — don't run it
|
|
90
90
|
* through this a second time. */
|
|
91
91
|
export function injectedRequestDataFrom(requests) {
|
|
92
|
-
return requests.map((req) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
return requests.map((req) => {
|
|
93
|
+
return {
|
|
94
|
+
...requestDataFromProvider(req),
|
|
95
|
+
required: req.required ?? true,
|
|
96
|
+
multiple: req.multiple ?? true,
|
|
97
|
+
templateParams: req.templateParams,
|
|
98
|
+
templateParamsMode: req.templateParamsMode,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
97
101
|
}
|
|
98
102
|
/** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
|
|
99
103
|
* drafted request becomes a `requestData` entry; provider-level fields (name)
|
package/lib/old/tools/build.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { authenticateTool } from "./authenticate.js";
|
|
2
2
|
import { getProviderInfoTool } from "./get-provider-info.js";
|
|
3
|
-
import { getProviderVersionConfigTool } from "./get-version-config.js";
|
|
4
3
|
import { getMeProvidersTool } from "./me-providers.js";
|
|
5
4
|
import { createVersionFromCaptureTool } from "./publish.js";
|
|
6
5
|
import { sessionAnalyticsLogsTool } from "./session-analytics-logs.js";
|
|
@@ -20,7 +19,6 @@ export function buildOldBackendTools(client, sdkClient) {
|
|
|
20
19
|
authenticateTool(client),
|
|
21
20
|
createVersionFromCaptureTool(client),
|
|
22
21
|
getMeProvidersTool(client),
|
|
23
|
-
getProviderVersionConfigTool(client),
|
|
24
22
|
getProviderInfoTool(sdkClient),
|
|
25
23
|
updateProviderMetadataTool(client),
|
|
26
24
|
sessionAnalyticsLogsTool(client),
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { type RegisteredTool } from '../../mcp/server.ts';
|
|
2
2
|
import type { ReclaimSdkClient } from '../sdk-client.ts';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* (`api.reclaimprotocol.org`) — the service the
|
|
6
|
-
* itself calls at runtime
|
|
7
|
-
*
|
|
8
|
-
* auth). Both underlying reads are unauthenticated.
|
|
4
|
+
* The single provider-read tool for old-devtools mode. Reads from the PUBLIC
|
|
5
|
+
* production SDK backend (`api.reclaimprotocol.org`) — the service the
|
|
6
|
+
* verification SDK/attestor itself calls at runtime. No auth required, works
|
|
7
|
+
* for ANY provider.
|
|
9
8
|
*
|
|
10
|
-
* Fetches BOTH shapes in one call, since they're complementary
|
|
11
|
-
*
|
|
9
|
+
* Fetches BOTH shapes in one call, since they're complementary and together
|
|
10
|
+
* cover everything a caller needs before re-publishing a new version:
|
|
11
|
+
* - `provider`: the full provider + providerConfig — `customInjection`,
|
|
12
|
+
* `userAgent`, `allowedInjectedRequestData`, `requestData`, everything.
|
|
13
|
+
* This is what you read before create_provider_version_from_capture so
|
|
14
|
+
* you can pass the whole config back (that tool carries nothing forward).
|
|
12
15
|
* - `configs`: the trimmed runtime recipe the attestor actually consumes —
|
|
13
16
|
* `customInjection` deliberately omitted, `isScriptRequestingClaim`
|
|
14
17
|
* computed instead.
|
|
@@ -13,14 +13,17 @@ async function attempt(label, call) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* (`api.reclaimprotocol.org`) — the service the
|
|
18
|
-
* itself calls at runtime
|
|
19
|
-
*
|
|
20
|
-
* auth). Both underlying reads are unauthenticated.
|
|
16
|
+
* The single provider-read tool for old-devtools mode. Reads from the PUBLIC
|
|
17
|
+
* production SDK backend (`api.reclaimprotocol.org`) — the service the
|
|
18
|
+
* verification SDK/attestor itself calls at runtime. No auth required, works
|
|
19
|
+
* for ANY provider.
|
|
21
20
|
*
|
|
22
|
-
* Fetches BOTH shapes in one call, since they're complementary
|
|
23
|
-
*
|
|
21
|
+
* Fetches BOTH shapes in one call, since they're complementary and together
|
|
22
|
+
* cover everything a caller needs before re-publishing a new version:
|
|
23
|
+
* - `provider`: the full provider + providerConfig — `customInjection`,
|
|
24
|
+
* `userAgent`, `allowedInjectedRequestData`, `requestData`, everything.
|
|
25
|
+
* This is what you read before create_provider_version_from_capture so
|
|
26
|
+
* you can pass the whole config back (that tool carries nothing forward).
|
|
24
27
|
* - `configs`: the trimmed runtime recipe the attestor actually consumes —
|
|
25
28
|
* `customInjection` deliberately omitted, `isScriptRequestingClaim`
|
|
26
29
|
* computed instead.
|
|
@@ -31,15 +34,18 @@ export function getProviderInfoTool(client) {
|
|
|
31
34
|
description: 'Read a PUBLISHED provider from the public production SDK backend ' +
|
|
32
35
|
'(api.reclaimprotocol.org — what the verification SDK/attestor ' +
|
|
33
36
|
'itself fetches at runtime), for ANY provider, no auth required. ' +
|
|
34
|
-
'
|
|
35
|
-
'devtools DASHBOARD backend and requires auth. \n\n' +
|
|
37
|
+
'This is the provider-read tool for old-devtools mode. \n\n' +
|
|
36
38
|
'Returns two complementary reads for the given version (latest ' +
|
|
37
|
-
'when `versionNumber` is omitted): `provider` (the
|
|
38
|
-
'+ providerConfig
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'`
|
|
42
|
-
'
|
|
39
|
+
'when `versionNumber` is omitted): `provider` (the FULL provider ' +
|
|
40
|
+
'+ providerConfig — `customInjection`, `userAgent`, ' +
|
|
41
|
+
'`allowedInjectedRequestData`, `requestData`, everything) and ' +
|
|
42
|
+
'`configs` (the trimmed recipe the attestor actually consumes — ' +
|
|
43
|
+
'`customInjection` is deliberately OMITTED there, replaced by a ' +
|
|
44
|
+
'computed `isScriptRequestingClaim` flag). Read `provider` here ' +
|
|
45
|
+
'BEFORE create_provider_version_from_capture so you can pass the ' +
|
|
46
|
+
'whole existing config back — that tool always creates a new ' +
|
|
47
|
+
'version and carries nothing forward. One endpoint failing (e.g. ' +
|
|
48
|
+
'an invalid `versionNumber`) is reported under `errors` without ' +
|
|
43
49
|
'hiding a result the other endpoint did return.',
|
|
44
50
|
inputSchema: {
|
|
45
51
|
type: 'object',
|
package/lib/old/tools/publish.js
CHANGED
|
@@ -4,53 +4,6 @@ import { INJECTION_GUIDE } from "../../provider/injection-doc.js";
|
|
|
4
4
|
import { countsFor, matchRedactionWarnings, } from "../../provider/match-redaction-warning.js";
|
|
5
5
|
import { bumpPatchString, extractVersions, injectedRequestDataFrom, providerToConfigBody, providerToRegisterBody, } from "../to-register.js";
|
|
6
6
|
const DEFAULT_VERSION_INFO = 'Updated from MCP browser capture';
|
|
7
|
-
/** Fields with no partial-patch equivalent on this backend: a version always
|
|
8
|
-
* replaces the WHOLE `providerConfig`, so omitting one of these on
|
|
9
|
-
* add-version must carry forward the current value rather than publish it
|
|
10
|
-
* blank. `allowedInjectedRequestData` is handled alongside these (further
|
|
11
|
-
* down, inline) since it needs translation when freshly authored, unlike
|
|
12
|
-
* these — which pass through unchanged either way. */
|
|
13
|
-
const PRESERVABLE_KEYS = [
|
|
14
|
-
'customInjection',
|
|
15
|
-
'userAgent',
|
|
16
|
-
'pageTitle',
|
|
17
|
-
'stepsToFollow',
|
|
18
|
-
'useIncognitoWebview',
|
|
19
|
-
'extensionConfig',
|
|
20
|
-
];
|
|
21
|
-
/** Pull `providerConfig` out of a `get_provider_version_config` /
|
|
22
|
-
* `getProviderConfig` response, tolerant of a missing/malformed shape. */
|
|
23
|
-
function extractProviderConfig(resp) {
|
|
24
|
-
if (resp && typeof resp === 'object') {
|
|
25
|
-
const cfg = resp.providerConfig;
|
|
26
|
-
if (cfg && typeof cfg === 'object') {
|
|
27
|
-
return cfg;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
/** Prefer the caller's explicit value; otherwise fall back to `current`'s
|
|
33
|
-
* value (if any) and record `fieldName` as carried forward.
|
|
34
|
-
*
|
|
35
|
-
* `current === null` is treated the SAME as `undefined` (nothing to carry),
|
|
36
|
-
* not as a real value to preserve. These are all optional fields with no
|
|
37
|
-
* Prisma default — a version where one was never set reads back as
|
|
38
|
-
* `null`, not a missing key, because Prisma normalizes an absent optional
|
|
39
|
-
* field to `null` on read. Carrying that `null` "forward" would falsely
|
|
40
|
-
* claim the field was preserved when really nothing was ever there — it
|
|
41
|
-
* most likely means an EARLIER version already lost it (e.g. published
|
|
42
|
-
* before this field had carry-forward support at all), not that this call
|
|
43
|
-
* is doing anything wrong. */
|
|
44
|
-
function resolveField(explicit, current, fieldName, carried) {
|
|
45
|
-
if (explicit !== undefined) {
|
|
46
|
-
return explicit;
|
|
47
|
-
}
|
|
48
|
-
if (current !== undefined && current !== null) {
|
|
49
|
-
carried.push(fieldName);
|
|
50
|
-
return current;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
7
|
/**
|
|
55
8
|
* The old backend lets you add a version only to a provider YOU own. A
|
|
56
9
|
* `providerId` for a public provider (or anyone else's) comes back as
|
|
@@ -111,23 +64,19 @@ export function createVersionFromCaptureTool(client) {
|
|
|
111
64
|
'For data gated behind login/navigation, pass `customInjection` — a ' +
|
|
112
65
|
'page-injection script (old-devtools\' name for the builder\'s ' +
|
|
113
66
|
'jsUserScripts) that drives the page to where the request fires. \n\n' +
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'explicit value (`""`, `[]`, `false`, ...) to intentionally clear ' +
|
|
128
|
-
'one of them instead, or call get_provider_version_config FIRST ' +
|
|
129
|
-
'to see exactly what is live today, rather than assuming this ' +
|
|
130
|
-
'call recovered something that may already be gone. ' +
|
|
67
|
+
'BY DESIGN, this backend only ever CREATES A NEW VERSION (POST) — ' +
|
|
68
|
+
'there is no partial-patch/edit endpoint, and this tool does NOT ' +
|
|
69
|
+
'read or carry anything forward from the previous version. Every ' +
|
|
70
|
+
'new version replaces the WHOLE providerConfig with exactly what ' +
|
|
71
|
+
'you pass in THIS call. Therefore any field you want present in the ' +
|
|
72
|
+
'new version MUST be supplied here — `customInjection`, `userAgent`, ' +
|
|
73
|
+
'`pageTitle`, `allowedInjectedRequestData`, `stepsToFollow`, ' +
|
|
74
|
+
'`useIncognitoWebview`, `extensionConfig`, and `requestData` (via ' +
|
|
75
|
+
'`provider`/`providers`). Anything you OMIT is published blank/' +
|
|
76
|
+
'default in the new version, NOT preserved. To change one field ' +
|
|
77
|
+
'while keeping the rest, first read the current version with ' +
|
|
78
|
+
'get_provider_info, then pass the full desired config (the existing ' +
|
|
79
|
+
'values plus your change) back into this call. ' +
|
|
131
80
|
'`allowedInjectedRequestData`/`useIncognitoWebview`/' +
|
|
132
81
|
'`extensionConfig` only take effect on add-version — the register ' +
|
|
133
82
|
'endpoint (no `providerId`) ignores them. \n\n' +
|
|
@@ -250,6 +199,26 @@ export function createVersionFromCaptureTool(client) {
|
|
|
250
199
|
type: 'boolean',
|
|
251
200
|
description: 'Fail validation if none match. Default true.',
|
|
252
201
|
},
|
|
202
|
+
templateParams: {
|
|
203
|
+
type: 'array',
|
|
204
|
+
items: { type: 'string' },
|
|
205
|
+
description: 'Names of `${var}` placeholders used in this ' +
|
|
206
|
+
'template\'s `responseMatches`/`responseRedactions` ' +
|
|
207
|
+
'that should be substituted at verify time from the ' +
|
|
208
|
+
'submitted proof\'s own witness params. Omit (or ' +
|
|
209
|
+
'leave empty) if the template has no such ' +
|
|
210
|
+
'placeholders.',
|
|
211
|
+
},
|
|
212
|
+
templateParamsMode: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
enum: ['separate', 'merge'],
|
|
215
|
+
description: 'How multiple values for `templateParams` expand ' +
|
|
216
|
+
'into request specs. `separate` (default): one ' +
|
|
217
|
+
'independent spec per value, each matching its own ' +
|
|
218
|
+
'proof. `merge`: fold all values into ONE spec with ' +
|
|
219
|
+
'one match/redaction entry per value, matching a ' +
|
|
220
|
+
'single proof that bundles them all into one claim.',
|
|
221
|
+
},
|
|
253
222
|
},
|
|
254
223
|
required: ['url', 'method'],
|
|
255
224
|
},
|
|
@@ -281,22 +250,22 @@ export function createVersionFromCaptureTool(client) {
|
|
|
281
250
|
'(an array of 1+ drafts).', { statusCode: 400 });
|
|
282
251
|
}
|
|
283
252
|
const providers = hasMany ? args.providers : [args.provider];
|
|
284
|
-
const opts = {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
253
|
+
const opts = {
|
|
254
|
+
initialUrl: args.initialUrl,
|
|
255
|
+
description: args.description,
|
|
256
|
+
geoLocation: args.geoLocation,
|
|
257
|
+
useProxy: args.useProxy,
|
|
258
|
+
providerType: args.providerType,
|
|
259
|
+
customInjection: args.customInjection,
|
|
260
|
+
userAgent: args.userAgent,
|
|
261
|
+
pageTitle: args.pageTitle,
|
|
262
|
+
stepsToFollow: args.stepsToFollow,
|
|
263
|
+
useIncognitoWebview: args.useIncognitoWebview,
|
|
264
|
+
extensionConfig: args.extensionConfig,
|
|
265
|
+
allowedInjectedRequestData: args.allowedInjectedRequestData
|
|
266
|
+
? injectedRequestDataFrom(args.allowedInjectedRequestData)
|
|
267
|
+
: undefined
|
|
268
|
+
};
|
|
300
269
|
// Non-blocking advisory per request — never alters or blocks the
|
|
301
270
|
// publish; the author keeps full control. Attaches under `_notes`.
|
|
302
271
|
const notes = matchRedactionWarnings(providers.map((p, i) => countsFor(`Request ${i + 1}`, p)));
|
|
@@ -325,20 +294,13 @@ export function createVersionFromCaptureTool(client) {
|
|
|
325
294
|
const ignoredOnRegister = REGISTER_IGNORED_KEYS
|
|
326
295
|
.filter((k) => args[k] !== undefined);
|
|
327
296
|
if (ignoredOnRegister.length) {
|
|
328
|
-
notes.push(`${ignoredOnRegister.join(', ')}
|
|
297
|
+
notes.push(`${ignoredOnRegister.join(', ')} ` +
|
|
298
|
+
`${ignoredOnRegister.length > 1 ? 'are' : 'is'} ` +
|
|
299
|
+
'ignored on register (no providerId) — the old backend only ' +
|
|
329
300
|
'reads them when adding a version. Publish once to create ' +
|
|
330
301
|
'the provider, then call this again WITH the returned ' +
|
|
331
302
|
'providerId to set them.');
|
|
332
303
|
}
|
|
333
|
-
if (args.userAgent !== undefined) {
|
|
334
|
-
opts.userAgent = args.userAgent;
|
|
335
|
-
}
|
|
336
|
-
if (args.pageTitle !== undefined) {
|
|
337
|
-
opts.pageTitle = args.pageTitle;
|
|
338
|
-
}
|
|
339
|
-
if (args.stepsToFollow !== undefined) {
|
|
340
|
-
opts.stepsToFollow = args.stepsToFollow;
|
|
341
|
-
}
|
|
342
304
|
const body = providerToRegisterBody(providers, opts);
|
|
343
305
|
return withNotes(await client.registerProvider(body));
|
|
344
306
|
}
|
|
@@ -348,42 +310,23 @@ export function createVersionFromCaptureTool(client) {
|
|
|
348
310
|
// workaround instead of a bare permission error.
|
|
349
311
|
const providerId = args.providerId;
|
|
350
312
|
try {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|| args.allowedInjectedRequestData === undefined;
|
|
358
|
-
const current = needsCurrent
|
|
359
|
-
? extractProviderConfig(await client.getProviderConfig(providerId))
|
|
360
|
-
: undefined;
|
|
361
|
-
const carried = [];
|
|
362
|
-
for (const key of PRESERVABLE_KEYS) {
|
|
363
|
-
const resolved = resolveField(args[key], current?.[key], key, carried);
|
|
364
|
-
if (resolved !== undefined) {
|
|
365
|
-
opts[key] = resolved;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const freshInjected = args.allowedInjectedRequestData !== undefined
|
|
369
|
-
? injectedRequestDataFrom(args.allowedInjectedRequestData)
|
|
370
|
-
: undefined;
|
|
371
|
-
const resolvedInjected = resolveField(freshInjected, current?.allowedInjectedRequestData, 'allowedInjectedRequestData', carried);
|
|
372
|
-
if (resolvedInjected !== undefined) {
|
|
373
|
-
opts.allowedInjectedRequestData = resolvedInjected;
|
|
374
|
-
}
|
|
375
|
-
if (carried.length) {
|
|
376
|
-
notes.push('Not passed, carried forward unchanged from the current ' +
|
|
377
|
-
`published version: ${carried.join(', ')}. Pass an explicit ` +
|
|
378
|
-
'value ("", [], false, ...) to override any of them, or ' +
|
|
379
|
-
'call get_provider_version_config first to see what is ' +
|
|
380
|
-
'live today.');
|
|
381
|
-
}
|
|
313
|
+
// By design this always POSTs a brand-new version and publishes
|
|
314
|
+
// EXACTLY the config assembled above — nothing is read back or
|
|
315
|
+
// carried forward. Any field the caller omitted is already absent
|
|
316
|
+
// from `opts` and will be blank/default in the new version. To
|
|
317
|
+
// preserve existing fields, the caller must have read them via
|
|
318
|
+
// get_provider_info and passed them back in.
|
|
382
319
|
if (args.customInjection !== undefined && opts.customInjection?.trim()) {
|
|
383
320
|
notes.push('customInjection will run in the verification client before ' +
|
|
384
321
|
'every page load (before navigating to initialUrl), matching ' +
|
|
385
322
|
'the builder\'s jsUserScripts.');
|
|
386
323
|
}
|
|
324
|
+
notes.push('A NEW version was created with only the fields passed in this ' +
|
|
325
|
+
'call — nothing was carried forward from the previous version. ' +
|
|
326
|
+
'If you intended to keep other fields (customInjection, ' +
|
|
327
|
+
'userAgent, allowedInjectedRequestData, ...), read the prior ' +
|
|
328
|
+
'version with get_provider_info and re-publish with them ' +
|
|
329
|
+
'included.');
|
|
387
330
|
const versionsResp = await client.listVersions(providerId);
|
|
388
331
|
const version = bumpPatchString(extractVersions(versionsResp));
|
|
389
332
|
const versionInfo = args.notes && args.notes.trim().length >= 10
|