station-kit 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/package.json +3 -1
- package/.next/standalone/packages/station-kit/.next/BUILD_ID +1 -1
- package/.next/standalone/packages/station-kit/.next/app-build-manifest.json +76 -17
- package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +11 -4
- package/.next/standalone/packages/station-kit/.next/build-manifest.json +3 -3
- package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +105 -9
- package/.next/standalone/packages/station-kit/.next/routes-manifest.json +49 -0
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found.rsc +7 -7
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/settings/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/settings.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/settings.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/signals/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +11 -4
- package/.next/standalone/packages/station-kit/.next/server/chunks/102.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/chunks/535.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/chunks/606.js +14 -14
- package/.next/standalone/packages/station-kit/.next/server/chunks/783.js +3 -3
- package/.next/standalone/packages/station-kit/.next/server/middleware-build-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/404.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/500.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/_app.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/_document.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/_error.js +9 -9
- package/.next/standalone/packages/station-kit/.next/server/pages-manifest.json +1 -1
- package/.next/standalone/packages/station-kit/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/145-9e370afd2e5aba39.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/285-ff198f0a909c4fdd.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/561-33d912169940283e.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/935-dff12960528de017.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/_not-found/{page-ce21b4ba9038a5a7.js → page-67ef312aee40cfeb.js} +1 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-fe2f5467a0c68fef.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/page-0d2505242014f51e.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/v/[n]/page-5eac0507f49a00ec.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/new/page-3d02707043d24dc7.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-dee500ccc01f0821.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-e14e14f3e5b0b8a9.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-aac41ef7a470daab.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/playground/expression/page-dc9d91f3f50f4716.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-9e4c4f751a4bea72.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/[id]/page-435f67be180b8e4f.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/new/page-f697c289c813496a.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/page-738d98dc0b63166e.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-fc5654b31f57ac21.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-4b1c09a539a1ebcd.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-d2f2403dfede87cc.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-a3774a320f58a018.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/demLiQWDy62JuUkBw-ILG/_buildManifest.js +1 -0
- package/.next/standalone/packages/station-kit/package.json +5 -1
- package/dist/config/schema.d.ts +13 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/server/auth/keys.d.ts +56 -8
- package/dist/server/auth/keys.d.ts.map +1 -1
- package/dist/server/auth/keys.js +155 -53
- package/dist/server/auth/keys.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +53 -6
- package/dist/server/index.js.map +1 -1
- package/dist/server/middleware/auth.js +1 -1
- package/dist/server/middleware/auth.js.map +1 -1
- package/dist/server/routes/v1/definitions.d.ts +21 -0
- package/dist/server/routes/v1/definitions.d.ts.map +1 -0
- package/dist/server/routes/v1/definitions.js +139 -0
- package/dist/server/routes/v1/definitions.js.map +1 -0
- package/dist/server/routes/v1/expressions.d.ts +3 -0
- package/dist/server/routes/v1/expressions.d.ts.map +1 -0
- package/dist/server/routes/v1/expressions.js +56 -0
- package/dist/server/routes/v1/expressions.js.map +1 -0
- package/dist/server/routes/v1/keys.js +3 -3
- package/dist/server/routes/v1/keys.js.map +1 -1
- package/dist/server/routes/v1/schedules.d.ts +10 -0
- package/dist/server/routes/v1/schedules.d.ts.map +1 -0
- package/dist/server/routes/v1/schedules.js +169 -0
- package/dist/server/routes/v1/schedules.js.map +1 -0
- package/dist/server/routes/v1/trigger.d.ts.map +1 -1
- package/dist/server/routes/v1/trigger.js +21 -0
- package/dist/server/routes/v1/trigger.js.map +1 -1
- package/package.json +11 -7
- package/src/app/broadcasts/components/broadcast-builder.tsx +535 -0
- package/src/app/broadcasts/components/dag-editor.tsx +510 -0
- package/src/app/broadcasts/dyn/[name]/dynamic-detail.tsx +243 -0
- package/src/app/broadcasts/dyn/[name]/page.tsx +10 -0
- package/src/app/broadcasts/dyn/[name]/v/[n]/page.tsx +10 -0
- package/src/app/broadcasts/dyn/[name]/v/[n]/version-view.tsx +285 -0
- package/src/app/broadcasts/new/page.tsx +102 -0
- package/src/app/broadcasts/page.tsx +176 -91
- package/src/app/components/api-panel.tsx +151 -0
- package/src/app/components/shell.tsx +23 -0
- package/src/app/hooks/use-api.ts +117 -0
- package/src/app/playground/expression/page.tsx +245 -0
- package/src/app/schedules/[id]/page.tsx +10 -0
- package/src/app/schedules/[id]/schedule-editor.tsx +195 -0
- package/src/app/schedules/components/schedule-form.tsx +140 -0
- package/src/app/schedules/new/page.tsx +166 -0
- package/src/app/schedules/page.tsx +126 -0
- package/src/config/schema.ts +14 -0
- package/src/server/auth/keys.ts +191 -56
- package/src/server/index.ts +72 -8
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/routes/v1/definitions.ts +164 -0
- package/src/server/routes/v1/expressions.ts +76 -0
- package/src/server/routes/v1/keys.ts +3 -3
- package/src/server/routes/v1/schedules.ts +176 -0
- package/src/server/routes/v1/trigger.ts +27 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/580-f007f4d4c050db4e.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a0a20cccda13a0e9.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-937eb876f9087bc9.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-68cd71116ba65cd8.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-70b0c0958c03459a.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-01f8040619fe56c5.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-beac11049f90da31.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-931e6a38a4a53d25.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-6a123a355d93fec5.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/xYd6dn0Ox68DaamIrH_pB/_buildManifest.js +0 -1
- /package/.next/standalone/packages/station-kit/.next/static/{xYd6dn0Ox68DaamIrH_pB → demLiQWDy62JuUkBw-ILG}/_ssgManifest.js +0 -0
package/src/server/index.ts
CHANGED
|
@@ -4,10 +4,15 @@ import { serve } from "@hono/node-server";
|
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import type { Server } from "node:http";
|
|
7
|
-
import { SignalRunner, MemoryAdapter } from "station-signal";
|
|
7
|
+
import { SignalRunner, MemoryAdapter, parseInterval } from "station-signal";
|
|
8
8
|
import { BroadcastRunner, BroadcastMemoryAdapter } from "station-broadcast";
|
|
9
9
|
import type { SignalQueueAdapter } from "station-signal";
|
|
10
10
|
import type { BroadcastQueueAdapter } from "station-broadcast";
|
|
11
|
+
import {
|
|
12
|
+
ScheduleReconciler,
|
|
13
|
+
type Schedule,
|
|
14
|
+
type ScheduleAdapter,
|
|
15
|
+
} from "station-schedules";
|
|
11
16
|
import type { StationConfig } from "../config/schema.js";
|
|
12
17
|
import { ensureStationDir } from "../station-dir.js";
|
|
13
18
|
import { WebSocketHub } from "./ws.js";
|
|
@@ -19,7 +24,7 @@ import { healthRoutes } from "./routes/health.js";
|
|
|
19
24
|
import { signalRoutes } from "./routes/signals.js";
|
|
20
25
|
import { runRoutes } from "./routes/runs.js";
|
|
21
26
|
import { broadcastRoutes } from "./routes/broadcasts.js";
|
|
22
|
-
import { KeyStore } from "./auth/keys.js";
|
|
27
|
+
import { KeyStore, SqliteKeyStorage } from "./auth/keys.js";
|
|
23
28
|
import { verifySessionToken, verifyCredentials, createSessionToken, type SessionConfig } from "./auth/session.js";
|
|
24
29
|
import { authResolver } from "./middleware/auth.js";
|
|
25
30
|
import { requireScope } from "./middleware/scope-guard.js";
|
|
@@ -32,9 +37,21 @@ import { v1TriggerRoutes } from "./routes/v1/trigger.js";
|
|
|
32
37
|
import { v1KeyRoutes } from "./routes/v1/keys.js";
|
|
33
38
|
import { v1AuthRoutes } from "./routes/v1/auth.js";
|
|
34
39
|
import { v1EventRoutes } from "./routes/v1/events.js";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
import { v1DefinitionRoutes, v1DefinitionReadRoutes } from "./routes/v1/definitions.js";
|
|
41
|
+
import { v1ScheduleRoutes, v1ScheduleReadRoutes } from "./routes/v1/schedules.js";
|
|
42
|
+
import { v1ExpressionRoutes } from "./routes/v1/expressions.js";
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
KeyStore,
|
|
46
|
+
SqliteKeyStorage,
|
|
47
|
+
MemoryKeyStorage,
|
|
48
|
+
} from "./auth/keys.js";
|
|
49
|
+
export type {
|
|
50
|
+
ApiKey,
|
|
51
|
+
ApiKeyPublic,
|
|
52
|
+
ApiKeyStorageAdapter,
|
|
53
|
+
SqliteKeyStorageOptions,
|
|
54
|
+
} from "./auth/keys.js";
|
|
38
55
|
|
|
39
56
|
export interface StationInstance {
|
|
40
57
|
start(): Promise<void>;
|
|
@@ -62,7 +79,9 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
|
|
|
62
79
|
let sessionConfig: SessionConfig | undefined;
|
|
63
80
|
|
|
64
81
|
if (config.auth) {
|
|
65
|
-
|
|
82
|
+
const storage = config.auth.keyStorage
|
|
83
|
+
?? new SqliteKeyStorage({ dbPath: resolve(dataDir, "station-keys.db") });
|
|
84
|
+
keyStore = new KeyStore(storage);
|
|
66
85
|
sessionConfig = {
|
|
67
86
|
username: config.auth.username,
|
|
68
87
|
password: config.auth.password,
|
|
@@ -94,8 +113,23 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
|
|
|
94
113
|
// Create runners if enabled
|
|
95
114
|
let signalRunner: SignalRunner | undefined;
|
|
96
115
|
let broadcastRunner: BroadcastRunner | undefined;
|
|
116
|
+
const scheduleAdapter: ScheduleAdapter | undefined = config.scheduleAdapter;
|
|
97
117
|
|
|
98
118
|
if (config.runRunners) {
|
|
119
|
+
// Build schedule reconcilers up front. Each reconciler handles only the
|
|
120
|
+
// kinds it's responsible for; the runner ticks it once per loop.
|
|
121
|
+
const signalScheduleReconciler = scheduleAdapter
|
|
122
|
+
? new ScheduleReconciler({
|
|
123
|
+
adapter: scheduleAdapter,
|
|
124
|
+
kinds: ["signal"],
|
|
125
|
+
parseInterval,
|
|
126
|
+
triggerFn: (s: Schedule) => signalRunner!.triggerSignal(s.target, s.input ?? {}),
|
|
127
|
+
hasPendingOrRunning: (s: Schedule) =>
|
|
128
|
+
signalRunner!.hasPendingOrRunningForSignal(s.target),
|
|
129
|
+
onError: (err) => console.error("[station] Signal schedule reconciler:", err),
|
|
130
|
+
})
|
|
131
|
+
: undefined;
|
|
132
|
+
|
|
99
133
|
signalRunner = new SignalRunner({
|
|
100
134
|
signalsDir,
|
|
101
135
|
adapter: signalAdapter,
|
|
@@ -104,15 +138,29 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
|
|
|
104
138
|
maxAttempts: config.runner.maxAttempts,
|
|
105
139
|
retryBackoffMs: config.runner.retryBackoffMs,
|
|
106
140
|
subscribers: [stationSignalSub],
|
|
141
|
+
scheduleReconciler: signalScheduleReconciler,
|
|
107
142
|
});
|
|
108
143
|
|
|
109
144
|
if (broadcastsDir || broadcastAdapter) {
|
|
145
|
+
const broadcastScheduleReconciler = scheduleAdapter
|
|
146
|
+
? new ScheduleReconciler({
|
|
147
|
+
adapter: scheduleAdapter,
|
|
148
|
+
kinds: ["broadcast-static", "broadcast-dynamic"],
|
|
149
|
+
parseInterval,
|
|
150
|
+
triggerFn: (s: Schedule) => broadcastRunner!.trigger(s.target, s.input ?? {}),
|
|
151
|
+
hasPendingOrRunning: (s: Schedule) =>
|
|
152
|
+
broadcastRunner!.hasPendingOrRunningForBroadcast(s.target),
|
|
153
|
+
onError: (err) => console.error("[station] Broadcast schedule reconciler:", err),
|
|
154
|
+
})
|
|
155
|
+
: undefined;
|
|
156
|
+
|
|
110
157
|
broadcastRunner = new BroadcastRunner({
|
|
111
158
|
signalRunner,
|
|
112
159
|
broadcastsDir,
|
|
113
160
|
adapter: broadcastAdapter ?? new BroadcastMemoryAdapter(),
|
|
114
161
|
pollIntervalMs: config.broadcastRunner.pollIntervalMs,
|
|
115
162
|
subscribers: [stationBroadcastSub],
|
|
163
|
+
scheduleReconciler: broadcastScheduleReconciler,
|
|
116
164
|
});
|
|
117
165
|
}
|
|
118
166
|
}
|
|
@@ -204,6 +252,15 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
|
|
|
204
252
|
readRoutes.route("/", v1RunRoutes({ signalRunner, signalAdapter, logBuffer, logStore }));
|
|
205
253
|
readRoutes.route("/", v1BroadcastRoutes({ broadcastRunner, broadcastAdapter, broadcastSubscriber: stationBroadcastSub }));
|
|
206
254
|
readRoutes.route("/", v1EventRoutes({ sseHub }));
|
|
255
|
+
readRoutes.route("/", v1ExpressionRoutes());
|
|
256
|
+
// Schedule GET + preview are read-scoped; mutating routes are mounted under admin below.
|
|
257
|
+
readRoutes.route("/", v1ScheduleReadRoutes({ scheduleAdapter }));
|
|
258
|
+
readRoutes.route("/", v1DefinitionReadRoutes({
|
|
259
|
+
broadcastRunner,
|
|
260
|
+
broadcastAdapter,
|
|
261
|
+
signalRunner,
|
|
262
|
+
signalSubscriber: stationSignalSub,
|
|
263
|
+
}));
|
|
207
264
|
v1.route("/", readRoutes);
|
|
208
265
|
|
|
209
266
|
// Trigger-scope routes
|
|
@@ -239,10 +296,17 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
|
|
|
239
296
|
});
|
|
240
297
|
v1.route("/", cancelRoutes);
|
|
241
298
|
|
|
242
|
-
// Admin-scope routes
|
|
299
|
+
// Admin-scope routes — destructive / mutating endpoints
|
|
243
300
|
const adminRoutes = new Hono();
|
|
244
301
|
adminRoutes.use("/*", requireScope("admin"));
|
|
245
302
|
adminRoutes.route("/", v1KeyRoutes({ keyStore }));
|
|
303
|
+
adminRoutes.route("/", v1DefinitionRoutes({
|
|
304
|
+
broadcastRunner,
|
|
305
|
+
broadcastAdapter,
|
|
306
|
+
signalRunner,
|
|
307
|
+
signalSubscriber: stationSignalSub,
|
|
308
|
+
}));
|
|
309
|
+
adminRoutes.route("/", v1ScheduleRoutes({ scheduleAdapter }));
|
|
246
310
|
v1.route("/", adminRoutes);
|
|
247
311
|
|
|
248
312
|
app.route("/api/v1", v1);
|
|
@@ -338,7 +402,7 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
|
|
|
338
402
|
wsHub.close();
|
|
339
403
|
sseHub.close();
|
|
340
404
|
logStore.close();
|
|
341
|
-
keyStore?.close();
|
|
405
|
+
await keyStore?.close();
|
|
342
406
|
if (httpServer) {
|
|
343
407
|
httpServer.close();
|
|
344
408
|
}
|
|
@@ -15,7 +15,7 @@ export function authResolver(deps: AuthDeps) {
|
|
|
15
15
|
if (authHeader?.startsWith("Bearer ") && deps.keyStore) {
|
|
16
16
|
const token = authHeader.slice(7);
|
|
17
17
|
if (token.startsWith("sk_")) {
|
|
18
|
-
const key = deps.keyStore.verify(token);
|
|
18
|
+
const key = await deps.keyStore.verify(token);
|
|
19
19
|
if (key) {
|
|
20
20
|
c.set("authType", "api-key");
|
|
21
21
|
c.set("apiKeyId", key.id);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type {
|
|
3
|
+
BroadcastRunner,
|
|
4
|
+
BroadcastQueueAdapter,
|
|
5
|
+
DynamicBroadcastSpec,
|
|
6
|
+
} from "station-broadcast";
|
|
7
|
+
import { validateDynamicSpec, type DynamicValidationContext } from "station-broadcast";
|
|
8
|
+
import type { SignalRunner } from "station-signal";
|
|
9
|
+
import type { SchemaField } from "station-expressions";
|
|
10
|
+
import type { StationSignalSubscriber } from "../../subscriber.js";
|
|
11
|
+
|
|
12
|
+
export interface V1DefinitionDeps {
|
|
13
|
+
broadcastRunner?: BroadcastRunner;
|
|
14
|
+
broadcastAdapter?: BroadcastQueueAdapter;
|
|
15
|
+
signalRunner?: SignalRunner;
|
|
16
|
+
signalSubscriber?: StationSignalSubscriber;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read-scope routes for dynamic broadcast definitions: list, get, version
|
|
21
|
+
* history, get-by-version, plus the read-only `validate` endpoint.
|
|
22
|
+
*/
|
|
23
|
+
export function v1DefinitionReadRoutes(deps: V1DefinitionDeps) {
|
|
24
|
+
const app = new Hono();
|
|
25
|
+
|
|
26
|
+
app.get("/broadcast-definitions", async (c) => {
|
|
27
|
+
if (!deps.broadcastAdapter?.listDefinitions) return c.json({ data: [] });
|
|
28
|
+
const list = await deps.broadcastAdapter.listDefinitions();
|
|
29
|
+
return c.json({ data: list.map(serializeSpec) });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.get("/broadcast-definitions/:name", async (c) => {
|
|
33
|
+
if (!deps.broadcastAdapter?.getDefinition) return c.json({ error: "unavailable" }, 503);
|
|
34
|
+
const spec = await deps.broadcastAdapter.getDefinition(c.req.param("name"));
|
|
35
|
+
if (!spec) return c.json({ error: "not_found" }, 404);
|
|
36
|
+
return c.json({ data: serializeSpec(spec) });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.get("/broadcast-definitions/:name/versions", async (c) => {
|
|
40
|
+
if (!deps.broadcastAdapter?.listDefinitionVersions) return c.json({ data: [] });
|
|
41
|
+
const versions = await deps.broadcastAdapter.listDefinitionVersions(c.req.param("name"));
|
|
42
|
+
return c.json({ data: versions.map(serializeSpec) });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.get("/broadcast-definitions/:name/versions/:n", async (c) => {
|
|
46
|
+
if (!deps.broadcastAdapter?.getDefinition) return c.json({ error: "unavailable" }, 503);
|
|
47
|
+
const version = parseInt(c.req.param("n"), 10);
|
|
48
|
+
if (Number.isNaN(version)) {
|
|
49
|
+
return c.json({ error: "bad_request", message: "Version must be a number." }, 400);
|
|
50
|
+
}
|
|
51
|
+
const spec = await deps.broadcastAdapter.getDefinition(c.req.param("name"), version);
|
|
52
|
+
if (!spec) return c.json({ error: "not_found" }, 404);
|
|
53
|
+
return c.json({ data: serializeSpec(spec) });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.post("/broadcast-definitions/validate", async (c) => {
|
|
57
|
+
const body = await c.req.json().catch(() => ({}));
|
|
58
|
+
const spec = body as DynamicBroadcastSpec;
|
|
59
|
+
if (!spec || typeof spec !== "object" || typeof spec.name !== "string") {
|
|
60
|
+
return c.json({ error: "bad_request", message: "Spec is missing required fields." }, 400);
|
|
61
|
+
}
|
|
62
|
+
const ctx = buildValidationContext(deps);
|
|
63
|
+
const result = validateDynamicSpec(spec, ctx);
|
|
64
|
+
return c.json({ data: result });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return app;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Admin-scope routes for mutating dynamic broadcast definitions. GETs are
|
|
72
|
+
* mounted under read scope in server/index.ts.
|
|
73
|
+
*/
|
|
74
|
+
export function v1DefinitionRoutes(deps: V1DefinitionDeps) {
|
|
75
|
+
const app = new Hono();
|
|
76
|
+
|
|
77
|
+
app.post("/broadcast-definitions", async (c) => {
|
|
78
|
+
if (!deps.broadcastAdapter?.saveDefinition) {
|
|
79
|
+
return c.json({ error: "unavailable", message: "Broadcast adapter does not support dynamic definitions." }, 503);
|
|
80
|
+
}
|
|
81
|
+
const body = await c.req.json().catch(() => ({}));
|
|
82
|
+
const incoming = body as DynamicBroadcastSpec;
|
|
83
|
+
if (!incoming?.name || !Array.isArray(incoming.nodes)) {
|
|
84
|
+
return c.json({ error: "bad_request", message: "Spec is missing required fields." }, 400);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ctx = buildValidationContext(deps);
|
|
88
|
+
const validation = validateDynamicSpec(incoming, ctx);
|
|
89
|
+
if (!validation.ok) {
|
|
90
|
+
return c.json({ error: "validation_failed", data: validation }, 422);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const apiKeyId = c.get("apiKeyId" as never) as string | undefined;
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const toSave: DynamicBroadcastSpec = {
|
|
96
|
+
...incoming,
|
|
97
|
+
version: 0,
|
|
98
|
+
createdAt: incoming.createdAt ?? now,
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
createdBy: apiKeyId,
|
|
101
|
+
failurePolicy: incoming.failurePolicy ?? "fail-fast",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const saved = await deps.broadcastAdapter.saveDefinition(toSave);
|
|
105
|
+
|
|
106
|
+
// Trigger an eager reconciliation so the new version is live immediately.
|
|
107
|
+
if (deps.broadcastRunner) {
|
|
108
|
+
void deps.broadcastRunner.reconcileDynamicDefinitions().catch(() => {});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return c.json({ data: serializeSpec(saved) }, 201);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// GET routes for definitions live in the read-scope mount in server/index.ts;
|
|
115
|
+
// this router only exposes mutating endpoints under admin scope.
|
|
116
|
+
|
|
117
|
+
app.delete("/broadcast-definitions/:name", async (c) => {
|
|
118
|
+
if (!deps.broadcastAdapter?.deleteDefinition) {
|
|
119
|
+
return c.json({ error: "unavailable" }, 503);
|
|
120
|
+
}
|
|
121
|
+
const success = await deps.broadcastAdapter.deleteDefinition(c.req.param("name"));
|
|
122
|
+
if (!success) return c.json({ error: "not_found" }, 404);
|
|
123
|
+
if (deps.broadcastRunner) {
|
|
124
|
+
void deps.broadcastRunner.reconcileDynamicDefinitions().catch(() => {});
|
|
125
|
+
}
|
|
126
|
+
return c.json({ data: { deleted: true } });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return app;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function serializeSpec(spec: DynamicBroadcastSpec): Record<string, unknown> {
|
|
133
|
+
return {
|
|
134
|
+
...spec,
|
|
135
|
+
createdAt: spec.createdAt?.toISOString?.() ?? spec.createdAt,
|
|
136
|
+
updatedAt: spec.updatedAt?.toISOString?.() ?? spec.updatedAt,
|
|
137
|
+
deletedAt: spec.deletedAt?.toISOString?.() ?? spec.deletedAt,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildValidationContext(deps: V1DefinitionDeps): DynamicValidationContext {
|
|
142
|
+
const signalSchemas = new Map<string, { inputSchema: SchemaField; outputSchema: SchemaField }>();
|
|
143
|
+
// Best-effort schema reflection: we don't know the precise SchemaField for
|
|
144
|
+
// each signal here without traversing Zod schemas. Use `any` for now — the
|
|
145
|
+
// structural checks (signal exists, deps exist, no cycles, expression
|
|
146
|
+
// wellformedness) still run. Zod input validation runs at trigger time.
|
|
147
|
+
const sigs = deps.signalRunner?.getAllSignals();
|
|
148
|
+
if (sigs) {
|
|
149
|
+
for (const name of sigs.keys()) {
|
|
150
|
+
signalSchemas.set(name, {
|
|
151
|
+
inputSchema: { type: "any" },
|
|
152
|
+
outputSchema: { type: "any" },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} else if (deps.signalSubscriber) {
|
|
156
|
+
for (const meta of deps.signalSubscriber.getAllSignalMeta()) {
|
|
157
|
+
signalSchemas.set(meta.name, {
|
|
158
|
+
inputSchema: { type: "any" },
|
|
159
|
+
outputSchema: { type: "any" },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { signalSchemas };
|
|
164
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
evaluate,
|
|
4
|
+
validate,
|
|
5
|
+
parse,
|
|
6
|
+
ExpressionEvalError,
|
|
7
|
+
ExpressionParseError,
|
|
8
|
+
type ExprNode,
|
|
9
|
+
type SchemaField,
|
|
10
|
+
} from "station-expressions";
|
|
11
|
+
|
|
12
|
+
export function v1ExpressionRoutes() {
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
|
|
15
|
+
app.post("/expressions/evaluate", async (c) => {
|
|
16
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17
|
+
const { node, context } = body as {
|
|
18
|
+
node?: ExprNode;
|
|
19
|
+
context?: { input?: unknown; upstream?: Record<string, unknown> };
|
|
20
|
+
};
|
|
21
|
+
if (!node) {
|
|
22
|
+
return c.json({ error: "bad_request", message: "Missing `node`." }, 400);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = evaluate(node, {
|
|
26
|
+
input: context?.input,
|
|
27
|
+
upstream: context?.upstream ?? {},
|
|
28
|
+
});
|
|
29
|
+
return c.json({ data: { value: result } });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
const status = err instanceof ExpressionEvalError ? 400 : 500;
|
|
33
|
+
return c.json({ error: "evaluation_failed", message }, status);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.post("/expressions/validate", async (c) => {
|
|
38
|
+
const body = await c.req.json().catch(() => ({}));
|
|
39
|
+
const { node, schemaContext } = body as {
|
|
40
|
+
node?: ExprNode;
|
|
41
|
+
schemaContext?: {
|
|
42
|
+
inputSchema?: SchemaField;
|
|
43
|
+
upstreamSchemas?: Record<string, SchemaField>;
|
|
44
|
+
expectedSchema?: SchemaField;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
if (!node) {
|
|
48
|
+
return c.json({ error: "bad_request", message: "Missing `node`." }, 400);
|
|
49
|
+
}
|
|
50
|
+
const result = validate(node, {
|
|
51
|
+
inputSchema: schemaContext?.inputSchema ?? { type: "any" },
|
|
52
|
+
upstreamSchemas: schemaContext?.upstreamSchemas ?? {},
|
|
53
|
+
expectedSchema: schemaContext?.expectedSchema,
|
|
54
|
+
});
|
|
55
|
+
return c.json({ data: result });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.post("/expressions/parse", async (c) => {
|
|
59
|
+
const body = await c.req.json().catch(() => ({}));
|
|
60
|
+
const { source } = body as { source?: string };
|
|
61
|
+
if (typeof source !== "string") {
|
|
62
|
+
return c.json({ error: "bad_request", message: "Missing `source` string." }, 400);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const node = parse(source);
|
|
66
|
+
return c.json({ data: { node } });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof ExpressionParseError) {
|
|
69
|
+
return c.json({ error: "parse_error", message: err.message, position: err.position }, 400);
|
|
70
|
+
}
|
|
71
|
+
return c.json({ error: "parse_error", message: err instanceof Error ? err.message : String(err) }, 400);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return app;
|
|
76
|
+
}
|
|
@@ -17,7 +17,7 @@ export function v1KeyRoutes(deps: V1KeyDeps) {
|
|
|
17
17
|
const name = body.name || "Unnamed key";
|
|
18
18
|
const scopes = Array.isArray(body.scopes) ? body.scopes : ["trigger", "read"];
|
|
19
19
|
|
|
20
|
-
const { key, record } = deps.keyStore.create(name, scopes);
|
|
20
|
+
const { key, record } = await deps.keyStore.create(name, scopes);
|
|
21
21
|
return c.json(
|
|
22
22
|
{
|
|
23
23
|
data: {
|
|
@@ -37,7 +37,7 @@ export function v1KeyRoutes(deps: V1KeyDeps) {
|
|
|
37
37
|
if (!deps.keyStore) {
|
|
38
38
|
return c.json({ error: "unavailable", message: "Auth not configured." }, 503);
|
|
39
39
|
}
|
|
40
|
-
const keys = deps.keyStore.list();
|
|
40
|
+
const keys = await deps.keyStore.list();
|
|
41
41
|
return c.json({ data: keys });
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -46,7 +46,7 @@ export function v1KeyRoutes(deps: V1KeyDeps) {
|
|
|
46
46
|
return c.json({ error: "unavailable", message: "Auth not configured." }, 503);
|
|
47
47
|
}
|
|
48
48
|
const id = c.req.param("id");
|
|
49
|
-
const success = deps.keyStore.revoke(id);
|
|
49
|
+
const success = await deps.keyStore.revoke(id);
|
|
50
50
|
if (!success) {
|
|
51
51
|
return c.json({ error: "not_found", message: "Key not found." }, 404);
|
|
52
52
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { parseInterval } from "station-signal";
|
|
3
|
+
import type { Schedule, ScheduleAdapter, ScheduleKind } from "station-schedules";
|
|
4
|
+
|
|
5
|
+
export interface V1ScheduleDeps {
|
|
6
|
+
scheduleAdapter?: ScheduleAdapter;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const KIND_VALUES: ScheduleKind[] = ["signal", "broadcast-static", "broadcast-dynamic"];
|
|
10
|
+
|
|
11
|
+
/** Read-scope routes: list / get / preview. */
|
|
12
|
+
export function v1ScheduleReadRoutes(deps: V1ScheduleDeps) {
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
|
|
15
|
+
app.get("/schedules", async (c) => {
|
|
16
|
+
if (!deps.scheduleAdapter) return c.json({ data: [] });
|
|
17
|
+
const kindParam = c.req.query("kind");
|
|
18
|
+
const enabledParam = c.req.query("enabled");
|
|
19
|
+
|
|
20
|
+
const kind = KIND_VALUES.includes(kindParam as ScheduleKind)
|
|
21
|
+
? (kindParam as ScheduleKind)
|
|
22
|
+
: undefined;
|
|
23
|
+
const enabled = enabledParam === "true" ? true : enabledParam === "false" ? false : undefined;
|
|
24
|
+
|
|
25
|
+
const list = await deps.scheduleAdapter.list({ kind, enabled });
|
|
26
|
+
return c.json({ data: list.map(serialize) });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
app.get("/schedules/:id", async (c) => {
|
|
30
|
+
if (!deps.scheduleAdapter) return c.json({ error: "unavailable" }, 503);
|
|
31
|
+
const s = await deps.scheduleAdapter.get(c.req.param("id"));
|
|
32
|
+
if (!s) return c.json({ error: "not_found" }, 404);
|
|
33
|
+
return c.json({ data: serialize(s) });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.post("/schedules/:id/preview", async (c) => {
|
|
37
|
+
if (!deps.scheduleAdapter) return c.json({ error: "unavailable" }, 503);
|
|
38
|
+
const id = c.req.param("id");
|
|
39
|
+
const s = await deps.scheduleAdapter.get(id);
|
|
40
|
+
if (!s) return c.json({ error: "not_found" }, 404);
|
|
41
|
+
const body = await c.req.json().catch(() => ({}));
|
|
42
|
+
const count = Math.min(20, Math.max(1, Number(body.count) || 5));
|
|
43
|
+
const ms = parseInterval(s.interval);
|
|
44
|
+
const fires: string[] = [];
|
|
45
|
+
let next = s.nextRunAt.getTime();
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
fires.push(new Date(next).toISOString());
|
|
48
|
+
next += ms;
|
|
49
|
+
}
|
|
50
|
+
return c.json({ data: { fires } });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return app;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Admin-scope routes: create / update / delete. */
|
|
57
|
+
export function v1ScheduleRoutes(deps: V1ScheduleDeps) {
|
|
58
|
+
const app = new Hono();
|
|
59
|
+
|
|
60
|
+
app.post("/schedules", async (c) => {
|
|
61
|
+
if (!deps.scheduleAdapter) return c.json({ error: "unavailable" }, 503);
|
|
62
|
+
const body = await c.req.json().catch(() => ({}));
|
|
63
|
+
const { kind, target, interval, input, enabled } = body as Partial<Schedule>;
|
|
64
|
+
|
|
65
|
+
if (!KIND_VALUES.includes(kind as ScheduleKind)) {
|
|
66
|
+
return c.json({ error: "bad_request", message: `kind must be one of ${KIND_VALUES.join(", ")}` }, 400);
|
|
67
|
+
}
|
|
68
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
69
|
+
return c.json({ error: "bad_request", message: "target is required" }, 400);
|
|
70
|
+
}
|
|
71
|
+
if (typeof interval !== "string") {
|
|
72
|
+
return c.json({ error: "bad_request", message: "interval is required" }, 400);
|
|
73
|
+
}
|
|
74
|
+
let intervalMs: number;
|
|
75
|
+
try {
|
|
76
|
+
intervalMs = parseInterval(interval);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return c.json({
|
|
79
|
+
error: "bad_request",
|
|
80
|
+
message: `interval invalid: ${err instanceof Error ? err.message : String(err)}`,
|
|
81
|
+
}, 400);
|
|
82
|
+
}
|
|
83
|
+
// Reject circular / non-serializable inputs before they reach the adapter,
|
|
84
|
+
// which would otherwise surface as a 500 with a stacktrace.
|
|
85
|
+
if (input !== undefined) {
|
|
86
|
+
try {
|
|
87
|
+
JSON.stringify(input);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return c.json({
|
|
90
|
+
error: "bad_request",
|
|
91
|
+
message: `input is not JSON-serializable: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
}, 400);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const apiKeyId = c.get("apiKeyId" as never) as string | undefined;
|
|
97
|
+
const now = new Date();
|
|
98
|
+
const schedule: Schedule = {
|
|
99
|
+
id: deps.scheduleAdapter.generateId(),
|
|
100
|
+
kind: kind as ScheduleKind,
|
|
101
|
+
target,
|
|
102
|
+
interval,
|
|
103
|
+
input,
|
|
104
|
+
enabled: enabled ?? true,
|
|
105
|
+
nextRunAt: new Date(now.getTime() + intervalMs),
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
createdBy: apiKeyId,
|
|
109
|
+
};
|
|
110
|
+
await deps.scheduleAdapter.add(schedule);
|
|
111
|
+
return c.json({ data: serialize(schedule) }, 201);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
app.patch("/schedules/:id", async (c) => {
|
|
115
|
+
if (!deps.scheduleAdapter) return c.json({ error: "unavailable" }, 503);
|
|
116
|
+
const id = c.req.param("id");
|
|
117
|
+
const existing = await deps.scheduleAdapter.get(id);
|
|
118
|
+
if (!existing) return c.json({ error: "not_found" }, 404);
|
|
119
|
+
|
|
120
|
+
const body = await c.req.json().catch(() => ({}));
|
|
121
|
+
const patch: Record<string, unknown> = {};
|
|
122
|
+
if ("interval" in body) {
|
|
123
|
+
try {
|
|
124
|
+
parseInterval(body.interval);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return c.json({
|
|
127
|
+
error: "bad_request",
|
|
128
|
+
message: `interval invalid: ${err instanceof Error ? err.message : String(err)}`,
|
|
129
|
+
}, 400);
|
|
130
|
+
}
|
|
131
|
+
patch.interval = body.interval;
|
|
132
|
+
}
|
|
133
|
+
if ("input" in body) {
|
|
134
|
+
try {
|
|
135
|
+
JSON.stringify(body.input);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return c.json({
|
|
138
|
+
error: "bad_request",
|
|
139
|
+
message: `input is not JSON-serializable: ${err instanceof Error ? err.message : String(err)}`,
|
|
140
|
+
}, 400);
|
|
141
|
+
}
|
|
142
|
+
patch.input = body.input;
|
|
143
|
+
}
|
|
144
|
+
if ("enabled" in body) patch.enabled = Boolean(body.enabled);
|
|
145
|
+
if ("nextRunAt" in body) {
|
|
146
|
+
const parsed = new Date(body.nextRunAt);
|
|
147
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
148
|
+
return c.json({ error: "bad_request", message: "nextRunAt must be a valid date" }, 400);
|
|
149
|
+
}
|
|
150
|
+
patch.nextRunAt = parsed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await deps.scheduleAdapter.update(id, patch);
|
|
154
|
+
const updated = await deps.scheduleAdapter.get(id);
|
|
155
|
+
return c.json({ data: updated ? serialize(updated) : null });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
app.delete("/schedules/:id", async (c) => {
|
|
159
|
+
if (!deps.scheduleAdapter) return c.json({ error: "unavailable" }, 503);
|
|
160
|
+
const ok = await deps.scheduleAdapter.delete(c.req.param("id"));
|
|
161
|
+
if (!ok) return c.json({ error: "not_found" }, 404);
|
|
162
|
+
return c.json({ data: { deleted: true } });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return app;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function serialize(s: Schedule): Record<string, unknown> {
|
|
169
|
+
return {
|
|
170
|
+
...s,
|
|
171
|
+
nextRunAt: s.nextRunAt.toISOString(),
|
|
172
|
+
lastRunAt: s.lastRunAt?.toISOString(),
|
|
173
|
+
createdAt: s.createdAt.toISOString(),
|
|
174
|
+
updatedAt: s.updatedAt.toISOString(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -185,6 +185,33 @@ export function v1TriggerRoutes(deps: V1TriggerDeps) {
|
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
app.post("/trigger-dynamic-broadcast", async (c) => {
|
|
189
|
+
if (!deps.broadcastRunner) {
|
|
190
|
+
return c.json({ error: "unavailable", message: "Broadcast runner not configured." }, 503);
|
|
191
|
+
}
|
|
192
|
+
const body = await c.req.json().catch(() => null);
|
|
193
|
+
if (!body?.broadcastName) {
|
|
194
|
+
return c.json({ error: "bad_request", message: "Missing broadcastName." }, 400);
|
|
195
|
+
}
|
|
196
|
+
const { broadcastName, input } = body;
|
|
197
|
+
if (!deps.broadcastRunner.hasDynamicBroadcast(broadcastName)) {
|
|
198
|
+
return c.json(
|
|
199
|
+
{ error: "not_found", message: `Dynamic broadcast "${broadcastName}" not registered.` },
|
|
200
|
+
404,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const id = await deps.broadcastRunner.triggerDynamic(broadcastName, input ?? {});
|
|
205
|
+
return c.json(
|
|
206
|
+
{ data: { id, broadcastName, status: "pending", createdAt: new Date().toISOString() } },
|
|
207
|
+
201,
|
|
208
|
+
);
|
|
209
|
+
} catch (err: unknown) {
|
|
210
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
211
|
+
return c.json({ error: "trigger_failed", message }, 400);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
188
215
|
app.post("/broadcast-runs/:id/rerun", async (c) => {
|
|
189
216
|
if (!deps.broadcastRunner || !deps.broadcastAdapter) {
|
|
190
217
|
return c.json({ error: "unavailable", message: "Broadcast runner not configured." }, 503);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[580],{706:(e,t)=>{function r(e){let t={};for(let[r,n]of e.entries()){let e=t[r];void 0===e?t[r]=n:Array.isArray(e)?e.push(n):t[r]=[e,n]}return t}function n(e){return"string"==typeof e?e:("number"!=typeof e||isNaN(e))&&"boolean"!=typeof e?"":String(e)}function o(e){let t=new URLSearchParams;for(let[r,o]of Object.entries(e))if(Array.isArray(o))for(let e of o)t.append(r,n(e));else t.set(r,n(o));return t}function u(e){for(var t=arguments.length,r=Array(t>1?t-1:0),n=1;n<t;n++)r[n-1]=arguments[n];for(let t of r){for(let r of t.keys())e.delete(r);for(let[r,n]of t.entries())e.append(r,n)}return e}Object.defineProperty(t,"__esModule",{value:!0}),!function(e,t){for(var r in t)Object.defineProperty(e,r,{enumerable:!0,get:t[r]})}(t,{assign:function(){return u},searchParamsToUrlQuery:function(){return r},urlQueryToSearchParams:function(){return o}})},2823:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"useMergedRef",{enumerable:!0,get:function(){return o}});let n=r(5271);function o(e,t){let r=(0,n.useRef)(null),o=(0,n.useRef)(null);return(0,n.useCallback)(n=>{if(null===n){let e=r.current;e&&(r.current=null,e());let t=o.current;t&&(o.current=null,t())}else e&&(r.current=u(e,n)),t&&(o.current=u(t,n))},[e,t])}function u(e,t){if("function"!=typeof e)return e.current=t,()=>{e.current=null};{let r=e(t);return"function"==typeof r?r:()=>e(null)}}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},4736:(e,t,r)=>{var n=r(1504);r.o(n,"useParams")&&r.d(t,{useParams:function(){return n.useParams}}),r.o(n,"usePathname")&&r.d(t,{usePathname:function(){return n.usePathname}}),r.o(n,"useRouter")&&r.d(t,{useRouter:function(){return n.useRouter}})},5073:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"errorOnce",{enumerable:!0,get:function(){return r}});let r=e=>{}},5935:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),!function(e,t){for(var r in t)Object.defineProperty(e,r,{enumerable:!0,get:t[r]})}(t,{default:function(){return g},useLinkStatus:function(){return b}});let n=r(4284),o=r(8111),u=n._(r(5271)),a=r(9642),i=r(5911),l=r(2823),f=r(7076),c=r(3790);r(4209);let s=r(7335),p=r(7843),d=r(587);r(5073);let h=r(5564);function y(e){return"string"==typeof e?e:(0,a.formatUrl)(e)}function g(e){var t;let r,n,a,[g,b]=(0,u.useOptimistic)(s.IDLE_LINK_STATUS),P=(0,u.useRef)(null),{href:_,as:v,children:E,prefetch:O=null,passHref:j,replace:S,shallow:C,scroll:N,onClick:T,onMouseEnter:R,onTouchStart:x,legacyBehavior:A=!1,onNavigate:M,ref:L,unstable_dynamicOnHover:k,...I}=e;r=E,A&&("string"==typeof r||"number"==typeof r)&&(r=(0,o.jsx)("a",{children:r}));let U=u.default.useContext(i.AppRouterContext),w=!1!==O,F=!1!==O?null===(t=O)||"auto"===t?h.FetchStrategy.PPR:h.FetchStrategy.Full:h.FetchStrategy.PPR,{href:D,as:K}=u.default.useMemo(()=>{let e=y(_);return{href:e,as:v?y(v):e}},[_,v]);A&&(n=u.default.Children.only(r));let B=A?n&&"object"==typeof n&&n.ref:L,z=u.default.useCallback(e=>(null!==U&&(P.current=(0,s.mountLinkInstance)(e,D,U,F,w,b)),()=>{P.current&&((0,s.unmountLinkForCurrentNavigation)(P.current),P.current=null),(0,s.unmountPrefetchableInstance)(e)}),[w,D,U,F,b]),Q={ref:(0,l.useMergedRef)(z,B),onClick(e){A||"function"!=typeof T||T(e),A&&n.props&&"function"==typeof n.props.onClick&&n.props.onClick(e),U&&(e.defaultPrevented||function(e,t,r,n,o,a,i){let{nodeName:l}=e.currentTarget;if(!("A"===l.toUpperCase()&&function(e){let t=e.currentTarget.getAttribute("target");return t&&"_self"!==t||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.nativeEvent&&2===e.nativeEvent.which}(e)||e.currentTarget.hasAttribute("download"))){if(!(0,p.isLocalURL)(t)){o&&(e.preventDefault(),location.replace(t));return}if(e.preventDefault(),i){let e=!1;if(i({preventDefault:()=>{e=!0}}),e)return}u.default.startTransition(()=>{(0,d.dispatchNavigateAction)(r||t,o?"replace":"push",null==a||a,n.current)})}}(e,D,K,P,S,N,M))},onMouseEnter(e){A||"function"!=typeof R||R(e),A&&n.props&&"function"==typeof n.props.onMouseEnter&&n.props.onMouseEnter(e),U&&w&&(0,s.onNavigationIntent)(e.currentTarget,!0===k)},onTouchStart:function(e){A||"function"!=typeof x||x(e),A&&n.props&&"function"==typeof n.props.onTouchStart&&n.props.onTouchStart(e),U&&w&&(0,s.onNavigationIntent)(e.currentTarget,!0===k)}};return(0,f.isAbsoluteUrl)(K)?Q.href=K:A&&!j&&("a"!==n.type||"href"in n.props)||(Q.href=(0,c.addBasePath)(K)),a=A?u.default.cloneElement(n,Q):(0,o.jsx)("a",{...I,...Q,children:r}),(0,o.jsx)(m.Provider,{value:g,children:a})}let m=(0,u.createContext)(s.IDLE_LINK_STATUS),b=()=>(0,u.useContext)(m);("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},7076:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),!function(e,t){for(var r in t)Object.defineProperty(e,r,{enumerable:!0,get:t[r]})}(t,{DecodeError:function(){return h},MiddlewareNotFoundError:function(){return b},MissingStaticPage:function(){return m},NormalizeError:function(){return y},PageNotFoundError:function(){return g},SP:function(){return p},ST:function(){return d},WEB_VITALS:function(){return r},execOnce:function(){return n},getDisplayName:function(){return l},getLocationOrigin:function(){return a},getURL:function(){return i},isAbsoluteUrl:function(){return u},isResSent:function(){return f},loadGetInitialProps:function(){return s},normalizeRepeatedSlashes:function(){return c},stringifyError:function(){return P}});let r=["CLS","FCP","FID","INP","LCP","TTFB"];function n(e){let t,r=!1;return function(){for(var n=arguments.length,o=Array(n),u=0;u<n;u++)o[u]=arguments[u];return r||(r=!0,t=e(...o)),t}}let o=/^[a-zA-Z][a-zA-Z\d+\-.]*?:/,u=e=>o.test(e);function a(){let{protocol:e,hostname:t,port:r}=window.location;return e+"//"+t+(r?":"+r:"")}function i(){let{href:e}=window.location,t=a();return e.substring(t.length)}function l(e){return"string"==typeof e?e:e.displayName||e.name||"Unknown"}function f(e){return e.finished||e.headersSent}function c(e){let t=e.split("?");return t[0].replace(/\\/g,"/").replace(/\/\/+/g,"/")+(t[1]?"?"+t.slice(1).join("?"):"")}async function s(e,t){let r=t.res||t.ctx&&t.ctx.res;if(!e.getInitialProps)return t.ctx&&t.Component?{pageProps:await s(t.Component,t.ctx)}:{};let n=await e.getInitialProps(t);if(r&&f(r))return n;if(!n)throw Object.defineProperty(Error('"'+l(e)+'.getInitialProps()" should resolve to an object. But found "'+n+'" instead.'),"__NEXT_ERROR_CODE",{value:"E394",enumerable:!1,configurable:!0});return n}let p="undefined"!=typeof performance,d=p&&["mark","measure","getEntriesByName"].every(e=>"function"==typeof performance[e]);class h extends Error{}class y extends Error{}class g extends Error{constructor(e){super(),this.code="ENOENT",this.name="PageNotFoundError",this.message="Cannot find module for page: "+e}}class m extends Error{constructor(e,t){super(),this.message="Failed to load static file for page: "+e+" "+t}}class b extends Error{constructor(){super(),this.code="ENOENT",this.message="Cannot find the middleware module"}}function P(e){return JSON.stringify({message:e.message,stack:e.stack})}},7843:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"isLocalURL",{enumerable:!0,get:function(){return u}});let n=r(7076),o=r(3506);function u(e){if(!(0,n.isAbsoluteUrl)(e))return!0;try{let t=(0,n.getLocationOrigin)(),r=new URL(e,t);return r.origin===t&&(0,o.hasBasePath)(r.pathname)}catch(e){return!1}}},9642:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),!function(e,t){for(var r in t)Object.defineProperty(e,r,{enumerable:!0,get:t[r]})}(t,{formatUrl:function(){return u},formatWithValidation:function(){return i},urlObjectKeys:function(){return a}});let n=r(4284)._(r(706)),o=/https?|ftp|gopher|file/;function u(e){let{auth:t,hostname:r}=e,u=e.protocol||"",a=e.pathname||"",i=e.hash||"",l=e.query||"",f=!1;t=t?encodeURIComponent(t).replace(/%3A/i,":")+"@":"",e.host?f=t+e.host:r&&(f=t+(~r.indexOf(":")?"["+r+"]":r),e.port&&(f+=":"+e.port)),l&&"object"==typeof l&&(l=String(n.urlQueryToSearchParams(l)));let c=e.search||l&&"?"+l||"";return u&&!u.endsWith(":")&&(u+=":"),e.slashes||(!u||o.test(u))&&!1!==f?(f="//"+(f||""),a&&"/"!==a[0]&&(a="/"+a)):f||(f=""),i&&"#"!==i[0]&&(i="#"+i),c&&"?"!==c[0]&&(c="?"+c),""+u+f+(a=a.replace(/[?#]/g,encodeURIComponent))+(c=c.replace("#","%23"))+i}let a=["auth","hash","host","hostname","href","path","pathname","port","protocol","query","search","slashes"];function i(e){return u(e)}}}]);
|