@syengup/friday-channel-next 0.0.34 → 0.0.37
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/attachments/0768c9b1-53b0-44df-83e8-be15c4ea188f.jpg +0 -0
- package/dist/attachments/0a379d01-116b-4da1-bf15-77cb2cbb0093.jpg +0 -0
- package/dist/attachments/181caab2-64a7-4004-a057-225a144f949e.mp3 +0 -0
- package/dist/attachments/19662331-e527-47d2-bc0e-0e19a7a91419.jpg +0 -0
- package/dist/attachments/26a23b2b-52df-4572-a5e1-15b34fb87e44.jpg +0 -0
- package/dist/attachments/2f9282c5-8db4-4c4a-a060-e65104f6f9ff.jpg +0 -0
- package/dist/attachments/3929ec3d-ea15-4de6-96bc-97e8b0b658a7.jpg +0 -0
- package/dist/attachments/403c0cbc-4e3c-4146-a3be-ff3746ee7cda.jpg +0 -0
- package/dist/attachments/441977f5-0f7b-4aa2-841a-1d63e787ea53.jpg +0 -0
- package/dist/attachments/453e8aa2-76e3-498d-8d6f-d7b96d6bf45b.jpg +0 -0
- package/dist/attachments/538cde71-d26e-4d3d-b901-e8dd905e668c.mp3 +0 -0
- package/dist/attachments/55c7f628-4ba2-4252-aa4b-4f3eb6045a8a.mp3 +0 -0
- package/dist/attachments/5f7683f5-8194-4698-b077-31d209525379.jpg +0 -0
- package/dist/attachments/60614a35-8f44-4197-b783-2f58f5a72ac8.jpeg +0 -0
- package/dist/attachments/62830489-8814-48b1-851c-3845e514f35e.mp3 +0 -0
- package/dist/attachments/66f4a62d-1531-4f38-a531-7456f9edf221.png +0 -0
- package/dist/attachments/6735d749-769e-483a-9b84-43b9338a720b.png +0 -0
- package/dist/attachments/6d1766b1-05e4-4b04-b3c8-1c25e9d182a1.png +0 -0
- package/dist/attachments/782b077b-06e3-484b-baf5-33e7160234ed.png +0 -0
- package/dist/attachments/7ad638b2-1f56-4d93-9ad8-b40346e0650f.jpg +0 -0
- package/dist/attachments/89f6fb15-e652-4111-a60c-baa414659052.png +0 -0
- package/dist/attachments/8a88b14f-442f-45fb-b01d-e51bab8f800d.mp3 +0 -0
- package/dist/attachments/92292034-9cf6-4f26-8d77-fddca3deb638.png +0 -0
- package/dist/attachments/92c2b414-d33d-4d93-bcb6-013da7bec9a4.jpg +0 -0
- package/dist/attachments/9664f69e-3c05-45ca-9a52-f2d0b9f9bf7e.jpg +0 -0
- package/dist/attachments/977d28c1-43c0-40e0-95e3-defe0f41afe8.jpg +0 -0
- package/dist/attachments/9df40f1a-c6e1-4177-8a03-06757a30b19e.png +0 -0
- package/dist/attachments/a68e6815-6163-4421-a70f-34493aa9a217.jpg +0 -0
- package/dist/attachments/aab32fea-6d99-47ec-ab1f-2340f31312eb.jpg +0 -0
- package/dist/attachments/ab403224-2fb1-49c1-8738-ea194ab65d44.png +0 -0
- package/dist/attachments/ac3da190-d6ee-4038-a673-8b893035a687.png +0 -0
- package/dist/attachments/af02be9c-87f7-4c5a-9969-7db32039bb58.png +0 -0
- package/dist/attachments/b011d42a-00e5-4f77-86bc-08da6112e6e1.mp3 +0 -0
- package/dist/attachments/b7d7df40-c627-4b1f-9b09-167b88545c25.mp3 +0 -0
- package/dist/attachments/c5e9bf09-a718-422c-bcb3-94c173e3755b.mp3 +0 -0
- package/dist/attachments/d5449e13-1995-44ba-9392-ecbfe5f9876f.jpg +0 -0
- package/dist/attachments/ea0069f5-01cf-4ea1-985e-3a1e426399c3.png +0 -0
- package/dist/attachments/f3989ff2-7b70-4a80-a896-74a6b197f7d8.png +0 -0
- package/dist/attachments/f64a4a14-e3aa-4eed-a8d9-1603f04baa5b.jpg +0 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +176 -0
- package/dist/src/agent/abort-run.d.ts +1 -0
- package/dist/src/agent/abort-run.js +11 -0
- package/dist/src/agent/active-runs.d.ts +9 -0
- package/dist/src/agent/active-runs.js +20 -0
- package/dist/src/agent/dispatch-bridge.d.ts +5 -0
- package/dist/src/agent/dispatch-bridge.js +12 -0
- package/dist/src/agent/media-bridge.d.ts +4 -0
- package/dist/src/agent/media-bridge.js +21 -0
- package/dist/src/agent/subagent-registry.d.ts +68 -0
- package/dist/src/agent/subagent-registry.js +142 -0
- package/dist/src/agent-forward-runtime.d.ts +17 -0
- package/dist/src/agent-forward-runtime.js +16 -0
- package/dist/src/agent-run-context-bridge.d.ts +13 -0
- package/dist/src/agent-run-context-bridge.js +23 -0
- package/dist/src/channel-actions.d.ts +13 -0
- package/dist/src/channel-actions.js +101 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +248 -0
- package/dist/src/collect-message-media-paths.d.ts +11 -0
- package/dist/src/collect-message-media-paths.js +143 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +39 -0
- package/dist/src/friday-inbound-stats.d.ts +2 -0
- package/dist/src/friday-inbound-stats.js +8 -0
- package/dist/src/friday-session.d.ts +40 -0
- package/dist/src/friday-session.js +395 -0
- package/dist/src/host-config.d.ts +1 -0
- package/dist/src/host-config.js +15 -0
- package/dist/src/http/handlers/cancel.d.ts +2 -0
- package/dist/src/http/handlers/cancel.js +33 -0
- package/dist/src/http/handlers/device-approve.d.ts +2 -0
- package/dist/src/http/handlers/device-approve.js +125 -0
- package/dist/src/http/handlers/device-token.d.ts +2 -0
- package/dist/src/http/handlers/device-token.js +43 -0
- package/dist/src/http/handlers/files-download.d.ts +10 -0
- package/dist/src/http/handlers/files-download.js +210 -0
- package/dist/src/http/handlers/files-upload.d.ts +8 -0
- package/dist/src/http/handlers/files-upload.js +136 -0
- package/dist/src/http/handlers/files.d.ts +75 -0
- package/dist/src/http/handlers/files.js +305 -0
- package/dist/src/http/handlers/import.d.ts +7 -0
- package/dist/src/http/handlers/import.js +69 -0
- package/dist/src/http/handlers/info.d.ts +2 -0
- package/dist/src/http/handlers/info.js +13 -0
- package/dist/src/http/handlers/messages-list.d.ts +7 -0
- package/dist/src/http/handlers/messages-list.js +44 -0
- package/dist/src/http/handlers/messages.d.ts +34 -0
- package/dist/src/http/handlers/messages.js +476 -0
- package/dist/src/http/handlers/models-list.d.ts +10 -0
- package/dist/src/http/handlers/models-list.js +113 -0
- package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
- package/dist/src/http/handlers/nodes-approve.js +146 -0
- package/dist/src/http/handlers/pair.d.ts +2 -0
- package/dist/src/http/handlers/pair.js +39 -0
- package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
- package/dist/src/http/handlers/sessions-delete.js +49 -0
- package/dist/src/http/handlers/sessions-list.d.ts +8 -0
- package/dist/src/http/handlers/sessions-list.js +24 -0
- package/dist/src/http/handlers/sessions-messages-get.d.ts +2 -0
- package/dist/src/http/handlers/sessions-messages-get.js +55 -0
- package/dist/src/http/handlers/sessions-messages-post.d.ts +2 -0
- package/dist/src/http/handlers/sessions-messages-post.js +92 -0
- package/dist/src/http/handlers/sessions-messages.d.ts +2 -0
- package/dist/src/http/handlers/sessions-messages.js +135 -0
- package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
- package/dist/src/http/handlers/sessions-settings.js +71 -0
- package/dist/src/http/handlers/sse.d.ts +2 -0
- package/dist/src/http/handlers/sse.js +70 -0
- package/dist/src/http/handlers/status.d.ts +2 -0
- package/dist/src/http/handlers/status.js +29 -0
- package/dist/src/http/handlers/sync.d.ts +7 -0
- package/dist/src/http/handlers/sync.js +56 -0
- package/dist/src/http/middleware/auth.d.ts +13 -0
- package/dist/src/http/middleware/auth.js +29 -0
- package/dist/src/http/middleware/body.d.ts +2 -0
- package/dist/src/http/middleware/body.js +24 -0
- package/dist/src/http/middleware/cors.d.ts +2 -0
- package/dist/src/http/middleware/cors.js +11 -0
- package/dist/src/http/server.d.ts +19 -0
- package/dist/src/http/server.js +87 -0
- package/dist/src/logging.d.ts +7 -0
- package/dist/src/logging.js +28 -0
- package/dist/src/push/apns.d.ts +15 -0
- package/dist/src/push/apns.js +56 -0
- package/dist/src/push/device-tokens.d.ts +3 -0
- package/dist/src/push/device-tokens.js +39 -0
- package/dist/src/run-metadata.d.ts +25 -0
- package/dist/src/run-metadata.js +139 -0
- package/dist/src/runtime.d.ts +13 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/session/session-manager.d.ts +22 -0
- package/dist/src/session/session-manager.js +190 -0
- package/dist/src/session-usage-snapshot.d.ts +23 -0
- package/dist/src/session-usage-snapshot.js +65 -0
- package/dist/src/sse/emitter.d.ts +59 -0
- package/dist/src/sse/emitter.js +219 -0
- package/dist/src/sse/offline-queue.d.ts +26 -0
- package/dist/src/sse/offline-queue.js +134 -0
- package/dist/src/sync/account-identity.d.ts +14 -0
- package/dist/src/sync/account-identity.js +101 -0
- package/dist/src/sync/archive.d.ts +9 -0
- package/dist/src/sync/archive.js +25 -0
- package/dist/src/sync/database.d.ts +66 -0
- package/dist/src/sync/database.js +364 -0
- package/dist/src/sync/init.d.ts +3 -0
- package/dist/src/sync/init.js +14 -0
- package/dist/src/sync/installation-id.d.ts +1 -0
- package/dist/src/sync/installation-id.js +41 -0
- package/dist/src/sync/message-accumulator.d.ts +29 -0
- package/dist/src/sync/message-accumulator.js +188 -0
- package/dist/src/sync/message-store.d.ts +68 -0
- package/dist/src/sync/message-store.js +262 -0
- package/dist/src/sync/push-store.d.ts +5 -0
- package/dist/src/sync/push-store.js +54 -0
- package/dist/src/sync/session-key.d.ts +12 -0
- package/dist/src/sync/session-key.js +47 -0
- package/dist/src/sync/sync-state.d.ts +5 -0
- package/dist/src/sync/sync-state.js +54 -0
- package/dist/src/sync/transcript-archive.d.ts +13 -0
- package/dist/src/sync/transcript-archive.js +37 -0
- package/dist/src/sync/transcript-store.d.ts +35 -0
- package/dist/src/sync/transcript-store.js +221 -0
- package/dist/src/sync/translate.d.ts +42 -0
- package/dist/src/sync/translate.js +171 -0
- package/dist/src/vendor/runtime-store.d.ts +26 -0
- package/dist/src/vendor/runtime-store.js +60 -0
- package/install.js +2 -2
- package/package.json +11 -10
- package/src/agent/subagent-registry.ts +195 -0
- package/src/channel.ts +6 -4
- package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
- package/src/e2e/subagent.e2e.test.ts +502 -0
- package/src/friday-session.ts +140 -1
- package/src/http/handlers/device-approve.test.ts +0 -1
- package/src/http/handlers/device-approve.ts +0 -2
- package/src/http/handlers/files-download.ts +4 -1
- package/src/http/handlers/files.ts +7 -4
- package/src/http/handlers/messages.ts +54 -4
- package/src/http/handlers/models-list.ts +24 -2
- package/src/http/handlers/nodes-approve.test.ts +288 -0
- package/src/http/handlers/nodes-approve.ts +189 -0
- package/src/http/server.ts +5 -0
- package/src/openclaw.d.ts +5 -0
- package/src/sse/emitter.ts +1 -1
- package/src/test-support/mock-runtime.ts +2 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { resolveFridayNextConfig } from "../../config.js";
|
|
2
|
+
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
3
|
+
import { getFridayNextRuntime } from "../../runtime.js";
|
|
4
|
+
import { sseEmitter } from "../../sse/emitter.js";
|
|
5
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
6
|
+
function parseLastEventId(req, url) {
|
|
7
|
+
const query = Number.parseInt(url.searchParams.get("lastEventId") ?? "", 10);
|
|
8
|
+
if (Number.isFinite(query))
|
|
9
|
+
return query;
|
|
10
|
+
const header = Number.parseInt(req.headers["last-event-id"] ?? "", 10);
|
|
11
|
+
return Number.isFinite(header) ? header : 0;
|
|
12
|
+
}
|
|
13
|
+
export async function handleSseStream(req, res) {
|
|
14
|
+
if (req.method !== "GET") {
|
|
15
|
+
res.statusCode = 405;
|
|
16
|
+
res.setHeader("Content-Type", "application/json");
|
|
17
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const token = extractBearerToken(req);
|
|
21
|
+
if (!token) {
|
|
22
|
+
res.statusCode = 401;
|
|
23
|
+
res.setHeader("Content-Type", "application/json");
|
|
24
|
+
res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
28
|
+
const deviceId = (url.searchParams.get("deviceId") ?? "").trim();
|
|
29
|
+
if (!deviceId) {
|
|
30
|
+
res.statusCode = 400;
|
|
31
|
+
res.setHeader("Content-Type", "application/json");
|
|
32
|
+
res.end(JSON.stringify({ error: "Missing required query parameter: deviceId" }));
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
res.statusCode = 200;
|
|
36
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
37
|
+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
38
|
+
res.setHeader("Connection", "keep-alive");
|
|
39
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
40
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
41
|
+
res.flushHeaders();
|
|
42
|
+
const conn = sseEmitter.addConnection(deviceId, res);
|
|
43
|
+
const normalized = deviceId.trim().toUpperCase();
|
|
44
|
+
const lastSeq = sseEmitter.latestSeqForDevice(normalized);
|
|
45
|
+
sseEmitter.broadcast({
|
|
46
|
+
type: "connected",
|
|
47
|
+
data: {
|
|
48
|
+
deviceId: normalized,
|
|
49
|
+
serverTime: Date.now(),
|
|
50
|
+
lastSeq,
|
|
51
|
+
},
|
|
52
|
+
}, deviceId, true);
|
|
53
|
+
const lastEventId = parseLastEventId(req, url);
|
|
54
|
+
if (lastEventId > 0)
|
|
55
|
+
sseEmitter.replayBacklog(deviceId, lastEventId);
|
|
56
|
+
const config = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
57
|
+
const keepalive = setInterval(() => {
|
|
58
|
+
if (conn.isClosed) {
|
|
59
|
+
clearInterval(keepalive);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
conn.sendRaw(": keepalive\n\n");
|
|
63
|
+
}, config.sseKeepaliveSec * 1000);
|
|
64
|
+
keepalive.unref();
|
|
65
|
+
req.on("close", () => {
|
|
66
|
+
clearInterval(keepalive);
|
|
67
|
+
sseEmitter.removeConnection(deviceId, conn);
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getActiveRunIds } from "../../agent/active-runs.js";
|
|
2
|
+
import { sseEmitter } from "../../sse/emitter.js";
|
|
3
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
4
|
+
export async function handleStatus(req, res) {
|
|
5
|
+
if (req.method !== "GET") {
|
|
6
|
+
res.statusCode = 405;
|
|
7
|
+
res.setHeader("Content-Type", "application/json");
|
|
8
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (!extractBearerToken(req)) {
|
|
12
|
+
res.statusCode = 401;
|
|
13
|
+
res.setHeader("Content-Type", "application/json");
|
|
14
|
+
res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const activeRuns = getActiveRunIds();
|
|
18
|
+
res.statusCode = 200;
|
|
19
|
+
res.setHeader("Content-Type", "application/json");
|
|
20
|
+
res.end(JSON.stringify({
|
|
21
|
+
ok: true,
|
|
22
|
+
channel: "friday-next",
|
|
23
|
+
version: "v2",
|
|
24
|
+
connections: sseEmitter.getConnectionCount(),
|
|
25
|
+
activeRuns,
|
|
26
|
+
activeRunCount: activeRuns.length,
|
|
27
|
+
}));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /friday-next/sessions/{sessionKey}/sync
|
|
3
|
+
*
|
|
4
|
+
* Bidirectional incremental sync: pull remote messages (primary) + push local messages (offline catch-up).
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
+
export declare function handleSync(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
2
|
+
import { readJsonBody } from "../middleware/body.js";
|
|
3
|
+
import { processSync } from "../../sync/message-store.js";
|
|
4
|
+
import { toSessionStoreKey } from "../../session/session-manager.js";
|
|
5
|
+
function extractSessionKey(url, prefix, suffix) {
|
|
6
|
+
const pathname = new URL(url, "http://localhost").pathname;
|
|
7
|
+
if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix))
|
|
8
|
+
return null;
|
|
9
|
+
const middle = pathname.slice(prefix.length, pathname.length - suffix.length);
|
|
10
|
+
return decodeURIComponent(middle) || null;
|
|
11
|
+
}
|
|
12
|
+
export async function handleSync(req, res) {
|
|
13
|
+
if (req.method !== "POST") {
|
|
14
|
+
res.statusCode = 405;
|
|
15
|
+
res.setHeader("Content-Type", "application/json");
|
|
16
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const token = extractBearerToken(req);
|
|
20
|
+
if (!token) {
|
|
21
|
+
res.statusCode = 401;
|
|
22
|
+
res.setHeader("Content-Type", "application/json");
|
|
23
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const rawKey = extractSessionKey(req.url ?? "/", "/friday-next/sessions/", "/sync");
|
|
27
|
+
if (!rawKey) {
|
|
28
|
+
res.statusCode = 400;
|
|
29
|
+
res.setHeader("Content-Type", "application/json");
|
|
30
|
+
res.end(JSON.stringify({ error: "Missing sessionKey in URL path" }));
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const sessionKey = toSessionStoreKey(rawKey);
|
|
34
|
+
const body = await readJsonBody(req);
|
|
35
|
+
if (!body) {
|
|
36
|
+
res.statusCode = 400;
|
|
37
|
+
res.setHeader("Content-Type", "application/json");
|
|
38
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const deviceId = typeof body.deviceId === "string" ? body.deviceId.trim() : "";
|
|
42
|
+
if (!deviceId) {
|
|
43
|
+
res.statusCode = 400;
|
|
44
|
+
res.setHeader("Content-Type", "application/json");
|
|
45
|
+
res.end(JSON.stringify({ error: "Missing required field: deviceId" }));
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const syncSeq = typeof body.syncSeq === "number" ? Math.floor(body.syncSeq) : 0;
|
|
49
|
+
const lastServerId = typeof body.lastServerId === "string" ? body.lastServerId : undefined;
|
|
50
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
51
|
+
const result = processSync({ sessionKey, deviceId, syncSeq, lastServerId, messages });
|
|
52
|
+
res.statusCode = 200;
|
|
53
|
+
res.setHeader("Content-Type", "application/json");
|
|
54
|
+
res.end(JSON.stringify(result));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer token authentication middleware for Friday HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the bearer token matches the gateway's configured auth token.
|
|
5
|
+
* This ensures plugin HTTP endpoints use the same token as gateway WS connections.
|
|
6
|
+
*/
|
|
7
|
+
import type { IncomingMessage } from "node:http";
|
|
8
|
+
/**
|
|
9
|
+
* Extract and validate bearer token from Authorization header.
|
|
10
|
+
* Returns the token only if it matches the gateway's configured auth token.
|
|
11
|
+
* Returns null if token is missing, malformed, or doesn't match.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractBearerToken(req: IncomingMessage): string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer token authentication middleware for Friday HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the bearer token matches the gateway's configured auth token.
|
|
5
|
+
* This ensures plugin HTTP endpoints use the same token as gateway WS connections.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveFridayNextConfig } from "../../config.js";
|
|
8
|
+
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
9
|
+
import { getFridayNextRuntime } from "../../runtime.js";
|
|
10
|
+
/**
|
|
11
|
+
* Extract and validate bearer token from Authorization header.
|
|
12
|
+
* Returns the token only if it matches the gateway's configured auth token.
|
|
13
|
+
* Returns null if token is missing, malformed, or doesn't match.
|
|
14
|
+
*/
|
|
15
|
+
export function extractBearerToken(req) {
|
|
16
|
+
const auth = req.headers.authorization;
|
|
17
|
+
if (!auth || typeof auth !== "string")
|
|
18
|
+
return null;
|
|
19
|
+
const parts = auth.trim().split(/\s+/);
|
|
20
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer")
|
|
21
|
+
return null;
|
|
22
|
+
const token = parts[1];
|
|
23
|
+
// Validate token matches the gateway's configured auth token.
|
|
24
|
+
const cfg = getHostOpenClawConfigSnapshot(getFridayNextRuntime().config);
|
|
25
|
+
const runtimeConfig = resolveFridayNextConfig(cfg);
|
|
26
|
+
if (!runtimeConfig.authToken || token !== runtimeConfig.authToken)
|
|
27
|
+
return null;
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
2
|
+
return await new Promise((resolve) => {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let total = 0;
|
|
5
|
+
req.on("data", (chunk) => {
|
|
6
|
+
total += chunk.length;
|
|
7
|
+
if (total > maxBytes) {
|
|
8
|
+
resolve(null);
|
|
9
|
+
req.destroy();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
chunks.push(chunk);
|
|
13
|
+
});
|
|
14
|
+
req.on("end", () => {
|
|
15
|
+
try {
|
|
16
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
resolve(null);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
req.on("error", () => resolve(null));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { resolveFridayNextConfig } from "../../config.js";
|
|
2
|
+
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
3
|
+
import { getFridayNextRuntime } from "../../runtime.js";
|
|
4
|
+
export function applyCorsHeaders(res) {
|
|
5
|
+
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
6
|
+
if (!cfg.corsEnabled)
|
|
7
|
+
return;
|
|
8
|
+
res.setHeader("Access-Control-Allow-Origin", cfg.corsAllowOrigin || "*");
|
|
9
|
+
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Last-Event-ID");
|
|
10
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS");
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP server registration for the Friday channel.
|
|
3
|
+
*
|
|
4
|
+
* Registers routes on the gateway HTTP server under the /friday-next/ path prefix.
|
|
5
|
+
* Routes are registered via the plugin API's registerHttpRoute method.
|
|
6
|
+
*/
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
8
|
+
export declare function registerFridayNextHttpRoutes(api: {
|
|
9
|
+
logger: {
|
|
10
|
+
info: (msg: string) => void;
|
|
11
|
+
warn: (msg: string) => void;
|
|
12
|
+
};
|
|
13
|
+
registerHttpRoute: (route: {
|
|
14
|
+
path: string;
|
|
15
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
16
|
+
auth: string;
|
|
17
|
+
match: string;
|
|
18
|
+
}) => void;
|
|
19
|
+
}): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP server registration for the Friday channel.
|
|
3
|
+
*
|
|
4
|
+
* Registers routes on the gateway HTTP server under the /friday-next/ path prefix.
|
|
5
|
+
* Routes are registered via the plugin API's registerHttpRoute method.
|
|
6
|
+
*/
|
|
7
|
+
import { handleMessages } from "./handlers/messages.js";
|
|
8
|
+
import { handleSseStream } from "./handlers/sse.js";
|
|
9
|
+
import { handleFilesUpload } from "./handlers/files-upload.js";
|
|
10
|
+
import { handleFilesDownload } from "./handlers/files-download.js";
|
|
11
|
+
import { handleCancel } from "./handlers/cancel.js";
|
|
12
|
+
import { handleDeviceApprove } from "./handlers/device-approve.js";
|
|
13
|
+
import { handleNodesApprove } from "./handlers/nodes-approve.js";
|
|
14
|
+
import { handleSessionsDelete } from "./handlers/sessions-delete.js";
|
|
15
|
+
import { handleSessionsSettings } from "./handlers/sessions-settings.js";
|
|
16
|
+
import { handleModelsList } from "./handlers/models-list.js";
|
|
17
|
+
import { handleStatus } from "./handlers/status.js";
|
|
18
|
+
import { applyCorsHeaders } from "./middleware/cors.js";
|
|
19
|
+
import { resolveFridayNextConfig } from "../config.js";
|
|
20
|
+
import { getHostOpenClawConfigSnapshot } from "../host-config.js";
|
|
21
|
+
import { getFridayNextRuntime } from "../runtime.js";
|
|
22
|
+
import { sseEmitter } from "../sse/emitter.js";
|
|
23
|
+
/** Route matcher - returns the matched handler or null. */
|
|
24
|
+
async function handleFridayNextRoute(req, res) {
|
|
25
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
26
|
+
const pathname = url.pathname;
|
|
27
|
+
applyCorsHeaders(res);
|
|
28
|
+
if (req.method === "OPTIONS") {
|
|
29
|
+
res.statusCode = 204;
|
|
30
|
+
res.end();
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// Route: GET /friday-next/events?deviceId=...
|
|
34
|
+
if (req.method === "GET" && pathname === "/friday-next/events") {
|
|
35
|
+
return await handleSseStream(req, res);
|
|
36
|
+
}
|
|
37
|
+
// Route: POST /friday-next/messages
|
|
38
|
+
if (req.method === "POST" && pathname === "/friday-next/messages") {
|
|
39
|
+
return await handleMessages(req, res);
|
|
40
|
+
}
|
|
41
|
+
// Route: POST /friday-next/files (multipart upload)
|
|
42
|
+
if (req.method === "POST" && pathname === "/friday-next/files") {
|
|
43
|
+
return await handleFilesUpload(req, res);
|
|
44
|
+
}
|
|
45
|
+
// Route: GET /friday-next/files/:id (download)
|
|
46
|
+
if (req.method === "GET" && pathname.startsWith("/friday-next/files/")) {
|
|
47
|
+
return await handleFilesDownload(req, res);
|
|
48
|
+
}
|
|
49
|
+
if (req.method === "POST" && pathname === "/friday-next/cancel") {
|
|
50
|
+
return await handleCancel(req, res);
|
|
51
|
+
}
|
|
52
|
+
if (req.method === "POST" && pathname === "/friday-next/device-approve") {
|
|
53
|
+
return await handleDeviceApprove(req, res);
|
|
54
|
+
}
|
|
55
|
+
if (req.method === "POST" && pathname === "/friday-next/nodes-approve") {
|
|
56
|
+
return await handleNodesApprove(req, res);
|
|
57
|
+
}
|
|
58
|
+
if (req.method === "DELETE" && pathname === "/friday-next/sessions") {
|
|
59
|
+
return await handleSessionsDelete(req, res);
|
|
60
|
+
}
|
|
61
|
+
if ((req.method === "PUT" || req.method === "GET") && pathname === "/friday-next/sessions/settings") {
|
|
62
|
+
return await handleSessionsSettings(req, res);
|
|
63
|
+
}
|
|
64
|
+
if (req.method === "GET" && pathname === "/friday-next/models") {
|
|
65
|
+
return await handleModelsList(req, res);
|
|
66
|
+
}
|
|
67
|
+
if (req.method === "GET" && pathname === "/friday-next/status") {
|
|
68
|
+
return await handleStatus(req, res);
|
|
69
|
+
}
|
|
70
|
+
// Not found
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
export function registerFridayNextHttpRoutes(api) {
|
|
74
|
+
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
75
|
+
sseEmitter.setBacklogLimit(cfg.sseBacklogPerDevice);
|
|
76
|
+
if (!cfg.authToken) {
|
|
77
|
+
api.logger.warn("friday-next authToken not configured; all requests will 401");
|
|
78
|
+
}
|
|
79
|
+
// Plugin handles its own auth via extractBearerToken()
|
|
80
|
+
api.registerHttpRoute({
|
|
81
|
+
path: "/friday-next",
|
|
82
|
+
handler: handleFridayNextRoute,
|
|
83
|
+
auth: "plugin",
|
|
84
|
+
match: "prefix",
|
|
85
|
+
});
|
|
86
|
+
api.logger.info("Friday Next channel HTTP routes registered at /friday-next/*");
|
|
87
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FridayNextLogLevel } from "./config.js";
|
|
2
|
+
export declare function createFridayNextLogger(scope: string, level?: FridayNextLogLevel): {
|
|
3
|
+
debug: (message: string) => void;
|
|
4
|
+
info: (message: string) => void;
|
|
5
|
+
warn: (message: string) => void;
|
|
6
|
+
error: (message: string) => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const levelOrder = {
|
|
2
|
+
debug: 10,
|
|
3
|
+
info: 20,
|
|
4
|
+
warn: 30,
|
|
5
|
+
error: 40,
|
|
6
|
+
};
|
|
7
|
+
export function createFridayNextLogger(scope, level = "info") {
|
|
8
|
+
const base = `[friday-next:${scope}]`;
|
|
9
|
+
const enabled = (current) => levelOrder[current] >= levelOrder[level];
|
|
10
|
+
return {
|
|
11
|
+
debug: (message) => {
|
|
12
|
+
if (enabled("debug"))
|
|
13
|
+
console.debug(`${base} ${message}`);
|
|
14
|
+
},
|
|
15
|
+
info: (message) => {
|
|
16
|
+
if (enabled("info"))
|
|
17
|
+
console.info(`${base} ${message}`);
|
|
18
|
+
},
|
|
19
|
+
warn: (message) => {
|
|
20
|
+
if (enabled("warn"))
|
|
21
|
+
console.warn(`${base} ${message}`);
|
|
22
|
+
},
|
|
23
|
+
error: (message) => {
|
|
24
|
+
if (enabled("error"))
|
|
25
|
+
console.error(`${base} ${message}`);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type DeliverNotificationOpts = {
|
|
2
|
+
deviceId: string;
|
|
3
|
+
token: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
sessionKey: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class FridayPushService {
|
|
9
|
+
private provider;
|
|
10
|
+
private apn;
|
|
11
|
+
private getProvider;
|
|
12
|
+
sendDeliverNotification(opts: DeliverNotificationOpts): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare const fridayPush: FridayPushService;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { resolveFridayNextConfig } from "../config.js";
|
|
2
|
+
import { getHostOpenClawConfigSnapshot } from "../host-config.js";
|
|
3
|
+
import { getFridayNextRuntime } from "../runtime.js";
|
|
4
|
+
export class FridayPushService {
|
|
5
|
+
provider = null;
|
|
6
|
+
apn = null;
|
|
7
|
+
async getProvider() {
|
|
8
|
+
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
9
|
+
const apnsCfg = cfg.push.apns;
|
|
10
|
+
if (!apnsCfg)
|
|
11
|
+
return null;
|
|
12
|
+
if (!this.apn) {
|
|
13
|
+
try {
|
|
14
|
+
this.apn = await import("apn");
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const { keyPath, keyId, teamId, bundleId, production } = apnsCfg;
|
|
21
|
+
if (!keyPath || !keyId || !teamId)
|
|
22
|
+
return null;
|
|
23
|
+
if (!this.provider) {
|
|
24
|
+
this.provider = new this.apn.Provider({
|
|
25
|
+
token: {
|
|
26
|
+
key: keyPath,
|
|
27
|
+
keyId,
|
|
28
|
+
teamId,
|
|
29
|
+
},
|
|
30
|
+
production,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return { provider: this.provider, apn: this.apn, bundleId };
|
|
34
|
+
}
|
|
35
|
+
async sendDeliverNotification(opts) {
|
|
36
|
+
const ctx = await this.getProvider();
|
|
37
|
+
if (!ctx)
|
|
38
|
+
return;
|
|
39
|
+
const { provider, apn, bundleId } = ctx;
|
|
40
|
+
const note = new apn.Notification();
|
|
41
|
+
note.alert = {
|
|
42
|
+
title: "Friday",
|
|
43
|
+
body: opts.summary || "New reply ready",
|
|
44
|
+
};
|
|
45
|
+
note.sound = "default";
|
|
46
|
+
note.mutableContent = 1;
|
|
47
|
+
note.topic = bundleId;
|
|
48
|
+
note.payload = {
|
|
49
|
+
sessionKey: opts.sessionKey,
|
|
50
|
+
runId: opts.runId,
|
|
51
|
+
deviceId: opts.deviceId,
|
|
52
|
+
};
|
|
53
|
+
await provider.send(note, opts.token);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const fridayPush = new FridayPushService();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function tokensPath() {
|
|
5
|
+
return path.join(os.homedir(), ".openclaw", "friday-next", "device-tokens.json");
|
|
6
|
+
}
|
|
7
|
+
function loadTokens() {
|
|
8
|
+
const file = tokensPath();
|
|
9
|
+
if (!fs.existsSync(file))
|
|
10
|
+
return {};
|
|
11
|
+
try {
|
|
12
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function saveTokens(tokens) {
|
|
20
|
+
const file = tokensPath();
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, JSON.stringify(tokens, null, 2), "utf8");
|
|
23
|
+
}
|
|
24
|
+
export function saveToken(deviceId, token) {
|
|
25
|
+
const key = deviceId.trim().toUpperCase();
|
|
26
|
+
const tokens = loadTokens();
|
|
27
|
+
tokens[key] = { token, updatedAt: Date.now() };
|
|
28
|
+
saveTokens(tokens);
|
|
29
|
+
}
|
|
30
|
+
export function getToken(deviceId) {
|
|
31
|
+
const key = deviceId.trim().toUpperCase();
|
|
32
|
+
return loadTokens()[key]?.token ?? null;
|
|
33
|
+
}
|
|
34
|
+
export function removeToken(deviceId) {
|
|
35
|
+
const key = deviceId.trim().toUpperCase();
|
|
36
|
+
const tokens = loadTokens();
|
|
37
|
+
delete tokens[key];
|
|
38
|
+
saveTokens(tokens);
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type RunRoute = {
|
|
2
|
+
runId: string;
|
|
3
|
+
deviceId: string;
|
|
4
|
+
sessionKey: string;
|
|
5
|
+
};
|
|
6
|
+
export type RunMetadata = {
|
|
7
|
+
modelName?: string;
|
|
8
|
+
totalTokens?: number;
|
|
9
|
+
/** Tokens counted toward the model context window (prompt-side: input + cache read + cache write when present). */
|
|
10
|
+
contextTokensUsed?: number;
|
|
11
|
+
/** Resolved model context window limit when the runtime exposes it. */
|
|
12
|
+
contextWindowMax?: number;
|
|
13
|
+
};
|
|
14
|
+
/** Vitest / harness: clears per-run metadata and final-delivered flags (not routes). */
|
|
15
|
+
export declare function resetRunMetadataForTest(): void;
|
|
16
|
+
export declare function registerRunRoute(route: RunRoute): void;
|
|
17
|
+
export declare function getRunRoute(runId: string): RunRoute | undefined;
|
|
18
|
+
export declare function setRunMetadata(runId: string, metadata: RunMetadata): void;
|
|
19
|
+
export declare function getRunMetadata(runId: string): RunMetadata | undefined;
|
|
20
|
+
export declare function markRunFinalDelivered(runId: string): void;
|
|
21
|
+
export declare function hasRunFinalDelivered(runId: string): boolean;
|
|
22
|
+
/** Best-effort prompt-side context footprint from a provider usage object. */
|
|
23
|
+
export declare function contextTokensFromUsageRecord(u: Record<string, unknown>): number | undefined;
|
|
24
|
+
export declare function ingestAgentEventMetadata(runId: string, data: Record<string, unknown>): void;
|
|
25
|
+
export {};
|