station-kit 1.1.0 → 2.0.0
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/packages/station-kit/.next/BUILD_ID +1 -1
- package/.next/standalone/packages/station-kit/.next/app-build-manifest.json +55 -34
- package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +11 -8
- package/.next/standalone/packages/station-kit/.next/build-manifest.json +2 -2
- package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +73 -25
- package/.next/standalone/packages/station-kit/.next/routes-manifest.json +20 -0
- 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 +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app/beacons/[name]/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons/[name]/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons/[name]/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/beacons.rsc +25 -0
- 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_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.rsc +9 -9
- 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 +9 -9
- package/.next/standalone/packages/station-kit/.next/server/app/environment/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/environment/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/environment/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/environment.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/environment.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/environment.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +9 -9
- 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_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.rsc +9 -9
- 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_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.rsc +9 -9
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.rsc +9 -9
- 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 +9 -9
- 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_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 +9 -9
- package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +11 -8
- package/.next/standalone/packages/station-kit/.next/server/chunks/102.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-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/285-583f23ee7827983f.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/561-05a7ef9669e3921a.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/beacons/[name]/page-eb1f82847734e145.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/beacons/page-64e296346197e44b.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/v/[n]/page-15edf07ab26241c9.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-b23a03097c8f75c4.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/environment/page-111242bec067fdee.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-e9ac56fcaf7a5ea7.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-ef78692958c2fc01.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/playground/expression/page-29294a705dd8f471.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-287bbb8f6dec2e23.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/page-c869218da203e3b4.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-05cf43b6ae6b3187.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-4027803be00c695a.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-8e076fc5d0188d72.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/css/ddb1646dc25ed100.css +1 -0
- package/.next/standalone/packages/station-kit/.next/static/vTGEzSqxhR_E57YAFn9DC/_buildManifest.js +1 -0
- package/.next/standalone/packages/station-kit/package.json +4 -1
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/server/auth/keys.d.ts +18 -1
- package/dist/server/auth/keys.d.ts.map +1 -1
- package/dist/server/auth/keys.js +38 -6
- package/dist/server/auth/keys.js.map +1 -1
- package/dist/server/auth/session.d.ts.map +1 -1
- package/dist/server/auth/session.js +34 -11
- package/dist/server/auth/session.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +135 -18
- package/dist/server/index.js.map +1 -1
- package/dist/server/metadata.d.ts +2 -0
- package/dist/server/metadata.d.ts.map +1 -1
- package/dist/server/metadata.js.map +1 -1
- package/dist/server/middleware/rate-limit.d.ts.map +1 -1
- package/dist/server/middleware/rate-limit.js +22 -1
- package/dist/server/middleware/rate-limit.js.map +1 -1
- package/dist/server/routes/beacons.d.ts +10 -0
- package/dist/server/routes/beacons.d.ts.map +1 -0
- package/dist/server/routes/beacons.js +98 -0
- package/dist/server/routes/beacons.js.map +1 -0
- package/dist/server/routes/broadcasts.d.ts.map +1 -1
- package/dist/server/routes/broadcasts.js +6 -2
- package/dist/server/routes/broadcasts.js.map +1 -1
- package/dist/server/routes/runs.d.ts.map +1 -1
- package/dist/server/routes/runs.js +19 -59
- package/dist/server/routes/runs.js.map +1 -1
- package/dist/server/routes/signals.d.ts.map +1 -1
- package/dist/server/routes/signals.js +17 -13
- package/dist/server/routes/signals.js.map +1 -1
- package/dist/server/routes/v1/env.d.ts +10 -0
- package/dist/server/routes/v1/env.d.ts.map +1 -0
- package/dist/server/routes/v1/env.js +132 -0
- package/dist/server/routes/v1/env.js.map +1 -0
- package/dist/server/routes/v1/events.js +2 -2
- package/dist/server/routes/v1/events.js.map +1 -1
- package/dist/server/routes/v1/runs.d.ts.map +1 -1
- package/dist/server/routes/v1/runs.js +10 -31
- package/dist/server/routes/v1/runs.js.map +1 -1
- package/dist/server/sse.d.ts +2 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +5 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/server/subscriber.d.ts +56 -0
- package/dist/server/subscriber.d.ts.map +1 -1
- package/dist/server/subscriber.js +94 -0
- package/dist/server/subscriber.js.map +1 -1
- package/dist/server/ws.d.ts +8 -2
- package/dist/server/ws.d.ts.map +1 -1
- package/dist/server/ws.js +28 -3
- package/dist/server/ws.js.map +1 -1
- package/package.json +11 -8
- package/src/app/beacons/[name]/beacon-detail.tsx +253 -0
- package/src/app/beacons/[name]/page.tsx +6 -0
- package/src/app/beacons/page.tsx +98 -0
- package/src/app/components/shell.tsx +24 -0
- package/src/app/components/status-badge.tsx +13 -1
- package/src/app/environment/page.tsx +415 -0
- package/src/app/globals.css +48 -0
- package/src/app/hooks/use-api.ts +80 -0
- package/src/config/schema.ts +21 -0
- package/src/index.ts +12 -0
- package/src/server/auth/keys.ts +50 -5
- package/src/server/auth/session.ts +38 -11
- package/src/server/index.ts +144 -18
- package/src/server/metadata.ts +2 -0
- package/src/server/middleware/rate-limit.ts +20 -1
- package/src/server/routes/beacons.ts +112 -0
- package/src/server/routes/broadcasts.ts +6 -2
- package/src/server/routes/runs.ts +20 -67
- package/src/server/routes/signals.ts +17 -13
- package/src/server/routes/v1/env.ts +144 -0
- package/src/server/routes/v1/events.ts +2 -2
- package/src/server/routes/v1/runs.ts +11 -35
- package/src/server/sse.ts +7 -2
- package/src/server/subscriber.ts +111 -0
- package/src/server/ws.ts +38 -4
- package/.next/standalone/packages/station-kit/.next/static/THKSkCipW_pj0F6DRXYEG/_buildManifest.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/285-ff198f0a909c4fdd.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/561-33d912169940283e.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/v/[n]/page-5eac0507f49a00ec.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-dee500ccc01f0821.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-e14e14f3e5b0b8a9.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-aac41ef7a470daab.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/playground/expression/page-dc9d91f3f50f4716.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-9e4c4f751a4bea72.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/page-738d98dc0b63166e.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-fc5654b31f57ac21.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-4b1c09a539a1ebcd.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-d2f2403dfede87cc.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/css/0be0e65a5f561f37.css +0 -1
- /package/.next/standalone/packages/station-kit/.next/static/{THKSkCipW_pj0F6DRXYEG → vTGEzSqxhR_E57YAFn9DC}/_ssgManifest.js +0 -0
package/src/app/hooks/use-api.ts
CHANGED
|
@@ -70,6 +70,26 @@ export interface SignalMeta {
|
|
|
70
70
|
maxConcurrency: number | null;
|
|
71
71
|
hasSteps: boolean;
|
|
72
72
|
stepNames: string[];
|
|
73
|
+
requiredEnv?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type EnvTargetKind = "signal" | "beacon";
|
|
77
|
+
|
|
78
|
+
export interface EnvTarget {
|
|
79
|
+
kind: EnvTargetKind;
|
|
80
|
+
name: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface EnvVar {
|
|
84
|
+
id: string;
|
|
85
|
+
key: string;
|
|
86
|
+
/** Null when the variable is secret (write-only). */
|
|
87
|
+
value: string | null;
|
|
88
|
+
secret: boolean;
|
|
89
|
+
targets: EnvTarget[];
|
|
90
|
+
createdAt: string;
|
|
91
|
+
updatedAt: string;
|
|
92
|
+
createdBy?: string;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
export interface BroadcastMeta {
|
|
@@ -81,6 +101,47 @@ export interface BroadcastMeta {
|
|
|
81
101
|
interval: string | null;
|
|
82
102
|
}
|
|
83
103
|
|
|
104
|
+
export type BeaconStatus =
|
|
105
|
+
| "stopped" | "starting" | "running" | "stopping" | "backoff" | "errored";
|
|
106
|
+
|
|
107
|
+
export interface BeaconInstance {
|
|
108
|
+
beaconName: string;
|
|
109
|
+
status: BeaconStatus;
|
|
110
|
+
desiredState: "running" | "stopped";
|
|
111
|
+
incarnation: number;
|
|
112
|
+
restartCount: number;
|
|
113
|
+
pid?: number;
|
|
114
|
+
config?: string;
|
|
115
|
+
startedAt?: string;
|
|
116
|
+
readyAt?: string;
|
|
117
|
+
lastHeartbeatAt?: string;
|
|
118
|
+
lastExitAt?: string;
|
|
119
|
+
lastExitReason?: "clean" | "failure" | "stopped" | "stalled";
|
|
120
|
+
lastError?: string;
|
|
121
|
+
nextRestartAt?: string;
|
|
122
|
+
createdAt: string;
|
|
123
|
+
updatedAt: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface BeaconListItem {
|
|
127
|
+
name: string;
|
|
128
|
+
filePath: string;
|
|
129
|
+
mode: "run" | "poll";
|
|
130
|
+
restartPolicy: string;
|
|
131
|
+
autoStart: boolean;
|
|
132
|
+
requiredEnv?: string[];
|
|
133
|
+
instance: BeaconInstance | null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface BeaconEvent {
|
|
137
|
+
id: string;
|
|
138
|
+
beaconName: string;
|
|
139
|
+
incarnation: number;
|
|
140
|
+
type: string;
|
|
141
|
+
message?: string;
|
|
142
|
+
at: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
84
145
|
// ─── Dynamic broadcasts / schedules / expressions ───────────────────
|
|
85
146
|
|
|
86
147
|
export type ScheduleKind = "signal" | "broadcast-static" | "broadcast-dynamic";
|
|
@@ -174,6 +235,16 @@ export function useApi() {
|
|
|
174
235
|
cancelBroadcastRun: (id: string) => fetchApi<{ cancelled: boolean }>(`/broadcast-runs/${id}/cancel`, { method: "POST" }),
|
|
175
236
|
rerunBroadcastRun: (id: string) => fetchApi<{ id: string; broadcastName: string; status: string }>(`/broadcast-runs/${id}/rerun`, { method: "POST" }),
|
|
176
237
|
|
|
238
|
+
// Beacons
|
|
239
|
+
getBeacons: () => fetchApi<BeaconListItem[]>("/beacons"),
|
|
240
|
+
getBeacon: (name: string) => fetchApi<BeaconListItem>(`/beacons/${encodeURIComponent(name)}`),
|
|
241
|
+
getBeaconEvents: (name: string) => fetchApi<BeaconEvent[]>(`/beacons/${encodeURIComponent(name)}/events`),
|
|
242
|
+
getBeaconLogs: (name: string) =>
|
|
243
|
+
fetchApi<Array<{ runId: string; signalName: string; level: string; message: string; timestamp: string }>>(`/beacons/${encodeURIComponent(name)}/logs`),
|
|
244
|
+
startBeacon: (name: string) => fetchApi<{ started: boolean }>(`/beacons/${encodeURIComponent(name)}/start`, { method: "POST" }),
|
|
245
|
+
stopBeacon: (name: string) => fetchApi<{ stopped: boolean }>(`/beacons/${encodeURIComponent(name)}/stop`, { method: "POST" }),
|
|
246
|
+
restartBeacon: (name: string) => fetchApi<{ restarted: boolean }>(`/beacons/${encodeURIComponent(name)}/restart`, { method: "POST" }),
|
|
247
|
+
|
|
177
248
|
// Dynamic broadcast definitions (v1)
|
|
178
249
|
getBroadcastDefinitions: () =>
|
|
179
250
|
fetchApi<DynamicBroadcastSpec[]>("/v1/broadcast-definitions"),
|
|
@@ -246,6 +317,15 @@ export function useApi() {
|
|
|
246
317
|
body: JSON.stringify({ source }),
|
|
247
318
|
}),
|
|
248
319
|
|
|
320
|
+
// Environment variables (v1 — GET is read-scoped, mutations admin-scoped)
|
|
321
|
+
getEnvVars: () => fetchApi<EnvVar[]>("/v1/env"),
|
|
322
|
+
createEnvVar: (input: { key: string; value: string; secret?: boolean; targets?: EnvTarget[] }) =>
|
|
323
|
+
fetchApi<EnvVar>("/v1/env", { method: "POST", body: JSON.stringify(input) }),
|
|
324
|
+
updateEnvVar: (id: string, patch: Partial<{ value: string; secret: boolean; targets: EnvTarget[] }>) =>
|
|
325
|
+
fetchApi<EnvVar>(`/v1/env/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
|
326
|
+
deleteEnvVar: (id: string) =>
|
|
327
|
+
fetchApi<{ deleted: boolean }>(`/v1/env/${id}`, { method: "DELETE" }),
|
|
328
|
+
|
|
249
329
|
// API Keys (v1 admin routes — session cookie provides admin scope)
|
|
250
330
|
getApiKeys: () => fetchApi<Array<{ id: string; name: string; keyPrefix: string; scopes: string[]; createdAt: string; lastUsed: string | null; expiresAt: string | null; revoked: boolean }>>("/v1/keys"),
|
|
251
331
|
createApiKey: (name: string, scopes: string[]) =>
|
package/src/config/schema.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SignalQueueAdapter } from "station-signal";
|
|
2
2
|
import type { BroadcastQueueAdapter } from "station-broadcast";
|
|
3
|
+
import type { BeaconStateAdapter } from "station-beacon";
|
|
3
4
|
import type { ScheduleAdapter } from "station-schedules";
|
|
5
|
+
import type { EnvStorageAdapter } from "station-env";
|
|
4
6
|
import type { ApiKeyStorageAdapter } from "../server/auth/keys.js";
|
|
5
7
|
import type { LogStorageAdapter } from "../server/log-store.js";
|
|
6
8
|
|
|
@@ -37,11 +39,26 @@ export interface StationConfig {
|
|
|
37
39
|
host: string;
|
|
38
40
|
adapter?: SignalQueueAdapter;
|
|
39
41
|
broadcastAdapter?: BroadcastQueueAdapter;
|
|
42
|
+
/**
|
|
43
|
+
* Optional beacon supervision-state adapter. When provided (or when
|
|
44
|
+
* `beaconsDir` is set), the dashboard supervises long-running beacons and
|
|
45
|
+
* exposes them under `/api/beacons`.
|
|
46
|
+
*/
|
|
47
|
+
beaconAdapter?: BeaconStateAdapter;
|
|
40
48
|
/**
|
|
41
49
|
* Optional schedule storage adapter. When provided, runtime-editable
|
|
42
50
|
* schedules are persisted here and reconciled by both runners.
|
|
43
51
|
*/
|
|
44
52
|
scheduleAdapter?: ScheduleAdapter;
|
|
53
|
+
/**
|
|
54
|
+
* Pluggable storage backend for runtime environment variables. Defaults to a
|
|
55
|
+
* JSON file at `<dataDir>/station-env.json` (no native dependencies). When
|
|
56
|
+
* set — or by default — signals/beacons can require env vars via `.env()`,
|
|
57
|
+
* and variables defined here (globally or scoped to specific targets) are
|
|
58
|
+
* injected into runs. For multi-process deployments pass a durable adapter
|
|
59
|
+
* from a `station-adapter-*` package's `/env` subpath.
|
|
60
|
+
*/
|
|
61
|
+
envStorage?: EnvStorageAdapter;
|
|
45
62
|
/**
|
|
46
63
|
* Pluggable storage backend for run logs. Defaults to a `FileLogStorage`
|
|
47
64
|
* (append-only JSONL file at `<dataDir>/station-logs.jsonl`, no native
|
|
@@ -52,6 +69,7 @@ export interface StationConfig {
|
|
|
52
69
|
logStorage?: LogStorageAdapter;
|
|
53
70
|
signalsDir?: string;
|
|
54
71
|
broadcastsDir?: string;
|
|
72
|
+
beaconsDir?: string;
|
|
55
73
|
stationDir: string;
|
|
56
74
|
runner: RunnerConfig;
|
|
57
75
|
broadcastRunner: BroadcastRunnerConfig;
|
|
@@ -108,10 +126,13 @@ export function resolveConfig(input: StationUserConfig): StationConfig {
|
|
|
108
126
|
host: input.host ?? envHost ?? DEFAULTS.host,
|
|
109
127
|
adapter: input.adapter,
|
|
110
128
|
broadcastAdapter: input.broadcastAdapter,
|
|
129
|
+
beaconAdapter: input.beaconAdapter,
|
|
111
130
|
scheduleAdapter: input.scheduleAdapter,
|
|
131
|
+
envStorage: input.envStorage,
|
|
112
132
|
logStorage: input.logStorage,
|
|
113
133
|
signalsDir: input.signalsDir,
|
|
114
134
|
broadcastsDir: input.broadcastsDir,
|
|
135
|
+
beaconsDir: input.beaconsDir,
|
|
115
136
|
stationDir: input.stationDir ?? DEFAULTS.stationDir,
|
|
116
137
|
runner: {
|
|
117
138
|
pollIntervalMs: input.runner?.pollIntervalMs ?? DEFAULTS.runner.pollIntervalMs,
|
package/src/index.ts
CHANGED
|
@@ -7,3 +7,15 @@ export function defineConfig(config: StationUserConfig): StationUserConfig {
|
|
|
7
7
|
export type { StationConfig, StationUserConfig, AuthConfig, DeployConfig } from "./config/schema.js";
|
|
8
8
|
export { resolveConfig } from "./config/schema.js";
|
|
9
9
|
export { loadConfig } from "./config/loader.js";
|
|
10
|
+
|
|
11
|
+
// Re-export the runtime env store surface so consumers can construct custom
|
|
12
|
+
// storage or the store itself without a separate `station-env` import.
|
|
13
|
+
export {
|
|
14
|
+
EnvStore,
|
|
15
|
+
MemoryEnvStorage,
|
|
16
|
+
FileEnvStorage,
|
|
17
|
+
type EnvStorageAdapter,
|
|
18
|
+
type EnvVar,
|
|
19
|
+
type EnvVarPublic,
|
|
20
|
+
type EnvTarget,
|
|
21
|
+
} from "station-env";
|
package/src/server/auth/keys.ts
CHANGED
|
@@ -137,8 +137,15 @@ export class FileKeyStorage implements ApiKeyStorageAdapter {
|
|
|
137
137
|
touch(id: string, lastUsedIso: string): void {
|
|
138
138
|
const r = this.records.get(id);
|
|
139
139
|
if (!r) return;
|
|
140
|
+
const prev = r.lastUsed ? Date.parse(r.lastUsed) : 0;
|
|
140
141
|
r.lastUsed = lastUsedIso;
|
|
141
|
-
|
|
142
|
+
// lastUsed is advisory. flush() rewrites and fsyncs the whole file
|
|
143
|
+
// synchronously — doing that on every authenticated request stalls the
|
|
144
|
+
// event loop, so only persist when the value moves by at least a minute.
|
|
145
|
+
// Reads stay fresh because list() serves from memory.
|
|
146
|
+
if (Date.parse(lastUsedIso) - prev >= 60_000) {
|
|
147
|
+
this.flush();
|
|
148
|
+
}
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
revoke(id: string): boolean {
|
|
@@ -264,7 +271,15 @@ export class SqliteKeyStorage implements ApiKeyStorageAdapter {
|
|
|
264
271
|
}));
|
|
265
272
|
}
|
|
266
273
|
|
|
274
|
+
/** Last persisted lastUsed per key id, to skip per-request UPDATEs. */
|
|
275
|
+
private lastTouched = new Map<string, number>();
|
|
276
|
+
|
|
267
277
|
touch(id: string, lastUsedIso: string): void {
|
|
278
|
+
// lastUsed is advisory — throttle writes to once a minute per key.
|
|
279
|
+
const prev = this.lastTouched.get(id) ?? 0;
|
|
280
|
+
const next = Date.parse(lastUsedIso);
|
|
281
|
+
if (next - prev < 60_000) return;
|
|
282
|
+
this.lastTouched.set(id, next);
|
|
268
283
|
this.db.prepare(`UPDATE ${this.table} SET last_used = ? WHERE id = ?`).run(lastUsedIso, id);
|
|
269
284
|
}
|
|
270
285
|
|
|
@@ -332,8 +347,20 @@ export class MemoryKeyStorage implements ApiKeyStorageAdapter {
|
|
|
332
347
|
|
|
333
348
|
// ─── KeyStore — owns crypto, delegates persistence ──────────────────
|
|
334
349
|
|
|
350
|
+
export interface KeyStoreOptions {
|
|
351
|
+
/**
|
|
352
|
+
* Server-side secret ("pepper") mixed into stored key hashes via HMAC.
|
|
353
|
+
* Defense-in-depth: a leaked key file alone can't be brute-forced or
|
|
354
|
+
* precomputed against without also stealing this secret. Optional — when
|
|
355
|
+
* omitted, keys are hashed with plain SHA-256 (legacy behavior). Existing
|
|
356
|
+
* plain-SHA-256 keys keep verifying after a pepper is added (see verify()).
|
|
357
|
+
*/
|
|
358
|
+
pepper?: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
335
361
|
export class KeyStore {
|
|
336
362
|
private storage: ApiKeyStorageAdapter;
|
|
363
|
+
private pepper?: Buffer;
|
|
337
364
|
|
|
338
365
|
/**
|
|
339
366
|
* Pass an `ApiKeyStorageAdapter` for any backend. The string overload is
|
|
@@ -341,7 +368,7 @@ export class KeyStore {
|
|
|
341
368
|
* at the given path. (Previously this returned a SqliteKeyStorage; SQLite
|
|
342
369
|
* is now opt-in to avoid native build dependencies.)
|
|
343
370
|
*/
|
|
344
|
-
constructor(storageOrPath: ApiKeyStorageAdapter | string) {
|
|
371
|
+
constructor(storageOrPath: ApiKeyStorageAdapter | string, options?: KeyStoreOptions) {
|
|
345
372
|
if (typeof storageOrPath === "string") {
|
|
346
373
|
const filePath = storageOrPath.endsWith(".db")
|
|
347
374
|
? storageOrPath.replace(/\.db$/, ".json")
|
|
@@ -350,13 +377,27 @@ export class KeyStore {
|
|
|
350
377
|
} else {
|
|
351
378
|
this.storage = storageOrPath;
|
|
352
379
|
}
|
|
380
|
+
if (options?.pepper) this.pepper = Buffer.from(options.pepper, "utf8");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Peppered hash for new keys — HMAC-SHA256 when a pepper is set, else SHA-256. */
|
|
384
|
+
private hashKey(rawKey: string): string {
|
|
385
|
+
if (this.pepper) {
|
|
386
|
+
return crypto.createHmac("sha256", this.pepper).update(rawKey).digest("hex");
|
|
387
|
+
}
|
|
388
|
+
return crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Unpeppered SHA-256 — used to verify keys stored before a pepper was configured. */
|
|
392
|
+
private legacyHashKey(rawKey: string): string {
|
|
393
|
+
return crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
353
394
|
}
|
|
354
395
|
|
|
355
396
|
/** Generate a new API key. Returns the full key (only shown once) and the stored record. */
|
|
356
397
|
async create(name: string, scopes: string[] = ["trigger", "read"]): Promise<{ key: string; record: ApiKey }> {
|
|
357
398
|
const id = crypto.randomUUID();
|
|
358
399
|
const rawKey = `sk_live_${crypto.randomBytes(16).toString("hex")}`;
|
|
359
|
-
const keyHash =
|
|
400
|
+
const keyHash = this.hashKey(rawKey);
|
|
360
401
|
const keyPrefix = rawKey.slice(0, 12);
|
|
361
402
|
const createdAt = new Date().toISOString();
|
|
362
403
|
|
|
@@ -370,8 +411,12 @@ export class KeyStore {
|
|
|
370
411
|
|
|
371
412
|
/** Verify an API key. Returns the key record if valid, null otherwise. */
|
|
372
413
|
async verify(rawKey: string): Promise<ApiKey | null> {
|
|
373
|
-
|
|
374
|
-
|
|
414
|
+
let record = await this.storage.findByHash(this.hashKey(rawKey));
|
|
415
|
+
// Fall back to the unpeppered hash for keys created before the pepper was
|
|
416
|
+
// configured, so enabling a pepper doesn't invalidate existing keys.
|
|
417
|
+
if (!record && this.pepper) {
|
|
418
|
+
record = await this.storage.findByHash(this.legacyHashKey(rawKey));
|
|
419
|
+
}
|
|
375
420
|
if (!record) return null;
|
|
376
421
|
if (record.revoked) return null;
|
|
377
422
|
if (record.expiresAt && new Date(record.expiresAt) < new Date()) return null;
|
|
@@ -8,17 +8,45 @@ export interface SessionConfig {
|
|
|
8
8
|
sessionTtlMs?: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Derive the HMAC signing key from the password via HKDF instead of using
|
|
13
|
+
* the cleartext password as the key directly — issued cookies should not be
|
|
14
|
+
* HMACs keyed with the literal admin credential. Cached because HKDF per
|
|
15
|
+
* request would be wasted CPU.
|
|
16
|
+
*/
|
|
17
|
+
const secretCache = new Map<string, Buffer>();
|
|
18
|
+
function sessionSecret(config: SessionConfig): Buffer {
|
|
19
|
+
let secret = secretCache.get(config.password);
|
|
20
|
+
if (!secret) {
|
|
21
|
+
secret = Buffer.from(
|
|
22
|
+
crypto.hkdfSync("sha256", config.password, "station-kit", "session-token-v1", 32),
|
|
23
|
+
);
|
|
24
|
+
secretCache.set(config.password, secret);
|
|
25
|
+
}
|
|
26
|
+
return secret;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sign(payload: string, config: SessionConfig): string {
|
|
30
|
+
const hmac = crypto.createHmac("sha256", sessionSecret(config));
|
|
14
31
|
hmac.update(payload);
|
|
15
32
|
return hmac.digest("hex");
|
|
16
33
|
}
|
|
17
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Timing-safe string comparison that doesn't leak length: both inputs are
|
|
37
|
+
* hashed to a fixed size before `timingSafeEqual` (which requires equal
|
|
38
|
+
* lengths and would otherwise short-circuit on a length check).
|
|
39
|
+
*/
|
|
40
|
+
function safeEqual(a: string, b: string): boolean {
|
|
41
|
+
const ha = crypto.createHash("sha256").update(a).digest();
|
|
42
|
+
const hb = crypto.createHash("sha256").update(b).digest();
|
|
43
|
+
return crypto.timingSafeEqual(ha, hb);
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
export function createSessionToken(config: SessionConfig): string {
|
|
19
47
|
const exp = Date.now() + (config.sessionTtlMs ?? SESSION_TTL_MS);
|
|
20
48
|
const payload = `${config.username}:${exp}`;
|
|
21
|
-
const signature = sign(payload, config
|
|
49
|
+
const signature = sign(payload, config);
|
|
22
50
|
return Buffer.from(`${payload}:${signature}`).toString("base64url");
|
|
23
51
|
}
|
|
24
52
|
|
|
@@ -31,18 +59,17 @@ export function verifySessionToken(token: string, config: SessionConfig): boolea
|
|
|
31
59
|
const exp = parseInt(expStr, 10);
|
|
32
60
|
if (isNaN(exp) || Date.now() > exp) return false;
|
|
33
61
|
if (username !== config.username) return false;
|
|
34
|
-
const expected = sign(`${username}:${expStr}`, config
|
|
35
|
-
return
|
|
62
|
+
const expected = sign(`${username}:${expStr}`, config);
|
|
63
|
+
return safeEqual(sig, expected);
|
|
36
64
|
} catch {
|
|
37
65
|
return false;
|
|
38
66
|
}
|
|
39
67
|
}
|
|
40
68
|
|
|
41
69
|
export function verifyCredentials(username: string, password: string, config: SessionConfig): boolean {
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const passMatch = password
|
|
46
|
-
crypto.timingSafeEqual(Buffer.from(password), Buffer.from(config.password));
|
|
70
|
+
// Evaluate both comparisons unconditionally so a valid username isn't
|
|
71
|
+
// distinguishable from an invalid one by response time.
|
|
72
|
+
const userMatch = safeEqual(username, config.username);
|
|
73
|
+
const passMatch = safeEqual(password, config.password);
|
|
47
74
|
return userMatch && passMatch;
|
|
48
75
|
}
|