@syengup/friday-channel-next 0.0.37 → 0.0.38

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.
Files changed (92) hide show
  1. package/dist/index.js +10 -4
  2. package/index.ts +10 -4
  3. package/package.json +1 -1
  4. package/dist/attachments/0768c9b1-53b0-44df-83e8-be15c4ea188f.jpg +0 -0
  5. package/dist/attachments/0a379d01-116b-4da1-bf15-77cb2cbb0093.jpg +0 -0
  6. package/dist/attachments/181caab2-64a7-4004-a057-225a144f949e.mp3 +0 -0
  7. package/dist/attachments/19662331-e527-47d2-bc0e-0e19a7a91419.jpg +0 -0
  8. package/dist/attachments/26a23b2b-52df-4572-a5e1-15b34fb87e44.jpg +0 -0
  9. package/dist/attachments/2f9282c5-8db4-4c4a-a060-e65104f6f9ff.jpg +0 -0
  10. package/dist/attachments/3929ec3d-ea15-4de6-96bc-97e8b0b658a7.jpg +0 -0
  11. package/dist/attachments/403c0cbc-4e3c-4146-a3be-ff3746ee7cda.jpg +0 -0
  12. package/dist/attachments/441977f5-0f7b-4aa2-841a-1d63e787ea53.jpg +0 -0
  13. package/dist/attachments/453e8aa2-76e3-498d-8d6f-d7b96d6bf45b.jpg +0 -0
  14. package/dist/attachments/538cde71-d26e-4d3d-b901-e8dd905e668c.mp3 +0 -0
  15. package/dist/attachments/55c7f628-4ba2-4252-aa4b-4f3eb6045a8a.mp3 +0 -0
  16. package/dist/attachments/5f7683f5-8194-4698-b077-31d209525379.jpg +0 -0
  17. package/dist/attachments/60614a35-8f44-4197-b783-2f58f5a72ac8.jpeg +0 -0
  18. package/dist/attachments/62830489-8814-48b1-851c-3845e514f35e.mp3 +0 -0
  19. package/dist/attachments/66f4a62d-1531-4f38-a531-7456f9edf221.png +0 -0
  20. package/dist/attachments/6735d749-769e-483a-9b84-43b9338a720b.png +0 -0
  21. package/dist/attachments/6d1766b1-05e4-4b04-b3c8-1c25e9d182a1.png +0 -0
  22. package/dist/attachments/782b077b-06e3-484b-baf5-33e7160234ed.png +0 -0
  23. package/dist/attachments/7ad638b2-1f56-4d93-9ad8-b40346e0650f.jpg +0 -0
  24. package/dist/attachments/89f6fb15-e652-4111-a60c-baa414659052.png +0 -0
  25. package/dist/attachments/8a88b14f-442f-45fb-b01d-e51bab8f800d.mp3 +0 -0
  26. package/dist/attachments/92292034-9cf6-4f26-8d77-fddca3deb638.png +0 -0
  27. package/dist/attachments/92c2b414-d33d-4d93-bcb6-013da7bec9a4.jpg +0 -0
  28. package/dist/attachments/9664f69e-3c05-45ca-9a52-f2d0b9f9bf7e.jpg +0 -0
  29. package/dist/attachments/977d28c1-43c0-40e0-95e3-defe0f41afe8.jpg +0 -0
  30. package/dist/attachments/9df40f1a-c6e1-4177-8a03-06757a30b19e.png +0 -0
  31. package/dist/attachments/a68e6815-6163-4421-a70f-34493aa9a217.jpg +0 -0
  32. package/dist/attachments/aab32fea-6d99-47ec-ab1f-2340f31312eb.jpg +0 -0
  33. package/dist/attachments/ab403224-2fb1-49c1-8738-ea194ab65d44.png +0 -0
  34. package/dist/attachments/ac3da190-d6ee-4038-a673-8b893035a687.png +0 -0
  35. package/dist/attachments/af02be9c-87f7-4c5a-9969-7db32039bb58.png +0 -0
  36. package/dist/attachments/b011d42a-00e5-4f77-86bc-08da6112e6e1.mp3 +0 -0
  37. package/dist/attachments/b7d7df40-c627-4b1f-9b09-167b88545c25.mp3 +0 -0
  38. package/dist/attachments/c5e9bf09-a718-422c-bcb3-94c173e3755b.mp3 +0 -0
  39. package/dist/attachments/d5449e13-1995-44ba-9392-ecbfe5f9876f.jpg +0 -0
  40. package/dist/attachments/ea0069f5-01cf-4ea1-985e-3a1e426399c3.png +0 -0
  41. package/dist/attachments/f3989ff2-7b70-4a80-a896-74a6b197f7d8.png +0 -0
  42. package/dist/attachments/f64a4a14-e3aa-4eed-a8d9-1603f04baa5b.jpg +0 -0
  43. package/dist/src/http/handlers/device-token.d.ts +0 -2
  44. package/dist/src/http/handlers/device-token.js +0 -43
  45. package/dist/src/http/handlers/import.d.ts +0 -7
  46. package/dist/src/http/handlers/import.js +0 -69
  47. package/dist/src/http/handlers/info.d.ts +0 -2
  48. package/dist/src/http/handlers/info.js +0 -13
  49. package/dist/src/http/handlers/messages-list.d.ts +0 -7
  50. package/dist/src/http/handlers/messages-list.js +0 -44
  51. package/dist/src/http/handlers/pair.d.ts +0 -2
  52. package/dist/src/http/handlers/pair.js +0 -39
  53. package/dist/src/http/handlers/sessions-list.d.ts +0 -8
  54. package/dist/src/http/handlers/sessions-list.js +0 -24
  55. package/dist/src/http/handlers/sessions-messages-get.d.ts +0 -2
  56. package/dist/src/http/handlers/sessions-messages-get.js +0 -55
  57. package/dist/src/http/handlers/sessions-messages-post.d.ts +0 -2
  58. package/dist/src/http/handlers/sessions-messages-post.js +0 -92
  59. package/dist/src/http/handlers/sessions-messages.d.ts +0 -2
  60. package/dist/src/http/handlers/sessions-messages.js +0 -135
  61. package/dist/src/http/handlers/sync.d.ts +0 -7
  62. package/dist/src/http/handlers/sync.js +0 -56
  63. package/dist/src/push/apns.d.ts +0 -15
  64. package/dist/src/push/apns.js +0 -56
  65. package/dist/src/push/device-tokens.d.ts +0 -3
  66. package/dist/src/push/device-tokens.js +0 -39
  67. package/dist/src/sync/account-identity.d.ts +0 -14
  68. package/dist/src/sync/account-identity.js +0 -101
  69. package/dist/src/sync/archive.d.ts +0 -9
  70. package/dist/src/sync/archive.js +0 -25
  71. package/dist/src/sync/database.d.ts +0 -66
  72. package/dist/src/sync/database.js +0 -364
  73. package/dist/src/sync/init.d.ts +0 -3
  74. package/dist/src/sync/init.js +0 -14
  75. package/dist/src/sync/installation-id.d.ts +0 -1
  76. package/dist/src/sync/installation-id.js +0 -41
  77. package/dist/src/sync/message-accumulator.d.ts +0 -29
  78. package/dist/src/sync/message-accumulator.js +0 -188
  79. package/dist/src/sync/message-store.d.ts +0 -68
  80. package/dist/src/sync/message-store.js +0 -262
  81. package/dist/src/sync/push-store.d.ts +0 -5
  82. package/dist/src/sync/push-store.js +0 -54
  83. package/dist/src/sync/session-key.d.ts +0 -12
  84. package/dist/src/sync/session-key.js +0 -47
  85. package/dist/src/sync/sync-state.d.ts +0 -5
  86. package/dist/src/sync/sync-state.js +0 -54
  87. package/dist/src/sync/transcript-archive.d.ts +0 -13
  88. package/dist/src/sync/transcript-archive.js +0 -37
  89. package/dist/src/sync/transcript-store.d.ts +0 -35
  90. package/dist/src/sync/transcript-store.js +0 -221
  91. package/dist/src/sync/translate.d.ts +0 -42
  92. package/dist/src/sync/translate.js +0 -171
