@reclaimprotocol/agent 0.1.23 → 0.1.24
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 +55 -20
- package/lib/authoring/backend.d.ts +5 -4
- package/lib/authoring/tools.js +30 -30
- package/lib/cdp/browser-proxy.d.ts +78 -0
- package/lib/cdp/browser-proxy.js +186 -0
- package/lib/cdp/capture-engine.js +8 -7
- package/lib/cdp/client.d.ts +10 -0
- package/lib/cdp/geo.d.ts +10 -0
- package/lib/cdp/geo.js +36 -0
- package/lib/cdp/index.d.ts +1 -0
- package/lib/cdp/index.js +1 -0
- package/lib/live/cloudflared.d.ts +15 -0
- package/lib/live/cloudflared.js +268 -0
- package/lib/live/page.d.ts +20 -0
- package/lib/live/page.js +134 -0
- package/lib/live/tunnel.d.ts +23 -0
- package/lib/live/tunnel.js +120 -0
- package/lib/live/viewer.d.ts +28 -0
- package/lib/live/viewer.js +232 -0
- package/lib/mcp/api/operations.gen.js +34 -26
- package/lib/mcp/api/to-zod.d.ts +1 -1
- package/lib/mcp/api/to-zod.js +1 -1
- package/lib/mcp/api/tools.js +1 -1
- package/lib/mcp/instructions.d.ts +15 -2
- package/lib/mcp/instructions.js +101 -38
- package/lib/mcp/start.js +4 -3
- package/lib/mcp/tools/agent/analyze.js +5 -1
- package/lib/mcp/tools/agent/attach.d.ts +8 -1
- package/lib/mcp/tools/agent/attach.js +166 -22
- package/lib/mcp/tools/agent/build.d.ts +13 -8
- package/lib/mcp/tools/agent/build.js +22 -11
- package/lib/mcp/tools/agent/capture.js +5 -1
- package/lib/mcp/tools/agent/eval.js +3 -1
- package/lib/mcp/tools/agent/find.js +6 -5
- package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
- package/lib/mcp/tools/agent/how-it-works.js +57 -17
- package/lib/mcp/tools/agent/live-view.d.ts +21 -0
- package/lib/mcp/tools/agent/live-view.js +153 -0
- package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
- package/lib/mcp/tools/agent/pod-proof.js +64 -0
- package/lib/mcp/tools/agent/propose.js +4 -2
- package/lib/mcp/tools/agent/proxy.d.ts +3 -0
- package/lib/mcp/tools/agent/proxy.js +199 -0
- package/lib/mcp/tools/agent/publish.js +47 -77
- package/lib/mcp/tools/agent/session.d.ts +2 -1
- package/lib/mcp/tools/agent/session.js +26 -7
- package/lib/mcp/tools/authenticate/authenticate.js +26 -13
- package/lib/mcp/tools/mode/mode.d.ts +1 -1
- package/lib/mcp/tools/mode/mode.js +10 -22
- package/lib/old/client.d.ts +1 -1
- package/lib/old/client.js +1 -1
- package/lib/old/instructions.d.ts +9 -1
- package/lib/old/instructions.js +4 -6
- package/lib/old/login-cdp.d.ts +2 -2
- package/lib/old/login-cdp.js +46 -6
- package/lib/old/to-register.d.ts +3 -2
- package/lib/old/to-register.js +1 -1
- package/lib/old/tools/authenticate.js +9 -1
- package/lib/old/tools/get-provider-info.js +6 -5
- package/lib/old/tools/issue-app-credentials.js +3 -2
- package/lib/old/tools/publish.d.ts +1 -1
- package/lib/old/tools/publish.js +90 -131
- package/lib/old/tools/update-provider-metadata.d.ts +1 -1
- package/lib/old/tools/update-provider-metadata.js +1 -1
- package/lib/old/tools/verify-sdk-setup.js +1 -1
- package/lib/proof/attestor.d.ts +3 -3
- package/lib/proof/debug-response-dump.js +4 -4
- package/lib/proof/index.d.ts +1 -0
- package/lib/proof/index.js +1 -0
- package/lib/proof/page-replay.d.ts +1 -1
- package/lib/proof/page-replay.js +1 -1
- package/lib/proof/pod-prover.d.ts +47 -0
- package/lib/proof/pod-prover.js +155 -0
- package/lib/provider/dom-xpath.js +11 -9
- package/lib/provider/draft.d.ts +10 -7
- package/lib/provider/draft.js +11 -9
- package/lib/provider/injection-doc.d.ts +23 -1
- package/lib/provider/injection-doc.js +34 -21
- package/lib/provider/match-redaction-warning.d.ts +2 -2
- package/lib/provider/match-redaction-warning.js +3 -3
- package/lib/provider/matcher.js +13 -12
- package/lib/provider/secrets.d.ts +3 -3
- package/lib/provider/secrets.js +3 -3
- package/lib/provider/to-version.d.ts +7 -6
- package/lib/provider/to-version.js +3 -3
- package/lib/tools/registry.d.ts +5 -5
- package/lib/tools/registry.js +5 -5
- package/package.json +3 -3
- package/samples/demo-server/README.md +7 -6
- package/skill/SKILL.builder.md +356 -189
- package/skill/SKILL.common.md +323 -97
- package/skill/SKILL.old.md +340 -155
- package/skill/builder-mode.md +58 -17
- package/skill/hash-validation.md +170 -0
- package/skill/interception.md +235 -0
- package/skill/js-sdk-integration.md +13 -7
- package/skill/old-mode.md +60 -21
- package/skill/user-script.md +61 -0
- package/src/authoring/backend.ts +5 -4
- package/src/authoring/tools.ts +31 -30
- package/src/cdp/capture-engine.ts +8 -7
- package/src/cdp/client.ts +8 -0
- package/src/cdp/geo.ts +52 -0
- package/src/cdp/index.ts +1 -0
- package/src/consts.ts +1 -0
- package/src/live/cloudflared.ts +348 -0
- package/src/live/page.ts +145 -0
- package/src/live/tunnel.ts +157 -0
- package/src/live/viewer.ts +293 -0
- package/src/mcp/api/operations.gen.ts +34 -26
- package/src/mcp/api/to-zod.ts +1 -1
- package/src/mcp/api/tools.ts +1 -1
- package/src/mcp/instructions.ts +121 -48
- package/src/mcp/start.ts +4 -3
- package/src/mcp/tools/agent/analyze.ts +5 -1
- package/src/mcp/tools/agent/attach.ts +194 -22
- package/src/mcp/tools/agent/build.ts +26 -15
- package/src/mcp/tools/agent/capture.ts +5 -1
- package/src/mcp/tools/agent/eval.ts +4 -1
- package/src/mcp/tools/agent/find.ts +6 -5
- package/src/mcp/tools/agent/how-it-works.ts +62 -21
- package/src/mcp/tools/agent/live-view.ts +198 -0
- package/src/mcp/tools/agent/pod-proof.ts +90 -0
- package/src/mcp/tools/agent/propose.ts +4 -2
- package/src/mcp/tools/agent/publish.ts +52 -77
- package/src/mcp/tools/agent/session.ts +26 -5
- package/src/mcp/tools/authenticate/authenticate.ts +28 -13
- package/src/mcp/tools/mode/mode.ts +10 -22
- package/src/old/client.ts +2 -2
- package/src/old/instructions.ts +4 -10
- package/src/old/login-cdp.ts +50 -6
- package/src/old/to-register.ts +3 -2
- package/src/old/tools/authenticate.ts +9 -1
- package/src/old/tools/get-provider-info.ts +6 -5
- package/src/old/tools/issue-app-credentials.ts +3 -2
- package/src/old/tools/publish.ts +95 -131
- package/src/old/tools/update-provider-metadata.ts +1 -1
- package/src/old/tools/verify-sdk-setup.ts +1 -1
- package/src/proof/attestor.ts +3 -3
- package/src/proof/debug-response-dump.ts +4 -4
- package/src/proof/index.ts +8 -0
- package/src/proof/page-replay.ts +1 -1
- package/src/proof/pod-prover.ts +231 -0
- package/src/provider/dom-xpath.ts +11 -9
- package/src/provider/draft.ts +13 -10
- package/src/provider/injection-doc.ts +37 -24
- package/src/provider/match-redaction-warning.ts +3 -3
- package/src/provider/matcher.ts +13 -12
- package/src/provider/secrets.ts +3 -3
- package/src/provider/to-version.ts +11 -9
- package/src/tools/registry.ts +5 -5
- package/skill/injection-api.md +0 -364
|
@@ -24,7 +24,7 @@ function hasValidBuilderSession(config: ConfigStore): boolean {
|
|
|
24
24
|
* `~/.reclaim/config.json` and applied the NEXT time the server starts —
|
|
25
25
|
* the registered tool set, server instructions, and backend client are all
|
|
26
26
|
* fixed at connect time, so the switch requires a restart/reconnect. Follows
|
|
27
|
-
* the `
|
|
27
|
+
* the `attach_browser` persist-through-config pattern.
|
|
28
28
|
*
|
|
29
29
|
* Registered in BOTH modes (so the user can switch either direction) and
|
|
30
30
|
* outside `buildAgentTools` (so it survives `RECLAIM_AGENT_DISABLED=1`). An
|
|
@@ -74,27 +74,15 @@ export function buildModeTools(
|
|
|
74
74
|
{
|
|
75
75
|
name: 'get_devtools_mode',
|
|
76
76
|
description:
|
|
77
|
-
'Report which backend this
|
|
78
|
-
+ '"
|
|
79
|
-
+ '
|
|
80
|
-
+ '
|
|
81
|
-
+ '
|
|
82
|
-
+ '
|
|
83
|
-
+ '
|
|
84
|
-
+ '
|
|
85
|
-
+ '
|
|
86
|
-
+ '(list_orgs) and remote-browser allocation. In OLD-DEVTOOLS '
|
|
87
|
-
+ 'mode it is a SEPARATE, additional login (authenticate_builder) '
|
|
88
|
-
+ 'needed only for two Builder-exclusive features: remote-browser '
|
|
89
|
-
+ '(Popcorn) allocation and org listing (list_builder_organizations '
|
|
90
|
-
+ '— old-devtools has no generated org tool of its own). '
|
|
91
|
-
+ 'oldDevtoolsAuthenticated (the OLD-mode reclaim_authenticate) is '
|
|
92
|
-
+ 'required separately for create_provider_version_from_capture '
|
|
93
|
-
+ 'and get_me_providers, and only exists/matters in old-devtools '
|
|
94
|
-
+ 'mode. Both are presence checks, not '
|
|
95
|
-
+ 'validity checks — a stale old-devtools token still reports '
|
|
96
|
-
+ 'true here; a real publish/list call will surface staleness '
|
|
97
|
-
+ 'with a clear error pointing back at reclaim_authenticate.',
|
|
77
|
+
'Report which backend this server targets ("old" devtools or '
|
|
78
|
+
+ '"builder"), WHY (a USE_OLD_DEVTOOLS env override, the persisted '
|
|
79
|
+
+ 'config, or the default), and which mode is persisted for the '
|
|
80
|
+
+ 'next start. Change it with set_devtools_mode. Also reports auth '
|
|
81
|
+
+ 'status for both login systems: builderAuthenticated and '
|
|
82
|
+
+ 'oldDevtoolsAuthenticated. Both are PRESENCE checks, not validity '
|
|
83
|
+
+ 'checks — a stale token still reports true; a real call surfaces '
|
|
84
|
+
+ 'the staleness with an error naming the tool to re-run. Which '
|
|
85
|
+
+ 'login authorizes what: how_it_works({ topic: "auth" }).',
|
|
98
86
|
inputSchema: { type: 'object', properties: {} },
|
|
99
87
|
},
|
|
100
88
|
async() => {
|
package/src/old/client.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface RegisterProviderBody {
|
|
|
50
50
|
/** Interception mechanism (`NONE` = standard replay flow, the default;
|
|
51
51
|
* `HAWKEYE`/`MSWJS`/`XHOOK` = JS-context interception; `CDP` = browser-
|
|
52
52
|
* level). Column default is `HAWKEYE` when omitted entirely — unlike
|
|
53
|
-
* `stepsToFollow
|
|
53
|
+
* `stepsToFollow`, and so on. above, whether the register endpoint actually
|
|
54
54
|
* PERSISTS this field is UNVERIFIED (no source access to confirm); sent
|
|
55
55
|
* here on the chance it does, but the publish tool always follows up
|
|
56
56
|
* with a config (add-version) call regardless, which is confirmed to
|
|
@@ -370,7 +370,7 @@ export class ReclaimOldClient {
|
|
|
370
370
|
// An identity ALREADY SET (as opposed to `#requireIdentity`'s "never
|
|
371
371
|
// authenticated" case, which never reaches the backend) plus either a
|
|
372
372
|
// 401/403 OR a message that itself names token verification means the
|
|
373
|
-
// backend rejected a token we had —
|
|
373
|
+
// backend rejected a token we had — that is, it's stale/invalid, not merely
|
|
374
374
|
// absent. The message check matters because this backend does NOT
|
|
375
375
|
// reliably use 401/403 for it — confirmed live: an actually-expired
|
|
376
376
|
// Firebase token surfaced as "Error while verifying token" with a
|
package/src/old/instructions.ts
CHANGED
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
* MCP server instructions shown to the LLM when running in OLD-devtools mode
|
|
3
3
|
* (`USE_OLD_DEVTOOLS` unset or not `false`). Describes the REDUCED tool
|
|
4
4
|
* surface — there are no org/credential-link/review flows and no generated
|
|
5
|
-
* API tools here.
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
5
|
+
* API tools here. Both modes' instructions load together in
|
|
6
|
+
* `mcp/instructions` (alongside the topic-split authoring guide); re-exported
|
|
7
|
+
* here so old-mode callers keep importing from their own module.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
11
|
-
|
|
12
|
-
export const OLD_MODE_INSTRUCTIONS = readFileSync(
|
|
13
|
-
fileURLToPath(new URL('../../skill/old-mode.md', import.meta.url)),
|
|
14
|
-
'utf8',
|
|
15
|
-
).trim()
|
|
9
|
+
export { OLD_MODE_INSTRUCTIONS } from '../mcp/instructions.ts'
|
package/src/old/login-cdp.ts
CHANGED
|
@@ -97,8 +97,8 @@ export interface CdpLoginResult {
|
|
|
97
97
|
* finish signing in, read the resulting Firebase ID token from page storage,
|
|
98
98
|
* then clean up. If we had to launch a dedicated Chrome for this, we close the
|
|
99
99
|
* whole browser afterwards; if we attached to a Chrome that was already up
|
|
100
|
-
* (
|
|
101
|
-
* disrupt it.
|
|
100
|
+
* (for example, the capture flow's), we only close the tab we opened so we
|
|
101
|
+
* don't disrupt it.
|
|
102
102
|
*/
|
|
103
103
|
export async function captureFirebaseTokenViaCdp(opts: {
|
|
104
104
|
loginUrl?: string
|
|
@@ -107,7 +107,13 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
107
107
|
}): Promise<CdpLoginResult> {
|
|
108
108
|
const loginUrl =
|
|
109
109
|
opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL
|
|
110
|
-
|
|
110
|
+
// 6 minutes, not 3. On a FRESH Chrome profile the dashboard is a cold load
|
|
111
|
+
// of an ~8.6 MB uncompressed bundle before the login form is even
|
|
112
|
+
// interactive — tens of seconds on a slow link — and only then does the
|
|
113
|
+
// developer start typing credentials and a 2FA code. The old 3-minute
|
|
114
|
+
// budget was routinely spent before they could finish, and the retry that
|
|
115
|
+
// followed looked like a 6-minute hang with no output.
|
|
116
|
+
const timeoutMs = opts.timeoutMs ?? 360_000
|
|
111
117
|
const pollMs = opts.pollMs ?? 1000
|
|
112
118
|
|
|
113
119
|
const cfg = new ConfigStore().read()?.browser_agent
|
|
@@ -123,7 +129,7 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
123
129
|
throw new Error(
|
|
124
130
|
`attach mode: nothing answering CDP on port ${port}. Launch Chrome ` +
|
|
125
131
|
`with --remote-debugging-port=${port} AND a non-default ` +
|
|
126
|
-
'--user-data-dir, or switch to dedicated mode (
|
|
132
|
+
'--user-data-dir, or switch to dedicated mode (attach_browser).',
|
|
127
133
|
)
|
|
128
134
|
}
|
|
129
135
|
|
|
@@ -139,6 +145,10 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
139
145
|
try {
|
|
140
146
|
const session = await handles.attachToTab(targetId)
|
|
141
147
|
const deadline = Date.now() + timeoutMs
|
|
148
|
+
// Tracks how far the page got, so a timeout can say WHICH thing stalled:
|
|
149
|
+
// a bundle that never finished downloading is a different problem from a
|
|
150
|
+
// developer who walked away mid-sign-in.
|
|
151
|
+
let sawInteractivePage = false
|
|
142
152
|
while(Date.now() < deadline) {
|
|
143
153
|
const result = await session.send('Runtime.evaluate', {
|
|
144
154
|
expression: EXTRACT_TOKEN_JS,
|
|
@@ -150,12 +160,24 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
150
160
|
return { token: value, closedBrowser: launchedByUs }
|
|
151
161
|
}
|
|
152
162
|
|
|
163
|
+
if(!sawInteractivePage) {
|
|
164
|
+
sawInteractivePage = await pageIsInteractive(session)
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
154
168
|
}
|
|
155
169
|
|
|
170
|
+
const waited = Math.round(timeoutMs / 1000)
|
|
156
171
|
throw new Error(
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
`Timed out after ${waited}s waiting for login. `
|
|
173
|
+
+ (sawInteractivePage
|
|
174
|
+
? 'The page finished loading, so sign-in was never completed — '
|
|
175
|
+
+ 'ask the developer to finish signing in at the opened window, '
|
|
176
|
+
+ 'then re-run reclaim_authenticate.'
|
|
177
|
+
: 'The page never became interactive, so the dashboard was still '
|
|
178
|
+
+ 'loading the whole time. On a fresh Chrome profile its bundle '
|
|
179
|
+
+ 'is a large cold download. Retry (the profile is now warm), or '
|
|
180
|
+
+ 'pass a bigger timeoutMs.'),
|
|
159
181
|
)
|
|
160
182
|
} finally {
|
|
161
183
|
// Best-effort cleanup — never mask the real result/error.
|
|
@@ -173,6 +195,28 @@ export async function captureFirebaseTokenViaCdp(opts: {
|
|
|
173
195
|
}
|
|
174
196
|
}
|
|
175
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Whether the login page has reached a usable state. `readyState` alone is not
|
|
200
|
+
* enough for a single-bundle SPA: the document is 'complete' while the app is
|
|
201
|
+
* still parsing, and nothing is clickable yet. Requiring a rendered root as
|
|
202
|
+
* well is what distinguishes "still loading" from "waiting for the human".
|
|
203
|
+
*/
|
|
204
|
+
async function pageIsInteractive(
|
|
205
|
+
session: { send: (m: string, p?: object) => Promise<unknown> },
|
|
206
|
+
): Promise<boolean> {
|
|
207
|
+
try {
|
|
208
|
+
const result = await session.send('Runtime.evaluate', {
|
|
209
|
+
expression:
|
|
210
|
+
'document.readyState === "complete" '
|
|
211
|
+
+ '&& !!document.body && document.body.innerText.trim().length > 0',
|
|
212
|
+
returnByValue: true,
|
|
213
|
+
})
|
|
214
|
+
return runtimeResultValue(result) === true
|
|
215
|
+
} catch{
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
176
220
|
function runtimeResultValue(value: unknown): unknown {
|
|
177
221
|
if(typeof value !== 'object' || !value || !('result' in value)) {
|
|
178
222
|
return undefined
|
package/src/old/to-register.ts
CHANGED
|
@@ -39,7 +39,8 @@ export interface OldProviderOpts {
|
|
|
39
39
|
/** Page-injection script — the devtools name for the builder's
|
|
40
40
|
* `webSettings.jsUserScripts`. Top-level on register, nested under
|
|
41
41
|
* `providerConfig` on config (add-version). The backend rejects a few
|
|
42
|
-
* deprecated injection APIs (
|
|
42
|
+
* deprecated injection APIs (for example, `window.payloadData`) with a
|
|
43
|
+
* 400. */
|
|
43
44
|
customInjection?: string
|
|
44
45
|
/** Accepted at register too (unlike the four fields below, which the
|
|
45
46
|
* backend only accepts on config/add-version). */
|
|
@@ -355,7 +356,7 @@ export interface SemverTriplet {
|
|
|
355
356
|
/**
|
|
356
357
|
* Pull `{major,minor,patch}` triplets out of a `/versions` response, tolerant
|
|
357
358
|
* of the exact envelope (array, `{ providerVersions: [...] }`, `{ items }`,
|
|
358
|
-
|
|
359
|
+
*, and so on). Each entry's `version` is the triplet.
|
|
359
360
|
*/
|
|
360
361
|
export function extractVersions(resp: unknown): SemverTriplet[] {
|
|
361
362
|
const list = pickArray(resp)
|
|
@@ -46,6 +46,11 @@ export function authenticateTool(client: ReclaimOldClient): RegisteredTool {
|
|
|
46
46
|
'and closes the tab. A previously stored identity that is still ' +
|
|
47
47
|
'valid is reused first (pass `useBrowser: true` to force a fresh ' +
|
|
48
48
|
'login). \n\n' +
|
|
49
|
+
'EXPECT THIS TO TAKE A WHILE ON A FIRST RUN: a fresh Chrome ' +
|
|
50
|
+
'profile downloads the dashboard cold, so tell the developer it is ' +
|
|
51
|
+
'loading and that the window is waiting for them — do not re-run ' +
|
|
52
|
+
'the tool while it is still waiting. It polls for up to 6 minutes ' +
|
|
53
|
+
'and reports whether the page ever became interactive.\n\n' +
|
|
49
54
|
'Manual overrides:\n' +
|
|
50
55
|
'- `token`: paste a Firebase ID token (skips the browser).\n' +
|
|
51
56
|
'- `ethAddress`: an `eth:0x…` / `0x…` uid for an already-provisioned ' +
|
|
@@ -73,7 +78,10 @@ export function authenticateTool(client: ReclaimOldClient): RegisteredTool {
|
|
|
73
78
|
timeoutMs: {
|
|
74
79
|
type: 'number',
|
|
75
80
|
description:
|
|
76
|
-
'How long to wait for sign-in, in ms (default
|
|
81
|
+
'How long to wait for sign-in, in ms (default 360000, i.e. 6 '
|
|
82
|
+
+ 'minutes). A fresh Chrome profile loads the dashboard cold, '
|
|
83
|
+
+ 'which can take tens of seconds before the form is usable — '
|
|
84
|
+
+ 'that time comes out of this budget.',
|
|
77
85
|
},
|
|
78
86
|
useBrowser: {
|
|
79
87
|
type: 'boolean',
|
|
@@ -3,8 +3,8 @@ import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
|
3
3
|
import type { ReclaimSdkClient } from '../sdk-client.ts'
|
|
4
4
|
|
|
5
5
|
/** Best-effort call: resolve to the response, or to an error string instead
|
|
6
|
-
* of throwing — one endpoint 404ing (
|
|
7
|
-
* hide a result the other endpoint DID have. */
|
|
6
|
+
* of throwing — one endpoint 404ing (for example, a bad `versionNumber`)
|
|
7
|
+
* shouldn't hide a result the other endpoint DID have. */
|
|
8
8
|
async function attempt(
|
|
9
9
|
label: string,
|
|
10
10
|
call: () => Promise<unknown>,
|
|
@@ -51,8 +51,9 @@ export function getProviderInfoTool(client: ReclaimSdkClient): RegisteredTool {
|
|
|
51
51
|
'computed `isScriptRequestingClaim` flag). Read `provider` here ' +
|
|
52
52
|
'BEFORE create_provider_version_from_capture so you can pass the ' +
|
|
53
53
|
'whole existing config back — that tool always creates a new ' +
|
|
54
|
-
'version and carries nothing forward. One endpoint failing (
|
|
55
|
-
'an invalid `versionNumber`) is reported under `errors`
|
|
54
|
+
'version and carries nothing forward. One endpoint failing (for ' +
|
|
55
|
+
'example, an invalid `versionNumber`) is reported under `errors` ' +
|
|
56
|
+
'without ' +
|
|
56
57
|
'hiding a result the other endpoint did return.',
|
|
57
58
|
inputSchema: {
|
|
58
59
|
type: 'object',
|
|
@@ -64,7 +65,7 @@ export function getProviderInfoTool(client: ReclaimSdkClient): RegisteredTool {
|
|
|
64
65
|
versionNumber: {
|
|
65
66
|
type: 'string',
|
|
66
67
|
description:
|
|
67
|
-
'Specific version,
|
|
68
|
+
'Specific version, for example, "1.0.2". Omit for the ' +
|
|
68
69
|
"provider's current latest version.",
|
|
69
70
|
},
|
|
70
71
|
},
|
|
@@ -24,8 +24,9 @@ export function issueAppCredentialsTool(
|
|
|
24
24
|
'starts UNLINKED and sandboxed (100 sessions) — call ' +
|
|
25
25
|
'link_app_to_account to attach it to your dashboard account and ' +
|
|
26
26
|
'check_app_status to check its state. Store `appSecret` like a ' +
|
|
27
|
-
'password (
|
|
28
|
-
'is returned once and cannot be
|
|
27
|
+
'password (for example, in a gitignored .env as ' +
|
|
28
|
+
'RECLAIM_APP_SECRET) — it is returned once and cannot be ' +
|
|
29
|
+
'recovered from the server.',
|
|
29
30
|
inputSchema: { type: 'object', properties: {} },
|
|
30
31
|
},
|
|
31
32
|
async() => {
|
package/src/old/tools/publish.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Boom, isBoom } from '@hapi/boom'
|
|
2
2
|
import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
3
3
|
import type { AttachState } from '../../mcp/tools/agent/attach.ts'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ALLOWED_REQUESTS_HINT,
|
|
6
|
+
USER_SCRIPT_HINT,
|
|
7
|
+
} from '../../provider/injection-doc.ts'
|
|
5
8
|
import {
|
|
6
9
|
countsFor,
|
|
7
10
|
matchRedactionWarnings,
|
|
@@ -35,8 +38,8 @@ function extractProviderId(resp: unknown): string | undefined {
|
|
|
35
38
|
* `providerId` for a public provider (or anyone else's) comes back as
|
|
36
39
|
* 401/403/404 — the dev's likely intent is to publish their OWN version of that
|
|
37
40
|
* site, which on this backend means registering a separate copy. Rethrow those
|
|
38
|
-
* failures with that actionable suggestion; leave other errors (
|
|
39
|
-
* version collision) untouched so their real detail surfaces.
|
|
41
|
+
* failures with that actionable suggestion; leave other errors (for example, a
|
|
42
|
+
* 400 version collision) untouched so their real detail surfaces.
|
|
40
43
|
*/
|
|
41
44
|
function rethrowOwnershipError(err: unknown, providerId: string): never {
|
|
42
45
|
const status = isBoom(err) ? err.output.statusCode : undefined
|
|
@@ -62,7 +65,7 @@ function rethrowOwnershipError(err: unknown, providerId: string): never {
|
|
|
62
65
|
/**
|
|
63
66
|
* Publish a drafted provider to the OLD devtools backend. Keeps the builder
|
|
64
67
|
* tool NAME (`create_provider_version_from_capture`) so the skill flow is
|
|
65
|
-
* unchanged, but the
|
|
68
|
+
* unchanged, but the behavior differs:
|
|
66
69
|
*
|
|
67
70
|
* - WITHOUT `providerId` → `POST /api/providers/register`: creates a
|
|
68
71
|
* brand-new provider + version 1.0.0. Re-running creates a DUPLICATE.
|
|
@@ -100,67 +103,43 @@ export function createVersionFromCaptureTool(
|
|
|
100
103
|
useIncognitoWebview?: boolean
|
|
101
104
|
extensionConfig?: unknown
|
|
102
105
|
injectionType?: 'NONE' | 'MSWJS' | 'XHOOK' | 'CDP' | 'HAWKEYE'
|
|
106
|
+
disableRequestReplay?: boolean
|
|
103
107
|
}>(
|
|
104
108
|
{
|
|
105
109
|
name: 'create_provider_version_from_capture',
|
|
106
110
|
description:
|
|
107
|
-
'Publish drafted ReclaimProvider(s) to the devtools backend. '
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'`
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'`
|
|
117
|
-
'
|
|
118
|
-
'(
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'`provider`/`providers`). Anything you OMIT is published blank/' +
|
|
140
|
-
'default in the new version, NOT preserved. To change one field ' +
|
|
141
|
-
'while keeping the rest, first read the current version with ' +
|
|
142
|
-
'get_provider_info, then pass the full desired config (the existing ' +
|
|
143
|
-
'values plus your change) back into this call. ' +
|
|
144
|
-
'`allowedInjectedRequestData`/`useIncognitoWebview`/' +
|
|
145
|
-
'`extensionConfig` only take effect on add-version — the register ' +
|
|
146
|
-
'endpoint (no `providerId`) ignores them, so on a fresh register ' +
|
|
147
|
-
'this tool automatically follows up with an add-version call that ' +
|
|
148
|
-
'applies them (also where document replay gets force-disabled — ' +
|
|
149
|
-
'see below). Per-request fields you may set on ' +
|
|
150
|
-
'`provider`/`providers`: `credentials` (omit/same-origin/include, ' +
|
|
151
|
-
'default include), `urlType` (REGEX/CONSTANT/TEMPLATE, default ' +
|
|
152
|
-
'derived from {{placeholders}}), and `isOptional` on each ' +
|
|
153
|
-
'responseMatch (default false). \n\n' +
|
|
154
|
-
'Captured requests must be GET/POST. OPRF redactions are supported. ' +
|
|
155
|
-
'Document replay — the HAWKEYE-level flag controlling whether the ' +
|
|
156
|
-
'page\'s own document/navigation request gets genuinely re-sent ' +
|
|
157
|
-
'through the interceptor at verification time — is ALWAYS ' +
|
|
158
|
-
'published disabled (`disableRequestReplay: true`); there is no ' +
|
|
159
|
-
'way to enable it here. ' +
|
|
160
|
-
'On success, if a browser is still attached, the response\'s ' +
|
|
161
|
-
'`_notes` reminds you to confirm with the developer before ' +
|
|
162
|
-
'calling `dispose_browser` — never close it unprompted, they ' +
|
|
163
|
-
'may want it for further edits.',
|
|
111
|
+
'Publish drafted ReclaimProvider(s) to the devtools backend. Needs '
|
|
112
|
+
+ 'its OWN reclaim_authenticate login — authenticate_builder does NOT '
|
|
113
|
+
+ 'authorize this call. Pass the `provider` object(s) from '
|
|
114
|
+
+ 'propose_provider VERBATIM, plus the `initialUrl` the user landed '
|
|
115
|
+
+ 'on (the login/landing page, NOT the API url). Exactly one of '
|
|
116
|
+
+ '`provider` (one request) or `providers` (a multi-request '
|
|
117
|
+
+ 'provider — every entry becomes a parallel requestData entry, all '
|
|
118
|
+
+ 'verified together). Captured requests must be GET/POST; OPRF '
|
|
119
|
+
+ 'redactions are supported.\n\n'
|
|
120
|
+
+ 'Omit `providerId` to REGISTER A NEW provider (creates it + '
|
|
121
|
+
+ 'version 1.0.0 — re-running creates a DUPLICATE). Pass '
|
|
122
|
+
+ '`providerId` (from get_me_providers) to add a NEW VERSION to it; '
|
|
123
|
+
+ 'the patch auto-bumps. You can only version a provider YOU own — '
|
|
124
|
+
+ 'to publish your own take on one you don\'t own, omit '
|
|
125
|
+
+ '`providerId` to register your own copy.\n\n'
|
|
126
|
+
+ 'THIS BACKEND ONLY EVER CREATES A NEW VERSION — there is no '
|
|
127
|
+
+ 'partial-patch endpoint, and this tool carries NOTHING forward '
|
|
128
|
+
+ 'from the previous version: every version replaces the WHOLE '
|
|
129
|
+
+ 'providerConfig with exactly what you pass in THIS call. Any '
|
|
130
|
+
+ 'field you want to survive must be re-supplied here '
|
|
131
|
+
+ '(`customInjection`, `userAgent`, `pageTitle`, '
|
|
132
|
+
+ '`allowedInjectedRequestData`, `stepsToFollow`, '
|
|
133
|
+
+ '`useIncognitoWebview`, `extensionConfig`, `requestData`); '
|
|
134
|
+
+ 'anything omitted is published blank. To change one field, read '
|
|
135
|
+
+ 'the current config with get_provider_info first and pass it back '
|
|
136
|
+
+ 'with your edit. `allowedInjectedRequestData`/'
|
|
137
|
+
+ '`useIncognitoWebview`/`extensionConfig` apply on add-version '
|
|
138
|
+
+ 'only, so a fresh register auto-follows with an add-version call '
|
|
139
|
+
+ 'that applies them (and disables document replay — see '
|
|
140
|
+
+ '`disableRequestReplay`).\n\n'
|
|
141
|
+
+ 'Always read the response\'s `_notes`. '
|
|
142
|
+
+ 'Full walkthrough: how_it_works({ topic: "publish" }).',
|
|
164
143
|
inputSchema: {
|
|
165
144
|
type: 'object',
|
|
166
145
|
properties: {
|
|
@@ -272,15 +251,11 @@ export function createVersionFromCaptureTool(
|
|
|
272
251
|
customInjection: {
|
|
273
252
|
type: 'string',
|
|
274
253
|
description:
|
|
275
|
-
|
|
276
|
-
'
|
|
277
|
-
'
|
|
278
|
-
'(window.
|
|
279
|
-
'
|
|
280
|
-
'only for rare edge cases, not navigation. The backend rejects ' +
|
|
281
|
-
'a few deprecated injection APIs (e.g. window.payloadData, ' +
|
|
282
|
-
'window.ReclaimInjected) with a 400. Guide:\n\n' +
|
|
283
|
-
INJECTION_GUIDE,
|
|
254
|
+
USER_SCRIPT_HINT
|
|
255
|
+
+ ' Old-devtools\' name for what the builder calls '
|
|
256
|
+
+ 'webSettings.jsUserScripts. The backend rejects a few '
|
|
257
|
+
+ 'deprecated injection APIs (window.payloadData, '
|
|
258
|
+
+ 'window.ReclaimInjected) with a 400.',
|
|
284
259
|
},
|
|
285
260
|
userAgent: {
|
|
286
261
|
type: 'object',
|
|
@@ -299,39 +274,15 @@ export function createVersionFromCaptureTool(
|
|
|
299
274
|
allowedInjectedRequestData: {
|
|
300
275
|
type: 'array',
|
|
301
276
|
description:
|
|
302
|
-
'Extra requests the customInjection script is allowed to '
|
|
303
|
-
'fire (
|
|
304
|
-
'
|
|
305
|
-
'
|
|
306
|
-
|
|
307
|
-
'
|
|
308
|
-
'
|
|
309
|
-
'
|
|
310
|
-
|
|
311
|
-
'the "bodySniff" section of the customInjection field\'s guide ' +
|
|
312
|
-
'for the exact mechanics and a worked example. ' +
|
|
313
|
-
'⚠️ BEFORE populating this to gate ' +
|
|
314
|
-
'`verifyProof(proof, { providerId })`: this whole entry (url, ' +
|
|
315
|
-
'method, body, responseMatches, responseRedactions) feeds a ' +
|
|
316
|
-
'client-side hash-equality check, and `url` is hashed VERBATIM ' +
|
|
317
|
-
'— no regex/template resolution, even with `urlType: REGEX` or ' +
|
|
318
|
-
'`TEMPLATE`. If the real captured URL contains ANY per-session ' +
|
|
319
|
-
'part (a tracking nonce, CSRF token, cache-busting timestamp), ' +
|
|
320
|
-
'no declared spec can EVER hash-match a real proof, no matter ' +
|
|
321
|
-
'how correct the rest of this entry is — populating' +
|
|
322
|
-
'it for such ' +
|
|
323
|
-
'a provider makes verifyProof reject every real proof,' +
|
|
324
|
-
'which is ' +
|
|
325
|
-
'worse than leaving this field empty. Diff the same endpoint\'s' +
|
|
326
|
-
' captured URL across two separate sessions before wiring this ' +
|
|
327
|
-
'up. If provider-hash validation keeps causing problems ' +
|
|
328
|
-
'(unstable url, or any other persistent mismatch), the ' +
|
|
329
|
-
'fallback on the verifying side is `verifyProof(proof, { ' +
|
|
330
|
-
'dangerouslyDisableContentValidation: true })` — trusts only ' +
|
|
331
|
-
'the attestor\'s signature, skips the hash-equality check ' +
|
|
332
|
-
'entirely; see the "Provider-hash validation" section of the ' +
|
|
333
|
-
'customInjection field\'s guide for the full mechanics and ' +
|
|
334
|
-
'when to reach for it.',
|
|
277
|
+
'Extra requests the customInjection user script is allowed to '
|
|
278
|
+
+ 'fire (for example, pagination) — add-version ONLY, the '
|
|
279
|
+
+ 'register '
|
|
280
|
+
+ 'endpoint ignores it. Each item has the same shape as '
|
|
281
|
+
+ '`provider`, plus `multiple`/`required` (default true). Set '
|
|
282
|
+
+ '`body` whenever the request has one (see its own description '
|
|
283
|
+
+ 'below). The builder calls the same concept '
|
|
284
|
+
+ '`allowedJsRequests`. '
|
|
285
|
+
+ ALLOWED_REQUESTS_HINT,
|
|
335
286
|
items: {
|
|
336
287
|
type: 'object',
|
|
337
288
|
properties: {
|
|
@@ -353,7 +304,8 @@ export function createVersionFromCaptureTool(
|
|
|
353
304
|
'body at all (skip only for a genuinely bodyless ' +
|
|
354
305
|
'request, typically GET). If the real body is FIXED ' +
|
|
355
306
|
'(identical for every user/run), put that literal body ' +
|
|
356
|
-
'text here verbatim. If only PART of it varies (
|
|
307
|
+
'text here verbatim. If only PART of it varies (for ' +
|
|
308
|
+
'example, ' +
|
|
357
309
|
'`{"request":1,"type":"user_info","id":"12345"}` where ' +
|
|
358
310
|
'only `id` differs per user), template ONLY that part: ' +
|
|
359
311
|
'`{"request":1,"type":"user_info",' +
|
|
@@ -376,7 +328,7 @@ export function createVersionFromCaptureTool(
|
|
|
376
328
|
'reproduces the real captured request body ' +
|
|
377
329
|
'byte-for-byte. Fallback if per-field templating keeps ' +
|
|
378
330
|
'mismatching (more fields vary than expected, unstable ' +
|
|
379
|
-
'key order,
|
|
331
|
+
'key order, and so on) and you can\'t pin down every ' +
|
|
380
332
|
'variable field: template the WHOLE body as one ' +
|
|
381
333
|
'placeholder instead, `{{REQ_BODY_GRD}}` — the `GRD` ' +
|
|
382
334
|
'suffix makes it a greedy match (captures everything) ' +
|
|
@@ -394,7 +346,7 @@ export function createVersionFromCaptureTool(
|
|
|
394
346
|
'`value` must literally appear (or match, if `regex`) ' +
|
|
395
347
|
'somewhere in the response body for the claim to be ' +
|
|
396
348
|
'accepted; it commonly embeds one or more ' +
|
|
397
|
-
'`{{paramName}}` extraction placeholders (
|
|
349
|
+
'`{{paramName}}` extraction placeholders (for example, ' +
|
|
398
350
|
'`"name":"{{name_0}}"`) whose real value is redacted ' +
|
|
399
351
|
'out and supplied separately as a witness/extracted ' +
|
|
400
352
|
'param. Use `${var}` (not `{{var}}`) inside this string ' +
|
|
@@ -410,7 +362,8 @@ export function createVersionFromCaptureTool(
|
|
|
410
362
|
'Each item: `{ jsonPath?: string, xPath?: string, ' +
|
|
411
363
|
'regex?: string, hash?: string }`. Identifies WHICH ' +
|
|
412
364
|
'part of the response the matching `{{paramName}}` in ' +
|
|
413
|
-
'`responseMatches` actually redacts
|
|
365
|
+
'`responseMatches` actually redacts or extracts — for ' +
|
|
366
|
+
'example, ' +
|
|
414
367
|
'`jsonPath: "$.data.items[0].name"` for a JSON response. ' +
|
|
415
368
|
'Order/count should line up with `responseMatches`\' ' +
|
|
416
369
|
'placeholders one-to-one for a typical claim.',
|
|
@@ -475,17 +428,28 @@ export function createVersionFromCaptureTool(
|
|
|
475
428
|
'unset — the devtools backend\'s own column default ' +
|
|
476
429
|
'(`HAWKEYE`) applies, which is inert and harmless for the ' +
|
|
477
430
|
'standard capture→replay flow with no interceptor script ' +
|
|
478
|
-
'installed. Only set
|
|
479
|
-
'
|
|
480
|
-
'that calls ' +
|
|
431
|
+
'installed. Only set `HAWKEYE`/`MSWJS`/`XHOOK` when you are ' +
|
|
432
|
+
'ALSO supplying a `customInjection` script that calls ' +
|
|
481
433
|
'`window.reclaimInterceptor.addResponseMiddleware(...)` for ' +
|
|
482
|
-
'genuine live interception, or set `NONE` explicitly
|
|
483
|
-
'
|
|
484
|
-
'
|
|
485
|
-
'
|
|
486
|
-
'
|
|
487
|
-
'
|
|
488
|
-
|
|
434
|
+
'genuine live interception, or set `NONE` explicitly to be ' +
|
|
435
|
+
'certain none is configured. Builder calls the equivalent ' +
|
|
436
|
+
'concept `interceptorType` (no `NONE` there — omitting the ' +
|
|
437
|
+
'whole `interceptorOptions` object is its no-interception ' +
|
|
438
|
+
'signal instead). What each value actually does: ' +
|
|
439
|
+
'how_it_works({ topic: "interception" }).',
|
|
440
|
+
},
|
|
441
|
+
disableRequestReplay: {
|
|
442
|
+
type: 'boolean',
|
|
443
|
+
description:
|
|
444
|
+
'Whether the page\'s own document/navigation request is ' +
|
|
445
|
+
'skipped (constructed locally) instead of genuinely replayed ' +
|
|
446
|
+
'through the interceptor. Defaults to `true` (disabled) when ' +
|
|
447
|
+
'omitted. Only meaningful for `injectionType` ' +
|
|
448
|
+
'HAWKEYE/MSWJS/XHOOK — pass `false` to enable real document ' +
|
|
449
|
+
'replay for those (for example, the target value is embedded ' +
|
|
450
|
+
'in the page\'s own HTML). Inert when `injectionType` is ' +
|
|
451
|
+
'`CDP` or ' +
|
|
452
|
+
'unset.',
|
|
489
453
|
},
|
|
490
454
|
},
|
|
491
455
|
required: ['initialUrl'],
|
|
@@ -529,9 +493,9 @@ export function createVersionFromCaptureTool(
|
|
|
529
493
|
// column default applies when omitted, same as before this field
|
|
530
494
|
// existed at all.
|
|
531
495
|
injectionType: args.injectionType,
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
disableRequestReplay: true,
|
|
496
|
+
// Author-settable; defaults to disabled (see tool description).
|
|
497
|
+
// Only meaningful for HAWKEYE/MSWJS/XHOOK — inert for CDP.
|
|
498
|
+
disableRequestReplay: args.disableRequestReplay ?? true,
|
|
535
499
|
allowedInjectedRequestData: args.allowedInjectedRequestData
|
|
536
500
|
? injectedRequestDataFrom(
|
|
537
501
|
args.allowedInjectedRequestData,
|
|
@@ -584,12 +548,12 @@ export function createVersionFromCaptureTool(
|
|
|
584
548
|
)
|
|
585
549
|
}
|
|
586
550
|
|
|
587
|
-
// disableRequestReplay is honored ONLY on add-version
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
// an explicit injectionType, when passed —
|
|
591
|
-
// register persists it — and any other
|
|
592
|
-
// caller passed).
|
|
551
|
+
// disableRequestReplay is honored ONLY on add-version — so every
|
|
552
|
+
// register is immediately followed by an add-version call that
|
|
553
|
+
// actually applies it (whatever value — default or author-set;
|
|
554
|
+
// which also lands an explicit injectionType, when passed —
|
|
555
|
+
// unverified whether register persists it — and any other
|
|
556
|
+
// register-ignored fields the caller passed).
|
|
593
557
|
const REGISTER_IGNORED_KEYS = [
|
|
594
558
|
'allowedInjectedRequestData',
|
|
595
559
|
'useIncognitoWebview',
|
|
@@ -603,8 +567,8 @@ export function createVersionFromCaptureTool(
|
|
|
603
567
|
`${ignoredOnRegister.length > 1 ? 'are' : 'is'} ` +
|
|
604
568
|
'ignored on register (no providerId) — the devtools backend only ' +
|
|
605
569
|
'reads them when adding a version. They were applied by the ' +
|
|
606
|
-
'automatic follow-up version this tool always creates to ' +
|
|
607
|
-
'
|
|
570
|
+
'automatic follow-up version this tool always creates to apply ' +
|
|
571
|
+
'disableRequestReplay.',
|
|
608
572
|
)
|
|
609
573
|
}
|
|
610
574
|
|
|
@@ -633,7 +597,7 @@ export function createVersionFromCaptureTool(
|
|
|
633
597
|
await client.updateProviderConfig(newProviderId, configBody)
|
|
634
598
|
notes.push(
|
|
635
599
|
`Created an automatic follow-up version (${followVersion}) to ` +
|
|
636
|
-
|
|
600
|
+
`apply disableRequestReplay (${opts.disableRequestReplay})` +
|
|
637
601
|
(opts.injectionType !== undefined
|
|
638
602
|
? ` and set injectionType (${opts.injectionType})`
|
|
639
603
|
: '') +
|
|
@@ -657,7 +621,7 @@ export function createVersionFromCaptureTool(
|
|
|
657
621
|
}
|
|
658
622
|
} else {
|
|
659
623
|
notes.push(
|
|
660
|
-
'Could not
|
|
624
|
+
'Could not apply disableRequestReplay: the ' +
|
|
661
625
|
'register response did not include the new providerId. Call ' +
|
|
662
626
|
'this tool again WITH `providerId` to create a version that ' +
|
|
663
627
|
'applies them.',
|
|
@@ -668,7 +632,7 @@ export function createVersionFromCaptureTool(
|
|
|
668
632
|
}
|
|
669
633
|
|
|
670
634
|
// New version of an existing provider — pick the next patch above
|
|
671
|
-
// the highest existing version. A provider you don't own (
|
|
635
|
+
// the highest existing version. A provider you don't own (for example, a
|
|
672
636
|
// public one) fails here with 401/403/404; surface the register-a-copy
|
|
673
637
|
// workaround instead of a bare permission error.
|
|
674
638
|
const providerId = args.providerId
|