@reclaimprotocol/agent 0.1.17 → 0.1.20
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/README.md +6 -2
- package/lib/authoring/backend.d.ts +4 -0
- package/lib/authoring/tools.js +4 -1
- package/lib/cdp/capture.d.ts +2 -2
- package/lib/cdp/capture.js +25 -21
- package/lib/cdp/client.d.ts +39 -1
- package/lib/cdp/client.js +125 -1
- package/lib/consts.d.ts +7 -0
- package/lib/consts.js +15 -0
- package/lib/mcp/api/operations.gen.js +33 -9
- package/lib/mcp/instructions.js +27 -3
- package/lib/mcp/start.js +12 -8
- package/lib/mcp/tools/agent/attach.d.ts +18 -4
- package/lib/mcp/tools/agent/attach.js +142 -52
- package/lib/mcp/tools/agent/build.d.ts +1 -1
- package/lib/mcp/tools/agent/build.js +5 -3
- package/lib/mcp/tools/agent/capture.d.ts +1 -0
- package/lib/mcp/tools/agent/capture.js +8 -1
- package/lib/mcp/tools/agent/eval.js +2 -3
- package/lib/mcp/tools/agent/how-it-works.d.ts +1 -1
- package/lib/mcp/tools/agent/how-it-works.js +1 -1
- package/lib/mcp/tools/agent/publish.js +55 -1
- package/lib/mcp/tools/agent/session.d.ts +2 -2
- package/lib/mcp/tools/agent/session.js +21 -5
- package/lib/mcp/tools/agent/shared.js +4 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +1 -1
- package/lib/mcp/tools/authenticate/authenticate.js +10 -4
- package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -0
- package/lib/mcp/tools/authenticate/builder-access.js +31 -0
- package/lib/mcp/tools/authenticate/index.d.ts +1 -0
- package/lib/mcp/tools/authenticate/index.js +1 -0
- package/lib/mcp/tools/mode/mode.d.ts +3 -2
- package/lib/mcp/tools/mode/mode.js +24 -4
- package/lib/old/login-cdp.js +11 -3
- package/lib/old/to-register.d.ts +16 -8
- package/lib/old/to-register.js +29 -21
- package/lib/old/tools/publish.js +109 -9
- package/lib/provider/draft.d.ts +20 -0
- package/lib/provider/draft.js +75 -51
- package/lib/provider/index.d.ts +1 -1
- package/lib/provider/index.js +1 -1
- package/lib/provider/injection-doc.js +4 -3
- package/lib/provider/schema.d.ts +26 -1
- package/lib/provider/secrets.d.ts +37 -0
- package/lib/provider/secrets.js +147 -6
- package/lib/provider/to-version.d.ts +26 -2
- package/lib/provider/to-version.js +29 -5
- package/package.json +2 -2
- package/skill/SKILL.builder.md +43 -95
- package/skill/SKILL.common.md +102 -0
- package/skill/SKILL.old.md +20 -74
- package/skill/builder-mode.md +4 -2
- package/skill/injection-api.md +95 -10
- package/skill/old-mode.md +5 -3
- package/src/authoring/backend.ts +4 -0
- package/src/authoring/tools.ts +4 -1
- package/src/cdp/capture.ts +24 -24
- package/src/cdp/client.ts +197 -2
- package/src/consts.ts +20 -0
- package/src/mcp/api/operations.gen.ts +33 -9
- package/src/mcp/instructions.ts +36 -3
- package/src/mcp/start.ts +13 -8
- package/src/mcp/tools/agent/attach.ts +198 -57
- package/src/mcp/tools/agent/build.ts +9 -2
- package/src/mcp/tools/agent/capture.ts +10 -1
- package/src/mcp/tools/agent/eval.ts +2 -3
- package/src/mcp/tools/agent/how-it-works.ts +1 -1
- package/src/mcp/tools/agent/publish.ts +64 -1
- package/src/mcp/tools/agent/session.ts +29 -8
- package/src/mcp/tools/agent/shared.ts +5 -10
- package/src/mcp/tools/authenticate/authenticate.ts +14 -4
- package/src/mcp/tools/authenticate/builder-access.ts +42 -0
- package/src/mcp/tools/authenticate/index.ts +4 -0
- package/src/mcp/tools/mode/mode.ts +28 -4
- package/src/old/login-cdp.ts +13 -3
- package/src/old/to-register.ts +50 -27
- package/src/old/tools/publish.ts +131 -9
- package/src/provider/draft.ts +98 -56
- package/src/provider/index.ts +9 -1
- package/src/provider/injection-doc.ts +4 -3
- package/src/provider/schema.ts +27 -1
- package/src/provider/secrets.ts +171 -6
- package/src/provider/to-version.ts +53 -7
- package/lib/authoring/find.d.ts +0 -3
- package/lib/authoring/find.js +0 -54
- package/lib/authoring/propose.d.ts +0 -3
- package/lib/authoring/propose.js +0 -109
- package/lib/authoring/replay.d.ts +0 -10
- package/lib/authoring/replay.js +0 -34
- package/lib/cli/args.d.ts +0 -14
- package/lib/cli/args.js +0 -19
- package/lib/cli/commands.d.ts +0 -1
- package/lib/cli/commands.js +0 -19
- package/lib/cli.d.ts +0 -1
- package/lib/cli.js +0 -7
- package/lib/mcp/credentials/authenticate.d.ts +0 -3
- package/lib/mcp/credentials/authenticate.js +0 -108
- package/lib/mcp/credentials/build.d.ts +0 -5
- package/lib/mcp/credentials/build.js +0 -18
- package/lib/mcp/credentials/cache.d.ts +0 -70
- package/lib/mcp/credentials/cache.js +0 -131
- package/lib/mcp/credentials/env-file.d.ts +0 -25
- package/lib/mcp/credentials/env-file.js +0 -71
- package/lib/mcp/credentials/import.d.ts +0 -2
- package/lib/mcp/credentials/import.js +0 -52
- package/lib/mcp/credentials/issue.d.ts +0 -2
- package/lib/mcp/credentials/issue.js +0 -84
- package/lib/mcp/credentials/keypair.d.ts +0 -17
- package/lib/mcp/credentials/keypair.js +0 -48
- package/lib/mcp/credentials/link.d.ts +0 -3
- package/lib/mcp/credentials/link.js +0 -107
- package/lib/mcp/credentials/list-cached.d.ts +0 -2
- package/lib/mcp/credentials/list-cached.js +0 -55
- package/lib/mcp/credentials/owner-key.d.ts +0 -23
- package/lib/mcp/credentials/owner-key.js +0 -58
- package/lib/mcp/credentials/paths.d.ts +0 -12
- package/lib/mcp/credentials/paths.js +0 -35
- package/lib/mcp/credentials/poll.d.ts +0 -27
- package/lib/mcp/credentials/poll.js +0 -61
- package/lib/mcp/credentials/register.d.ts +0 -18
- package/lib/mcp/credentials/register.js +0 -30
- package/lib/mcp/credentials/resolve-owner-key.d.ts +0 -12
- package/lib/mcp/credentials/resolve-owner-key.js +0 -68
- package/lib/mcp/credentials/session-cache.d.ts +0 -14
- package/lib/mcp/credentials/session-cache.js +0 -42
- package/lib/mcp/credentials/status.d.ts +0 -3
- package/lib/mcp/credentials/status.js +0 -76
- package/lib/mcp/register.d.ts +0 -38
- package/lib/mcp/register.js +0 -80
- package/lib/mcp/tools/analyze.d.ts +0 -3
- package/lib/mcp/tools/analyze.js +0 -26
- package/lib/mcp/tools/attach.d.ts +0 -11
- package/lib/mcp/tools/attach.js +0 -72
- package/lib/mcp/tools/capture.d.ts +0 -10
- package/lib/mcp/tools/capture.js +0 -142
- package/lib/mcp/tools/eval.d.ts +0 -5
- package/lib/mcp/tools/eval.js +0 -127
- package/lib/mcp/tools/find.d.ts +0 -3
- package/lib/mcp/tools/find.js +0 -60
- package/lib/mcp/tools/owner-key.d.ts +0 -2
- package/lib/mcp/tools/owner-key.js +0 -63
- package/lib/mcp/tools/proof.d.ts +0 -3
- package/lib/mcp/tools/proof.js +0 -214
- package/lib/mcp/tools/propose.d.ts +0 -9
- package/lib/mcp/tools/propose.js +0 -75
- package/lib/mcp/tools/publish.d.ts +0 -3
- package/lib/mcp/tools/publish.js +0 -59
- package/lib/mcp/tools/replay.d.ts +0 -3
- package/lib/mcp/tools/replay.js +0 -46
- package/lib/mcp/tools/session.d.ts +0 -10
- package/lib/mcp/tools/session.js +0 -29
- package/lib/mcp/tools/verification/diagnose.d.ts +0 -11
- package/lib/mcp/tools/verification/diagnose.js +0 -132
- package/lib/mcp/verifications/build.d.ts +0 -5
- package/lib/mcp/verifications/build.js +0 -14
- package/lib/mcp/verifications/create.d.ts +0 -3
- package/lib/mcp/verifications/create.js +0 -57
- package/lib/mcp/verifications/find.d.ts +0 -3
- package/lib/mcp/verifications/find.js +0 -37
- package/lib/mcp/verifications/get.d.ts +0 -3
- package/lib/mcp/verifications/get.js +0 -26
- package/lib/mcp/verifications/update.d.ts +0 -3
- package/lib/mcp/verifications/update.js +0 -78
- package/lib/old/tools/get-version-config.d.ts +0 -17
- package/lib/old/tools/get-version-config.js +0 -47
- package/lib/routes.gen.d.ts +0 -271
- package/lib/routes.gen.js +0 -73
- package/lib/types/openapi.d.ts +0 -4
- package/lib/types/openapi.gen.d.ts +0 -3967
- package/lib/types/openapi.gen.js +0 -5
- package/lib/types/openapi.js +0 -1
package/lib/old/login-cdp.js
CHANGED
|
@@ -113,15 +113,14 @@ export async function captureFirebaseTokenViaCdp(opts) {
|
|
|
113
113
|
const { targetId } = await Target.createTarget({ url: loginUrl });
|
|
114
114
|
try {
|
|
115
115
|
const session = await handles.attachToTab(targetId);
|
|
116
|
-
const { Runtime } = session;
|
|
117
116
|
const deadline = Date.now() + timeoutMs;
|
|
118
117
|
while (Date.now() < deadline) {
|
|
119
|
-
const
|
|
118
|
+
const result = await session.send('Runtime.evaluate', {
|
|
120
119
|
expression: EXTRACT_TOKEN_JS,
|
|
121
120
|
awaitPromise: true,
|
|
122
121
|
returnByValue: true,
|
|
123
122
|
});
|
|
124
|
-
const value =
|
|
123
|
+
const value = runtimeResultValue(result);
|
|
125
124
|
if (typeof value === 'string' && value.length > 0) {
|
|
126
125
|
return { token: value, closedBrowser: launchedByUs };
|
|
127
126
|
}
|
|
@@ -146,3 +145,12 @@ export async function captureFirebaseTokenViaCdp(opts) {
|
|
|
146
145
|
await handles.close().catch(() => { });
|
|
147
146
|
}
|
|
148
147
|
}
|
|
148
|
+
function runtimeResultValue(value) {
|
|
149
|
+
if (typeof value !== 'object' || !value || !('result' in value)) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const result = value.result;
|
|
153
|
+
return typeof result === 'object' && result && 'value' in result
|
|
154
|
+
? result.value
|
|
155
|
+
: undefined;
|
|
156
|
+
}
|
package/lib/old/to-register.d.ts
CHANGED
|
@@ -3,15 +3,15 @@ import type { AllowedJsRequest } from '../provider/to-version.ts';
|
|
|
3
3
|
import type { RegisterProviderBody, UpdateProviderConfigBody } from './client.ts';
|
|
4
4
|
/**
|
|
5
5
|
* Translate a drafted `ReclaimProvider` (from `propose_provider`) into the
|
|
6
|
-
*
|
|
6
|
+
* devtools backend shapes. Deliberately a SEPARATE module from
|
|
7
7
|
* `provider/to-version.ts` (the builder translator) — the two backends have
|
|
8
8
|
* structurally different bodies:
|
|
9
9
|
*
|
|
10
10
|
* - builder NESTS each redaction into its match's `extract`;
|
|
11
|
-
* -
|
|
11
|
+
* - devtools keeps `responseMatches` and `responseRedactions` as
|
|
12
12
|
* PARALLEL arrays on each `requestData` entry.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Devtools limit we enforce here (a clear error beats a confusing
|
|
15
15
|
* server-side 400): only `GET` / `POST` methods. OPRF redactions are passed
|
|
16
16
|
* through unchanged — the backend's `hash` field accepts them.
|
|
17
17
|
*/
|
|
@@ -23,7 +23,7 @@ export interface OldProviderOpts {
|
|
|
23
23
|
/** Defaults to `true` when omitted. Register only (Provider-level flag). */
|
|
24
24
|
useProxy?: boolean;
|
|
25
25
|
providerType?: 'PRIVATE' | 'PUBLIC';
|
|
26
|
-
/** Page-injection script —
|
|
26
|
+
/** Page-injection script — the devtools name for the builder's
|
|
27
27
|
* `webSettings.jsUserScripts`. Top-level on register, nested under
|
|
28
28
|
* `providerConfig` on config (add-version). The backend rejects a few
|
|
29
29
|
* deprecated injection APIs (e.g. `window.payloadData`) with a 400. */
|
|
@@ -50,6 +50,14 @@ export interface OldProviderOpts {
|
|
|
50
50
|
/** Config (add-version) ONLY — the register endpoint never reads this
|
|
51
51
|
* field from the request body. Free-form JSON. */
|
|
52
52
|
extensionConfig?: unknown;
|
|
53
|
+
/** Provider-level `disableRequestReplay` (HAWKEYE document replay skip).
|
|
54
|
+
* Config (add-version) ONLY: the register endpoint hardcodes it to false
|
|
55
|
+
* — the publish tool follows a register up with an add-version call to
|
|
56
|
+
* apply it for new providers (see `old/tools/publish.ts`). NOTE the
|
|
57
|
+
* builder backend has NO equivalent; its inverse is
|
|
58
|
+
* `webSettings.clientOptions.inapp.interceptorOptions
|
|
59
|
+
* .isDocumentRequestReplayEnabled`. */
|
|
60
|
+
disableRequestReplay?: boolean;
|
|
53
61
|
}
|
|
54
62
|
interface OldResponseMatch {
|
|
55
63
|
value: string;
|
|
@@ -68,11 +76,11 @@ interface OldResponseRedaction {
|
|
|
68
76
|
}
|
|
69
77
|
interface OldRequestData {
|
|
70
78
|
url: string;
|
|
71
|
-
urlType: 'CONSTANT' | 'TEMPLATE';
|
|
79
|
+
urlType: 'REGEX' | 'CONSTANT' | 'TEMPLATE';
|
|
72
80
|
method: 'GET' | 'POST';
|
|
73
81
|
responseMatches: OldResponseMatch[];
|
|
74
82
|
responseRedactions: OldResponseRedaction[];
|
|
75
|
-
credentials: 'include';
|
|
83
|
+
credentials: 'omit' | 'same-origin' | 'include';
|
|
76
84
|
bodySniff?: {
|
|
77
85
|
enabled: boolean;
|
|
78
86
|
template: string;
|
|
@@ -88,7 +96,7 @@ export interface OldInjectedRequestData extends OldRequestData {
|
|
|
88
96
|
}
|
|
89
97
|
/** Translate fresh `allowedInjectedRequestData` drafts (the author's input
|
|
90
98
|
* shape, `AllowedJsRequest[]` — same shape the builder translator uses for
|
|
91
|
-
* its own `allowedJsRequests`) into the
|
|
99
|
+
* its own `allowedJsRequests`) into the devtools backend's shape. A
|
|
92
100
|
* carried-forward value read back from an existing version is ALREADY in
|
|
93
101
|
* this shape and should be passed straight through instead — don't run it
|
|
94
102
|
* through this a second time. */
|
|
@@ -101,7 +109,7 @@ export declare function providerToRegisterBody(providers: ReclaimProvider[], opt
|
|
|
101
109
|
* an EXISTING provider. `version` must not already exist; `versionInfo` is
|
|
102
110
|
* required and must be ≥10 chars (enforced by the backend). */
|
|
103
111
|
export declare function providerToConfigBody(providers: ReclaimProvider[], opts: OldProviderOpts, version: string, versionInfo: string): UpdateProviderConfigBody;
|
|
104
|
-
/** Minimal version triplet as the
|
|
112
|
+
/** Minimal version triplet as the devtools backend stores it. */
|
|
105
113
|
export interface SemverTriplet {
|
|
106
114
|
major: number;
|
|
107
115
|
minor: number;
|
package/lib/old/to-register.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
1
2
|
/** Default web settings, mirroring the builder openapi defaults
|
|
2
|
-
* (`ProviderWebSettings.geoLocation`, `PortalClientOptions.useProxy`). The
|
|
3
|
+
* (`ProviderWebSettings.geoLocation`, `PortalClientOptions.useProxy`). The
|
|
3
4
|
* backend accepts both: `geoLocation` lives on the providerConfig, `useProxy`
|
|
4
5
|
* is a Provider-level flag (register only). */
|
|
5
6
|
const DEFAULT_GEO_LOCATION = '{{DYNAMIC_GEO}}';
|
|
@@ -10,29 +11,29 @@ const DEFAULT_USE_PROXY = true;
|
|
|
10
11
|
* entries — a `ReclaimProvider` satisfies it structurally either way. */
|
|
11
12
|
function requestDataFromProvider(provider) {
|
|
12
13
|
const method = String(provider.method).toUpperCase();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
// The old backend has no dotted interpolation: consumer-supplied params
|
|
14
|
+
assert(method === 'GET' || method === 'POST', new Error('Devtools supports only GET/POST requests; got ' +
|
|
15
|
+
`"${provider.method}". Re-capture against a GET/POST endpoint.`));
|
|
16
|
+
// The devtools backend has no dotted interpolation: consumer-supplied params
|
|
18
17
|
// must use the `{{context_<name>}}` form here, not the builder's
|
|
19
18
|
// `{{context.<name>}}`. Catch the mix-up at publish with a clear error
|
|
20
19
|
// instead of a template that silently never resolves.
|
|
21
20
|
const dotted = `${provider.url}\n${provider.body ?? ''}`
|
|
22
21
|
.match(/\{\{context\.[A-Za-z_][A-Za-z0-9_]*\}\}/);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'for this backend.');
|
|
27
|
-
}
|
|
22
|
+
assert(!dotted, new Error(`Devtools does not support ${dotted?.[0]} — rename ` +
|
|
23
|
+
'consumer-supplied params to the {{context_<name>}} form ' +
|
|
24
|
+
'for this backend.'));
|
|
28
25
|
const responseMatches = (provider.responseMatches ?? [])
|
|
29
26
|
.map((m, i) => ({
|
|
30
27
|
value: m.value,
|
|
31
28
|
type: m.type ?? 'contains',
|
|
32
|
-
isOptional
|
|
29
|
+
// The backend honors isOptional on BOTH register and add-version
|
|
30
|
+
// (responseMatches pass through verbatim) — forward the author's
|
|
31
|
+
// flag, default false. (`invert` has no old-backend equivalent and
|
|
32
|
+
// stays dropped.)
|
|
33
|
+
isOptional: m.isOptional ?? false,
|
|
33
34
|
order: i,
|
|
34
35
|
}));
|
|
35
|
-
//
|
|
36
|
+
// Devtools wants `responseMatches` and `responseRedactions` as PARALLEL
|
|
36
37
|
// arrays — same shape the builder uses, so pass them through directly. We do
|
|
37
38
|
// NOT enforce any match/redaction count relationship here (and never pad —
|
|
38
39
|
// empty placeholder matches break proof generation): the author keeps full
|
|
@@ -58,33 +59,35 @@ function requestDataFromProvider(provider) {
|
|
|
58
59
|
});
|
|
59
60
|
const requestData = {
|
|
60
61
|
url: provider.url,
|
|
61
|
-
// `{{param}}` placeholders → TEMPLATE
|
|
62
|
-
//
|
|
63
|
-
urlType: provider.
|
|
62
|
+
// Explicit override wins; otherwise `{{param}}` placeholders → TEMPLATE
|
|
63
|
+
// so the backend templates them, else a fixed CONSTANT url.
|
|
64
|
+
urlType: provider.urlType
|
|
65
|
+
?? (provider.url.includes('{{') ? 'TEMPLATE' : 'CONSTANT'),
|
|
64
66
|
method: method,
|
|
65
67
|
responseMatches,
|
|
66
68
|
responseRedactions,
|
|
67
69
|
// The verification client re-supplies cookies/auth at runtime; secrets
|
|
68
70
|
// are never persisted (propose_provider already stripped them).
|
|
69
|
-
credentials: 'include',
|
|
71
|
+
credentials: provider.credentials ?? 'include',
|
|
70
72
|
};
|
|
71
73
|
if (provider.body !== undefined) {
|
|
72
74
|
requestData.bodySniff = { enabled: true, template: provider.body };
|
|
73
75
|
}
|
|
74
|
-
// NOTE: `responseVariables` is intentionally omitted — the
|
|
76
|
+
// NOTE: `responseVariables` is intentionally omitted — the devtools backend
|
|
75
77
|
// auto-extracts it from `{{var}}` placeholders in responseMatches. Sending
|
|
76
78
|
// it ourselves would risk drifting from the server's own extraction.
|
|
77
79
|
return requestData;
|
|
78
80
|
}
|
|
79
81
|
/** One `requestData` entry per drafted provider. A multi-request provider
|
|
80
|
-
* becomes parallel entries that the
|
|
81
|
-
* (the same way the builder version carries multiple
|
|
82
|
+
* becomes parallel entries that the devtools backend runs together at
|
|
83
|
+
* verification (the same way the builder version carries multiple
|
|
84
|
+
* `requests`). */
|
|
82
85
|
function requestDataFrom(providers) {
|
|
83
86
|
return providers.map(requestDataFromProvider);
|
|
84
87
|
}
|
|
85
88
|
/** Translate fresh `allowedInjectedRequestData` drafts (the author's input
|
|
86
89
|
* shape, `AllowedJsRequest[]` — same shape the builder translator uses for
|
|
87
|
-
* its own `allowedJsRequests`) into the
|
|
90
|
+
* its own `allowedJsRequests`) into the devtools backend's shape. A
|
|
88
91
|
* carried-forward value read back from an existing version is ALREADY in
|
|
89
92
|
* this shape and should be passed straight through instead — don't run it
|
|
90
93
|
* through this a second time. */
|
|
@@ -165,6 +168,11 @@ export function providerToConfigBody(providers, opts, version, versionInfo) {
|
|
|
165
168
|
if (opts.extensionConfig !== undefined) {
|
|
166
169
|
providerConfig.extensionConfig = opts.extensionConfig;
|
|
167
170
|
}
|
|
171
|
+
// The config (add-version) controller persists providerConfig verbatim,
|
|
172
|
+
// so this lands as-is; the register controller ignores it entirely.
|
|
173
|
+
if (opts.disableRequestReplay !== undefined) {
|
|
174
|
+
providerConfig.disableRequestReplay = opts.disableRequestReplay;
|
|
175
|
+
}
|
|
168
176
|
return { providerConfig, version, versionInfo };
|
|
169
177
|
}
|
|
170
178
|
/**
|
package/lib/old/tools/publish.js
CHANGED
|
@@ -4,6 +4,12 @@ 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
|
+
/** Pull the new provider's id out of a register response, tolerant of the
|
|
8
|
+
* envelope (`{providerId}`, `{provider:{providerId}}`, `{id}`). */
|
|
9
|
+
function extractProviderId(resp) {
|
|
10
|
+
const obj = resp;
|
|
11
|
+
return obj?.providerId ?? obj?.provider?.providerId ?? obj?.id;
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* The old backend lets you add a version only to a provider YOU own. A
|
|
9
15
|
* `providerId` for a public provider (or anyone else's) comes back as
|
|
@@ -44,7 +50,7 @@ function rethrowOwnershipError(err, providerId) {
|
|
|
44
50
|
export function createVersionFromCaptureTool(client) {
|
|
45
51
|
return defineTool({
|
|
46
52
|
name: 'create_provider_version_from_capture',
|
|
47
|
-
description: 'Publish drafted ReclaimProvider(s) to the
|
|
53
|
+
description: 'Publish drafted ReclaimProvider(s) to the devtools backend. ' +
|
|
48
54
|
'Pass the `provider` object from propose_provider VERBATIM and the ' +
|
|
49
55
|
'`initialUrl` the user landed on (the login/landing page, NOT the ' +
|
|
50
56
|
'API url). \n\n' +
|
|
@@ -62,7 +68,7 @@ export function createVersionFromCaptureTool(client) {
|
|
|
62
68
|
"PUBLIC provider you don't own, omit `providerId` to register your " +
|
|
63
69
|
'own copy (you cannot edit someone else\'s provider here). \n\n' +
|
|
64
70
|
'For data gated behind login/navigation, pass `customInjection` — a ' +
|
|
65
|
-
'page-injection script (
|
|
71
|
+
'page-injection script (the devtools name for the builder\'s ' +
|
|
66
72
|
'jsUserScripts) that drives the page to where the request fires. \n\n' +
|
|
67
73
|
'BY DESIGN, this backend only ever CREATES A NEW VERSION (POST) — ' +
|
|
68
74
|
'there is no partial-patch/edit endpoint, and this tool does NOT ' +
|
|
@@ -79,7 +85,14 @@ export function createVersionFromCaptureTool(client) {
|
|
|
79
85
|
'values plus your change) back into this call. ' +
|
|
80
86
|
'`allowedInjectedRequestData`/`useIncognitoWebview`/' +
|
|
81
87
|
'`extensionConfig` only take effect on add-version — the register ' +
|
|
82
|
-
'endpoint (no `providerId`) ignores them.
|
|
88
|
+
'endpoint (no `providerId`) ignores them. `disableRequestReplay` ' +
|
|
89
|
+
'is also add-version-only, but when it is true the tool follows ' +
|
|
90
|
+
'a register up with an add-version call automatically so it takes ' +
|
|
91
|
+
'effect on new providers too. Per-request fields you may set on ' +
|
|
92
|
+
'`provider`/`providers`: `credentials` (omit/same-origin/include, ' +
|
|
93
|
+
'default include), `urlType` (REGEX/CONSTANT/TEMPLATE, default ' +
|
|
94
|
+
'derived from {{placeholders}}), and `isOptional` on each ' +
|
|
95
|
+
'responseMatch (default false). \n\n' +
|
|
83
96
|
'Captured requests must be GET/POST. OPRF redactions are supported.',
|
|
84
97
|
inputSchema: {
|
|
85
98
|
type: 'object',
|
|
@@ -94,6 +107,19 @@ export function createVersionFromCaptureTool(client) {
|
|
|
94
107
|
method: { type: 'string' },
|
|
95
108
|
headers: { type: 'object' },
|
|
96
109
|
body: { type: 'string' },
|
|
110
|
+
credentials: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
enum: ['omit', 'same-origin', 'include'],
|
|
113
|
+
description: 'How the verification client supplies cookies/auth ' +
|
|
114
|
+
'when replaying this request. Default `include`.',
|
|
115
|
+
},
|
|
116
|
+
urlType: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
enum: ['REGEX', 'CONSTANT', 'TEMPLATE'],
|
|
119
|
+
description: 'How the backend treats the URL. Default derived: ' +
|
|
120
|
+
'TEMPLATE when the URL has {{placeholders}}, else ' +
|
|
121
|
+
'CONSTANT.',
|
|
122
|
+
},
|
|
97
123
|
responseMatches: { type: 'array' },
|
|
98
124
|
responseRedactions: { type: 'array' },
|
|
99
125
|
paramValues: { type: 'object' },
|
|
@@ -122,6 +148,14 @@ export function createVersionFromCaptureTool(client) {
|
|
|
122
148
|
method: { type: 'string' },
|
|
123
149
|
headers: { type: 'object' },
|
|
124
150
|
body: { type: 'string' },
|
|
151
|
+
credentials: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
enum: ['omit', 'same-origin', 'include'],
|
|
154
|
+
},
|
|
155
|
+
urlType: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
enum: ['REGEX', 'CONSTANT', 'TEMPLATE'],
|
|
158
|
+
},
|
|
125
159
|
responseMatches: { type: 'array' },
|
|
126
160
|
responseRedactions: { type: 'array' },
|
|
127
161
|
paramValues: { type: 'object' },
|
|
@@ -196,12 +230,41 @@ export function createVersionFromCaptureTool(client) {
|
|
|
196
230
|
'the whole payload), template just that part with ' +
|
|
197
231
|
'`{{paramName}}` and keep the rest as fixed literal text; see ' +
|
|
198
232
|
'the "bodySniff" section of the customInjection field\'s guide ' +
|
|
199
|
-
'for the exact mechanics and a worked example.'
|
|
233
|
+
'for the exact mechanics and a worked example. ' +
|
|
234
|
+
'⚠️ BEFORE populating this to gate ' +
|
|
235
|
+
'`verifyProof(proof, { providerId })`: this whole entry (url, ' +
|
|
236
|
+
'method, body, responseMatches, responseRedactions) feeds a ' +
|
|
237
|
+
'client-side hash-equality check, and `url` is hashed VERBATIM ' +
|
|
238
|
+
'— no regex/template resolution, even with `urlType: REGEX` or ' +
|
|
239
|
+
'`TEMPLATE`. If the real captured URL contains ANY per-session ' +
|
|
240
|
+
'part (a tracking nonce, CSRF token, cache-busting timestamp), ' +
|
|
241
|
+
'no declared spec can EVER hash-match a real proof, no matter ' +
|
|
242
|
+
'how correct the rest of this entry is — populating' +
|
|
243
|
+
'it for such ' +
|
|
244
|
+
'a provider makes verifyProof reject every real proof,' +
|
|
245
|
+
'which is ' +
|
|
246
|
+
'worse than leaving this field empty. Diff the same endpoint\'s' +
|
|
247
|
+
' captured URL across two separate sessions before wiring this ' +
|
|
248
|
+
'up. If provider-hash validation keeps causing problems ' +
|
|
249
|
+
'(unstable url, or any other persistent mismatch), the ' +
|
|
250
|
+
'fallback on the verifying side is `verifyProof(proof, { ' +
|
|
251
|
+
'dangerouslyDisableContentValidation: true })` — trusts only ' +
|
|
252
|
+
'the attestor\'s signature, skips the hash-equality check ' +
|
|
253
|
+
'entirely; see the "Provider-hash validation" section of the ' +
|
|
254
|
+
'customInjection field\'s guide for the full mechanics and ' +
|
|
255
|
+
'when to reach for it.',
|
|
200
256
|
items: {
|
|
201
257
|
type: 'object',
|
|
202
258
|
properties: {
|
|
203
259
|
url: { type: 'string' },
|
|
204
260
|
method: { type: 'string' },
|
|
261
|
+
urlType: {
|
|
262
|
+
type: 'string',
|
|
263
|
+
enum: ['REGEX', 'CONSTANT', 'TEMPLATE'],
|
|
264
|
+
description: 'How the backend treats this URL. Default derived ' +
|
|
265
|
+
'from {{placeholders}} (TEMPLATE) like a normal ' +
|
|
266
|
+
'request entry.',
|
|
267
|
+
},
|
|
205
268
|
body: {
|
|
206
269
|
type: 'string',
|
|
207
270
|
description: 'Becomes `bodySniff: { enabled: true, template: <this ' +
|
|
@@ -315,6 +378,14 @@ export function createVersionFromCaptureTool(client) {
|
|
|
315
378
|
description: 'Free-form JSON passed through to the client. Add-version ' +
|
|
316
379
|
'ONLY.',
|
|
317
380
|
},
|
|
381
|
+
disableRequestReplay: {
|
|
382
|
+
type: 'boolean',
|
|
383
|
+
description: 'Skip replaying the main document request (HAWKEYE ' +
|
|
384
|
+
'provider-level flag). Honored on add-version; for a ' +
|
|
385
|
+
'provider (no `providerId`) the register endpoint ignores ' +
|
|
386
|
+
'it, so when true the tool automatically follows the ' +
|
|
387
|
+
'register up with an add-version call that applies it.',
|
|
388
|
+
},
|
|
318
389
|
},
|
|
319
390
|
required: ['initialUrl'],
|
|
320
391
|
},
|
|
@@ -348,6 +419,7 @@ export function createVersionFromCaptureTool(client) {
|
|
|
348
419
|
stepsToFollow: args.stepsToFollow,
|
|
349
420
|
useIncognitoWebview: args.useIncognitoWebview,
|
|
350
421
|
extensionConfig: args.extensionConfig,
|
|
422
|
+
disableRequestReplay: args.disableRequestReplay,
|
|
351
423
|
allowedInjectedRequestData: args.allowedInjectedRequestData
|
|
352
424
|
? injectedRequestDataFrom(args.allowedInjectedRequestData)
|
|
353
425
|
: undefined
|
|
@@ -372,6 +444,11 @@ export function createVersionFromCaptureTool(client) {
|
|
|
372
444
|
'every page load (before navigating to initialUrl), matching ' +
|
|
373
445
|
'the builder\'s jsUserScripts.');
|
|
374
446
|
}
|
|
447
|
+
// disableRequestReplay is honored ONLY on add-version, so a
|
|
448
|
+
// register can't set it — when true we immediately follow the
|
|
449
|
+
// register with an add-version call (which also lands any other
|
|
450
|
+
// register-ignored fields).
|
|
451
|
+
const followUp = args.disableRequestReplay === true;
|
|
375
452
|
const REGISTER_IGNORED_KEYS = [
|
|
376
453
|
'allowedInjectedRequestData',
|
|
377
454
|
'useIncognitoWebview',
|
|
@@ -382,13 +459,36 @@ export function createVersionFromCaptureTool(client) {
|
|
|
382
459
|
if (ignoredOnRegister.length) {
|
|
383
460
|
notes.push(`${ignoredOnRegister.join(', ')} ` +
|
|
384
461
|
`${ignoredOnRegister.length > 1 ? 'are' : 'is'} ` +
|
|
385
|
-
'ignored on register (no providerId) — the
|
|
386
|
-
'reads them when adding a version.
|
|
387
|
-
|
|
388
|
-
|
|
462
|
+
'ignored on register (no providerId) — the devtools backend only ' +
|
|
463
|
+
'reads them when adding a version. ' +
|
|
464
|
+
(followUp
|
|
465
|
+
? 'They were applied by the follow-up version the tool ' +
|
|
466
|
+
'created to set disableRequestReplay.'
|
|
467
|
+
: 'Publish once to create the provider, then call this ' +
|
|
468
|
+
'again WITH the returned providerId to set them.'));
|
|
389
469
|
}
|
|
390
470
|
const body = providerToRegisterBody(providers, opts);
|
|
391
|
-
|
|
471
|
+
const registered = await client.registerProvider(body);
|
|
472
|
+
if (followUp) {
|
|
473
|
+
const newProviderId = extractProviderId(registered);
|
|
474
|
+
if (newProviderId) {
|
|
475
|
+
const versionsResp = await client.listVersions(newProviderId);
|
|
476
|
+
const followVersion = bumpPatchString(extractVersions(versionsResp));
|
|
477
|
+
const configBody = providerToConfigBody(providers, { ...opts, disableRequestReplay: true }, followVersion, DEFAULT_VERSION_INFO);
|
|
478
|
+
await client.updateProviderConfig(newProviderId, configBody);
|
|
479
|
+
notes.push('disableRequestReplay is ignored by the register endpoint — ' +
|
|
480
|
+
`applied it via a follow-up version (${followVersion}) on ` +
|
|
481
|
+
'the new provider, which also lands any register-ignored ' +
|
|
482
|
+
'fields you passed.');
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
notes.push('Could not apply disableRequestReplay: the register ' +
|
|
486
|
+
'response did not include the new providerId. Call this ' +
|
|
487
|
+
'tool again WITH `providerId` and `disableRequestReplay: ' +
|
|
488
|
+
'true` to set it.');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return withNotes(registered);
|
|
392
492
|
}
|
|
393
493
|
// New version of an existing provider — pick the next patch above
|
|
394
494
|
// the highest existing version. A provider you don't own (e.g. a
|
package/lib/provider/draft.d.ts
CHANGED
|
@@ -37,4 +37,24 @@ export interface DraftResult {
|
|
|
37
37
|
* `{{param}}`. Throwing discards the draft so the agent re-captures or
|
|
38
38
|
* re-proposes rather than shipping a provider pinned to this one user. */
|
|
39
39
|
export declare function assertPortableMatches(matches: ResponseMatch[], redactions: ResponseRedaction[], targets: DraftTarget[]): void;
|
|
40
|
+
/** Reject a draft that OPRF-hashes a target whose value ALSO appears in the
|
|
41
|
+
* request URL. The whole point of `hash` is that the attestor never sees
|
|
42
|
+
* the value in cleartext — but URL `paramValues` are always signed into the
|
|
43
|
+
* attestor's context in the clear (attestor-core: "those in URL... will be
|
|
44
|
+
* put into context and signed"), so a value that's both hashed in the
|
|
45
|
+
* response AND present in the URL is visible there anyway. This isn't a
|
|
46
|
+
* privacy nuance the author can weigh (unlike {@link assertPortableMatches}
|
|
47
|
+
* bare-literal cases) — it doesn't verify. Throwing here, at draft time,
|
|
48
|
+
* surfaces that immediately instead of a confusing later proof failure. */
|
|
49
|
+
export declare function assertNoOprfUrlConflict(url: string, targets: DraftTarget[], effectiveHashes: (OprfMode | undefined)[]): void;
|
|
50
|
+
/** Reject a draft whose URL bakes in a secret header/cookie's value beyond
|
|
51
|
+
* what write-redaction can actually hide there. A secret value used in the
|
|
52
|
+
* URL is NOT an automatic leak — write-redaction (`key-update`/`zk`) can
|
|
53
|
+
* redact it from the attestor's view up to {@link secretUrlCharBudget}
|
|
54
|
+
* characters, summed across every secret value that appears in that URL
|
|
55
|
+
* (not per-value). Below the budget the header buys real privacy; at or
|
|
56
|
+
* over it, the value is effectively visible regardless of hiding the
|
|
57
|
+
* header (same class of problem as {@link assertNoOprfUrlConflict} for
|
|
58
|
+
* OPRF-hashed targets, which has no such budget at all). */
|
|
59
|
+
export declare function assertNoSecretUrlConflict(url: string, secrets: Record<string, string>, writeRedactionMode?: ReclaimProvider['writeRedactionMode']): void;
|
|
40
60
|
export declare function draftProvider(req: CapturedRequest, targets: DraftTarget[], name: string, hash?: OprfMode): DraftResult;
|
package/lib/provider/draft.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isOprfHashable, OPRF_MAX_BYTES, synthesizeMatcher } from "./matcher.js";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeCandidates } from "./normalize.js";
|
|
3
|
+
import { secretUrlCharBudget, splitSecrets, stripTransportHeaders, } from "./secrets.js";
|
|
3
4
|
/** OPRF mode for the auto-hash floor — TEE-native (attestor rejects oprf-raw,
|
|
4
5
|
* isn't set up for gnark oprf). */
|
|
5
6
|
const DEFAULT_SENSITIVE_HASH = 'oprf-mpc';
|
|
@@ -29,49 +30,6 @@ export function defaultSensitiveHash(value) {
|
|
|
29
30
|
&& Buffer.byteLength(value, 'utf8') <= OPRF_MAX_BYTES;
|
|
30
31
|
return hashable ? DEFAULT_SENSITIVE_HASH : undefined;
|
|
31
32
|
}
|
|
32
|
-
/** Request headers that must NOT survive into the provider params. Three kinds:
|
|
33
|
-
* - Conditional headers (`if-none-match`/`if-modified-since`/…): a captured
|
|
34
|
-
* cached ETag/date makes the origin answer 304/412 when the attestor
|
|
35
|
-
* re-issues, and the attestor treats non-2xx as failure. We always want the
|
|
36
|
-
* full fresh response.
|
|
37
|
-
* - Transfer headers the ATTESTOR sets itself (`connection: close`,
|
|
38
|
-
* `accept-encoding: identity`). Passing our own duplicates/collides with the
|
|
39
|
-
* request the attestor builds, so leave them out — the attestor already
|
|
40
|
-
* forces `identity` (uncompressed, so its byte-matching works).
|
|
41
|
-
* - Host / framing headers the TRANSPORT owns (`host`, `content-length`,
|
|
42
|
-
* `transfer-encoding`) and HTTP/2 pseudo-headers (`:authority`/`:method`/…,
|
|
43
|
-
* which CDP surfaces on h2 captures). The attestor derives `Host` from the
|
|
44
|
-
* URL and sets its own; a baked `Host` produces a DUPLICATE
|
|
45
|
-
* (`found: h,h`) that fails the receipt check / 400s the origin. A stale
|
|
46
|
-
* `content-length` breaks a re-serialized body. Pseudo-headers aren't valid
|
|
47
|
-
* real headers at all. */
|
|
48
|
-
const STRIP_REQUEST_HEADERS = new Set([
|
|
49
|
-
'if-none-match',
|
|
50
|
-
'if-modified-since',
|
|
51
|
-
'if-match',
|
|
52
|
-
'if-unmodified-since',
|
|
53
|
-
'if-range',
|
|
54
|
-
'connection',
|
|
55
|
-
'accept-encoding',
|
|
56
|
-
'host',
|
|
57
|
-
'content-length',
|
|
58
|
-
'transfer-encoding',
|
|
59
|
-
// Per-session anti-CSRF tokens. Not caught by splitSecrets (no `-token`
|
|
60
|
-
// suffix), so without this they baked in as PUBLIC headers and went stale at
|
|
61
|
-
// verify time — the attestor replayed one user's captured CSRF for everyone,
|
|
62
|
-
// which the origin rejects. A CSRF token is never portable; drop it.
|
|
63
|
-
'csrf',
|
|
64
|
-
'x-csrf',
|
|
65
|
-
'xsrf',
|
|
66
|
-
'x-xsrf',
|
|
67
|
-
'x-csrf-token',
|
|
68
|
-
'xsrf-token',
|
|
69
|
-
]);
|
|
70
|
-
/** True for an HTTP/2 pseudo-header (`:authority`, `:method`, `:path`,
|
|
71
|
-
* `:scheme`) — never a valid request header the attestor can replay. */
|
|
72
|
-
function isPseudoHeader(name) {
|
|
73
|
-
return name.startsWith(':');
|
|
74
|
-
}
|
|
75
33
|
/** A header VALUE that is a per-session/request token — a UUID or a long opaque
|
|
76
34
|
* hex/base64 blob. Such a value is captured-user-specific: baked into the
|
|
77
35
|
* provider it goes stale and the origin rejects the replay for everyone else.
|
|
@@ -84,15 +42,17 @@ function isVolatileHeaderValue(value) {
|
|
|
84
42
|
|| /^[0-9a-f]{16,}$/i.test(v)
|
|
85
43
|
|| /^[A-Za-z0-9_-]{32,}$/.test(v);
|
|
86
44
|
}
|
|
87
|
-
/** Drop
|
|
88
|
-
*
|
|
89
|
-
*
|
|
45
|
+
/** Drop a PUBLIC header whose value is per-session/request volatile (see
|
|
46
|
+
* {@link isVolatileHeaderValue}) — public headers get frozen into the
|
|
47
|
+
* persisted provider, so a volatile one goes stale and fails every other
|
|
48
|
+
* user's replay. Secret headers are re-captured fresh per verification
|
|
49
|
+
* (never persisted), so this doesn't apply to them; keep every other
|
|
50
|
+
* captured public header so the attestor's replay looks like the browser's
|
|
51
|
+
* request. */
|
|
90
52
|
function sanitizeRequestHeaders(headers) {
|
|
91
53
|
const out = {};
|
|
92
54
|
for (const [k, v] of Object.entries(headers)) {
|
|
93
|
-
if (
|
|
94
|
-
|| isPseudoHeader(k)
|
|
95
|
-
|| isVolatileHeaderValue(v)) {
|
|
55
|
+
if (isVolatileHeaderValue(v)) {
|
|
96
56
|
continue;
|
|
97
57
|
}
|
|
98
58
|
out[k] = v;
|
|
@@ -167,6 +127,63 @@ export function assertPortableMatches(matches, redactions, targets) {
|
|
|
167
127
|
}
|
|
168
128
|
}
|
|
169
129
|
}
|
|
130
|
+
/** Reject a draft that OPRF-hashes a target whose value ALSO appears in the
|
|
131
|
+
* request URL. The whole point of `hash` is that the attestor never sees
|
|
132
|
+
* the value in cleartext — but URL `paramValues` are always signed into the
|
|
133
|
+
* attestor's context in the clear (attestor-core: "those in URL... will be
|
|
134
|
+
* put into context and signed"), so a value that's both hashed in the
|
|
135
|
+
* response AND present in the URL is visible there anyway. This isn't a
|
|
136
|
+
* privacy nuance the author can weigh (unlike {@link assertPortableMatches}
|
|
137
|
+
* bare-literal cases) — it doesn't verify. Throwing here, at draft time,
|
|
138
|
+
* surfaces that immediately instead of a confusing later proof failure. */
|
|
139
|
+
export function assertNoOprfUrlConflict(url, targets, effectiveHashes) {
|
|
140
|
+
for (const [i, t] of targets.entries()) {
|
|
141
|
+
const hash = effectiveHashes[i];
|
|
142
|
+
if (!hash) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const inUrl = [t.value, ...normalizeCandidates(t.value)]
|
|
146
|
+
.some((v) => url.includes(v));
|
|
147
|
+
if (inUrl) {
|
|
148
|
+
throw new Error(`target "${t.name}" is OPRF-hashed (${hash}) but its value also `
|
|
149
|
+
+ `appears in the request URL ("${url}") — the attestor signs URL `
|
|
150
|
+
+ 'param values into context in the clear, so it would see this '
|
|
151
|
+
+ 'value regardless of the hash. Drop `hash` for this target, or '
|
|
152
|
+
+ "capture a request whose URL doesn't carry the value.");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Reject a draft whose URL bakes in a secret header/cookie's value beyond
|
|
157
|
+
* what write-redaction can actually hide there. A secret value used in the
|
|
158
|
+
* URL is NOT an automatic leak — write-redaction (`key-update`/`zk`) can
|
|
159
|
+
* redact it from the attestor's view up to {@link secretUrlCharBudget}
|
|
160
|
+
* characters, summed across every secret value that appears in that URL
|
|
161
|
+
* (not per-value). Below the budget the header buys real privacy; at or
|
|
162
|
+
* over it, the value is effectively visible regardless of hiding the
|
|
163
|
+
* header (same class of problem as {@link assertNoOprfUrlConflict} for
|
|
164
|
+
* OPRF-hashed targets, which has no such budget at all). */
|
|
165
|
+
export function assertNoSecretUrlConflict(url, secrets, writeRedactionMode) {
|
|
166
|
+
let redactedChars = 0;
|
|
167
|
+
const overlapping = [];
|
|
168
|
+
for (const [name, value] of Object.entries(secrets)) {
|
|
169
|
+
const inUrl = [value, ...normalizeCandidates(value)]
|
|
170
|
+
.some((v) => url.includes(v));
|
|
171
|
+
if (inUrl) {
|
|
172
|
+
redactedChars += value.length;
|
|
173
|
+
overlapping.push(name);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const budget = secretUrlCharBudget(writeRedactionMode);
|
|
177
|
+
if (redactedChars > budget) {
|
|
178
|
+
throw new Error(`secret header(s) ${overlapping.map((n) => `"${n}"`).join(', ')} `
|
|
179
|
+
+ `also appear in the request URL ("${url}"), totaling `
|
|
180
|
+
+ `${redactedChars} character(s) to hide — over the ${budget}-`
|
|
181
|
+
+ `character budget write-redaction (${writeRedactionMode
|
|
182
|
+
?? 'key-update'}) can actually hide there. Drop the header, `
|
|
183
|
+
+ "shorten/remove the value's overlap with the URL, or switch to "
|
|
184
|
+
+ "'zk' if that raises the budget enough.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
170
187
|
/** Derive a `paramValues` key (and capture-group name) from the provider
|
|
171
188
|
* name's last alphanumeric token. `"GitHub Username"` → `"username"`,
|
|
172
189
|
* `"x-followers"` → `"followers"`. */
|
|
@@ -188,6 +205,7 @@ export function draftProvider(req, targets, name, hash) {
|
|
|
188
205
|
const matches = [];
|
|
189
206
|
const redactions = [];
|
|
190
207
|
const extractions = [];
|
|
208
|
+
const effectiveHashes = [];
|
|
191
209
|
let paramValues;
|
|
192
210
|
const seenParams = new Set();
|
|
193
211
|
for (const t of targets) {
|
|
@@ -202,6 +220,7 @@ export function draftProvider(req, targets, name, hash) {
|
|
|
202
220
|
// The model makes the contextual calls (which named fields are sensitive)
|
|
203
221
|
// via an explicit per-target hash; this floor only guarantees the obvious.
|
|
204
222
|
const effectiveHash = t.hash ?? hash ?? defaultSensitiveHash(t.value);
|
|
223
|
+
effectiveHashes.push(effectiveHash);
|
|
205
224
|
const synthesis = synthesizeMatcher(req.responseBody, req.contentType, t.value, paramName, effectiveHash);
|
|
206
225
|
if (!synthesis) {
|
|
207
226
|
throw new Error(`target value "${t.value}" not found in`
|
|
@@ -222,7 +241,12 @@ export function draftProvider(req, targets, name, hash) {
|
|
|
222
241
|
}
|
|
223
242
|
// Portability gate: never ship a draft that bakes a per-user value literally.
|
|
224
243
|
assertPortableMatches(matches, redactions, targets);
|
|
225
|
-
|
|
244
|
+
// OPRF gate: never ship a draft that hashes a value the URL also reveals.
|
|
245
|
+
assertNoOprfUrlConflict(req.url, targets, effectiveHashes);
|
|
246
|
+
const { secrets, publics } = splitSecrets(stripTransportHeaders(req.requestHeaders));
|
|
247
|
+
// Secret-header gate: never ship a draft whose URL reveals the same value a
|
|
248
|
+
// stripped secret header carries — hiding the header buys nothing then.
|
|
249
|
+
assertNoSecretUrlConflict(req.url, secrets);
|
|
226
250
|
// Sanitize the public headers before they enter the provider. Two problems
|
|
227
251
|
// a captured browser request carries: (1) `accept-encoding: gzip, …`, which
|
|
228
252
|
// makes the attestor match against compressed bytes (it does no transparent
|
package/lib/provider/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { DraftResult } from './draft.ts';
|
|
|
3
3
|
export { detectConcerns } from './constraints.ts';
|
|
4
4
|
export { extractGraphqlOp, looksLikeGraphqlEndpoint } from './graphql.ts';
|
|
5
5
|
export type { CapturedRequest, HttpMethod, OprfMode, ReclaimProvider, RequestConcern, ResponseMatch, ResponseRedaction, SecretParamRef, } from './schema.ts';
|
|
6
|
-
export { classifyHeader, splitSecrets } from './secrets.ts';
|
|
6
|
+
export { assertSecretParamValuesFitUrlBudget, classifyHeader, classifyParamName, secretUrlCharBudget, splitSecretParamValues, splitSecrets, stripTransportHeaders, } from './secrets.ts';
|
|
7
7
|
export { providerToRequestSelection } from './to-version.ts';
|
|
8
8
|
export { hashProviderParams, providers } from '@reclaimprotocol/attestor-core';
|
|
9
9
|
export type { ProviderParams } from '@reclaimprotocol/attestor-core';
|
package/lib/provider/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export { draftProvider } from "./draft.js";
|
|
7
7
|
export { detectConcerns } from "./constraints.js";
|
|
8
8
|
export { extractGraphqlOp, looksLikeGraphqlEndpoint } from "./graphql.js";
|
|
9
|
-
export { classifyHeader, splitSecrets } from "./secrets.js";
|
|
9
|
+
export { assertSecretParamValuesFitUrlBudget, classifyHeader, classifyParamName, secretUrlCharBudget, splitSecretParamValues, splitSecrets, stripTransportHeaders, } from "./secrets.js";
|
|
10
10
|
// Recipe → version `requests[]` (a `RequestSelection`) — the same mapping the
|
|
11
11
|
// MCP publish uses, so the cloud authoring publish stays byte-identical.
|
|
12
12
|
export { providerToRequestSelection } from "./to-version.js";
|