package/dist/index.js CHANGED
@@ -14,8 +14,13 @@ export { setFridayNextRuntime } from "./src/runtime.js";
14
14
  /** `api.on` returns void — register tool hooks at most once per process. */
15
15
  let fridayNextToolHooksRegistered = false;
16
16
  let disposeAgentEventListener = null;
17
- /** Avoid duplicate `registerHttpRoute` when gateway re-invokes `registerFull`. */
18
- let fridayNextPluginHttpRegistered = false;
17
+ /**
18
+ * Track the last `api` instance on which HTTP routes were registered.
19
+ * When the health-monitor restarts the plugin, `registerFull` receives a fresh `api` whose
20
+ * old routes are gone — we must re-register. A WeakRef lets us distinguish "same api,
21
+ * re-entered" (skip) from "new api after restart" (re-register).
22
+ */
23
+ let lastApiRoutesRegistered = null;
19
24
  function deviceIdFromToolContext(ctx) {
20
25
  if (ctx.runId) {
21
26
  const d = sseEmitter.getDeviceIdByRunId(ctx.runId);
@@ -70,8 +75,9 @@ export default defineChannelPluginEntry({
70
75
  setRuntime: setFridayNextRuntime,
71
76
  registerFull: (api) => {
72
77
  setFridayAgentForwardRuntime(api);
73
- if (!fridayNextPluginHttpRegistered) {
74
- fridayNextPluginHttpRegistered = true;
78
+ const sameApi = lastApiRoutesRegistered?.deref() === api;
79
+ if (!sameApi) {
80
+ lastApiRoutesRegistered = new WeakRef(api);
75
81
  registerFridayNextHttpRoutes(api);
76
82
  }
77
83
  else {
package/index.ts CHANGED
@@ -23,8 +23,13 @@ export { setFridayNextRuntime } from "./src/runtime.js";
23
23
  /** `api.on` returns void — register tool hooks at most once per process. */
24
24
  let fridayNextToolHooksRegistered = false;
25
25
  let disposeAgentEventListener: (() => void) | null = null;
26
- /** Avoid duplicate `registerHttpRoute` when gateway re-invokes `registerFull`. */
27
- let fridayNextPluginHttpRegistered = false;
26
+ /**
27
+ * Track the last `api` instance on which HTTP routes were registered.
28
+ * When the health-monitor restarts the plugin, `registerFull` receives a fresh `api` whose
29
+ * old routes are gone — we must re-register. A WeakRef lets us distinguish "same api,
30
+ * re-entered" (skip) from "new api after restart" (re-register).
31
+ */
32
+ let lastApiRoutesRegistered: WeakRef<OpenClawPluginApi> | null = null;
28
33
 
29
34
  function deviceIdFromToolContext(ctx: PluginHookToolContext): string | null {
30
35
  if (ctx.runId) {
@@ -77,8 +82,9 @@ export default defineChannelPluginEntry({
77
82
  setRuntime: setFridayNextRuntime,
78
83
  registerFull: (api: OpenClawPluginApi) => {
79
84
  setFridayAgentForwardRuntime(api);
80
- if (!fridayNextPluginHttpRegistered) {
81
- fridayNextPluginHttpRegistered = true;
85
+ const sameApi = lastApiRoutesRegistered?.deref() === api;
86
+ if (!sameApi) {
87
+ lastApiRoutesRegistered = new WeakRef(api);
82
88
  registerFridayNextHttpRoutes(api);
83
89
  } else {
84
90
  const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,2 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- export declare function handleDeviceToken(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,43 +0,0 @@
1
- import { extractBearerToken } from "../middleware/auth.js";
2
- import { readJsonBody } from "../middleware/body.js";
3
- import { saveToken } from "../../push/device-tokens.js";
4
- export async function handleDeviceToken(req, res) {
5
- if (req.method !== "POST") {
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
- let body;
18
- try {
19
- body = await readJsonBody(req);
20
- }
21
- catch {
22
- body = null;
23
- }
24
- if (!body) {
25
- res.statusCode = 400;
26
- res.setHeader("Content-Type", "application/json");
27
- res.end(JSON.stringify({ error: "Invalid JSON body" }));
28
- return true;
29
- }
30
- const deviceId = typeof body.deviceId === "string" && body.deviceId.trim() ? body.deviceId.trim() : "";
31
- const token = typeof body.token === "string" && body.token.trim() ? body.token.trim() : "";
32
- if (!deviceId || !token) {
33
- res.statusCode = 400;
34
- res.setHeader("Content-Type", "application/json");
35
- res.end(JSON.stringify({ error: "deviceId and token are required" }));
36
- return true;
37
- }
38
- saveToken(deviceId, token);
39
- res.statusCode = 200;
40
- res.setHeader("Content-Type", "application/json");
41
- res.end(JSON.stringify({ ok: true }));
42
- return true;
43
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * POST /friday-next/sessions/{sessionKey}/import
3
- *
4
- * First-time bulk import. Rejects if session already has data (409 Conflict).
5
- */
6
- import type { IncomingMessage, ServerResponse } from "node:http";
7
- export declare function handleImport(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,69 +0,0 @@
1
- import { extractBearerToken } from "../middleware/auth.js";
2
- import { readJsonBody } from "../middleware/body.js";
3
- import { importMessages, ConflictError } from "../../sync/message-store.js";
4
- import { toSessionStoreKey } from "../../session/session-manager.js";
5
- export async function handleImport(req, res) {
6
- if (req.method !== "POST") {
7
- res.statusCode = 405;
8
- res.setHeader("Content-Type", "application/json");
9
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
10
- return true;
11
- }
12
- const token = extractBearerToken(req);
13
- if (!token) {
14
- res.statusCode = 401;
15
- res.setHeader("Content-Type", "application/json");
16
- res.end(JSON.stringify({ error: "Unauthorized" }));
17
- return true;
18
- }
19
- const url = new URL(req.url ?? "/", "http://localhost");
20
- const pathname = url.pathname;
21
- const prefix = "/friday-next/sessions/";
22
- const suffix = "/import";
23
- if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix)) {
24
- res.statusCode = 400;
25
- res.setHeader("Content-Type", "application/json");
26
- res.end(JSON.stringify({ error: "Missing sessionKey in URL path" }));
27
- return true;
28
- }
29
- const rawKey = pathname.slice(prefix.length, pathname.length - suffix.length);
30
- const sessionKey = toSessionStoreKey(decodeURIComponent(rawKey));
31
- const body = await readJsonBody(req, 50 * 1024 * 1024); // 50 MB for bulk import
32
- if (!body) {
33
- res.statusCode = 400;
34
- res.setHeader("Content-Type", "application/json");
35
- res.end(JSON.stringify({ error: "Invalid JSON body" }));
36
- return true;
37
- }
38
- const deviceId = typeof body.deviceId === "string" ? body.deviceId.trim() : "";
39
- if (!deviceId) {
40
- res.statusCode = 400;
41
- res.setHeader("Content-Type", "application/json");
42
- res.end(JSON.stringify({ error: "Missing required field: deviceId" }));
43
- return true;
44
- }
45
- const messages = Array.isArray(body.messages) ? body.messages : [];
46
- if (messages.length === 0) {
47
- res.statusCode = 400;
48
- res.setHeader("Content-Type", "application/json");
49
- res.end(JSON.stringify({ error: "Missing required field: messages (non-empty array)" }));
50
- return true;
51
- }
52
- try {
53
- const result = importMessages({ sessionKey, deviceId, messages });
54
- res.statusCode = 200;
55
- res.setHeader("Content-Type", "application/json");
56
- res.end(JSON.stringify(result));
57
- }
58
- catch (err) {
59
- if (err instanceof ConflictError) {
60
- res.statusCode = 409;
61
- res.setHeader("Content-Type", "application/json");
62
- res.end(JSON.stringify({ error: err.message }));
63
- }
64
- else {
65
- throw err;
66
- }
67
- }
68
- return true;
69
- }
@@ -1,2 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- export declare function handleInfo(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,13 +0,0 @@
1
- import { getInstallationId } from "../../sync/installation-id.js";
2
- export async function handleInfo(req, res) {
3
- if (req.method !== "GET") {
4
- res.statusCode = 405;
5
- res.setHeader("Content-Type", "application/json");
6
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
7
- return true;
8
- }
9
- res.statusCode = 200;
10
- res.setHeader("Content-Type", "application/json");
11
- res.end(JSON.stringify({ ok: true, channel: "friday-next", version: "v2", installationId: getInstallationId() }));
12
- return true;
13
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * GET /friday-next/sessions/{sessionKey}/messages?before=&limit=
3
- *
4
- * Paginated pull of historical messages for a session.
5
- */
6
- import type { IncomingMessage, ServerResponse } from "node:http";
7
- export declare function handleMessagesList(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,44 +0,0 @@
1
- import { extractBearerToken } from "../middleware/auth.js";
2
- import { getMessagesBefore } from "../../sync/message-store.js";
3
- import { toSessionStoreKey } from "../../session/session-manager.js";
4
- export async function handleMessagesList(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
- const token = extractBearerToken(req);
12
- if (!token) {
13
- res.statusCode = 401;
14
- res.setHeader("Content-Type", "application/json");
15
- res.end(JSON.stringify({ error: "Unauthorized" }));
16
- return true;
17
- }
18
- const url = new URL(req.url ?? "/", "http://localhost");
19
- const pathname = url.pathname;
20
- const prefix = "/friday-next/sessions/";
21
- const suffix = "/messages";
22
- if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix)) {
23
- res.statusCode = 400;
24
- res.setHeader("Content-Type", "application/json");
25
- res.end(JSON.stringify({ error: "Missing sessionKey in URL path" }));
26
- return true;
27
- }
28
- const rawKey = pathname.slice(prefix.length, pathname.length - suffix.length);
29
- const sessionKey = toSessionStoreKey(decodeURIComponent(rawKey));
30
- const before = url.searchParams.get("before") ?? undefined;
31
- const limitRaw = url.searchParams.get("limit");
32
- const limit = limitRaw ? parseInt(limitRaw, 10) : 25;
33
- if (limitRaw && (isNaN(limit) || limit < 1 || limit > 50)) {
34
- res.statusCode = 400;
35
- res.setHeader("Content-Type", "application/json");
36
- res.end(JSON.stringify({ error: "Invalid limit (1-50)" }));
37
- return true;
38
- }
39
- const result = getMessagesBefore(sessionKey, before, limit);
40
- res.statusCode = 200;
41
- res.setHeader("Content-Type", "application/json");
42
- res.end(JSON.stringify(result));
43
- return true;
44
- }
@@ -1,2 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- export declare function handlePair(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,39 +0,0 @@
1
- import { getOrCreateAccountId } from "../../sync/account-identity.js";
2
- import { getInstallationId } from "../../sync/installation-id.js";
3
- import { extractBearerToken } from "../middleware/auth.js";
4
- import { readJsonBody } from "../middleware/body.js";
5
- export async function handlePair(req, res) {
6
- if (req.method !== "POST") {
7
- res.statusCode = 405;
8
- res.setHeader("Content-Type", "application/json");
9
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
10
- return true;
11
- }
12
- if (!extractBearerToken(req)) {
13
- res.statusCode = 401;
14
- res.setHeader("Content-Type", "application/json");
15
- res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
16
- return true;
17
- }
18
- const body = await readJsonBody(req);
19
- const deviceId = typeof body?.deviceId === "string" ? body.deviceId.trim() : "";
20
- if (!deviceId) {
21
- res.statusCode = 400;
22
- res.setHeader("Content-Type", "application/json");
23
- res.end(JSON.stringify({ error: "Missing required field: deviceId" }));
24
- return true;
25
- }
26
- const claimAccountId = typeof body?.claimAccountId === "string" ? body.claimAccountId.trim() : undefined;
27
- const claim = body?.claim && typeof body.claim === "object" && !Array.isArray(body.claim)
28
- ? body.claim
29
- : {};
30
- const result = getOrCreateAccountId({
31
- deviceId,
32
- claimAccountId,
33
- claimConfirmed: claim.confirmed === true,
34
- });
35
- res.statusCode = 200;
36
- res.setHeader("Content-Type", "application/json");
37
- res.end(JSON.stringify({ ok: true, installationId: getInstallationId(), ...result }));
38
- return true;
39
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * GET /friday-next/sessions?deviceId=...
3
- *
4
- * List sessions. If deviceId is provided, filters by device.
5
- * If deviceId is omitted, returns all sessions (for app reinstall recovery when deviceId changed).
6
- */
7
- import type { IncomingMessage, ServerResponse } from "node:http";
8
- export declare function handleSessionsList(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,24 +0,0 @@
1
- import { extractBearerToken } from "../middleware/auth.js";
2
- import { listSessions, listAllSessionsList } from "../../sync/message-store.js";
3
- export async function handleSessionsList(req, res) {
4
- if (req.method !== "GET") {
5
- res.statusCode = 405;
6
- res.setHeader("Content-Type", "application/json");
7
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
8
- return true;
9
- }
10
- const token = extractBearerToken(req);
11
- if (!token) {
12
- res.statusCode = 401;
13
- res.setHeader("Content-Type", "application/json");
14
- res.end(JSON.stringify({ error: "Unauthorized" }));
15
- return true;
16
- }
17
- const url = new URL(req.url ?? "/", "http://localhost");
18
- const deviceId = url.searchParams.get("deviceId")?.trim();
19
- const sessions = deviceId ? listSessions(deviceId) : listAllSessionsList();
20
- res.statusCode = 200;
21
- res.setHeader("Content-Type", "application/json");
22
- res.end(JSON.stringify({ sessions }));
23
- return true;
24
- }
@@ -1,2 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- export declare function handleSessionMessagesGet(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,55 +0,0 @@
1
- import { resolveAccountIdFromRequest } from "../../sync/account-identity.js";
2
- import { getTranscriptStore } from "../../sync/transcript-store.js";
3
- import { extractBearerToken } from "../middleware/auth.js";
4
- function sessionKeyFromPath(pathname) {
5
- const prefix = "/friday-next/sessions/";
6
- const suffix = "/messages";
7
- if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix))
8
- return "";
9
- return decodeURIComponent(pathname.slice(prefix.length, -suffix.length));
10
- }
11
- export async function handleSessionMessagesGet(req, res) {
12
- if (req.method !== "GET") {
13
- res.statusCode = 405;
14
- res.setHeader("Content-Type", "application/json");
15
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
16
- return true;
17
- }
18
- if (!extractBearerToken(req)) {
19
- res.statusCode = 401;
20
- res.setHeader("Content-Type", "application/json");
21
- res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
22
- return true;
23
- }
24
- const accountId = resolveAccountIdFromRequest(req);
25
- if (!accountId) {
26
- res.statusCode = 403;
27
- res.setHeader("Content-Type", "application/json");
28
- res.end(JSON.stringify({ error: "Unknown or missing X-Account-Id" }));
29
- return true;
30
- }
31
- const url = new URL(req.url ?? "/", "http://localhost");
32
- const sessionKey = sessionKeyFromPath(url.pathname);
33
- if (!sessionKey) {
34
- res.statusCode = 400;
35
- res.setHeader("Content-Type", "application/json");
36
- res.end(JSON.stringify({ error: "Missing session key in path" }));
37
- return true;
38
- }
39
- if (!sessionKey.includes(`friday:account:${accountId}:`) && !sessionKey.includes(`friday%3Aaccount%3A${accountId}%3A`)) {
40
- const legacyAllowed = sessionKey.startsWith("friday:direct:") || sessionKey.startsWith("agent:main:friday:direct:");
41
- if (!legacyAllowed) {
42
- res.statusCode = 403;
43
- res.setHeader("Content-Type", "application/json");
44
- res.end(JSON.stringify({ error: "Session does not belong to account" }));
45
- return true;
46
- }
47
- }
48
- const since = Number.parseInt(url.searchParams.get("since") ?? "0", 10);
49
- const limit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
50
- const events = getTranscriptStore().readSince(accountId, sessionKey, Number.isFinite(since) ? since : 0, limit);
51
- res.statusCode = 200;
52
- res.setHeader("Content-Type", "application/json");
53
- res.end(JSON.stringify({ ok: true, events, nextSince: events.at(-1)?.seq ?? since }));
54
- return true;
55
- }
@@ -1,2 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- export declare function handleSessionMessagesPost(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,92 +0,0 @@
1
- import { resolveAccountIdFromRequest } from "../../sync/account-identity.js";
2
- import { getTranscriptStore } from "../../sync/transcript-store.js";
3
- import { extractBearerToken } from "../middleware/auth.js";
4
- import { readJsonBody } from "../middleware/body.js";
5
- const allowedKinds = new Set([
6
- "user_message",
7
- "assistant_message",
8
- "thought_event",
9
- "tool_event",
10
- "outbound_text",
11
- "outbound_media",
12
- "sync_upsert",
13
- ]);
14
- function sessionKeyFromPath(pathname) {
15
- const prefix = "/friday-next/sessions/";
16
- const suffix = "/messages";
17
- if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix))
18
- return "";
19
- return decodeURIComponent(pathname.slice(prefix.length, -suffix.length));
20
- }
21
- function parseEvent(raw, accountId) {
22
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
23
- return null;
24
- const obj = raw;
25
- const kind = typeof obj.kind === "string" && allowedKinds.has(obj.kind)
26
- ? obj.kind
27
- : null;
28
- if (!kind)
29
- return null;
30
- const payload = obj.payload && typeof obj.payload === "object" && !Array.isArray(obj.payload)
31
- ? obj.payload
32
- : null;
33
- if (!payload)
34
- return null;
35
- return {
36
- eventId: typeof obj.eventId === "string" ? obj.eventId.trim() : undefined,
37
- clientId: typeof obj.clientId === "string" && obj.clientId.trim() ? obj.clientId.trim() : accountId,
38
- kind,
39
- payload,
40
- ts: typeof obj.ts === "number" ? obj.ts : undefined,
41
- lamportTs: typeof obj.lamportTs === "number" ? obj.lamportTs : undefined,
42
- };
43
- }
44
- export async function handleSessionMessagesPost(req, res) {
45
- if (req.method !== "POST") {
46
- res.statusCode = 405;
47
- res.setHeader("Content-Type", "application/json");
48
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
49
- return true;
50
- }
51
- if (!extractBearerToken(req)) {
52
- res.statusCode = 401;
53
- res.setHeader("Content-Type", "application/json");
54
- res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
55
- return true;
56
- }
57
- const accountId = resolveAccountIdFromRequest(req);
58
- if (!accountId) {
59
- res.statusCode = 403;
60
- res.setHeader("Content-Type", "application/json");
61
- res.end(JSON.stringify({ error: "Unknown or missing X-Account-Id" }));
62
- return true;
63
- }
64
- const url = new URL(req.url ?? "/", "http://localhost");
65
- const sessionKey = sessionKeyFromPath(url.pathname);
66
- if (!sessionKey) {
67
- res.statusCode = 400;
68
- res.setHeader("Content-Type", "application/json");
69
- res.end(JSON.stringify({ error: "Missing session key in path" }));
70
- return true;
71
- }
72
- if (!sessionKey.includes(`friday:account:${accountId}:`) && !sessionKey.startsWith("friday:direct:")) {
73
- res.statusCode = 403;
74
- res.setHeader("Content-Type", "application/json");
75
- res.end(JSON.stringify({ error: "Session does not belong to account" }));
76
- return true;
77
- }
78
- const body = await readJsonBody(req, 8 * 1024 * 1024);
79
- const rawEvents = Array.isArray(body?.events) ? body.events : [];
80
- const events = rawEvents.map((e) => parseEvent(e, accountId)).filter((e) => e !== null);
81
- if (events.length !== rawEvents.length) {
82
- res.statusCode = 400;
83
- res.setHeader("Content-Type", "application/json");
84
- res.end(JSON.stringify({ error: "Invalid transcript event batch" }));
85
- return true;
86
- }
87
- const appended = getTranscriptStore().appendEvents(accountId, sessionKey, events);
88
- res.statusCode = 200;
89
- res.setHeader("Content-Type", "application/json");
90
- res.end(JSON.stringify({ ok: true, appended: appended.length, events: appended }));
91
- return true;
92
- }
@@ -1,2 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- export declare function handleSessionsMessages(req: IncomingMessage, res: ServerResponse): Promise<boolean>;