@femtomc/mu-server 26.2.69 → 26.2.71
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/README.md +7 -3
- package/dist/api/activities.d.ts +2 -0
- package/dist/api/activities.js +160 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +45 -0
- package/dist/api/control_plane.d.ts +2 -0
- package/dist/api/control_plane.js +28 -0
- package/dist/api/cron.d.ts +2 -0
- package/dist/api/cron.js +182 -0
- package/dist/api/events.js +77 -19
- package/dist/api/forum.js +52 -18
- package/dist/api/heartbeats.d.ts +2 -0
- package/dist/api/heartbeats.js +211 -0
- package/dist/api/identities.d.ts +2 -0
- package/dist/api/identities.js +103 -0
- package/dist/api/issues.js +120 -33
- package/dist/api/runs.d.ts +2 -0
- package/dist/api/runs.js +207 -0
- package/dist/cli.js +58 -3
- package/dist/config.d.ts +4 -21
- package/dist/config.js +24 -75
- package/dist/control_plane.d.ts +7 -114
- package/dist/control_plane.js +238 -654
- package/dist/control_plane_bootstrap_helpers.d.ts +16 -0
- package/dist/control_plane_bootstrap_helpers.js +85 -0
- package/dist/control_plane_contract.d.ts +176 -0
- package/dist/control_plane_contract.js +1 -0
- package/dist/control_plane_reload.d.ts +63 -0
- package/dist/control_plane_reload.js +525 -0
- package/dist/control_plane_run_outbox.d.ts +7 -0
- package/dist/control_plane_run_outbox.js +52 -0
- package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
- package/dist/control_plane_run_queue_coordinator.js +327 -0
- package/dist/control_plane_telegram_generation.d.ts +27 -0
- package/dist/control_plane_telegram_generation.js +520 -0
- package/dist/control_plane_wake_delivery.d.ts +50 -0
- package/dist/control_plane_wake_delivery.js +123 -0
- package/dist/cron_request.d.ts +8 -0
- package/dist/cron_request.js +65 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -1
- package/dist/run_queue.d.ts +95 -0
- package/dist/run_queue.js +817 -0
- package/dist/run_supervisor.d.ts +20 -0
- package/dist/run_supervisor.js +25 -1
- package/dist/server.d.ts +12 -49
- package/dist/server.js +365 -2128
- package/dist/server_program_orchestration.d.ts +38 -0
- package/dist/server_program_orchestration.js +254 -0
- package/dist/server_routing.d.ts +31 -0
- package/dist/server_routing.js +230 -0
- package/dist/server_runtime.d.ts +30 -0
- package/dist/server_runtime.js +43 -0
- package/dist/server_types.d.ts +3 -0
- package/dist/server_types.js +16 -0
- package/dist/session_lifecycle.d.ts +11 -0
- package/dist/session_lifecycle.js +149 -0
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -103,7 +103,8 @@ Bun.serve(server);
|
|
|
103
103
|
### Issues
|
|
104
104
|
|
|
105
105
|
- `GET /api/issues` - List issues
|
|
106
|
-
- Query params: `?status=open&tag=bug`
|
|
106
|
+
- Query params: `?status=open&tag=bug&contains=crash&limit=50`
|
|
107
|
+
- `limit` defaults to `200` and is clamped to `<= 200`.
|
|
107
108
|
- `GET /api/issues/:id` - Get issue by ID
|
|
108
109
|
- `POST /api/issues` - Create new issue
|
|
109
110
|
```json
|
|
@@ -123,14 +124,17 @@ Bun.serve(server);
|
|
|
123
124
|
```
|
|
124
125
|
- `POST /api/issues/:id/claim` - Claim issue (changes status to in_progress)
|
|
125
126
|
- `GET /api/issues/ready` - Get ready issues
|
|
126
|
-
- Query
|
|
127
|
+
- Query params: `?root=issue-id&contains=worker&limit=20`
|
|
128
|
+
- `limit` defaults to `200` and is clamped to `<= 200`.
|
|
127
129
|
|
|
128
130
|
### Forum
|
|
129
131
|
|
|
130
132
|
- `GET /api/forum/topics` - List forum topics
|
|
131
|
-
- Query
|
|
133
|
+
- Query params: `?prefix=issue:&limit=20`
|
|
134
|
+
- `limit` defaults to `100` and is clamped to `<= 200`.
|
|
132
135
|
- `GET /api/forum/read` - Read messages from topic
|
|
133
136
|
- Query params: `?topic=issue:123&limit=50`
|
|
137
|
+
- `limit` defaults to `50` and is clamped to `<= 200`.
|
|
134
138
|
- `POST /api/forum/post` - Post message to topic
|
|
135
139
|
```json
|
|
136
140
|
{
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export async function activityRoutes(request, url, deps, headers) {
|
|
2
|
+
const path = url.pathname;
|
|
3
|
+
if (path === "/api/activities") {
|
|
4
|
+
if (request.method !== "GET") {
|
|
5
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
6
|
+
}
|
|
7
|
+
const statusRaw = url.searchParams.get("status")?.trim().toLowerCase();
|
|
8
|
+
const status = statusRaw === "running" || statusRaw === "completed" || statusRaw === "failed" || statusRaw === "cancelled"
|
|
9
|
+
? statusRaw
|
|
10
|
+
: undefined;
|
|
11
|
+
const kind = url.searchParams.get("kind")?.trim() || undefined;
|
|
12
|
+
const limitRaw = url.searchParams.get("limit");
|
|
13
|
+
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
14
|
+
const activities = deps.activitySupervisor.list({ status, kind, limit });
|
|
15
|
+
return Response.json({ count: activities.length, activities }, { headers });
|
|
16
|
+
}
|
|
17
|
+
if (path === "/api/activities/start") {
|
|
18
|
+
if (request.method !== "POST") {
|
|
19
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
20
|
+
}
|
|
21
|
+
let body;
|
|
22
|
+
try {
|
|
23
|
+
body = (await request.json());
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
27
|
+
}
|
|
28
|
+
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
29
|
+
if (!title) {
|
|
30
|
+
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
31
|
+
}
|
|
32
|
+
const kind = typeof body.kind === "string" ? body.kind.trim() : undefined;
|
|
33
|
+
const heartbeatEveryMs = typeof body.heartbeat_every_ms === "number" && Number.isFinite(body.heartbeat_every_ms)
|
|
34
|
+
? Math.max(0, Math.trunc(body.heartbeat_every_ms))
|
|
35
|
+
: undefined;
|
|
36
|
+
const source = body.source === "api" || body.source === "command" || body.source === "system" ? body.source : "api";
|
|
37
|
+
try {
|
|
38
|
+
const activity = deps.activitySupervisor.start({
|
|
39
|
+
title,
|
|
40
|
+
kind,
|
|
41
|
+
heartbeatEveryMs,
|
|
42
|
+
metadata: body.metadata ?? undefined,
|
|
43
|
+
source,
|
|
44
|
+
});
|
|
45
|
+
return Response.json({ ok: true, activity }, { status: 201, headers });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (path === "/api/activities/progress") {
|
|
52
|
+
if (request.method !== "POST") {
|
|
53
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
54
|
+
}
|
|
55
|
+
let body;
|
|
56
|
+
try {
|
|
57
|
+
body = (await request.json());
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
61
|
+
}
|
|
62
|
+
const result = deps.activitySupervisor.progress({
|
|
63
|
+
activityId: typeof body.activity_id === "string" ? body.activity_id : null,
|
|
64
|
+
message: typeof body.message === "string" ? body.message : null,
|
|
65
|
+
});
|
|
66
|
+
if (result.ok) {
|
|
67
|
+
return Response.json(result, { headers });
|
|
68
|
+
}
|
|
69
|
+
if (result.reason === "missing_target") {
|
|
70
|
+
return Response.json(result, { status: 400, headers });
|
|
71
|
+
}
|
|
72
|
+
if (result.reason === "not_running") {
|
|
73
|
+
return Response.json(result, { status: 409, headers });
|
|
74
|
+
}
|
|
75
|
+
return Response.json(result, { status: 404, headers });
|
|
76
|
+
}
|
|
77
|
+
if (path === "/api/activities/heartbeat") {
|
|
78
|
+
if (request.method !== "POST") {
|
|
79
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
80
|
+
}
|
|
81
|
+
let body;
|
|
82
|
+
try {
|
|
83
|
+
body = (await request.json());
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
87
|
+
}
|
|
88
|
+
const result = deps.activitySupervisor.heartbeat({
|
|
89
|
+
activityId: typeof body.activity_id === "string" ? body.activity_id : null,
|
|
90
|
+
reason: typeof body.reason === "string" ? body.reason : null,
|
|
91
|
+
});
|
|
92
|
+
if (result.ok) {
|
|
93
|
+
return Response.json(result, { headers });
|
|
94
|
+
}
|
|
95
|
+
if (result.reason === "missing_target") {
|
|
96
|
+
return Response.json(result, { status: 400, headers });
|
|
97
|
+
}
|
|
98
|
+
if (result.reason === "not_running") {
|
|
99
|
+
return Response.json(result, { status: 409, headers });
|
|
100
|
+
}
|
|
101
|
+
return Response.json(result, { status: 404, headers });
|
|
102
|
+
}
|
|
103
|
+
if (path === "/api/activities/complete" || path === "/api/activities/fail" || path === "/api/activities/cancel") {
|
|
104
|
+
if (request.method !== "POST") {
|
|
105
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
106
|
+
}
|
|
107
|
+
let body;
|
|
108
|
+
try {
|
|
109
|
+
body = (await request.json());
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
113
|
+
}
|
|
114
|
+
const activityId = typeof body.activity_id === "string" ? body.activity_id : null;
|
|
115
|
+
const message = typeof body.message === "string" ? body.message : null;
|
|
116
|
+
const result = path === "/api/activities/complete"
|
|
117
|
+
? deps.activitySupervisor.complete({ activityId, message })
|
|
118
|
+
: path === "/api/activities/fail"
|
|
119
|
+
? deps.activitySupervisor.fail({ activityId, message })
|
|
120
|
+
: deps.activitySupervisor.cancel({ activityId, message });
|
|
121
|
+
if (result.ok) {
|
|
122
|
+
return Response.json(result, { headers });
|
|
123
|
+
}
|
|
124
|
+
if (result.reason === "missing_target") {
|
|
125
|
+
return Response.json(result, { status: 400, headers });
|
|
126
|
+
}
|
|
127
|
+
if (result.reason === "not_running") {
|
|
128
|
+
return Response.json(result, { status: 409, headers });
|
|
129
|
+
}
|
|
130
|
+
return Response.json(result, { status: 404, headers });
|
|
131
|
+
}
|
|
132
|
+
if (path.startsWith("/api/activities/")) {
|
|
133
|
+
if (request.method !== "GET") {
|
|
134
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
135
|
+
}
|
|
136
|
+
const rest = path.slice("/api/activities/".length);
|
|
137
|
+
const [rawId, maybeSub] = rest.split("/");
|
|
138
|
+
const activityId = decodeURIComponent(rawId ?? "").trim();
|
|
139
|
+
if (activityId.length === 0) {
|
|
140
|
+
return Response.json({ error: "missing activity id" }, { status: 400, headers });
|
|
141
|
+
}
|
|
142
|
+
if (maybeSub === "events") {
|
|
143
|
+
const limitRaw = url.searchParams.get("limit");
|
|
144
|
+
const limit = limitRaw && /^\d+$/.test(limitRaw)
|
|
145
|
+
? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
|
|
146
|
+
: undefined;
|
|
147
|
+
const events = deps.activitySupervisor.events(activityId, { limit });
|
|
148
|
+
if (!events) {
|
|
149
|
+
return Response.json({ error: "activity not found" }, { status: 404, headers });
|
|
150
|
+
}
|
|
151
|
+
return Response.json({ count: events.length, events }, { headers });
|
|
152
|
+
}
|
|
153
|
+
const activity = deps.activitySupervisor.get(activityId);
|
|
154
|
+
if (!activity) {
|
|
155
|
+
return Response.json({ error: "activity not found" }, { status: 404, headers });
|
|
156
|
+
}
|
|
157
|
+
return Response.json(activity, { headers });
|
|
158
|
+
}
|
|
159
|
+
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
160
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { applyMuConfigPatch, getMuConfigPath, muConfigPresence, redactMuConfigSecrets, } from "../config.js";
|
|
2
|
+
export async function configRoutes(request, _url, deps, headers) {
|
|
3
|
+
if (request.method === "GET") {
|
|
4
|
+
try {
|
|
5
|
+
const config = await deps.loadConfigFromDisk();
|
|
6
|
+
return Response.json({
|
|
7
|
+
repo_root: deps.context.repoRoot,
|
|
8
|
+
config_path: getMuConfigPath(deps.context.repoRoot),
|
|
9
|
+
config: redactMuConfigSecrets(config),
|
|
10
|
+
presence: muConfigPresence(config),
|
|
11
|
+
}, { headers });
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
return Response.json({ error: `failed to read config: ${deps.describeError(err)}` }, { status: 500, headers });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (request.method === "POST") {
|
|
18
|
+
let body;
|
|
19
|
+
try {
|
|
20
|
+
body = (await request.json());
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
24
|
+
}
|
|
25
|
+
if (!body || !("patch" in body)) {
|
|
26
|
+
return Response.json({ error: "missing patch payload" }, { status: 400, headers });
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const base = await deps.loadConfigFromDisk();
|
|
30
|
+
const next = applyMuConfigPatch(base, body.patch);
|
|
31
|
+
const configPath = await deps.writeConfig(deps.context.repoRoot, next);
|
|
32
|
+
return Response.json({
|
|
33
|
+
ok: true,
|
|
34
|
+
repo_root: deps.context.repoRoot,
|
|
35
|
+
config_path: configPath,
|
|
36
|
+
config: redactMuConfigSecrets(next),
|
|
37
|
+
presence: muConfigPresence(next),
|
|
38
|
+
}, { headers });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return Response.json({ error: `failed to write config: ${deps.describeError(err)}` }, { status: 500, headers });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function controlPlaneRoutes(request, url, deps, headers) {
|
|
2
|
+
const path = url.pathname;
|
|
3
|
+
if (path === "/api/control-plane/reload") {
|
|
4
|
+
if (request.method !== "POST") {
|
|
5
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
6
|
+
}
|
|
7
|
+
let reason = "api_control_plane_reload";
|
|
8
|
+
try {
|
|
9
|
+
const body = (await request.json());
|
|
10
|
+
if (typeof body.reason === "string" && body.reason.trim().length > 0) {
|
|
11
|
+
reason = body.reason.trim();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// ignore invalid body for reason
|
|
16
|
+
}
|
|
17
|
+
const result = await deps.reloadControlPlane(reason);
|
|
18
|
+
return Response.json(result, { status: result.ok ? 200 : 500, headers });
|
|
19
|
+
}
|
|
20
|
+
if (path === "/api/control-plane/rollback") {
|
|
21
|
+
if (request.method !== "POST") {
|
|
22
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
23
|
+
}
|
|
24
|
+
const result = await deps.reloadControlPlane("rollback");
|
|
25
|
+
return Response.json(result, { status: result.ok ? 200 : 500, headers });
|
|
26
|
+
}
|
|
27
|
+
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
28
|
+
}
|
package/dist/api/cron.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { cronScheduleInputFromBody, hasCronScheduleInput, parseCronTarget, } from "../cron_request.js";
|
|
2
|
+
import { normalizeWakeMode } from "../server_types.js";
|
|
3
|
+
export async function cronRoutes(request, url, deps, headers) {
|
|
4
|
+
const path = url.pathname;
|
|
5
|
+
if (path === "/api/cron/status") {
|
|
6
|
+
if (request.method !== "GET") {
|
|
7
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
8
|
+
}
|
|
9
|
+
const status = await deps.cronPrograms.status();
|
|
10
|
+
return Response.json(status, { headers });
|
|
11
|
+
}
|
|
12
|
+
if (path === "/api/cron") {
|
|
13
|
+
if (request.method !== "GET") {
|
|
14
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
15
|
+
}
|
|
16
|
+
const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
|
|
17
|
+
const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
|
|
18
|
+
const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
|
|
19
|
+
const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
|
|
20
|
+
const scheduleKindRaw = url.searchParams.get("schedule_kind")?.trim().toLowerCase();
|
|
21
|
+
const scheduleKind = scheduleKindRaw === "at" || scheduleKindRaw === "every" || scheduleKindRaw === "cron"
|
|
22
|
+
? scheduleKindRaw
|
|
23
|
+
: undefined;
|
|
24
|
+
const limitRaw = url.searchParams.get("limit");
|
|
25
|
+
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
26
|
+
const programs = await deps.cronPrograms.list({ enabled, targetKind, scheduleKind, limit });
|
|
27
|
+
return Response.json({ count: programs.length, programs }, { headers });
|
|
28
|
+
}
|
|
29
|
+
if (path === "/api/cron/create") {
|
|
30
|
+
if (request.method !== "POST") {
|
|
31
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
32
|
+
}
|
|
33
|
+
let body;
|
|
34
|
+
try {
|
|
35
|
+
body = (await request.json());
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
39
|
+
}
|
|
40
|
+
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
41
|
+
if (!title) {
|
|
42
|
+
return Response.json({ error: "title is required" }, { status: 400, headers });
|
|
43
|
+
}
|
|
44
|
+
const parsedTarget = parseCronTarget(body);
|
|
45
|
+
if (!parsedTarget.target) {
|
|
46
|
+
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
47
|
+
}
|
|
48
|
+
if (!hasCronScheduleInput(body)) {
|
|
49
|
+
return Response.json({ error: "schedule is required" }, { status: 400, headers });
|
|
50
|
+
}
|
|
51
|
+
const schedule = cronScheduleInputFromBody(body);
|
|
52
|
+
const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
|
|
53
|
+
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
54
|
+
const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
|
|
55
|
+
try {
|
|
56
|
+
const program = await deps.cronPrograms.create({
|
|
57
|
+
title,
|
|
58
|
+
target: parsedTarget.target,
|
|
59
|
+
schedule,
|
|
60
|
+
reason,
|
|
61
|
+
wakeMode,
|
|
62
|
+
enabled,
|
|
63
|
+
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
64
|
+
? body.metadata
|
|
65
|
+
: undefined,
|
|
66
|
+
});
|
|
67
|
+
return Response.json({ ok: true, program }, { status: 201, headers });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (path === "/api/cron/update") {
|
|
74
|
+
if (request.method !== "POST") {
|
|
75
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
76
|
+
}
|
|
77
|
+
let body;
|
|
78
|
+
try {
|
|
79
|
+
body = (await request.json());
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
83
|
+
}
|
|
84
|
+
const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
|
|
85
|
+
if (!programId) {
|
|
86
|
+
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
87
|
+
}
|
|
88
|
+
let target;
|
|
89
|
+
if (typeof body.target_kind === "string") {
|
|
90
|
+
const parsedTarget = parseCronTarget(body);
|
|
91
|
+
if (!parsedTarget.target) {
|
|
92
|
+
return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
|
|
93
|
+
}
|
|
94
|
+
target = parsedTarget.target;
|
|
95
|
+
}
|
|
96
|
+
const schedule = hasCronScheduleInput(body) ? cronScheduleInputFromBody(body) : undefined;
|
|
97
|
+
const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
|
|
98
|
+
try {
|
|
99
|
+
const result = await deps.cronPrograms.update({
|
|
100
|
+
programId,
|
|
101
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
102
|
+
reason: typeof body.reason === "string" ? body.reason : undefined,
|
|
103
|
+
wakeMode,
|
|
104
|
+
enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
|
|
105
|
+
target,
|
|
106
|
+
schedule,
|
|
107
|
+
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
108
|
+
? body.metadata
|
|
109
|
+
: undefined,
|
|
110
|
+
});
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
return Response.json(result, { headers });
|
|
113
|
+
}
|
|
114
|
+
if (result.reason === "not_found") {
|
|
115
|
+
return Response.json(result, { status: 404, headers });
|
|
116
|
+
}
|
|
117
|
+
return Response.json(result, { status: 400, headers });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (path === "/api/cron/delete") {
|
|
124
|
+
if (request.method !== "POST") {
|
|
125
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
126
|
+
}
|
|
127
|
+
let body;
|
|
128
|
+
try {
|
|
129
|
+
body = (await request.json());
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
133
|
+
}
|
|
134
|
+
const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
|
|
135
|
+
if (!programId) {
|
|
136
|
+
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
137
|
+
}
|
|
138
|
+
const result = await deps.cronPrograms.remove(programId);
|
|
139
|
+
return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
|
|
140
|
+
}
|
|
141
|
+
if (path === "/api/cron/trigger") {
|
|
142
|
+
if (request.method !== "POST") {
|
|
143
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
144
|
+
}
|
|
145
|
+
let body;
|
|
146
|
+
try {
|
|
147
|
+
body = (await request.json());
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
151
|
+
}
|
|
152
|
+
const result = await deps.cronPrograms.trigger({
|
|
153
|
+
programId: typeof body.program_id === "string" ? body.program_id : null,
|
|
154
|
+
reason: typeof body.reason === "string" ? body.reason : null,
|
|
155
|
+
});
|
|
156
|
+
if (result.ok) {
|
|
157
|
+
return Response.json(result, { headers });
|
|
158
|
+
}
|
|
159
|
+
if (result.reason === "missing_target") {
|
|
160
|
+
return Response.json(result, { status: 400, headers });
|
|
161
|
+
}
|
|
162
|
+
if (result.reason === "not_found") {
|
|
163
|
+
return Response.json(result, { status: 404, headers });
|
|
164
|
+
}
|
|
165
|
+
return Response.json(result, { status: 409, headers });
|
|
166
|
+
}
|
|
167
|
+
if (path.startsWith("/api/cron/")) {
|
|
168
|
+
if (request.method !== "GET") {
|
|
169
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
170
|
+
}
|
|
171
|
+
const id = decodeURIComponent(path.slice("/api/cron/".length)).trim();
|
|
172
|
+
if (!id) {
|
|
173
|
+
return Response.json({ error: "missing program id" }, { status: 400, headers });
|
|
174
|
+
}
|
|
175
|
+
const program = await deps.cronPrograms.get(id);
|
|
176
|
+
if (!program) {
|
|
177
|
+
return Response.json({ error: "program not found" }, { status: 404, headers });
|
|
178
|
+
}
|
|
179
|
+
return Response.json(program, { headers });
|
|
180
|
+
}
|
|
181
|
+
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
182
|
+
}
|
package/dist/api/events.js
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
function trimOrNull(value) {
|
|
2
|
+
if (value == null) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
7
|
+
}
|
|
8
|
+
function previewText(value) {
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
if (value == null) {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function includeByContains(event, contains) {
|
|
23
|
+
if (!contains) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const needle = contains.toLowerCase();
|
|
27
|
+
const haystack = [
|
|
28
|
+
event.type ?? "",
|
|
29
|
+
event.source ?? "",
|
|
30
|
+
event.issue_id ?? "",
|
|
31
|
+
event.run_id ?? "",
|
|
32
|
+
previewText(event.payload),
|
|
33
|
+
]
|
|
34
|
+
.join("\n")
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
return haystack.includes(needle);
|
|
37
|
+
}
|
|
38
|
+
function applyEventFilters(events, filters) {
|
|
39
|
+
return events
|
|
40
|
+
.filter((event) => (filters.type ? event.type === filters.type : true))
|
|
41
|
+
.filter((event) => (filters.source ? event.source === filters.source : true))
|
|
42
|
+
.filter((event) => (filters.issueId ? event.issue_id === filters.issueId : true))
|
|
43
|
+
.filter((event) => (filters.runId ? event.run_id === filters.runId : true))
|
|
44
|
+
.filter((event) => {
|
|
45
|
+
if (filters.sinceMs == null) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (typeof event.ts_ms !== "number") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return event.ts_ms >= filters.sinceMs;
|
|
52
|
+
})
|
|
53
|
+
.filter((event) => includeByContains(event, filters.contains));
|
|
54
|
+
}
|
|
55
|
+
function parseSinceMs(value) {
|
|
56
|
+
if (value == null || value.trim().length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const parsed = Number.parseInt(value, 10);
|
|
60
|
+
if (!Number.isFinite(parsed)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
1
65
|
export async function eventRoutes(request, context) {
|
|
2
66
|
const url = new URL(request.url);
|
|
3
67
|
const path = url.pathname.replace("/api/events", "") || "/";
|
|
@@ -6,31 +70,25 @@ export async function eventRoutes(request, context) {
|
|
|
6
70
|
return new Response("Method Not Allowed", { status: 405 });
|
|
7
71
|
}
|
|
8
72
|
try {
|
|
9
|
-
const allEvents = await context.eventsStore.read();
|
|
73
|
+
const allEvents = (await context.eventsStore.read());
|
|
74
|
+
const filters = {
|
|
75
|
+
type: trimOrNull(url.searchParams.get("type")),
|
|
76
|
+
source: trimOrNull(url.searchParams.get("source")),
|
|
77
|
+
issueId: trimOrNull(url.searchParams.get("issue_id")),
|
|
78
|
+
runId: trimOrNull(url.searchParams.get("run_id")),
|
|
79
|
+
sinceMs: parseSinceMs(url.searchParams.get("since")),
|
|
80
|
+
contains: trimOrNull(url.searchParams.get("contains")),
|
|
81
|
+
};
|
|
10
82
|
// Tail - GET /api/events/tail?n=50
|
|
11
83
|
if (path === "/tail") {
|
|
12
84
|
const n = Math.min(Math.max(1, parseInt(url.searchParams.get("n") ?? "50", 10) || 50), 500);
|
|
13
|
-
|
|
85
|
+
const filtered = applyEventFilters(allEvents, filters);
|
|
86
|
+
return Response.json(filtered.slice(-n));
|
|
14
87
|
}
|
|
15
|
-
// Query - GET /api/events?type=...&source=...&since=...&limit=50
|
|
88
|
+
// Query - GET /api/events?type=...&source=...&issue_id=...&run_id=...&since=...&contains=...&limit=50
|
|
16
89
|
if (path === "/") {
|
|
17
|
-
const typeFilter = url.searchParams.get("type");
|
|
18
|
-
const sourceFilter = url.searchParams.get("source");
|
|
19
|
-
const sinceRaw = url.searchParams.get("since");
|
|
20
90
|
const limit = Math.min(Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50), 500);
|
|
21
|
-
|
|
22
|
-
if (typeFilter) {
|
|
23
|
-
filtered = filtered.filter((e) => e.type === typeFilter);
|
|
24
|
-
}
|
|
25
|
-
if (sourceFilter) {
|
|
26
|
-
filtered = filtered.filter((e) => e.source === sourceFilter);
|
|
27
|
-
}
|
|
28
|
-
if (sinceRaw) {
|
|
29
|
-
const sinceMs = parseInt(sinceRaw, 10);
|
|
30
|
-
if (!Number.isNaN(sinceMs)) {
|
|
31
|
-
filtered = filtered.filter((e) => e.ts_ms >= sinceMs);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
91
|
+
const filtered = applyEventFilters(allEvents, filters);
|
|
34
92
|
return Response.json(filtered.slice(-limit));
|
|
35
93
|
}
|
|
36
94
|
return new Response("Not Found", { status: 404 });
|
package/dist/api/forum.js
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
import { DEFAULT_FORUM_TOPICS_LIMIT, ForumStoreValidationError, normalizeForumPrefix, normalizeForumReadLimit, normalizeForumTopic, normalizeForumTopicsLimit, } from "@femtomc/mu-forum";
|
|
2
|
+
async function readJsonBody(request) {
|
|
3
|
+
let body;
|
|
4
|
+
try {
|
|
5
|
+
body = await request.json();
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new ForumStoreValidationError("invalid json body");
|
|
9
|
+
}
|
|
10
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
11
|
+
throw new ForumStoreValidationError("json body must be an object");
|
|
12
|
+
}
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
function errorResponse(status, message) {
|
|
16
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
17
|
+
status,
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function mapForumRouteError(error) {
|
|
22
|
+
if (error instanceof ForumStoreValidationError) {
|
|
23
|
+
return errorResponse(400, error.message);
|
|
24
|
+
}
|
|
25
|
+
if (error instanceof Error && error.name === "ZodError") {
|
|
26
|
+
return errorResponse(400, error.message);
|
|
27
|
+
}
|
|
28
|
+
console.error("Forum API error:", error);
|
|
29
|
+
return errorResponse(500, error instanceof Error ? error.message : "Internal server error");
|
|
30
|
+
}
|
|
1
31
|
export async function forumRoutes(request, context) {
|
|
2
32
|
const url = new URL(request.url);
|
|
3
33
|
const path = url.pathname.replace("/api/forum", "") || "/";
|
|
@@ -5,37 +35,41 @@ export async function forumRoutes(request, context) {
|
|
|
5
35
|
try {
|
|
6
36
|
// List topics - GET /api/forum/topics
|
|
7
37
|
if (path === "/topics" && method === "GET") {
|
|
8
|
-
const prefix = url.searchParams.get("prefix");
|
|
9
|
-
const
|
|
38
|
+
const prefix = normalizeForumPrefix(url.searchParams.get("prefix"));
|
|
39
|
+
const limit = normalizeForumTopicsLimit(url.searchParams.get("limit"), {
|
|
40
|
+
defaultLimit: DEFAULT_FORUM_TOPICS_LIMIT,
|
|
41
|
+
});
|
|
42
|
+
const topics = await context.forumStore.topics(prefix, { limit });
|
|
10
43
|
return Response.json(topics);
|
|
11
44
|
}
|
|
12
45
|
// Read messages - GET /api/forum/read
|
|
13
46
|
if (path === "/read" && method === "GET") {
|
|
14
|
-
const topic = url.searchParams.get("topic");
|
|
15
|
-
const limit = url.searchParams.get("limit");
|
|
16
|
-
|
|
17
|
-
return new Response("Topic is required", { status: 400 });
|
|
18
|
-
}
|
|
19
|
-
const messages = await context.forumStore.read(topic, limit ? parseInt(limit, 10) : 50);
|
|
47
|
+
const topic = normalizeForumTopic(url.searchParams.get("topic"));
|
|
48
|
+
const limit = normalizeForumReadLimit(url.searchParams.get("limit"));
|
|
49
|
+
const messages = await context.forumStore.read(topic, limit);
|
|
20
50
|
return Response.json(messages);
|
|
21
51
|
}
|
|
22
52
|
// Post message - POST /api/forum/post
|
|
23
53
|
if (path === "/post" && method === "POST") {
|
|
24
|
-
const body =
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
54
|
+
const body = await readJsonBody(request);
|
|
55
|
+
const topic = normalizeForumTopic(body.topic);
|
|
56
|
+
if (typeof body.body !== "string" || body.body.trim().length === 0) {
|
|
57
|
+
return errorResponse(400, "body is required");
|
|
58
|
+
}
|
|
59
|
+
const messageBody = body.body;
|
|
60
|
+
let author = "system";
|
|
61
|
+
if (body.author != null) {
|
|
62
|
+
if (typeof body.author !== "string" || body.author.trim().length === 0) {
|
|
63
|
+
return errorResponse(400, "author must be a non-empty string when provided");
|
|
64
|
+
}
|
|
65
|
+
author = body.author.trim();
|
|
28
66
|
}
|
|
29
|
-
const message = await context.forumStore.post(topic, messageBody, author
|
|
67
|
+
const message = await context.forumStore.post(topic, messageBody, author);
|
|
30
68
|
return Response.json(message, { status: 201 });
|
|
31
69
|
}
|
|
32
70
|
return new Response("Not Found", { status: 404 });
|
|
33
71
|
}
|
|
34
72
|
catch (error) {
|
|
35
|
-
|
|
36
|
-
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }), {
|
|
37
|
-
status: 500,
|
|
38
|
-
headers: { "Content-Type": "application/json" },
|
|
39
|
-
});
|
|
73
|
+
return mapForumRouteError(error);
|
|
40
74
|
}
|
|
41
75
|
}
|