@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
|
@@ -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
|
-
|
|
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
|
|
13
|
+
approvedAtMs: number;
|
|
19
14
|
caps?: string[];
|
|
20
15
|
commands?: string[];
|
|
21
16
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
67
|
+
let listData;
|
|
100
68
|
try {
|
|
101
|
-
listData =
|
|
102
|
-
} catch {
|
|
103
|
-
log.error(`
|
|
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: "
|
|
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
|
|
87
|
+
let approved;
|
|
120
88
|
try {
|
|
121
|
-
|
|
122
|
-
approveStdout = result.stdout;
|
|
89
|
+
approved = await approveNodePairing(requestId, {});
|
|
123
90
|
} catch (err) {
|
|
124
|
-
|
|
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
|
|
130
|
-
detail:
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
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:
|
|
152
|
-
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
|
-
//
|
|
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
|
+
}
|