@femtomc/mu-server 26.2.70 → 26.2.72

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 (42) hide show
  1. package/dist/api/activities.d.ts +2 -0
  2. package/dist/api/activities.js +160 -0
  3. package/dist/api/config.d.ts +2 -0
  4. package/dist/api/config.js +45 -0
  5. package/dist/api/control_plane.d.ts +2 -0
  6. package/dist/api/control_plane.js +28 -0
  7. package/dist/api/cron.d.ts +2 -0
  8. package/dist/api/cron.js +182 -0
  9. package/dist/api/heartbeats.d.ts +2 -0
  10. package/dist/api/heartbeats.js +211 -0
  11. package/dist/api/identities.d.ts +2 -0
  12. package/dist/api/identities.js +103 -0
  13. package/dist/api/runs.d.ts +2 -0
  14. package/dist/api/runs.js +207 -0
  15. package/dist/cli.js +58 -3
  16. package/dist/config.d.ts +4 -21
  17. package/dist/config.js +24 -75
  18. package/dist/control_plane.d.ts +4 -2
  19. package/dist/control_plane.js +226 -25
  20. package/dist/control_plane_bootstrap_helpers.d.ts +2 -1
  21. package/dist/control_plane_bootstrap_helpers.js +11 -1
  22. package/dist/control_plane_contract.d.ts +57 -0
  23. package/dist/control_plane_contract.js +1 -1
  24. package/dist/control_plane_reload.d.ts +63 -0
  25. package/dist/control_plane_reload.js +525 -0
  26. package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
  27. package/dist/control_plane_run_queue_coordinator.js +327 -0
  28. package/dist/control_plane_telegram_generation.js +0 -1
  29. package/dist/control_plane_wake_delivery.d.ts +50 -0
  30. package/dist/control_plane_wake_delivery.js +123 -0
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +2 -0
  33. package/dist/run_queue.d.ts +95 -0
  34. package/dist/run_queue.js +817 -0
  35. package/dist/run_supervisor.d.ts +20 -0
  36. package/dist/run_supervisor.js +25 -1
  37. package/dist/server.d.ts +5 -10
  38. package/dist/server.js +337 -528
  39. package/dist/server_program_orchestration.js +2 -0
  40. package/dist/server_routing.d.ts +3 -2
  41. package/dist/server_routing.js +28 -900
  42. package/package.json +7 -6
