@syengup/friday-channel-next 0.1.8 → 0.1.9

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.
@@ -29,10 +29,16 @@ function resolveOpenClawDist() {
29
29
  const candidates = [
30
30
  process.env.OPENCLAW_DIST,
31
31
  fromBin,
32
+ // Windows: standard npm -g locations
32
33
  join(process.env.APPDATA ?? "", "npm/node_modules/openclaw/dist"),
34
+ join(process.env.LOCALAPPDATA ?? "", "npm/node_modules/openclaw/dist"),
35
+ // Cross-platform: version-manager paths detected from PATH resolution
36
+ // (nvm/fnm/asdf installs are found by resolveOpenClawDistFromPath via PATH)
33
37
  "/opt/homebrew/lib/node_modules/openclaw/dist",
34
38
  "/home/linuxbrew/.linuxbrew/lib/node_modules/openclaw/dist",
35
39
  "/usr/local/lib/node_modules/openclaw/dist",
40
+ // Linux: npm -g with prefix=/usr
41
+ "/usr/lib/node_modules/openclaw/dist",
36
42
  ].filter((v) => typeof v === "string" && v.length > 0);
37
43
  for (const root of candidates) {
38
44
  try {
@@ -1,3 +1,4 @@
1
+ import { homedir } from "node:os";
1
2
  function asObject(value) {
2
3
  return value && typeof value === "object" && !Array.isArray(value)
3
4
  ? value
@@ -28,7 +29,7 @@ export function resolveFridayNextConfig(cfg) {
28
29
  pathPrefix: asString(section.pathPrefix, "/friday-next"),
29
30
  transport: asString(section.transport, "http+sse"),
30
31
  historyLimit: asNumber(section.historyLimit, 25, 1, 200),
31
- historyDir: asString(section.historyDir, `${process.env.HOME ?? ""}/.openclaw/friday-next/history`),
32
+ historyDir: asString(section.historyDir, `${homedir()}/.openclaw/friday-next/history`),
32
33
  logLevel: asString(section.logLevel, "info"),
33
34
  authToken,
34
35
  corsEnabled: asBool(cors.enabled, false),
@@ -15,7 +15,6 @@ export interface HealthCheckResult {
15
15
  timestamp: number;
16
16
  deviceId: string;
17
17
  nodeDeviceId: string;
18
- devicePairing?: HealthComponentStatus;
19
18
  nodePairing?: HealthComponentStatus;
20
19
  repairActions?: RepairAction[];
21
20
  }
@@ -1,4 +1,3 @@
1
- import { listDevicePairing, approveDevicePairing } from "openclaw/plugin-sdk/device-bootstrap";
2
1
  import { extractBearerToken } from "../middleware/auth.js";
3
2
  import { loadNodePairingModule } from "../../agent/node-pairing-bridge.js";
4
3
  import { createFridayNextLogger } from "../../logging.js";
@@ -39,95 +38,15 @@ export async function handleHealth(req, res) {
39
38
  nodeDeviceId,
40
39
  };
41
40
  const log = createFridayNextLogger("health");
42
- if (deviceId) {
43
- result.devicePairing = await checkDevicePairing(deviceId, selfHeal, result, log);
44
- }
45
41
  if (nodeDeviceId) {
46
42
  result.nodePairing = await checkNodePairing(nodeDeviceId, selfHeal, result, log);
47
43
  }
48
- const statuses = [
49
- result.devicePairing?.status,
50
- result.nodePairing?.status,
51
- ].filter(Boolean);
52
- result.ok = statuses.length === 0 || statuses.every((s) => s === "ok" || s === "pending");
44
+ result.ok = !result.nodePairing || (result.nodePairing.status === "ok" || result.nodePairing.status === "pending");
53
45
  res.statusCode = 200;
54
46
  res.setHeader("Content-Type", "application/json");
55
47
  res.end(JSON.stringify(result));
56
48
  return true;
57
49
  }
58
- async function checkDevicePairing(deviceId, selfHeal, result, log) {
59
- const normalizedDeviceId = deviceId.trim().toUpperCase();
60
- let pairing;
61
- try {
62
- pairing = await listDevicePairing();
63
- }
64
- catch (err) {
65
- log.error(`listDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`);
66
- return {
67
- status: "failed",
68
- detail: `listDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`,
69
- devicePaired: false,
70
- };
71
- }
72
- const pairedDevice = (pairing?.paired ?? []).find((entry) => entry.deviceId?.trim().toUpperCase() === normalizedDeviceId);
73
- if (pairedDevice) {
74
- const approvedScopes = pairedDevice.approvedScopes ?? [];
75
- const tokens = pairedDevice.tokens ?? {};
76
- const hasValidToken = Object.values(tokens).some((t) => !t.revokedAtMs);
77
- if (approvedScopes.length === 0 || !hasValidToken) {
78
- const issues = [];
79
- if (approvedScopes.length === 0)
80
- issues.push("no approved scopes");
81
- if (!hasValidToken)
82
- issues.push("all tokens revoked");
83
- return {
84
- status: "degraded",
85
- detail: `Device paired but degraded: ${issues.join(", ")}`,
86
- devicePaired: true,
87
- approvedScopesEmpty: approvedScopes.length === 0,
88
- tokensRevoked: !hasValidToken,
89
- };
90
- }
91
- return { status: "ok", detail: "Device paired and healthy", devicePaired: true };
92
- }
93
- const pendingDevice = (pairing?.pending ?? []).find((entry) => entry.deviceId?.trim().toUpperCase() === normalizedDeviceId);
94
- if (pendingDevice && selfHeal) {
95
- try {
96
- const approved = await approveDevicePairing(pendingDevice.requestId);
97
- const succeeded = approved && approved.status === "approved";
98
- (result.repairActions ??= []).push({
99
- component: "devicePairing",
100
- action: "approveDevicePairing",
101
- result: succeeded ? "ok" : "failed",
102
- detail: succeeded
103
- ? `Auto-approved device ${normalizedDeviceId}`
104
- : `approveDevicePairing returned status=${approved?.status ?? "null"}`,
105
- });
106
- if (succeeded) {
107
- log.info(`Auto-approved device ${normalizedDeviceId}`);
108
- return { status: "ok", detail: "Device was pending, auto-approved", devicePaired: true };
109
- }
110
- }
111
- catch (err) {
112
- log.error(`approveDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`);
113
- (result.repairActions ??= []).push({
114
- component: "devicePairing",
115
- action: "approveDevicePairing",
116
- result: "failed",
117
- detail: err instanceof Error ? err.message : String(err),
118
- });
119
- }
120
- return {
121
- status: "degraded",
122
- detail: "Device pending but auto-approve failed",
123
- devicePaired: false,
124
- };
125
- }
126
- if (pendingDevice) {
127
- return { status: "pending", detail: "Device is pending approval", devicePaired: false };
128
- }
129
- return { status: "not_found", detail: `Device ${normalizedDeviceId} not registered`, devicePaired: false };
130
- }
131
50
  async function checkNodePairing(nodeDeviceId, selfHeal, result, log) {
132
51
  const normalizedNodeId = nodeDeviceId.trim().toUpperCase();
133
52
  let listData, listNodePairing, approveNodePairing;
package/install.js CHANGED
@@ -14,7 +14,11 @@ function realHome() {
14
14
  const h = execSync(`sh -c 'echo ~${sudoUser}'`, { encoding: "utf8" }).trim();
15
15
  if (h && !h.startsWith("~") && existsSync(h)) return h;
16
16
  } catch {}
17
- for (const g of [`/home/${sudoUser}`, `/Users/${sudoUser}`]) {
17
+ for (const g of [
18
+ `/home/${sudoUser}`,
19
+ `/Users/${sudoUser}`,
20
+ `C:\\Users\\${sudoUser}`,
21
+ ]) {
18
22
  if (existsSync(g)) return g;
19
23
  }
20
24
  return current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,15 +12,6 @@
12
12
  "tsconfig.json",
13
13
  "openclaw.plugin.json"
14
14
  ],
15
- "scripts": {
16
- "build": "tsc -p tsconfig.json",
17
- "prepublishOnly": "pnpm build",
18
- "test": "npm run test:unit && npm run test:e2e",
19
- "test:unit": "vitest run",
20
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
21
- "test:smoke": "node scripts/e2e-smoke.mjs",
22
- "test:msg-live": "node scripts/message-roundtrip-live.mjs"
23
- },
24
15
  "bin": {
25
16
  "friday-channel-next": "install.js"
26
17
  },
@@ -66,5 +57,13 @@
66
57
  "typescript": "^6.0.3",
67
58
  "vitest": "^4.1.5",
68
59
  "zod": "^4.3.6"
60
+ },
61
+ "scripts": {
62
+ "build": "tsc -p tsconfig.json",
63
+ "test": "npm run test:unit && npm run test:e2e",
64
+ "test:unit": "vitest run",
65
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
66
+ "test:smoke": "node scripts/e2e-smoke.mjs",
67
+ "test:msg-live": "node scripts/message-roundtrip-live.mjs"
69
68
  }
70
- }
69
+ }
@@ -30,10 +30,16 @@ function resolveOpenClawDist(): string {
30
30
  const candidates: string[] = [
31
31
  process.env.OPENCLAW_DIST,
32
32
  fromBin,
33
+ // Windows: standard npm -g locations
33
34
  join(process.env.APPDATA ?? "", "npm/node_modules/openclaw/dist"),
35
+ join(process.env.LOCALAPPDATA ?? "", "npm/node_modules/openclaw/dist"),
36
+ // Cross-platform: version-manager paths detected from PATH resolution
37
+ // (nvm/fnm/asdf installs are found by resolveOpenClawDistFromPath via PATH)
34
38
  "/opt/homebrew/lib/node_modules/openclaw/dist",
35
39
  "/home/linuxbrew/.linuxbrew/lib/node_modules/openclaw/dist",
36
40
  "/usr/local/lib/node_modules/openclaw/dist",
41
+ // Linux: npm -g with prefix=/usr
42
+ "/usr/lib/node_modules/openclaw/dist",
37
43
  ].filter((v): v is string => typeof v === "string" && v.length > 0);
38
44
 
39
45
  for (const root of candidates) {
package/src/config.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { homedir } from "node:os";
2
+
1
3
  export type FridayNextLogLevel = "debug" | "info" | "warn" | "error";
2
4
 
3
5
  export type FridayNextConfig = {
@@ -52,7 +54,7 @@ export function resolveFridayNextConfig(cfg: unknown): FridayNextConfig {
52
54
  historyLimit: asNumber(section.historyLimit, 25, 1, 200),
53
55
  historyDir: asString(
54
56
  section.historyDir,
55
- `${process.env.HOME ?? ""}/.openclaw/friday-next/history`,
57
+ `${homedir()}/.openclaw/friday-next/history`,
56
58
  ),
57
59
  logLevel: asString(section.logLevel, "info") as FridayNextLogLevel,
58
60
  authToken,
@@ -291,55 +291,6 @@ describe("e2e two-step auto-approval", () => {
291
291
 
292
292
  // ── Health endpoint with selfHeal ────────────────────────────────
293
293
 
294
- it("GET /friday-next/health?selfHeal=true 自动批准 pending device", async () => {
295
- mockListDevices.mockResolvedValueOnce({
296
- pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
297
- paired: [],
298
- });
299
- mockApproveDevice.mockResolvedValueOnce({
300
- status: "approved",
301
- requestId: DEVICE_REQUEST_ID,
302
- device: { deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
303
- });
304
-
305
- const app = createAppSimulator({ token: "test-token" });
306
- const res = await app.rawRequest({
307
- method: "GET",
308
- path: `/friday-next/health?deviceId=${FAKE_DEVICE_ID}&selfHeal=true`,
309
- });
310
-
311
- expect(res.status).toBe(200);
312
- const body = JSON.parse(res.body);
313
- expect(body.ok).toBe(true);
314
- expect(body.devicePairing.status).toBe("ok");
315
- expect(body.devicePairing.detail).toContain("auto-approved");
316
- expect(body.repairActions).toHaveLength(1);
317
- expect(body.repairActions[0].component).toBe("devicePairing");
318
- expect(body.repairActions[0].result).toBe("ok");
319
- expect(mockApproveDevice).toHaveBeenCalledWith(DEVICE_REQUEST_ID);
320
- });
321
-
322
- it("GET /friday-next/health 不带 selfHeal 时不自动批准 (只返回 pending)", async () => {
323
- mockListDevices.mockResolvedValueOnce({
324
- pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
325
- paired: [],
326
- });
327
-
328
- const app = createAppSimulator({ token: "test-token" });
329
- const res = await app.rawRequest({
330
- method: "GET",
331
- path: `/friday-next/health?deviceId=${FAKE_DEVICE_ID}`,
332
- });
333
-
334
- expect(res.status).toBe(200);
335
- const body = JSON.parse(res.body);
336
- expect(body.devicePairing.status).toBe("pending");
337
- expect(body.devicePairing.detail).toContain("pending approval");
338
- expect(mockApproveDevice).not.toHaveBeenCalled();
339
- // ok 仍为 true,因为 pending 也视为 ok
340
- expect(body.ok).toBe(true);
341
- });
342
-
343
294
  it("GET /friday-next/health?selfHeal=true 自动批准 pending node", async () => {
344
295
  mockListDevices.mockResolvedValue({ pending: [], paired: [] }); // device 不相关
345
296
 
@@ -371,19 +322,9 @@ describe("e2e two-step auto-approval", () => {
371
322
  expect(body.repairActions[0].result).toBe("ok");
372
323
  });
373
324
 
374
- it("GET /friday-next/health?selfHeal=true 同时自动批准 device node", async () => {
375
- // Device is pending
376
- mockListDevices.mockResolvedValueOnce({
377
- pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
378
- paired: [],
379
- });
380
- mockApproveDevice.mockResolvedValueOnce({
381
- status: "approved",
382
- requestId: DEVICE_REQUEST_ID,
383
- device: { deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
384
- });
385
-
386
- // Node is pending
325
+ it("GET /friday-next/health?selfHeal=true 自动批准 pending node (忽略 deviceId)", async () => {
326
+ // Node is pending (deviceId is ignored — device approval is handled by
327
+ // the WebSocket hello-ok path, not by the health endpoint)
387
328
  const mockListNodePairing = vi.fn().mockResolvedValueOnce({
388
329
  pending: [{ requestId: NODE_REQUEST_ID, nodeId: FAKE_DEVICE_ID }],
389
330
  paired: [],
@@ -406,10 +347,9 @@ describe("e2e two-step auto-approval", () => {
406
347
  expect(res.status).toBe(200);
407
348
  const body = JSON.parse(res.body);
408
349
  expect(body.ok).toBe(true);
409
- expect(body.devicePairing.status).toBe("ok");
350
+ expect(body.devicePairing).toBeUndefined();
410
351
  expect(body.nodePairing.status).toBe("ok");
411
- expect(body.repairActions).toHaveLength(2);
412
- expect(body.repairActions[0].component).toBe("devicePairing");
413
- expect(body.repairActions[1].component).toBe("nodePairing");
352
+ expect(body.repairActions).toHaveLength(1);
353
+ expect(body.repairActions[0].component).toBe("nodePairing");
414
354
  });
415
355
  });
@@ -4,16 +4,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
4
4
  import { setMockRuntime } from "../../test-support/mock-runtime.js";
5
5
  import { __setMockNodePairingForTests } from "../../agent/node-pairing-bridge.js";
6
6
 
7
- const { mockListDevices, mockApproveDevice } = vi.hoisted(() => ({
8
- mockListDevices: vi.fn(),
9
- mockApproveDevice: vi.fn(),
10
- }));
11
-
12
- vi.mock("openclaw/plugin-sdk/device-bootstrap", () => ({
13
- listDevicePairing: mockListDevices,
14
- approveDevicePairing: mockApproveDevice,
15
- }));
16
-
17
7
  const { mockListNodePairing, mockApproveNodePairing } = vi.hoisted(() => ({
18
8
  mockListNodePairing: vi.fn(),
19
9
  mockApproveNodePairing: vi.fn(),
@@ -45,8 +35,6 @@ describe("handleHealth", () => {
45
35
  beforeEach(() => {
46
36
  setMockRuntime();
47
37
  vi.clearAllMocks();
48
- mockListDevices.mockResolvedValue({ pending: [], paired: [] });
49
- mockApproveDevice.mockResolvedValue({ status: "approved", requestId: REQUEST_ID });
50
38
  __setMockNodePairingForTests({
51
39
  listNodePairing: mockListNodePairing,
52
40
  approveNodePairing: mockApproveNodePairing,
@@ -85,226 +73,6 @@ describe("handleHealth", () => {
85
73
  expect(body.repairActions).toBeUndefined();
86
74
  });
87
75
 
88
- // --- selfHeal parameter ---
89
-
90
- it("selfHeal defaults to false (opt-in)", async () => {
91
- mockListDevices.mockResolvedValueOnce({
92
- pending: [{ requestId: REQUEST_ID, deviceId: DEVICE_ID }],
93
- paired: [],
94
- });
95
-
96
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}`, {
97
- authorization: "Bearer test-token",
98
- });
99
- const res = new MockRes() as unknown as ServerResponse;
100
- await handleHealth(req, res);
101
- expect((res as unknown as MockRes).statusCode).toBe(200);
102
- const body = JSON.parse((res as unknown as MockRes).body);
103
- expect(body.devicePairing.status).toBe("pending");
104
- expect(mockApproveDevice).not.toHaveBeenCalled();
105
- });
106
-
107
- it("selfHeal=true enables auto-approve", async () => {
108
- mockListDevices.mockResolvedValueOnce({
109
- pending: [{ requestId: REQUEST_ID, deviceId: DEVICE_ID }],
110
- paired: [],
111
- });
112
-
113
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&selfHeal=true`, {
114
- authorization: "Bearer test-token",
115
- });
116
- const res = new MockRes() as unknown as ServerResponse;
117
- await handleHealth(req, res);
118
- const body = JSON.parse((res as unknown as MockRes).body);
119
- expect(body.devicePairing.status).toBe("ok");
120
- expect(body.repairActions).toHaveLength(1);
121
- expect(body.repairActions[0].component).toBe("devicePairing");
122
- expect(body.repairActions[0].result).toBe("ok");
123
- });
124
-
125
- it("selfHeal=True (capital T) is case-insensitive and enables auto-heal", async () => {
126
- mockListDevices.mockResolvedValueOnce({
127
- pending: [{ requestId: REQUEST_ID, deviceId: DEVICE_ID }],
128
- paired: [],
129
- });
130
-
131
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&selfHeal=True`, {
132
- authorization: "Bearer test-token",
133
- });
134
- const res = new MockRes() as unknown as ServerResponse;
135
- await handleHealth(req, res);
136
- const body = JSON.parse((res as unknown as MockRes).body);
137
- expect(body.devicePairing.status).toBe("ok");
138
- });
139
-
140
- it("selfHeal=FALSE is case-insensitive and stays disabled", async () => {
141
- mockListDevices.mockResolvedValueOnce({
142
- pending: [{ requestId: REQUEST_ID, deviceId: DEVICE_ID }],
143
- paired: [],
144
- });
145
-
146
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&selfHeal=FALSE`, {
147
- authorization: "Bearer test-token",
148
- });
149
- const res = new MockRes() as unknown as ServerResponse;
150
- await handleHealth(req, res);
151
- const body = JSON.parse((res as unknown as MockRes).body);
152
- expect(body.devicePairing.status).toBe("pending");
153
- expect(mockApproveDevice).not.toHaveBeenCalled();
154
- });
155
-
156
- // --- Device pairing: paired + healthy ---
157
-
158
- it("returns ok when device is paired and healthy", async () => {
159
- mockListDevices.mockResolvedValueOnce({
160
- pending: [],
161
- paired: [
162
- { deviceId: DEVICE_ID, approvedScopes: ["operator.read"], tokens: { t1: {} } },
163
- ],
164
- });
165
-
166
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}`, {
167
- authorization: "Bearer test-token",
168
- });
169
- const res = new MockRes() as unknown as ServerResponse;
170
- await handleHealth(req, res);
171
- const body = JSON.parse((res as unknown as MockRes).body);
172
- expect(body.ok).toBe(true);
173
- expect(body.devicePairing.status).toBe("ok");
174
- expect(body.devicePairing.devicePaired).toBe(true);
175
- });
176
-
177
- // --- Device pairing: degraded ---
178
-
179
- it("returns degraded when device has no approved scopes", async () => {
180
- mockListDevices.mockResolvedValueOnce({
181
- pending: [],
182
- paired: [
183
- { deviceId: DEVICE_ID, approvedScopes: [], tokens: { t1: {} } },
184
- ],
185
- });
186
-
187
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}`, {
188
- authorization: "Bearer test-token",
189
- });
190
- const res = new MockRes() as unknown as ServerResponse;
191
- await handleHealth(req, res);
192
- const body = JSON.parse((res as unknown as MockRes).body);
193
- expect(body.ok).toBe(false);
194
- expect(body.devicePairing.status).toBe("degraded");
195
- expect(body.devicePairing.approvedScopesEmpty).toBe(true);
196
- });
197
-
198
- it("returns degraded when all tokens are revoked", async () => {
199
- mockListDevices.mockResolvedValueOnce({
200
- pending: [],
201
- paired: [
202
- {
203
- deviceId: DEVICE_ID,
204
- approvedScopes: ["operator.read"],
205
- tokens: { t1: { revokedAtMs: 1000 } },
206
- },
207
- ],
208
- });
209
-
210
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}`, {
211
- authorization: "Bearer test-token",
212
- });
213
- const res = new MockRes() as unknown as ServerResponse;
214
- await handleHealth(req, res);
215
- const body = JSON.parse((res as unknown as MockRes).body);
216
- expect(body.ok).toBe(false);
217
- expect(body.devicePairing.status).toBe("degraded");
218
- expect(body.devicePairing.tokensRevoked).toBe(true);
219
- });
220
-
221
- // --- Device pairing: pending ---
222
-
223
- it("returns pending when device is pending and selfHeal is false", async () => {
224
- mockListDevices.mockResolvedValueOnce({
225
- pending: [{ requestId: REQUEST_ID, deviceId: DEVICE_ID }],
226
- paired: [],
227
- });
228
-
229
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&selfHeal=false`, {
230
- authorization: "Bearer test-token",
231
- });
232
- const res = new MockRes() as unknown as ServerResponse;
233
- await handleHealth(req, res);
234
- const body = JSON.parse((res as unknown as MockRes).body);
235
- expect(body.devicePairing.status).toBe("pending");
236
- expect(mockApproveDevice).not.toHaveBeenCalled();
237
- });
238
-
239
- // --- Device pairing: not_found ---
240
-
241
- it("returns not_found when device is not in paired or pending", async () => {
242
- mockListDevices.mockResolvedValueOnce({ pending: [], paired: [] });
243
-
244
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}`, {
245
- authorization: "Bearer test-token",
246
- });
247
- const res = new MockRes() as unknown as ServerResponse;
248
- await handleHealth(req, res);
249
- const body = JSON.parse((res as unknown as MockRes).body);
250
- expect(body.ok).toBe(false);
251
- expect(body.devicePairing.status).toBe("not_found");
252
- });
253
-
254
- // --- Device pairing: listDevicePairing throws ---
255
-
256
- it("returns failed when listDevicePairing throws", async () => {
257
- mockListDevices.mockRejectedValueOnce(new Error("ENOENT"));
258
-
259
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}`, {
260
- authorization: "Bearer test-token",
261
- });
262
- const res = new MockRes() as unknown as ServerResponse;
263
- await handleHealth(req, res);
264
- const body = JSON.parse((res as unknown as MockRes).body);
265
- expect(body.devicePairing.status).toBe("failed");
266
- expect(body.devicePairing.detail).toContain("ENOENT");
267
- });
268
-
269
- // --- Device pairing: approveDevicePairing throws during self-heal ---
270
-
271
- it("returns degraded when auto-approve device throws", async () => {
272
- mockListDevices.mockResolvedValueOnce({
273
- pending: [{ requestId: REQUEST_ID, deviceId: DEVICE_ID }],
274
- paired: [],
275
- });
276
- mockApproveDevice.mockRejectedValueOnce(new Error("unknown requestId"));
277
-
278
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&selfHeal=true`, {
279
- authorization: "Bearer test-token",
280
- });
281
- const res = new MockRes() as unknown as ServerResponse;
282
- await handleHealth(req, res);
283
- const body = JSON.parse((res as unknown as MockRes).body);
284
- expect(body.devicePairing.status).toBe("degraded");
285
- expect(body.repairActions).toHaveLength(1);
286
- expect(body.repairActions[0].result).toBe("failed");
287
- });
288
-
289
- // --- Device pairing: normalize case-insensitively ---
290
-
291
- it("matches deviceId case-insensitively", async () => {
292
- mockListDevices.mockResolvedValueOnce({
293
- pending: [],
294
- paired: [
295
- { deviceId: DEVICE_ID.toUpperCase(), approvedScopes: ["operator.read"], tokens: { t1: {} } },
296
- ],
297
- });
298
-
299
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID.toLowerCase()}`, {
300
- authorization: "Bearer test-token",
301
- });
302
- const res = new MockRes() as unknown as ServerResponse;
303
- await handleHealth(req, res);
304
- const body = JSON.parse((res as unknown as MockRes).body);
305
- expect(body.devicePairing.status).toBe("ok");
306
- });
307
-
308
76
  // --- Node pairing: paired + healthy ---
309
77
 
310
78
  it("returns ok when node is paired with required caps and commands", async () => {
@@ -459,15 +227,9 @@ describe("handleHealth", () => {
459
227
  expect(body.nodePairing.status).toBe("failed");
460
228
  });
461
229
 
462
- // --- Combined: device + node ---
230
+ // --- Combined: deviceId + nodeDeviceId ---
463
231
 
464
- it("checks both device and node and reports overall ok", async () => {
465
- mockListDevices.mockResolvedValueOnce({
466
- pending: [],
467
- paired: [
468
- { deviceId: DEVICE_ID, approvedScopes: ["operator.read"], tokens: { t1: {} } },
469
- ],
470
- });
232
+ it("ignores deviceId and only checks node pairing", async () => {
471
233
  mockListNodePairing.mockResolvedValueOnce({
472
234
  pending: [],
473
235
  paired: [
@@ -486,30 +248,7 @@ describe("handleHealth", () => {
486
248
  await handleHealth(req, res);
487
249
  const body = JSON.parse((res as unknown as MockRes).body);
488
250
  expect(body.ok).toBe(true);
489
- expect(body.devicePairing.status).toBe("ok");
251
+ expect(body.devicePairing).toBeUndefined();
490
252
  expect(body.nodePairing.status).toBe("ok");
491
253
  });
492
-
493
- it("reports ok=false when device is ok but node is degraded", async () => {
494
- mockListDevices.mockResolvedValueOnce({
495
- pending: [],
496
- paired: [
497
- { deviceId: DEVICE_ID, approvedScopes: ["operator.read"], tokens: { t1: {} } },
498
- ],
499
- });
500
- mockListNodePairing.mockResolvedValueOnce({
501
- pending: [],
502
- paired: [{ nodeId: NODE_ID, caps: [], commands: [] }],
503
- });
504
-
505
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&nodeDeviceId=${NODE_ID}`, {
506
- authorization: "Bearer test-token",
507
- });
508
- const res = new MockRes() as unknown as ServerResponse;
509
- await handleHealth(req, res);
510
- const body = JSON.parse((res as unknown as MockRes).body);
511
- expect(body.ok).toBe(false);
512
- expect(body.devicePairing.status).toBe("ok");
513
- expect(body.nodePairing.status).toBe("degraded");
514
- });
515
254
  });
@@ -1,5 +1,4 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { listDevicePairing, approveDevicePairing } from "openclaw/plugin-sdk/device-bootstrap";
3
2
  import { extractBearerToken } from "../middleware/auth.js";
4
3
  import { loadNodePairingModule } from "../../agent/node-pairing-bridge.js";
5
4
  import { createFridayNextLogger } from "../../logging.js";
@@ -35,7 +34,6 @@ export interface HealthCheckResult {
35
34
  timestamp: number;
36
35
  deviceId: string;
37
36
  nodeDeviceId: string;
38
- devicePairing?: HealthComponentStatus;
39
37
  nodePairing?: HealthComponentStatus;
40
38
  repairActions?: RepairAction[];
41
39
  }
@@ -70,112 +68,17 @@ export async function handleHealth(req: IncomingMessage, res: ServerResponse): P
70
68
 
71
69
  const log = createFridayNextLogger("health");
72
70
 
73
- if (deviceId) {
74
- result.devicePairing = await checkDevicePairing(deviceId, selfHeal, result, log);
75
- }
76
-
77
71
  if (nodeDeviceId) {
78
72
  result.nodePairing = await checkNodePairing(nodeDeviceId, selfHeal, result, log);
79
73
  }
80
74
 
81
- const statuses = [
82
- result.devicePairing?.status,
83
- result.nodePairing?.status,
84
- ].filter(Boolean);
85
- result.ok = statuses.length === 0 || statuses.every((s) => s === "ok" || s === "pending");
75
+ result.ok = !result.nodePairing || (result.nodePairing.status === "ok" || result.nodePairing.status === "pending");
86
76
 
87
77
  res.statusCode = 200;
88
78
  res.setHeader("Content-Type", "application/json");
89
79
  res.end(JSON.stringify(result));
90
80
  return true;
91
81
  }
92
-
93
- async function checkDevicePairing(
94
- deviceId: string,
95
- selfHeal: boolean,
96
- result: HealthCheckResult,
97
- log: ReturnType<typeof createFridayNextLogger>,
98
- ): Promise<HealthComponentStatus> {
99
- const normalizedDeviceId = deviceId.trim().toUpperCase();
100
-
101
- let pairing;
102
- try {
103
- pairing = await listDevicePairing();
104
- } catch (err) {
105
- log.error(`listDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`);
106
- return {
107
- status: "failed",
108
- detail: `listDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`,
109
- devicePaired: false,
110
- };
111
- }
112
-
113
- const pairedDevice = (pairing?.paired ?? []).find(
114
- (entry) => entry.deviceId?.trim().toUpperCase() === normalizedDeviceId,
115
- );
116
- if (pairedDevice) {
117
- const approvedScopes: string[] = (pairedDevice as any).approvedScopes ?? [];
118
- const tokens: Record<string, { revokedAtMs?: number }> = (pairedDevice as any).tokens ?? {};
119
- const hasValidToken = Object.values(tokens).some((t: any) => !t.revokedAtMs);
120
-
121
- if (approvedScopes.length === 0 || !hasValidToken) {
122
- const issues: string[] = [];
123
- if (approvedScopes.length === 0) issues.push("no approved scopes");
124
- if (!hasValidToken) issues.push("all tokens revoked");
125
- return {
126
- status: "degraded",
127
- detail: `Device paired but degraded: ${issues.join(", ")}`,
128
- devicePaired: true,
129
- approvedScopesEmpty: approvedScopes.length === 0,
130
- tokensRevoked: !hasValidToken,
131
- };
132
- }
133
-
134
- return { status: "ok", detail: "Device paired and healthy", devicePaired: true };
135
- }
136
-
137
- const pendingDevice = (pairing?.pending ?? []).find(
138
- (entry) => entry.deviceId?.trim().toUpperCase() === normalizedDeviceId,
139
- );
140
- if (pendingDevice && selfHeal) {
141
- try {
142
- const approved = await approveDevicePairing(pendingDevice.requestId);
143
- const succeeded = approved && approved.status === "approved";
144
- (result.repairActions ??= []).push({
145
- component: "devicePairing",
146
- action: "approveDevicePairing",
147
- result: succeeded ? "ok" : "failed",
148
- detail: succeeded
149
- ? `Auto-approved device ${normalizedDeviceId}`
150
- : `approveDevicePairing returned status=${(approved as any)?.status ?? "null"}`,
151
- });
152
- if (succeeded) {
153
- log.info(`Auto-approved device ${normalizedDeviceId}`);
154
- return { status: "ok", detail: "Device was pending, auto-approved", devicePaired: true };
155
- }
156
- } catch (err) {
157
- log.error(`approveDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`);
158
- (result.repairActions ??= []).push({
159
- component: "devicePairing",
160
- action: "approveDevicePairing",
161
- result: "failed",
162
- detail: err instanceof Error ? err.message : String(err),
163
- });
164
- }
165
- return {
166
- status: "degraded",
167
- detail: "Device pending but auto-approve failed",
168
- devicePaired: false,
169
- };
170
- }
171
-
172
- if (pendingDevice) {
173
- return { status: "pending", detail: "Device is pending approval", devicePaired: false };
174
- }
175
-
176
- return { status: "not_found", detail: `Device ${normalizedDeviceId} not registered`, devicePaired: false };
177
- }
178
-
179
82
  async function checkNodePairing(
180
83
  nodeDeviceId: string,
181
84
  selfHeal: boolean,