@syengup/friday-channel-next 0.0.38 → 0.0.40
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/src/agent/node-pairing-bridge.d.ts +9 -0
- package/dist/src/agent/node-pairing-bridge.js +19 -0
- package/dist/src/http/handlers/device-approve.js +21 -53
- package/dist/src/http/handlers/nodes-approve.js +22 -52
- package/install.js +132 -257
- package/package.json +1 -1
- package/src/agent/node-pairing-bridge.ts +29 -0
- package/src/http/handlers/device-approve.test.ts +40 -67
- package/src/http/handlers/device-approve.ts +23 -68
- package/src/http/handlers/nodes-approve.test.ts +48 -87
- package/src/http/handlers/nodes-approve.ts +37 -68
- package/src/openclaw.d.ts +30 -0
- package/src/test-support/mock-device-bootstrap.ts +10 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function loadNodePairingModule(): {
|
|
2
|
+
listNodePairing: Function;
|
|
3
|
+
approveNodePairing: Function;
|
|
4
|
+
};
|
|
5
|
+
/** Vitest-only: inject mock pairing functions. */
|
|
6
|
+
export declare function __setMockNodePairingForTests(mock: {
|
|
7
|
+
listNodePairing: Function;
|
|
8
|
+
approveNodePairing: Function;
|
|
9
|
+
}): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const OPENCLAW_DIST = "/opt/homebrew/lib/node_modules/openclaw/dist";
|
|
5
|
+
let cache = null;
|
|
6
|
+
export function loadNodePairingModule() {
|
|
7
|
+
if (cache)
|
|
8
|
+
return cache;
|
|
9
|
+
const file = readdirSync(OPENCLAW_DIST).find((f) => f.startsWith("node-pairing-") && f.endsWith(".js") && !f.includes("authz"));
|
|
10
|
+
if (!file)
|
|
11
|
+
throw new Error("node-pairing module not found in OpenClaw dist");
|
|
12
|
+
const gatewayRequire = createRequire(join(OPENCLAW_DIST, "_"));
|
|
13
|
+
cache = gatewayRequire(`./${file.replace(/\.js$/, "")}`);
|
|
14
|
+
return cache;
|
|
15
|
+
}
|
|
16
|
+
/** Vitest-only: inject mock pairing functions. */
|
|
17
|
+
export function __setMockNodePairingForTests(mock) {
|
|
18
|
+
cache = mock;
|
|
19
|
+
}
|
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listDevicePairing, approveDevicePairing } from "openclaw/plugin-sdk/device-bootstrap";
|
|
2
2
|
import { readJsonBody } from "../middleware/body.js";
|
|
3
3
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
4
4
|
import { createFridayNextLogger } from "../../logging.js";
|
|
5
|
-
const EXEC_ENV = process.platform === "win32"
|
|
6
|
-
? process.env
|
|
7
|
-
: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:${process.env.PATH ?? ""}` };
|
|
8
|
-
function execAsync(command, timeoutMs) {
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
const child = exec(command, { encoding: "utf-8", timeout: timeoutMs, maxBuffer: 1024 * 1024, env: EXEC_ENV }, (error, stdout, stderr) => {
|
|
11
|
-
if (error) {
|
|
12
|
-
reject(error);
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
resolve({ stdout, stderr });
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
child.stdout?.on("data", () => { });
|
|
19
|
-
child.stderr?.on("data", () => { });
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
5
|
export async function handleDeviceApprove(req, res) {
|
|
23
6
|
const log = createFridayNextLogger("device-approve");
|
|
24
7
|
if (req.method !== "POST") {
|
|
@@ -49,32 +32,18 @@ export async function handleDeviceApprove(req, res) {
|
|
|
49
32
|
return true;
|
|
50
33
|
}
|
|
51
34
|
const normalizedDeviceId = rawDeviceId.trim().toUpperCase();
|
|
52
|
-
let
|
|
35
|
+
let pairing;
|
|
53
36
|
try {
|
|
54
|
-
|
|
55
|
-
listStdout = result.stdout;
|
|
37
|
+
pairing = await listDevicePairing();
|
|
56
38
|
}
|
|
57
39
|
catch (err) {
|
|
58
|
-
|
|
59
|
-
log.error(`devices list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
-
res.statusCode = 502;
|
|
61
|
-
res.setHeader("Content-Type", "application/json");
|
|
62
|
-
res.end(JSON.stringify({ error: "Failed to list devices from gateway", detail: stderr || undefined }));
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
let listData;
|
|
66
|
-
try {
|
|
67
|
-
listData = JSON.parse(listStdout);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
log.error(`devices list returned invalid JSON: ${listStdout.slice(0, 200)}`);
|
|
40
|
+
log.error(`listDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
41
|
res.statusCode = 502;
|
|
72
42
|
res.setHeader("Content-Type", "application/json");
|
|
73
|
-
res.end(JSON.stringify({ error: "
|
|
43
|
+
res.end(JSON.stringify({ error: "Failed to list devices from gateway" }));
|
|
74
44
|
return true;
|
|
75
45
|
}
|
|
76
|
-
const
|
|
77
|
-
const match = pending.find((entry) => entry.deviceId.trim().toUpperCase() === normalizedDeviceId);
|
|
46
|
+
const match = pairing.pending.find((entry) => entry.deviceId.trim().toUpperCase() === normalizedDeviceId);
|
|
78
47
|
if (!match) {
|
|
79
48
|
res.statusCode = 404;
|
|
80
49
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -86,31 +55,30 @@ export async function handleDeviceApprove(req, res) {
|
|
|
86
55
|
}
|
|
87
56
|
const requestId = match.requestId;
|
|
88
57
|
log.info(`approving deviceId=${normalizedDeviceId} requestId=${requestId}`);
|
|
89
|
-
let
|
|
58
|
+
let approved;
|
|
90
59
|
try {
|
|
91
|
-
|
|
92
|
-
approveStdout = result.stdout;
|
|
60
|
+
approved = await approveDevicePairing(requestId);
|
|
93
61
|
}
|
|
94
62
|
catch (err) {
|
|
95
|
-
|
|
96
|
-
log.error(`devices approve failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
log.error(`approveDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
97
64
|
res.statusCode = 502;
|
|
98
65
|
res.setHeader("Content-Type", "application/json");
|
|
99
66
|
res.end(JSON.stringify({
|
|
100
|
-
error: "Device approval
|
|
101
|
-
detail:
|
|
67
|
+
error: "Device approval failed",
|
|
68
|
+
detail: err instanceof Error ? err.message : "Unknown error",
|
|
102
69
|
}));
|
|
103
70
|
return true;
|
|
104
71
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
72
|
+
if (!approved) {
|
|
73
|
+
res.statusCode = 404;
|
|
74
|
+
res.setHeader("Content-Type", "application/json");
|
|
75
|
+
res.end(JSON.stringify({ error: "Pending device request not found" }));
|
|
76
|
+
return true;
|
|
108
77
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
res.statusCode = 502;
|
|
78
|
+
if (approved.status === "forbidden") {
|
|
79
|
+
res.statusCode = 403;
|
|
112
80
|
res.setHeader("Content-Type", "application/json");
|
|
113
|
-
res.end(JSON.stringify({ error:
|
|
81
|
+
res.end(JSON.stringify({ error: `Device approval forbidden: ${approved.reason ?? "unknown"}` }));
|
|
114
82
|
return true;
|
|
115
83
|
}
|
|
116
84
|
res.statusCode = 200;
|
|
@@ -118,8 +86,8 @@ export async function handleDeviceApprove(req, res) {
|
|
|
118
86
|
res.end(JSON.stringify({
|
|
119
87
|
ok: true,
|
|
120
88
|
deviceId: normalizedDeviceId,
|
|
121
|
-
requestId:
|
|
122
|
-
approvedAtMs:
|
|
89
|
+
requestId: approved.requestId,
|
|
90
|
+
approvedAtMs: approved.device?.approvedAtMs,
|
|
123
91
|
}));
|
|
124
92
|
return true;
|
|
125
93
|
}
|
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
1
|
import { readJsonBody } from "../middleware/body.js";
|
|
3
2
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
4
3
|
import { createFridayNextLogger } from "../../logging.js";
|
|
5
|
-
|
|
6
|
-
? process.env
|
|
7
|
-
: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:${process.env.PATH ?? ""}` };
|
|
8
|
-
function execAsync(command, timeoutMs) {
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
const child = exec(command, { encoding: "utf-8", timeout: timeoutMs, maxBuffer: 1024 * 1024, env: EXEC_ENV }, (error, stdout, stderr) => {
|
|
11
|
-
if (error) {
|
|
12
|
-
reject(error);
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
resolve({ stdout, stderr });
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
child.stdout?.on("data", () => { });
|
|
19
|
-
child.stderr?.on("data", () => { });
|
|
20
|
-
});
|
|
21
|
-
}
|
|
4
|
+
import { loadNodePairingModule } from "../../agent/node-pairing-bridge.js";
|
|
22
5
|
export async function handleNodesApprove(req, res) {
|
|
23
6
|
const log = createFridayNextLogger("nodes-approve");
|
|
24
7
|
if (req.method !== "POST") {
|
|
@@ -49,28 +32,16 @@ export async function handleNodesApprove(req, res) {
|
|
|
49
32
|
return true;
|
|
50
33
|
}
|
|
51
34
|
const normalizedNodeId = rawNodeId.trim().toUpperCase();
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const result = await execAsync("openclaw nodes list --json", 15000);
|
|
55
|
-
listStdout = result.stdout;
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
const stderr = err?.stderr?.trim();
|
|
59
|
-
log.error(`nodes list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
-
res.statusCode = 502;
|
|
61
|
-
res.setHeader("Content-Type", "application/json");
|
|
62
|
-
res.end(JSON.stringify({ error: "Failed to list nodes from gateway", detail: stderr || undefined }));
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
35
|
+
const { listNodePairing, approveNodePairing } = loadNodePairingModule();
|
|
65
36
|
let listData;
|
|
66
37
|
try {
|
|
67
|
-
listData =
|
|
38
|
+
listData = await listNodePairing();
|
|
68
39
|
}
|
|
69
|
-
catch {
|
|
70
|
-
log.error(`
|
|
40
|
+
catch (err) {
|
|
41
|
+
log.error(`listNodePairing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
42
|
res.statusCode = 502;
|
|
72
43
|
res.setHeader("Content-Type", "application/json");
|
|
73
|
-
res.end(JSON.stringify({ error: "
|
|
44
|
+
res.end(JSON.stringify({ error: "Failed to list nodes from gateway" }));
|
|
74
45
|
return true;
|
|
75
46
|
}
|
|
76
47
|
const pending = listData.pending ?? [];
|
|
@@ -78,31 +49,30 @@ export async function handleNodesApprove(req, res) {
|
|
|
78
49
|
if (pendingMatch) {
|
|
79
50
|
const requestId = pendingMatch.requestId;
|
|
80
51
|
log.info(`approving nodeId=${normalizedNodeId} requestId=${requestId}`);
|
|
81
|
-
let
|
|
52
|
+
let approved;
|
|
82
53
|
try {
|
|
83
|
-
|
|
84
|
-
approveStdout = result.stdout;
|
|
54
|
+
approved = await approveNodePairing(requestId, {});
|
|
85
55
|
}
|
|
86
56
|
catch (err) {
|
|
87
|
-
|
|
88
|
-
log.error(`nodes approve failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
log.error(`approveNodePairing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
58
|
res.statusCode = 502;
|
|
90
59
|
res.setHeader("Content-Type", "application/json");
|
|
91
60
|
res.end(JSON.stringify({
|
|
92
|
-
error: "Node approval
|
|
93
|
-
detail:
|
|
61
|
+
error: "Node approval failed",
|
|
62
|
+
detail: err instanceof Error ? err.message : "Unknown error",
|
|
94
63
|
}));
|
|
95
64
|
return true;
|
|
96
65
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
66
|
+
if (!approved) {
|
|
67
|
+
res.statusCode = 404;
|
|
68
|
+
res.setHeader("Content-Type", "application/json");
|
|
69
|
+
res.end(JSON.stringify({ error: "Pending node request not found" }));
|
|
70
|
+
return true;
|
|
100
71
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
res.statusCode = 502;
|
|
72
|
+
if ("status" in approved && approved.status === "forbidden") {
|
|
73
|
+
res.statusCode = 403;
|
|
104
74
|
res.setHeader("Content-Type", "application/json");
|
|
105
|
-
res.end(JSON.stringify({ error:
|
|
75
|
+
res.end(JSON.stringify({ error: `Node approval forbidden: ${approved.missingScope ?? "unknown"}` }));
|
|
106
76
|
return true;
|
|
107
77
|
}
|
|
108
78
|
res.statusCode = 200;
|
|
@@ -110,12 +80,12 @@ export async function handleNodesApprove(req, res) {
|
|
|
110
80
|
res.end(JSON.stringify({
|
|
111
81
|
ok: true,
|
|
112
82
|
nodeId: normalizedNodeId,
|
|
113
|
-
requestId:
|
|
114
|
-
approvedAtMs:
|
|
83
|
+
requestId: approved.requestId,
|
|
84
|
+
approvedAtMs: approved.node?.approvedAtMs,
|
|
115
85
|
}));
|
|
116
86
|
return true;
|
|
117
87
|
}
|
|
118
|
-
//
|
|
88
|
+
// Check if already paired with non-empty caps/commands
|
|
119
89
|
const paired = listData.paired ?? [];
|
|
120
90
|
const pairedMatch = paired.find((entry) => entry.nodeId.trim().toUpperCase() === normalizedNodeId);
|
|
121
91
|
if (pairedMatch) {
|