@@ -0,0 +1,2 @@
1
+ import type { ServerRoutingDependencies } from "../server_routing.js";
2
+ export declare function activityRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;
@@ -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,2 @@
1
+ import type { ServerRoutingDependencies } from "../server_routing.js";
2
+ export declare function configRoutes(request: Request, _url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;
@@ -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,2 @@
1
+ import type { ServerRoutingDependencies } from "../server_routing.js";
2
+ export declare function controlPlaneRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { ServerRoutingDependencies } from "../server_routing.js";
2
+ export declare function cronRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { ServerRoutingDependencies } from "../server_routing.js";
2
+ export declare function heartbeatRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;
@@ -0,0 +1,211 @@
1
+ import { normalizeWakeMode } from "../server_types.js";
2
+ export async function heartbeatRoutes(request, url, deps, headers) {
3
+ const path = url.pathname;
4
+ if (path === "/api/heartbeats") {
5
+ if (request.method !== "GET") {
6
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
7
+ }
8
+ const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
9
+ const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
10
+ const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
11
+ const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : 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 programs = await deps.heartbeatPrograms.list({ enabled, targetKind, limit });
15
+ return Response.json({ count: programs.length, programs }, { headers });
16
+ }
17
+ if (path === "/api/heartbeats/create") {
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 targetKind = typeof body.target_kind === "string" ? body.target_kind.trim().toLowerCase() : "";
33
+ let target = null;
34
+ if (targetKind === "run") {
35
+ const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
36
+ const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
37
+ if (!jobId && !rootIssueId) {
38
+ return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
39
+ }
40
+ target = {
41
+ kind: "run",
42
+ job_id: jobId || null,
43
+ root_issue_id: rootIssueId || null,
44
+ };
45
+ }
46
+ else if (targetKind === "activity") {
47
+ const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
48
+ if (!activityId) {
49
+ return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
50
+ }
51
+ target = {
52
+ kind: "activity",
53
+ activity_id: activityId,
54
+ };
55
+ }
56
+ else {
57
+ return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
58
+ }
59
+ const everyMs = typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
60
+ ? Math.max(0, Math.trunc(body.every_ms))
61
+ : undefined;
62
+ const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
63
+ const wakeMode = normalizeWakeMode(body.wake_mode);
64
+ const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
65
+ try {
66
+ const program = await deps.heartbeatPrograms.create({
67
+ title,
68
+ target,
69
+ everyMs,
70
+ reason,
71
+ wakeMode,
72
+ enabled,
73
+ metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
74
+ ? body.metadata
75
+ : undefined,
76
+ });
77
+ return Response.json({ ok: true, program }, { status: 201, headers });
78
+ }
79
+ catch (err) {
80
+ return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
81
+ }
82
+ }
83
+ if (path === "/api/heartbeats/update") {
84
+ if (request.method !== "POST") {
85
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
86
+ }
87
+ let body;
88
+ try {
89
+ body = (await request.json());
90
+ }
91
+ catch {
92
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
93
+ }
94
+ const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
95
+ if (!programId) {
96
+ return Response.json({ error: "program_id is required" }, { status: 400, headers });
97
+ }
98
+ let target;
99
+ if (typeof body.target_kind === "string") {
100
+ const targetKind = body.target_kind.trim().toLowerCase();
101
+ if (targetKind === "run") {
102
+ const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
103
+ const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
104
+ if (!jobId && !rootIssueId) {
105
+ return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
106
+ }
107
+ target = {
108
+ kind: "run",
109
+ job_id: jobId || null,
110
+ root_issue_id: rootIssueId || null,
111
+ };
112
+ }
113
+ else if (targetKind === "activity") {
114
+ const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
115
+ if (!activityId) {
116
+ return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
117
+ }
118
+ target = {
119
+ kind: "activity",
120
+ activity_id: activityId,
121
+ };
122
+ }
123
+ else {
124
+ return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
125
+ }
126
+ }
127
+ const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
128
+ try {
129
+ const result = await deps.heartbeatPrograms.update({
130
+ programId,
131
+ title: typeof body.title === "string" ? body.title : undefined,
132
+ target,
133
+ everyMs: typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
134
+ ? Math.max(0, Math.trunc(body.every_ms))
135
+ : undefined,
136
+ reason: typeof body.reason === "string" ? body.reason : undefined,
137
+ wakeMode,
138
+ enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
139
+ metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
140
+ ? body.metadata
141
+ : undefined,
142
+ });
143
+ if (result.ok) {
144
+ return Response.json(result, { headers });
145
+ }
146
+ return Response.json(result, { status: result.reason === "not_found" ? 404 : 400, headers });
147
+ }
148
+ catch (err) {
149
+ return Response.json({ error: deps.describeError(err) }, { status: 400, headers });
150
+ }
151
+ }
152
+ if (path === "/api/heartbeats/delete") {
153
+ if (request.method !== "POST") {
154
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
155
+ }
156
+ let body;
157
+ try {
158
+ body = (await request.json());
159
+ }
160
+ catch {
161
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
162
+ }
163
+ const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
164
+ if (!programId) {
165
+ return Response.json({ error: "program_id is required" }, { status: 400, headers });
166
+ }
167
+ const result = await deps.heartbeatPrograms.remove(programId);
168
+ return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
169
+ }
170
+ if (path === "/api/heartbeats/trigger") {
171
+ if (request.method !== "POST") {
172
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
173
+ }
174
+ let body;
175
+ try {
176
+ body = (await request.json());
177
+ }
178
+ catch {
179
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
180
+ }
181
+ const result = await deps.heartbeatPrograms.trigger({
182
+ programId: typeof body.program_id === "string" ? body.program_id : null,
183
+ reason: typeof body.reason === "string" ? body.reason : null,
184
+ });
185
+ if (result.ok) {
186
+ return Response.json(result, { headers });
187
+ }
188
+ if (result.reason === "missing_target") {
189
+ return Response.json(result, { status: 400, headers });
190
+ }
191
+ if (result.reason === "not_found") {
192
+ return Response.json(result, { status: 404, headers });
193
+ }
194
+ return Response.json(result, { status: 409, headers });
195
+ }
196
+ if (path.startsWith("/api/heartbeats/")) {
197
+ if (request.method !== "GET") {
198
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
199
+ }
200
+ const id = decodeURIComponent(path.slice("/api/heartbeats/".length)).trim();
201
+ if (!id) {
202
+ return Response.json({ error: "missing program id" }, { status: 400, headers });
203
+ }
204
+ const program = await deps.heartbeatPrograms.get(id);
205
+ if (!program) {
206
+ return Response.json({ error: "program not found" }, { status: 404, headers });
207
+ }
208
+ return Response.json(program, { headers });
209
+ }
210
+ return Response.json({ error: "Not Found" }, { status: 404, headers });
211
+ }
@@ -0,0 +1,2 @@
1
+ import type { ServerRoutingDependencies } from "../server_routing.js";
2
+ export declare function identityRoutes(request: Request, url: URL, deps: ServerRoutingDependencies, headers: Headers): Promise<Response>;