@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.
@@ -1,48 +1,27 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { exec } from "node:child_process";
3
2
  import { readJsonBody } from "../middleware/body.js";
4
3
  import { extractBearerToken } from "../middleware/auth.js";
5
4
  import { createFridayNextLogger } from "../../logging.js";
5
+ import { loadNodePairingModule } from "../../agent/node-pairing-bridge.js";
6
6
 
7
- const EXEC_ENV = process.platform === "win32"
8
- ? process.env
9
- : { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:${process.env.PATH ?? ""}` };
10
-
11
- interface PendingNode {
7
+ interface PendingNodeEntry {
12
8
  requestId: string;
13
9
  nodeId: string;
14
10
  }
15
-
16
- interface PairedNode {
11
+ interface PairedNodeEntry {
17
12
  nodeId: string;
18
- approvedAtMs?: number;
13
+ approvedAtMs: number;
19
14
  caps?: string[];
20
15
  commands?: string[];
21
16
  }
22
-
23
- interface NodeListJson {
24
- pending?: PendingNode[];
25
- paired?: PairedNode[];
26
- }
27
-
28
- interface ApproveJson {
29
- requestId: string;
30
- node?: { nodeId: string; approvedAtMs?: number };
31
- }
32
-
33
- function execAsync(command: string, timeoutMs: number): Promise<{ stdout: string; stderr: string }> {
34
- return new Promise((resolve, reject) => {
35
- const child = exec(command, { encoding: "utf-8", timeout: timeoutMs, maxBuffer: 1024 * 1024, env: EXEC_ENV }, (error, stdout, stderr) => {
36
- if (error) {
37
- reject(error);
38
- } else {
39
- resolve({ stdout, stderr });
40
- }
41
- });
42
- child.stdout?.on("data", () => { /* drain */ });
43
- child.stderr?.on("data", () => { /* drain */ });
44
- });
17
+ interface NodePairingList {
18
+ pending: PendingNodeEntry[];
19
+ paired: PairedNodeEntry[];
45
20
  }
21
+ type ApproveNodePairingResult =
22
+ | { requestId: string; node: PairedNodeEntry }
23
+ | { status: "forbidden"; missingScope: string }
24
+ | null;
46
25
 
47
26
  export async function handleNodesApprove(
48
27
  req: IncomingMessage,
@@ -83,63 +62,53 @@ export async function handleNodesApprove(
83
62
 
84
63
  const normalizedNodeId = rawNodeId.trim().toUpperCase();
85
64
 
86
- let listStdout: string;
87
- try {
88
- const result = await execAsync("openclaw nodes list --json", 15000);
89
- listStdout = result.stdout;
90
- } catch (err) {
91
- const stderr = (err as { stderr?: string })?.stderr?.trim();
92
- log.error(`nodes list failed: ${err instanceof Error ? err.message : String(err)}`);
93
- res.statusCode = 502;
94
- res.setHeader("Content-Type", "application/json");
95
- res.end(JSON.stringify({ error: "Failed to list nodes from gateway", detail: stderr || undefined }));
96
- return true;
97
- }
65
+ const { listNodePairing, approveNodePairing } = loadNodePairingModule();
98
66
 
99
- let listData: NodeListJson;
67
+ let listData;
100
68
  try {
101
- listData = JSON.parse(listStdout) as NodeListJson;
102
- } catch {
103
- log.error(`nodes list returned invalid JSON: ${listStdout.slice(0, 200)}`);
69
+ listData = await listNodePairing();
70
+ } catch (err) {
71
+ log.error(`listNodePairing failed: ${err instanceof Error ? err.message : String(err)}`);
104
72
  res.statusCode = 502;
105
73
  res.setHeader("Content-Type", "application/json");
106
- res.end(JSON.stringify({ error: "Unexpected response from gateway node list" }));
74
+ res.end(JSON.stringify({ error: "Failed to list nodes from gateway" }));
107
75
  return true;
108
76
  }
109
77
 
110
78
  const pending = listData.pending ?? [];
111
79
  const pendingMatch = pending.find(
112
- (entry) => entry.nodeId.trim().toUpperCase() === normalizedNodeId,
80
+ (entry: PendingNodeEntry) => entry.nodeId.trim().toUpperCase() === normalizedNodeId,
113
81
  );
114
82
 
115
83
  if (pendingMatch) {
116
84
  const requestId = pendingMatch.requestId;
117
85
  log.info(`approving nodeId=${normalizedNodeId} requestId=${requestId}`);
118
86
 
119
- let approveStdout: string;
87
+ let approved;
120
88
  try {
121
- const result = await execAsync(`openclaw nodes approve ${requestId} --json`, 15000);
122
- approveStdout = result.stdout;
89
+ approved = await approveNodePairing(requestId, {});
123
90
  } catch (err) {
124
- const stderr = (err as { stderr?: string })?.stderr?.trim();
125
- log.error(`nodes approve failed: ${err instanceof Error ? err.message : String(err)}`);
91
+ log.error(`approveNodePairing failed: ${err instanceof Error ? err.message : String(err)}`);
126
92
  res.statusCode = 502;
127
93
  res.setHeader("Content-Type", "application/json");
128
94
  res.end(JSON.stringify({
129
- error: "Node approval command failed",
130
- detail: stderr || (err instanceof Error ? err.message : "Unknown error"),
95
+ error: "Node approval failed",
96
+ detail: err instanceof Error ? err.message : "Unknown error",
131
97
  }));
132
98
  return true;
133
99
  }
134
100
 
135
- let approveData: ApproveJson;
136
- try {
137
- approveData = JSON.parse(approveStdout) as ApproveJson;
138
- } catch {
139
- log.error(`nodes approve returned non-JSON: ${approveStdout.slice(0, 200)}`);
140
- res.statusCode = 502;
101
+ if (!approved) {
102
+ res.statusCode = 404;
103
+ res.setHeader("Content-Type", "application/json");
104
+ res.end(JSON.stringify({ error: "Pending node request not found" }));
105
+ return true;
106
+ }
107
+
108
+ if ("status" in approved && approved.status === "forbidden") {
109
+ res.statusCode = 403;
141
110
  res.setHeader("Content-Type", "application/json");
142
- res.end(JSON.stringify({ error: "Unexpected response from node approval" }));
111
+ res.end(JSON.stringify({ error: `Node approval forbidden: ${(approved as any).missingScope ?? "unknown"}` }));
143
112
  return true;
144
113
  }
145
114
 
@@ -148,16 +117,16 @@ export async function handleNodesApprove(
148
117
  res.end(JSON.stringify({
149
118
  ok: true,
150
119
  nodeId: normalizedNodeId,
151
- requestId: approveData.requestId,
152
- approvedAtMs: approveData.node?.approvedAtMs,
120
+ requestId: (approved as any).requestId,
121
+ approvedAtMs: (approved as any).node?.approvedAtMs,
153
122
  }));
154
123
  return true;
155
124
  }
156
125
 
157
- // Not in pending — check if already paired with non-empty caps/commands
126
+ // Check if already paired with non-empty caps/commands
158
127
  const paired = listData.paired ?? [];
159
128
  const pairedMatch = paired.find(
160
- (entry) => entry.nodeId.trim().toUpperCase() === normalizedNodeId,
129
+ (entry: PairedNodeEntry) => entry.nodeId.trim().toUpperCase() === normalizedNodeId,
161
130
  );
162
131
 
163
132
  if (pairedMatch) {
package/src/openclaw.d.ts CHANGED
@@ -3,6 +3,36 @@ declare module "openclaw/plugin-sdk/agent-harness" {
3
3
  export const runAgentHarness: (...args: any[]) => any;
4
4
  }
5
5
 
6
+ declare module "openclaw/plugin-sdk/device-bootstrap" {
7
+ export const listDevicePairing: (baseDir?: string) => Promise<DevicePairingList>;
8
+ export const approveDevicePairing: (requestId: string, options?: { callerScopes?: readonly string[] }, baseDir?: string) => Promise<ApproveDevicePairingResult>;
9
+
10
+ interface DevicePairingPendingRequest {
11
+ requestId: string;
12
+ deviceId: string;
13
+ publicKey: string;
14
+ displayName?: string;
15
+ platform?: string;
16
+ ts: number;
17
+ }
18
+ interface PairedDevice {
19
+ deviceId: string;
20
+ approvedAtMs: number;
21
+ }
22
+ interface DevicePairingList {
23
+ pending: DevicePairingPendingRequest[];
24
+ paired: PairedDevice[];
25
+ }
26
+ type ApproveDevicePairingResult = {
27
+ status: "approved";
28
+ requestId: string;
29
+ device: PairedDevice;
30
+ } | {
31
+ status: "forbidden";
32
+ reason: string;
33
+ } | null;
34
+ }
35
+
6
36
  declare module "openclaw/plugin-sdk/core" {
7
37
  export const defineChannelPluginEntry: (...args: any[]) => any;
8
38
  export const createChatChannelPlugin: (...args: any[]) => any;
@@ -0,0 +1,10 @@
1
+ // Mock module for openclaw/plugin-sdk/device-bootstrap in tests.
2
+ // Tests replace these via vi.mock + vi.fn() in hoisted blocks.
3
+
4
+ export function listDevicePairing(): Promise<any> {
5
+ throw new Error("listDevicePairing not mocked");
6
+ }
7
+
8
+ export function approveDevicePairing(): Promise<any> {
9
+ throw new Error("approveDevicePairing not mocked");
10
+ }