@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.
@@ -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 { exec } from "node:child_process";
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 listStdout;
35
+ let pairing;
53
36
  try {
54
- const result = await execAsync("openclaw devices list --json", 15000);
55
- listStdout = result.stdout;
37
+ pairing = await listDevicePairing();
56
38
  }
57
39
  catch (err) {
58
- const stderr = err?.stderr?.trim();
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: "Unexpected response from gateway device list" }));
43
+ res.end(JSON.stringify({ error: "Failed to list devices from gateway" }));
74
44
  return true;
75
45
  }
76
- const pending = listData.pending ?? [];
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 approveStdout;
58
+ let approved;
90
59
  try {
91
- const result = await execAsync(`openclaw devices approve ${requestId} --json`, 15000);
92
- approveStdout = result.stdout;
60
+ approved = await approveDevicePairing(requestId);
93
61
  }
94
62
  catch (err) {
95
- const stderr = err?.stderr?.trim();
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 command failed",
101
- detail: stderr || (err instanceof Error ? err.message : "Unknown error"),
67
+ error: "Device approval failed",
68
+ detail: err instanceof Error ? err.message : "Unknown error",
102
69
  }));
103
70
  return true;
104
71
  }
105
- let approveData;
106
- try {
107
- approveData = JSON.parse(approveStdout);
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
- catch {
110
- log.error(`devices approve returned non-JSON: ${approveStdout.slice(0, 200)}`);
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: "Unexpected response from device approval" }));
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: approveData.requestId,
122
- approvedAtMs: approveData.device?.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
- 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
- }
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
- let listStdout;
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 = JSON.parse(listStdout);
38
+ listData = await listNodePairing();
68
39
  }
69
- catch {
70
- log.error(`nodes list returned invalid JSON: ${listStdout.slice(0, 200)}`);
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: "Unexpected response from gateway node list" }));
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 approveStdout;
52
+ let approved;
82
53
  try {
83
- const result = await execAsync(`openclaw nodes approve ${requestId} --json`, 15000);
84
- approveStdout = result.stdout;
54
+ approved = await approveNodePairing(requestId, {});
85
55
  }
86
56
  catch (err) {
87
- const stderr = err?.stderr?.trim();
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 command failed",
93
- detail: stderr || (err instanceof Error ? err.message : "Unknown error"),
61
+ error: "Node approval failed",
62
+ detail: err instanceof Error ? err.message : "Unknown error",
94
63
  }));
95
64
  return true;
96
65
  }
97
- let approveData;
98
- try {
99
- approveData = JSON.parse(approveStdout);
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
- catch {
102
- log.error(`nodes approve returned non-JSON: ${approveStdout.slice(0, 200)}`);
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: "Unexpected response from node approval" }));
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: approveData.requestId,
114
- approvedAtMs: approveData.node?.approvedAtMs,
83
+ requestId: approved.requestId,
84
+ approvedAtMs: approved.node?.approvedAtMs,
115
85
  }));
116
86
  return true;
117
87
  }
118
- // Not in pending — check if already paired with non-empty caps/commands
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) {