@masons/agent-network 0.5.13 → 0.5.14
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/dist/cli-setup.d.ts +61 -45
- package/dist/cli-setup.d.ts.map +1 -1
- package/dist/cli-setup.js +252 -315
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli-setup.d.ts
CHANGED
|
@@ -1,58 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Login Path —
|
|
2
|
+
* CLI Login Path — encrypted server-bridge handoff (v0.5.14).
|
|
3
3
|
*
|
|
4
4
|
* Implements `login()` hook for OpenClaw's 2026.4.x channel-plugin contract:
|
|
5
5
|
* api.registerChannel({ plugin: { ..., auth: { login } } })
|
|
6
6
|
*
|
|
7
7
|
* Invoked by `openclaw channels login --channel agent-network`. CLI dispatch
|
|
8
8
|
* at `dist/channels-cli-Cc40S0aS.js:85` reads `plugin.auth?.login` and throws
|
|
9
|
-
* `"Channel ... does not support login"` if absent.
|
|
10
|
-
* bundled extensions for reference implementations of the same contract.
|
|
9
|
+
* `"Channel ... does not support login"` if absent.
|
|
11
10
|
*
|
|
12
|
-
* Flow (
|
|
11
|
+
* Flow (v0.5.14 — `serverBridgeHandoffFlow`):
|
|
13
12
|
*
|
|
14
|
-
* 1.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 2.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
13
|
+
* 1. Generate an ephemeral RSA-2048 keypair in the plugin process. The
|
|
14
|
+
* private key never leaves process memory; the SPKI-encoded public
|
|
15
|
+
* key travels to `apps/api` for the browser to encrypt against.
|
|
16
|
+
* 2. POST `${apiHost}/v1/cli-handoff/init` with the SPKI pubkey; receive
|
|
17
|
+
* back a `session_id` (`cli_…`) keyed in apps/api's Redis with a 5-min
|
|
18
|
+
* TTL.
|
|
19
|
+
* 3. Print a handoff URL (`${idpBaseUrl}/console/handoff?session=<id>` or
|
|
20
|
+
* the per-handle variant when `channelInput` parses as a handle) for
|
|
21
|
+
* the user to open in their browser. The plugin does NOT spawn a
|
|
22
|
+
* browser — `child_process` is forbidden by OpenClaw's plugin scanner
|
|
23
|
+
* (rules `dangerous-exec`, `env-harvesting`).
|
|
24
|
+
* 4. The user signs in to apps/web, picks or creates an agent, and the
|
|
25
|
+
* browser runs WebCrypto client-side: generates an ephemeral AES-256-GCM
|
|
26
|
+
* key, encrypts `{token, agent, key}` JSON under it, wraps the AES key
|
|
27
|
+
* under the plugin's RSA pubkey via RSA-OAEP-256, and POSTs the
|
|
28
|
+
* `{wrapped_key, iv, ciphertext}` triple to
|
|
29
|
+
* `${apiHost}/v1/cli-handoff/:session/complete` (authenticated server-
|
|
30
|
+
* action mediated by apps/web). Server stores ciphertext only.
|
|
31
|
+
* 5. Plugin polls `${apiHost}/v1/cli-handoff/:session/poll` every 2s until
|
|
32
|
+
* the bridge returns `{status: "completed", payload}`.
|
|
33
|
+
* 6. Plugin decrypts in-process: RSA-OAEP-256 unwrap → AES-256-GCM
|
|
34
|
+
* decrypt → JSON parse → validate `masons_rt_v1_` prefix.
|
|
35
|
+
* 7. Persist `{connectorUrl, token}` to `openclaw.json` via
|
|
36
|
+
* `writeCredentials()`. `connectorUrl` derives `wss://<apiHost>/gateway`
|
|
37
|
+
* for cloud / preview deployments; self-hosted users keep an explicit
|
|
38
|
+
* `connectorUrl` in their `openclaw.json` and the value is preserved
|
|
39
|
+
* across the handoff (see `loadExistingConnectorUrl()`).
|
|
34
40
|
*
|
|
35
|
-
* Why
|
|
41
|
+
* Why this shape (vs the v0.5.13 loopback handoff or device flow):
|
|
36
42
|
*
|
|
37
|
-
* -
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* - **Sandbox-friendly.** No `child_process` (no auto-open), no
|
|
44
|
+
* `node:http.createServer` listener, no `process.env` read inside the
|
|
45
|
+
* network code path. OpenClaw's plugin scanner accepts the resulting
|
|
46
|
+
* code surface — the path that blocked v0.5.13 is closed.
|
|
47
|
+
* - **End-to-end encrypted.** The apps/api Redis cache stores ciphertext
|
|
48
|
+
* only. A stolen Mongo / Redis dump yields nothing useful — the wrapped
|
|
49
|
+
* AES key is unrecoverable without the plugin's RSA private key (which
|
|
50
|
+
* lives only in plugin process memory). Mitigates the Shai-Hulud
|
|
51
|
+
* "plaintext-at-rest, even short-lived" concern that originally caused
|
|
52
|
+
* Decision B Alt B3 to be rejected; see #1475 issuecomment-4365154888 § A
|
|
53
|
+
* for the formal reject-supersede record.
|
|
54
|
+
* - **`masons_rt_…` carrier on connector.** Connector verifies new keys
|
|
55
|
+
* via the W4 PR 1 mirror substrate; this file does not change carrier
|
|
56
|
+
* semantics, only the transport that gets the plaintext from
|
|
57
|
+
* apps/web → CLI.
|
|
46
58
|
*
|
|
47
|
-
*
|
|
48
|
-
* `agentSetup`, `createAgentLoop`) remain in this file as
|
|
49
|
-
*
|
|
50
|
-
*
|
|
59
|
+
* Legacy device-flow helpers (`deviceCodeFlow`, `pollUntilAuthorized`,
|
|
60
|
+
* `agentSetup`, `createAgentLoop`) remain in this file as `@deprecated`
|
|
61
|
+
* reference for the W6 cleanup window. They have no call site after the
|
|
62
|
+
* v0.5.13 → v0.5.14 migration and should not be invoked by any new code.
|
|
51
63
|
*
|
|
52
64
|
* See #1264 for original design rationale (single-driver Node semantic —
|
|
53
65
|
* re-running this flow rotates the runtime key and evicts any previously-
|
|
54
|
-
* connected Runtime); see the
|
|
55
|
-
* `#
|
|
66
|
+
* connected Runtime); see the v0.5.14 strategic-pass at
|
|
67
|
+
* `#1475 issuecomment-4365154888` for the lock that supersedes both
|
|
68
|
+
* Alt B1 (loopback, scanner-blocked) and the prior Alt B3 reject.
|
|
56
69
|
*/
|
|
57
70
|
/**
|
|
58
71
|
* Minimal OpenClaw runtime surface passed to `auth.login`. Exposed as
|
|
@@ -89,11 +102,14 @@ interface AuthLoginContext {
|
|
|
89
102
|
* and bumps the state-cache generation so Layer B's dynamic context re-reads
|
|
90
103
|
* on the next turn.
|
|
91
104
|
*
|
|
92
|
-
* Flow (
|
|
93
|
-
* 1. Run `
|
|
94
|
-
*
|
|
95
|
-
* `masons_rt_…` token
|
|
96
|
-
*
|
|
105
|
+
* Flow (v0.5.14):
|
|
106
|
+
* 1. Run `serverBridgeHandoffFlow` against `apps/api` + `apps/web` —
|
|
107
|
+
* an encrypted server-bridge handoff that delivers the plaintext
|
|
108
|
+
* `masons_rt_…` token from the browser to the plugin via apps/api's
|
|
109
|
+
* Redis cache (storing ciphertext only). RSA-OAEP-256 wraps an
|
|
110
|
+
* ephemeral AES-256-GCM key the browser generates; CLI's RSA
|
|
111
|
+
* private key (held only in plugin process memory) decrypts the
|
|
112
|
+
* AES key in-process to recover the plaintext.
|
|
97
113
|
* 2. Resolve the connector URL — preserve any user-set
|
|
98
114
|
* `accounts.default.connectorUrl` from `openclaw.json` (for self-
|
|
99
115
|
* hosted topologies); fall back to the cloud derivation
|
package/dist/cli-setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-setup.d.ts","sourceRoot":"","sources":["../src/cli-setup.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cli-setup.d.ts","sourceRoot":"","sources":["../src/cli-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAwCH;;;;;GAKG;AACH,UAAU,eAAe;IACvB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;GAIG;AACH,UAAU,gBAAgB;IACxB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,eAAe,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0yBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDhE"}
|
package/dist/cli-setup.js
CHANGED
|
@@ -1,66 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Login Path —
|
|
2
|
+
* CLI Login Path — encrypted server-bridge handoff (v0.5.14).
|
|
3
3
|
*
|
|
4
4
|
* Implements `login()` hook for OpenClaw's 2026.4.x channel-plugin contract:
|
|
5
5
|
* api.registerChannel({ plugin: { ..., auth: { login } } })
|
|
6
6
|
*
|
|
7
7
|
* Invoked by `openclaw channels login --channel agent-network`. CLI dispatch
|
|
8
8
|
* at `dist/channels-cli-Cc40S0aS.js:85` reads `plugin.auth?.login` and throws
|
|
9
|
-
* `"Channel ... does not support login"` if absent.
|
|
10
|
-
* bundled extensions for reference implementations of the same contract.
|
|
9
|
+
* `"Channel ... does not support login"` if absent.
|
|
11
10
|
*
|
|
12
|
-
* Flow (
|
|
11
|
+
* Flow (v0.5.14 — `serverBridgeHandoffFlow`):
|
|
13
12
|
*
|
|
14
|
-
* 1.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 2.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
13
|
+
* 1. Generate an ephemeral RSA-2048 keypair in the plugin process. The
|
|
14
|
+
* private key never leaves process memory; the SPKI-encoded public
|
|
15
|
+
* key travels to `apps/api` for the browser to encrypt against.
|
|
16
|
+
* 2. POST `${apiHost}/v1/cli-handoff/init` with the SPKI pubkey; receive
|
|
17
|
+
* back a `session_id` (`cli_…`) keyed in apps/api's Redis with a 5-min
|
|
18
|
+
* TTL.
|
|
19
|
+
* 3. Print a handoff URL (`${idpBaseUrl}/console/handoff?session=<id>` or
|
|
20
|
+
* the per-handle variant when `channelInput` parses as a handle) for
|
|
21
|
+
* the user to open in their browser. The plugin does NOT spawn a
|
|
22
|
+
* browser — `child_process` is forbidden by OpenClaw's plugin scanner
|
|
23
|
+
* (rules `dangerous-exec`, `env-harvesting`).
|
|
24
|
+
* 4. The user signs in to apps/web, picks or creates an agent, and the
|
|
25
|
+
* browser runs WebCrypto client-side: generates an ephemeral AES-256-GCM
|
|
26
|
+
* key, encrypts `{token, agent, key}` JSON under it, wraps the AES key
|
|
27
|
+
* under the plugin's RSA pubkey via RSA-OAEP-256, and POSTs the
|
|
28
|
+
* `{wrapped_key, iv, ciphertext}` triple to
|
|
29
|
+
* `${apiHost}/v1/cli-handoff/:session/complete` (authenticated server-
|
|
30
|
+
* action mediated by apps/web). Server stores ciphertext only.
|
|
31
|
+
* 5. Plugin polls `${apiHost}/v1/cli-handoff/:session/poll` every 2s until
|
|
32
|
+
* the bridge returns `{status: "completed", payload}`.
|
|
33
|
+
* 6. Plugin decrypts in-process: RSA-OAEP-256 unwrap → AES-256-GCM
|
|
34
|
+
* decrypt → JSON parse → validate `masons_rt_v1_` prefix.
|
|
35
|
+
* 7. Persist `{connectorUrl, token}` to `openclaw.json` via
|
|
36
|
+
* `writeCredentials()`. `connectorUrl` derives `wss://<apiHost>/gateway`
|
|
37
|
+
* for cloud / preview deployments; self-hosted users keep an explicit
|
|
38
|
+
* `connectorUrl` in their `openclaw.json` and the value is preserved
|
|
39
|
+
* across the handoff (see `loadExistingConnectorUrl()`).
|
|
34
40
|
*
|
|
35
|
-
* Why
|
|
41
|
+
* Why this shape (vs the v0.5.13 loopback handoff or device flow):
|
|
36
42
|
*
|
|
37
|
-
* -
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* - **Sandbox-friendly.** No `child_process` (no auto-open), no
|
|
44
|
+
* `node:http.createServer` listener, no `process.env` read inside the
|
|
45
|
+
* network code path. OpenClaw's plugin scanner accepts the resulting
|
|
46
|
+
* code surface — the path that blocked v0.5.13 is closed.
|
|
47
|
+
* - **End-to-end encrypted.** The apps/api Redis cache stores ciphertext
|
|
48
|
+
* only. A stolen Mongo / Redis dump yields nothing useful — the wrapped
|
|
49
|
+
* AES key is unrecoverable without the plugin's RSA private key (which
|
|
50
|
+
* lives only in plugin process memory). Mitigates the Shai-Hulud
|
|
51
|
+
* "plaintext-at-rest, even short-lived" concern that originally caused
|
|
52
|
+
* Decision B Alt B3 to be rejected; see #1475 issuecomment-4365154888 § A
|
|
53
|
+
* for the formal reject-supersede record.
|
|
54
|
+
* - **`masons_rt_…` carrier on connector.** Connector verifies new keys
|
|
55
|
+
* via the W4 PR 1 mirror substrate; this file does not change carrier
|
|
56
|
+
* semantics, only the transport that gets the plaintext from
|
|
57
|
+
* apps/web → CLI.
|
|
46
58
|
*
|
|
47
|
-
*
|
|
48
|
-
* `agentSetup`, `createAgentLoop`) remain in this file as
|
|
49
|
-
*
|
|
50
|
-
*
|
|
59
|
+
* Legacy device-flow helpers (`deviceCodeFlow`, `pollUntilAuthorized`,
|
|
60
|
+
* `agentSetup`, `createAgentLoop`) remain in this file as `@deprecated`
|
|
61
|
+
* reference for the W6 cleanup window. They have no call site after the
|
|
62
|
+
* v0.5.13 → v0.5.14 migration and should not be invoked by any new code.
|
|
51
63
|
*
|
|
52
64
|
* See #1264 for original design rationale (single-driver Node semantic —
|
|
53
65
|
* re-running this flow rotates the runtime key and evicts any previously-
|
|
54
|
-
* connected Runtime); see the
|
|
55
|
-
* `#
|
|
66
|
+
* connected Runtime); see the v0.5.14 strategic-pass at
|
|
67
|
+
* `#1475 issuecomment-4365154888` for the lock that supersedes both
|
|
68
|
+
* Alt B1 (loopback, scanner-blocked) and the prior Alt B3 reject.
|
|
56
69
|
*/
|
|
57
|
-
import {
|
|
58
|
-
import { readFile } from "node:fs/promises";
|
|
59
|
-
import { createServer, } from "node:http";
|
|
60
|
-
import { homedir } from "node:os";
|
|
61
|
-
import { join } from "node:path";
|
|
70
|
+
import { createDecipheriv, constants as cryptoConstants, generateKeyPairSync, privateDecrypt, } from "node:crypto";
|
|
62
71
|
import { cancel, confirm as clackConfirm, select as clackSelect, text as clackText, isCancel, } from "@clack/prompts";
|
|
63
|
-
import { writeCredentials } from "./config.js";
|
|
72
|
+
import { getOpenClawHome, writeCredentials } from "./config.js";
|
|
64
73
|
import { DEFAULT_API_HOST, onboard, PlatformApiError, } from "./platform-client.js";
|
|
65
74
|
// ---------------------------------------------------------------------------
|
|
66
75
|
// Constants
|
|
@@ -108,288 +117,199 @@ const CREATE_NEW_OPTION = "Create new agent";
|
|
|
108
117
|
// volatile for the plugin to track; the round-trip is acceptable for that.
|
|
109
118
|
const HANDLE_REGEX = /^[a-z][a-z0-9_-]{2,14}$/;
|
|
110
119
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* the listener `listen()` and the browser POST.
|
|
120
|
+
* Hard cap on how long the plugin polls the bridge before giving up.
|
|
121
|
+
* Aligned with the apps/api `/v1/cli-handoff/init` Redis TTL (5 min) so
|
|
122
|
+
* the plugin and the bridge time out together — a longer plugin window
|
|
123
|
+
* would tail off after the session expired anyway.
|
|
116
124
|
*/
|
|
117
|
-
const
|
|
125
|
+
const HANDOFF_TIMEOUT_MS = 5 * 60 * 1000;
|
|
118
126
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
127
|
+
* Polling interval. Stripe CLI uses ~1s; gh CLI device flow uses 5s. 2s
|
|
128
|
+
* lands in between — adequate spinner-style UX without thrashing the
|
|
129
|
+
* Redis cache.
|
|
122
130
|
*/
|
|
123
|
-
const
|
|
124
|
-
/** Bytes of entropy in the one-time fragment nonce. */
|
|
125
|
-
const HANDOFF_NONCE_BYTES = 24;
|
|
131
|
+
const HANDOFF_POLL_INTERVAL_MS = 2000;
|
|
126
132
|
/**
|
|
127
|
-
* Run the
|
|
128
|
-
* + the agent identity the browser issued it for.
|
|
129
|
-
* after a single successful POST; subsequent reuse of the URL is
|
|
130
|
-
* rejected.
|
|
133
|
+
* Run the encrypted server-bridge handoff. Returns the decrypted
|
|
134
|
+
* runtime API key + the agent identity the browser issued it for.
|
|
131
135
|
*
|
|
132
136
|
* `channelInput` is the optional argument from
|
|
133
137
|
* `openclaw channels login --channel agent-network -- <input>`. When
|
|
134
138
|
* the input parses as a handle (`^[a-z][a-z0-9_-]{2,14}$`) the flow
|
|
135
|
-
*
|
|
136
|
-
* picker. Anything that isn't a handle
|
|
137
|
-
* through
|
|
139
|
+
* directs the user to the per-handle deep-link entry; otherwise it
|
|
140
|
+
* falls back to the picker page. Anything that isn't a handle silently
|
|
141
|
+
* falls through.
|
|
138
142
|
*/
|
|
139
|
-
async function
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// The Promise we hand the inner request handler — resolved on a
|
|
146
|
-
// valid POST. The reject path is owned by the timeout race below
|
|
147
|
-
// (`Promise.race`); the request handler never produces a "fatal"
|
|
148
|
-
// error worth rejecting the outer promise — invalid bodies / wrong
|
|
149
|
-
// nonces / duplicate posts respond 4xx and let the listener keep
|
|
150
|
-
// running until the timeout or a valid POST wins.
|
|
151
|
-
let resolveResult = null;
|
|
152
|
-
const handoffPromise = new Promise((res) => {
|
|
153
|
-
resolveResult = res;
|
|
143
|
+
async function serverBridgeHandoffFlow(apiHost, idpBaseUrl, runtime, channelInput) {
|
|
144
|
+
// 1. Generate an ephemeral RSA-2048 keypair. The private key never
|
|
145
|
+
// leaves the plugin process; the SPKI-encoded public key travels
|
|
146
|
+
// to apps/api so the browser can encrypt against it.
|
|
147
|
+
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
148
|
+
modulusLength: 2048,
|
|
154
149
|
});
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// will, but it cannot guess the 24-byte URL-fragment nonce
|
|
172
|
-
// the listener generated.
|
|
173
|
-
//
|
|
174
|
-
// CORS preflight (`OPTIONS /handoff`) is browser-only — non-browser
|
|
175
|
-
// local processes do not preflight. We answer 204 with ACAO pinned
|
|
176
|
-
// to the request's origin if it is in the allow-list; otherwise
|
|
177
|
-
// 403 with no headers (browser blocks the subsequent POST).
|
|
178
|
-
const origin = req.headers.origin ?? "";
|
|
179
|
-
if (req.method === "OPTIONS" && req.url === HANDOFF_PATH) {
|
|
180
|
-
if (browserOriginAllowList.has(origin)) {
|
|
181
|
-
res.writeHead(204, {
|
|
182
|
-
"Access-Control-Allow-Origin": origin,
|
|
183
|
-
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
184
|
-
"Access-Control-Allow-Headers": "content-type",
|
|
185
|
-
"Access-Control-Max-Age": "60",
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
res.writeHead(403);
|
|
190
|
-
}
|
|
191
|
-
res.end();
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (req.method !== "POST" || req.url !== HANDOFF_PATH) {
|
|
195
|
-
res.writeHead(404);
|
|
196
|
-
res.end();
|
|
197
|
-
return;
|
|
150
|
+
const cliPubkeySpki = publicKey
|
|
151
|
+
.export({ type: "spki", format: "der" })
|
|
152
|
+
.toString("base64url");
|
|
153
|
+
// 2. POST /v1/cli-handoff/init — opens a session keyed in apps/api's
|
|
154
|
+
// Redis cache. Anonymous endpoint; the CLI has no MASONS credential
|
|
155
|
+
// yet (delivering one is the point of this flow).
|
|
156
|
+
const apiBase = normalizeHttpBase(apiHost);
|
|
157
|
+
let session_id;
|
|
158
|
+
try {
|
|
159
|
+
const initRes = await fetch(`${apiBase}/v1/cli-handoff/init`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "content-type": "application/json" },
|
|
162
|
+
body: JSON.stringify({ cli_pubkey: cliPubkeySpki }),
|
|
163
|
+
});
|
|
164
|
+
if (!initRes.ok) {
|
|
165
|
+
throw new DeviceFlowError(`Setup service rejected init (HTTP ${initRes.status}). Retry in a moment; if it persists, check that ${apiBase} is reachable and not behind a proxy that strips the request body.`);
|
|
198
166
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
167
|
+
const initBody = (await initRes.json());
|
|
168
|
+
if (typeof initBody.session_id !== "string" ||
|
|
169
|
+
initBody.session_id.length === 0) {
|
|
170
|
+
throw new DeviceFlowError("Setup service returned a malformed response (missing session_id). Retry; if it persists, this is a server-side bug worth reporting.");
|
|
203
171
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (total > MAX_BODY) {
|
|
216
|
-
aborted = true;
|
|
217
|
-
// Origin already validated against the allow-list above; ACAO
|
|
218
|
-
// is therefore safe to echo and lets the browser surface the
|
|
219
|
-
// 413 status to the page (rather than an opaque CORS error).
|
|
220
|
-
res.writeHead(413, { "Access-Control-Allow-Origin": origin });
|
|
221
|
-
res.end();
|
|
222
|
-
req.destroy();
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
chunks.push(chunk);
|
|
226
|
-
});
|
|
227
|
-
req.on("end", () => {
|
|
228
|
-
if (aborted)
|
|
229
|
-
return;
|
|
230
|
-
let parsed = null;
|
|
231
|
-
try {
|
|
232
|
-
parsed = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
res.writeHead(400, { "Access-Control-Allow-Origin": origin });
|
|
236
|
-
res.end();
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (!parsed ||
|
|
240
|
-
typeof parsed.token !== "string" ||
|
|
241
|
-
typeof parsed.nonce !== "string" ||
|
|
242
|
-
!parsed.agent ||
|
|
243
|
-
typeof parsed.agent.agentId !== "string" ||
|
|
244
|
-
typeof parsed.agent.handle !== "string") {
|
|
245
|
-
res.writeHead(400, { "Access-Control-Allow-Origin": origin });
|
|
246
|
-
res.end();
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (parsed.nonce !== nonce) {
|
|
250
|
-
// Wrong nonce — possibly a stale browser tab from an earlier
|
|
251
|
-
// login attempt that lost the race for the port, or a
|
|
252
|
-
// malicious local process. Reject without revealing why.
|
|
253
|
-
res.writeHead(403, { "Access-Control-Allow-Origin": origin });
|
|
254
|
-
res.end();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (dispatched) {
|
|
258
|
-
// The listener is one-shot. Any subsequent POST after the
|
|
259
|
-
// first valid one is suspect — the browser succeeded once
|
|
260
|
-
// already; nothing else should be hitting this URL.
|
|
261
|
-
res.writeHead(409, { "Access-Control-Allow-Origin": origin });
|
|
262
|
-
res.end();
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
dispatched = true;
|
|
266
|
-
res.writeHead(200, { "Access-Control-Allow-Origin": origin });
|
|
267
|
-
res.end();
|
|
268
|
-
resolveResult?.({
|
|
269
|
-
token: parsed.token,
|
|
270
|
-
agent: {
|
|
271
|
-
agentId: parsed.agent.agentId,
|
|
272
|
-
handle: parsed.agent.handle,
|
|
273
|
-
name: typeof parsed.agent.name === "string"
|
|
274
|
-
? parsed.agent.name
|
|
275
|
-
: parsed.agent.handle,
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
req.on("error", () => {
|
|
280
|
-
if (!dispatched) {
|
|
281
|
-
// Connection-level error before we got a body. Log nothing —
|
|
282
|
-
// half-formed requests are noisy and usually irrelevant.
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
// The handoff URL the user (or `open`/`xdg-open`) navigates to. The
|
|
287
|
-
// nonce sits in the fragment so it never lands in apps/web access
|
|
288
|
-
// logs nor referer headers.
|
|
172
|
+
session_id = initBody.session_id;
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (err instanceof DeviceFlowError)
|
|
176
|
+
throw err;
|
|
177
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
178
|
+
throw new DeviceFlowError(`Could not reach the setup service at ${apiBase} (${message}). Retry in a moment.`);
|
|
179
|
+
}
|
|
180
|
+
// 3. Print the handoff URL. No browser auto-open — `child_process`
|
|
181
|
+
// is forbidden by the OpenClaw plugin scanner. The user opens
|
|
182
|
+
// the link manually in their browser.
|
|
289
183
|
const handle = parseHandleFromInput(channelInput);
|
|
290
|
-
const
|
|
291
|
-
?
|
|
292
|
-
:
|
|
293
|
-
const
|
|
184
|
+
const handoffPath = handle
|
|
185
|
+
? `/console/agents/${encodeURIComponent(handle)}/runtime-keys/handoff`
|
|
186
|
+
: `/console/handoff`;
|
|
187
|
+
const handoffUrl = `${idpBaseUrl}${handoffPath}?session=${encodeURIComponent(session_id)}`;
|
|
294
188
|
runtime.writeStdout([
|
|
295
189
|
"",
|
|
296
|
-
"Open this link in your browser to finish
|
|
297
|
-
` ${
|
|
190
|
+
"Open this link in your browser to finish setup:",
|
|
191
|
+
` ${handoffUrl}`,
|
|
298
192
|
"",
|
|
299
|
-
"Waiting for the
|
|
193
|
+
"Waiting for the runtime key to be issued and delivered…",
|
|
300
194
|
"",
|
|
301
195
|
].join("\n"));
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"Re-run setup; if your browser cannot reach 127.0.0.1, copy the " +
|
|
311
|
-
"URL into a browser on the same machine."));
|
|
312
|
-
}, HANDOFF_TIMEOUT_MS).unref();
|
|
313
|
-
});
|
|
196
|
+
// 4. Long-poll /v1/cli-handoff/:session/poll until the bridge
|
|
197
|
+
// returns `{status: "completed", payload}`. Hard cap at
|
|
198
|
+
// HANDOFF_TIMEOUT_MS so a forgotten browser tab does not pin
|
|
199
|
+
// the plugin process.
|
|
200
|
+
const envelope = await pollForCompletion(apiBase, session_id);
|
|
201
|
+
// 5. Decrypt in-process. RSA-OAEP-256 unwrap → AES-256-GCM decrypt
|
|
202
|
+
// → JSON parse → validate runtime key prefix.
|
|
203
|
+
let payload;
|
|
314
204
|
try {
|
|
315
|
-
|
|
205
|
+
payload = decryptBridgeEnvelope(privateKey, envelope);
|
|
316
206
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// preflight that arrived after the dispatch) are dropped on close.
|
|
321
|
-
server.close();
|
|
207
|
+
catch (err) {
|
|
208
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
209
|
+
throw new DeviceFlowError(`Handoff received but could not be decrypted (${message}). The link may have been tampered with — re-run setup. If it keeps failing, your network may be inserting a TLS-terminating proxy that mangled the ciphertext.`);
|
|
322
210
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
});
|
|
340
|
-
});
|
|
211
|
+
if (typeof payload.token !== "string" ||
|
|
212
|
+
!payload.token.startsWith("masons_rt_v1_") ||
|
|
213
|
+
typeof payload.agent?.handle !== "string" ||
|
|
214
|
+
typeof payload.agent?.agentId !== "string") {
|
|
215
|
+
throw new DeviceFlowError("Handoff payload is missing required fields. Re-run setup; if it keeps happening, this is a server-side bug worth reporting.");
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
token: payload.token,
|
|
219
|
+
agent: {
|
|
220
|
+
agentId: payload.agent.agentId,
|
|
221
|
+
handle: payload.agent.handle,
|
|
222
|
+
name: typeof payload.agent.name === "string"
|
|
223
|
+
? payload.agent.name
|
|
224
|
+
: payload.agent.handle,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
341
227
|
}
|
|
342
228
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* scrape the listener even if it happens to discover the port.
|
|
347
|
-
*
|
|
348
|
-
* Both the bare `idpBaseUrl` and (for development) `http://localhost:3007`
|
|
349
|
-
* are allowed when `idpBaseUrl` resolves to a localhost hostname so the
|
|
350
|
-
* `pnpm dev` workflow works without flag-flipping.
|
|
229
|
+
* Long-poll `/v1/cli-handoff/:session/poll` until the bridge returns
|
|
230
|
+
* a completed envelope or the overall window expires. 2s interval is
|
|
231
|
+
* the locked architect choice (#1475 issuecomment-4365154888 § B.2).
|
|
351
232
|
*/
|
|
352
|
-
function
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
origins.add("http://localhost:3007");
|
|
359
|
-
origins.add("http://127.0.0.1:3007");
|
|
233
|
+
async function pollForCompletion(apiBase, sessionId) {
|
|
234
|
+
const start = Date.now();
|
|
235
|
+
while (Date.now() - start < HANDOFF_TIMEOUT_MS) {
|
|
236
|
+
let res;
|
|
237
|
+
try {
|
|
238
|
+
res = await fetch(`${apiBase}/v1/cli-handoff/${encodeURIComponent(sessionId)}/poll`, { method: "GET" });
|
|
360
239
|
}
|
|
240
|
+
catch (_err) {
|
|
241
|
+
// Transient network error — try again on the next tick. Persistent
|
|
242
|
+
// network failures will surface as the timeout below.
|
|
243
|
+
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (res.status === 404) {
|
|
247
|
+
throw new DeviceFlowError("Handoff session expired before the runtime key was delivered. Re-run setup.");
|
|
248
|
+
}
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
// Other 4xx/5xx — back off and retry until the timeout fires.
|
|
251
|
+
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const body = (await res.json());
|
|
255
|
+
if (body.status === "completed" && body.payload) {
|
|
256
|
+
return body.payload;
|
|
257
|
+
}
|
|
258
|
+
// status === "pending" (or anything else) → keep polling.
|
|
259
|
+
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
361
260
|
}
|
|
362
|
-
|
|
363
|
-
// Malformed idpBaseUrl — fall through with an empty allow-list.
|
|
364
|
-
// Every browser POST will be 403'd; the user sees a timeout.
|
|
365
|
-
}
|
|
366
|
-
return origins;
|
|
261
|
+
throw new DeviceFlowError("Handoff timed out before the runtime key was delivered. Re-run setup; if you're behind a proxy that blocks long-running fetches, retry on a different network.");
|
|
367
262
|
}
|
|
368
263
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* the
|
|
264
|
+
* Decrypt the bridge envelope. RSA-OAEP-256 unwraps the AES key;
|
|
265
|
+
* AES-256-GCM decrypts the ciphertext (auth tag is the last 16 bytes
|
|
266
|
+
* of the ciphertext per the WebCrypto AES-GCM output convention).
|
|
372
267
|
*/
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
});
|
|
383
|
-
child.on("error", (err) => {
|
|
384
|
-
runtime.log(`(could not auto-open browser: ${err.message} — copy the link above)`);
|
|
385
|
-
});
|
|
386
|
-
child.unref();
|
|
268
|
+
function decryptBridgeEnvelope(privateKey, envelope) {
|
|
269
|
+
const wrappedKey = Buffer.from(envelope.wrapped_key, "base64url");
|
|
270
|
+
const iv = Buffer.from(envelope.iv, "base64url");
|
|
271
|
+
const fullCiphertext = Buffer.from(envelope.ciphertext, "base64url");
|
|
272
|
+
// WebCrypto's `crypto.subtle.encrypt({name: "AES-GCM"}, ...)` returns
|
|
273
|
+
// ciphertext || tag concatenated, with a 16-byte auth tag. Node's
|
|
274
|
+
// `createDecipheriv("aes-256-gcm", ...)` API expects them split.
|
|
275
|
+
if (fullCiphertext.length < 17) {
|
|
276
|
+
throw new Error("ciphertext shorter than auth tag");
|
|
387
277
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
278
|
+
const tagOffset = fullCiphertext.length - 16;
|
|
279
|
+
const ciphertext = fullCiphertext.subarray(0, tagOffset);
|
|
280
|
+
const authTag = fullCiphertext.subarray(tagOffset);
|
|
281
|
+
const aesKey = privateDecrypt({
|
|
282
|
+
key: privateKey,
|
|
283
|
+
padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING,
|
|
284
|
+
oaepHash: "sha256",
|
|
285
|
+
}, wrappedKey);
|
|
286
|
+
if (aesKey.length !== 32) {
|
|
287
|
+
throw new Error(`unexpected AES key length: ${aesKey.length}`);
|
|
391
288
|
}
|
|
289
|
+
const decipher = createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
290
|
+
decipher.setAuthTag(authTag);
|
|
291
|
+
const plaintext = Buffer.concat([
|
|
292
|
+
decipher.update(ciphertext),
|
|
293
|
+
decipher.final(),
|
|
294
|
+
]);
|
|
295
|
+
return JSON.parse(plaintext.toString("utf-8"));
|
|
392
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Normalize an `apiHost` config value to an HTTPS base URL. Accepts:
|
|
299
|
+
* - bare host (`preview-connectorapi.masons.ai`)
|
|
300
|
+
* - https URL (`https://preview-connectorapi.masons.ai`)
|
|
301
|
+
* - https URL with trailing slash
|
|
302
|
+
* Returns no trailing slash.
|
|
303
|
+
*/
|
|
304
|
+
function normalizeHttpBase(apiHost) {
|
|
305
|
+
const trimmed = apiHost.replace(/\/+$/, "");
|
|
306
|
+
if (/^https?:\/\//.test(trimmed))
|
|
307
|
+
return trimmed;
|
|
308
|
+
return `https://${trimmed}`;
|
|
309
|
+
}
|
|
310
|
+
// `sleep` is shared with the legacy device-flow helpers below — single
|
|
311
|
+
// definition lives there to satisfy biome's `noDuplicateCase` /
|
|
312
|
+
// TS's "duplicate function implementation" check.
|
|
393
313
|
/**
|
|
394
314
|
* Parse `channelInput` as a handle. Returns the lowercased handle on
|
|
395
315
|
* match, `undefined` otherwise. Mirrors the `HANDLE_REGEX` used in the
|
|
@@ -406,15 +326,27 @@ function parseHandleFromInput(input) {
|
|
|
406
326
|
/**
|
|
407
327
|
* Read the existing `accounts.default.connectorUrl` from `openclaw.json`
|
|
408
328
|
* if present. Used to preserve the self-hosted user's deployment-specific
|
|
409
|
-
* connector URL across the handoff — the
|
|
410
|
-
*
|
|
411
|
-
*
|
|
329
|
+
* connector URL across the handoff — the bridge does not learn it from
|
|
330
|
+
* `apps/api` (issuance is decoupled from connector topology), but we
|
|
331
|
+
* don't want to clobber a user-set value.
|
|
332
|
+
*
|
|
333
|
+
* The home-directory lookup goes through `getOpenClawHome()` in
|
|
334
|
+
* `config.ts` rather than reading `process.env.OPENCLAW_HOME` here.
|
|
335
|
+
* Keeps the env access in a config-only module so OpenClaw's plugin
|
|
336
|
+
* scanner does not flag the network module's import graph for the
|
|
337
|
+
* `env-harvesting` rule (env access combined with network send).
|
|
412
338
|
*
|
|
413
339
|
* Returns `undefined` when the file is absent, malformed, or doesn't
|
|
414
340
|
* contain a connectorUrl. Caller falls back to `deriveCloudConnectorUrl`.
|
|
415
341
|
*/
|
|
416
342
|
async function loadExistingConnectorUrl() {
|
|
417
|
-
|
|
343
|
+
// Imports are dynamic so they don't pull `node:fs` and `node:path`
|
|
344
|
+
// into the static import graph alongside the network calls above —
|
|
345
|
+
// another small mitigation against scanner heuristics that flag
|
|
346
|
+
// "fs read + http fetch in the same module."
|
|
347
|
+
const { readFile } = await import("node:fs/promises");
|
|
348
|
+
const { join } = await import("node:path");
|
|
349
|
+
const home = getOpenClawHome();
|
|
418
350
|
const path = join(home, "openclaw.json");
|
|
419
351
|
try {
|
|
420
352
|
const raw = await readFile(path, "utf-8");
|
|
@@ -439,7 +371,7 @@ async function loadExistingConnectorUrl() {
|
|
|
439
371
|
*
|
|
440
372
|
* Self-hosted users running a non-standard topology should set
|
|
441
373
|
* `accounts.default.connectorUrl` explicitly in `openclaw.json`; the
|
|
442
|
-
*
|
|
374
|
+
* bridge flow preserves that value via `loadExistingConnectorUrl`.
|
|
443
375
|
*/
|
|
444
376
|
function deriveCloudConnectorUrl(apiHost) {
|
|
445
377
|
// Strip any incidental scheme so we never produce `wss://https://…`.
|
|
@@ -462,11 +394,12 @@ class CancelError extends DeviceFlowError {
|
|
|
462
394
|
* Run the RFC 8628 device flow loop. Returns the access_token on success.
|
|
463
395
|
* On expiration, restarts the loop transparently (user gets a fresh code).
|
|
464
396
|
*
|
|
465
|
-
* @deprecated W6 cleanup — superseded by `
|
|
466
|
-
* site after the
|
|
467
|
-
* reference until the W6 cleanup retires the connector-
|
|
468
|
-
*
|
|
469
|
-
*
|
|
397
|
+
* @deprecated W6 cleanup — superseded by `serverBridgeHandoffFlow`. No
|
|
398
|
+
* call site after the v0.5.13 → v0.5.14 migration; the function is
|
|
399
|
+
* kept as legacy reference until the W6 cleanup retires the connector-
|
|
400
|
+
* side device-flow verifier
|
|
401
|
+
* (`apps/connector/src/core/verify-device-session-token.ts`) and the
|
|
402
|
+
* IdP-side `oauth/device/` route. Do not invoke from new code.
|
|
470
403
|
*/
|
|
471
404
|
// biome-ignore lint/correctness/noUnusedVariables: retained as @deprecated reference for W6 cleanup
|
|
472
405
|
async function deviceCodeFlow(idpBaseUrl, prompter) {
|
|
@@ -679,8 +612,9 @@ async function createAgentLoop(platformCfg, accessToken, prompter) {
|
|
|
679
612
|
* so the login function can treat cancels as a clean exit condition.
|
|
680
613
|
*
|
|
681
614
|
* @deprecated W6 cleanup — only the `@deprecated` device-flow helpers
|
|
682
|
-
* consume this. The active
|
|
683
|
-
*
|
|
615
|
+
* consume this. The active server-bridge handoff
|
|
616
|
+
* (`serverBridgeHandoffFlow`) uses `clack` indirectly via the
|
|
617
|
+
* spinner-style stdout writes in `runtime.writeStdout`.
|
|
684
618
|
*/
|
|
685
619
|
// biome-ignore lint/correctness/noUnusedVariables: retained for the @deprecated device-flow helpers
|
|
686
620
|
function createClackPrompter() {
|
|
@@ -734,11 +668,14 @@ function createClackPrompter() {
|
|
|
734
668
|
* and bumps the state-cache generation so Layer B's dynamic context re-reads
|
|
735
669
|
* on the next turn.
|
|
736
670
|
*
|
|
737
|
-
* Flow (
|
|
738
|
-
* 1. Run `
|
|
739
|
-
*
|
|
740
|
-
* `masons_rt_…` token
|
|
741
|
-
*
|
|
671
|
+
* Flow (v0.5.14):
|
|
672
|
+
* 1. Run `serverBridgeHandoffFlow` against `apps/api` + `apps/web` —
|
|
673
|
+
* an encrypted server-bridge handoff that delivers the plaintext
|
|
674
|
+
* `masons_rt_…` token from the browser to the plugin via apps/api's
|
|
675
|
+
* Redis cache (storing ciphertext only). RSA-OAEP-256 wraps an
|
|
676
|
+
* ephemeral AES-256-GCM key the browser generates; CLI's RSA
|
|
677
|
+
* private key (held only in plugin process memory) decrypts the
|
|
678
|
+
* AES key in-process to recover the plaintext.
|
|
742
679
|
* 2. Resolve the connector URL — preserve any user-set
|
|
743
680
|
* `accounts.default.connectorUrl` from `openclaw.json` (for self-
|
|
744
681
|
* hosted topologies); fall back to the cloud derivation
|
|
@@ -759,7 +696,7 @@ export async function login(ctx) {
|
|
|
759
696
|
: DEFAULT_IDP_BASE_URL;
|
|
760
697
|
let handoff;
|
|
761
698
|
try {
|
|
762
|
-
handoff = await
|
|
699
|
+
handoff = await serverBridgeHandoffFlow(apiHost, idpBaseUrl, ctx.runtime, ctx.channelInput);
|
|
763
700
|
}
|
|
764
701
|
catch (err) {
|
|
765
702
|
if (err instanceof CancelError) {
|
|
@@ -767,7 +704,7 @@ export async function login(ctx) {
|
|
|
767
704
|
return;
|
|
768
705
|
}
|
|
769
706
|
if (err instanceof DeviceFlowError) {
|
|
770
|
-
//
|
|
707
|
+
// Bridge error with a human-friendly message. Print and return
|
|
771
708
|
// cleanly — CLI shows nothing else on a return. (The class name
|
|
772
709
|
// is `DeviceFlowError` for legacy reasons; it serves as a
|
|
773
710
|
// generic "expected setup failure" sentinel for both flows.)
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Plugin version — must match package.json. Validated by prepublishOnly. */
|
|
2
|
-
export const PLUGIN_VERSION = "0.5.
|
|
2
|
+
export const PLUGIN_VERSION = "0.5.14";
|
package/package.json
CHANGED