station-kit 1.0.7 → 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 +10 -3
- package/.next/standalone/packages/station-kit/.next/build-manifest.json +3 -3
- package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +112 -16
- 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 +10 -3
- 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 +10 -2
- 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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.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 +7 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +55 -5
- 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 +15 -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/index.ts +2 -0
- package/src/server/auth/keys.ts +191 -56
- package/src/server/index.ts +78 -5
- 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/pHHaxeGaet0VW1dhcIcuY/_buildManifest.js +0 -1
- /package/.next/standalone/packages/station-kit/.next/static/{pHHaxeGaet0VW1dhcIcuY → demLiQWDy62JuUkBw-ILG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useApi } from "../../hooks/use-api";
|
|
6
|
+
import { useBreadcrumb } from "../../hooks/use-breadcrumb";
|
|
7
|
+
import { ScheduleForm, type ScheduleFormValue } from "../components/schedule-form";
|
|
8
|
+
import { ApiPanel } from "../../components/api-panel";
|
|
9
|
+
|
|
10
|
+
const INITIAL: ScheduleFormValue = {
|
|
11
|
+
kind: "signal",
|
|
12
|
+
target: "",
|
|
13
|
+
interval: "5m",
|
|
14
|
+
input: "{}",
|
|
15
|
+
enabled: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const INTERVAL_REGEX = /^(\d+)(ms|s|m|h|d|w)$/i;
|
|
19
|
+
const UNIT_MS: Record<string, number> = {
|
|
20
|
+
ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function parseIntervalLocal(s: string): number | null {
|
|
24
|
+
const m = INTERVAL_REGEX.exec(s.trim());
|
|
25
|
+
if (!m) return null;
|
|
26
|
+
return Number(m[1]) * UNIT_MS[m[2].toLowerCase()];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function NewSchedulePage() {
|
|
30
|
+
const api = useApi();
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
const [value, setValue] = useState<ScheduleFormValue>(INITIAL);
|
|
33
|
+
const [busy, setBusy] = useState(false);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
useBreadcrumb(
|
|
37
|
+
[
|
|
38
|
+
{ label: "Schedules", href: "/schedules" },
|
|
39
|
+
{ label: "New" },
|
|
40
|
+
],
|
|
41
|
+
"schedules",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Compute the next 5 fire times locally so users can see the cadence
|
|
45
|
+
// before saving.
|
|
46
|
+
const previewFires = useMemo(() => {
|
|
47
|
+
const ms = parseIntervalLocal(value.interval);
|
|
48
|
+
if (ms === null || ms <= 0) return [];
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const out: string[] = [];
|
|
51
|
+
for (let i = 1; i <= 5; i++) {
|
|
52
|
+
out.push(new Date(now + ms * i).toISOString());
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}, [value.interval]);
|
|
56
|
+
|
|
57
|
+
async function handleSave() {
|
|
58
|
+
setError(null);
|
|
59
|
+
if (!value.target) {
|
|
60
|
+
setError("Target is required.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let inputParsed: unknown = undefined;
|
|
64
|
+
if (value.input.trim()) {
|
|
65
|
+
try {
|
|
66
|
+
inputParsed = JSON.parse(value.input);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
setError(`Input JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
setBusy(true);
|
|
73
|
+
try {
|
|
74
|
+
const res = await api.createSchedule({
|
|
75
|
+
kind: value.kind,
|
|
76
|
+
target: value.target,
|
|
77
|
+
interval: value.interval,
|
|
78
|
+
input: inputParsed,
|
|
79
|
+
enabled: value.enabled,
|
|
80
|
+
});
|
|
81
|
+
router.push(`/schedules/${res.data.id}`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
84
|
+
} finally {
|
|
85
|
+
setBusy(false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<h1 className="page-title">New schedule</h1>
|
|
92
|
+
|
|
93
|
+
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr)", gap: "2rem" }}>
|
|
94
|
+
<div>
|
|
95
|
+
<ScheduleForm value={value} onChange={setValue} />
|
|
96
|
+
|
|
97
|
+
{error && (
|
|
98
|
+
<div style={{
|
|
99
|
+
marginTop: "1rem",
|
|
100
|
+
padding: "0.625rem 0.75rem",
|
|
101
|
+
background: "var(--error-bg, #fee)",
|
|
102
|
+
color: "var(--error, #b00)",
|
|
103
|
+
border: "1px solid var(--error, #b00)",
|
|
104
|
+
borderRadius: "4px",
|
|
105
|
+
fontSize: "0.8125rem",
|
|
106
|
+
maxWidth: "640px",
|
|
107
|
+
}}>{error}</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<div style={{ marginTop: "1.5rem", display: "flex", gap: "0.5rem" }}>
|
|
111
|
+
<button className="btn btn--primary" onClick={handleSave} disabled={busy}>
|
|
112
|
+
{busy ? "Creating..." : "Create schedule"}
|
|
113
|
+
</button>
|
|
114
|
+
<button className="btn" onClick={() => router.push("/schedules")}>Cancel</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<aside>
|
|
119
|
+
<h3 className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "0.5rem" }}>
|
|
120
|
+
Next fire times (preview)
|
|
121
|
+
</h3>
|
|
122
|
+
{previewFires.length === 0 ? (
|
|
123
|
+
<div className="mono" style={{ fontSize: "0.75rem", color: "var(--error, #b00)" }}>
|
|
124
|
+
Invalid interval. Use formats like "30s", "5m", "1h", "1d", "1w".
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
|
|
128
|
+
{previewFires.map((iso, i) => (
|
|
129
|
+
<li key={i} className="mono" style={{
|
|
130
|
+
fontSize: "0.8125rem",
|
|
131
|
+
padding: "0.375rem 0",
|
|
132
|
+
borderBottom: "1px dashed var(--border)",
|
|
133
|
+
color: i === 0 ? "var(--text)" : "var(--muted)",
|
|
134
|
+
}}>
|
|
135
|
+
{new Date(iso).toLocaleString()}
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
)}
|
|
140
|
+
</aside>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<ApiPanel
|
|
144
|
+
title="Create this schedule"
|
|
145
|
+
snippets={[
|
|
146
|
+
{
|
|
147
|
+
label: "POST /api/v1/schedules",
|
|
148
|
+
method: "POST",
|
|
149
|
+
path: "/api/v1/schedules",
|
|
150
|
+
body: {
|
|
151
|
+
kind: value.kind,
|
|
152
|
+
target: value.target,
|
|
153
|
+
interval: value.interval,
|
|
154
|
+
enabled: value.enabled,
|
|
155
|
+
input: tryParse(value.input),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
]}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function tryParse(s: string): unknown {
|
|
165
|
+
try { return JSON.parse(s); } catch { return s; }
|
|
166
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { useApi, type Schedule } from "../hooks/use-api";
|
|
7
|
+
import { useBreadcrumb } from "../hooks/use-breadcrumb";
|
|
8
|
+
import { ApiPanel } from "../components/api-panel";
|
|
9
|
+
|
|
10
|
+
const KIND_LABEL: Record<Schedule["kind"], string> = {
|
|
11
|
+
signal: "Signal",
|
|
12
|
+
"broadcast-static": "Broadcast (file)",
|
|
13
|
+
"broadcast-dynamic": "Broadcast (dynamic)",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function SchedulesPage() {
|
|
17
|
+
const api = useApi();
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
useBreadcrumb([{ label: "Schedules" }], "schedules");
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
api.getSchedules()
|
|
27
|
+
.then((res) => setSchedules(res.data))
|
|
28
|
+
.catch((err) => setError(err instanceof Error ? err.message : String(err)))
|
|
29
|
+
.finally(() => setLoading(false));
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
if (loading) {
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<h1 className="page-title">Schedules</h1>
|
|
36
|
+
<div className="loading-bar"><div className="loading-bar-fill" /></div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "1rem" }}>
|
|
44
|
+
<h1 className="page-title" style={{ margin: 0 }}>Schedules</h1>
|
|
45
|
+
<Link href="/schedules/new" className="btn btn--primary">+ New schedule</Link>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{error && (
|
|
49
|
+
<div style={{
|
|
50
|
+
marginBottom: "1rem",
|
|
51
|
+
padding: "0.625rem 0.75rem",
|
|
52
|
+
background: "var(--error-bg, #fee)",
|
|
53
|
+
color: "var(--error, #b00)",
|
|
54
|
+
borderRadius: "4px",
|
|
55
|
+
fontSize: "0.8125rem",
|
|
56
|
+
}}>{error}</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{schedules.length === 0 ? (
|
|
60
|
+
<div className="empty-state">
|
|
61
|
+
<p className="empty-state-text">
|
|
62
|
+
No schedules yet. <Link href="/schedules/new">Create one</Link> to fire signals or broadcasts on intervals.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
) : (
|
|
66
|
+
<table className="station-table">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>
|
|
69
|
+
<th>Target</th>
|
|
70
|
+
<th>Kind</th>
|
|
71
|
+
<th>Interval</th>
|
|
72
|
+
<th>Next run</th>
|
|
73
|
+
<th>Last run</th>
|
|
74
|
+
<th>Status</th>
|
|
75
|
+
<th></th>
|
|
76
|
+
</tr>
|
|
77
|
+
</thead>
|
|
78
|
+
<tbody>
|
|
79
|
+
{schedules.map((s) => (
|
|
80
|
+
<tr
|
|
81
|
+
key={s.id}
|
|
82
|
+
className="clickable-row"
|
|
83
|
+
onClick={() => router.push(`/schedules/${s.id}`)}
|
|
84
|
+
>
|
|
85
|
+
<td className="mono">{s.target}</td>
|
|
86
|
+
<td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>{KIND_LABEL[s.kind]}</td>
|
|
87
|
+
<td className="mono">{s.interval}</td>
|
|
88
|
+
<td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>
|
|
89
|
+
{new Date(s.nextRunAt).toLocaleString()}
|
|
90
|
+
</td>
|
|
91
|
+
<td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>
|
|
92
|
+
{s.lastRunAt ? new Date(s.lastRunAt).toLocaleString() : "—"}
|
|
93
|
+
</td>
|
|
94
|
+
<td className="mono" style={{ fontSize: "0.75rem" }}>
|
|
95
|
+
<span style={{
|
|
96
|
+
padding: "0.125rem 0.375rem",
|
|
97
|
+
borderRadius: "3px",
|
|
98
|
+
background: s.enabled ? "var(--success-bg, #efe)" : "var(--surface)",
|
|
99
|
+
color: s.enabled ? "var(--success, #060)" : "var(--muted)",
|
|
100
|
+
}}>
|
|
101
|
+
{s.enabled ? "enabled" : "disabled"}
|
|
102
|
+
</span>
|
|
103
|
+
</td>
|
|
104
|
+
<td></td>
|
|
105
|
+
</tr>
|
|
106
|
+
))}
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<ApiPanel
|
|
112
|
+
title="List & manage schedules"
|
|
113
|
+
snippets={[
|
|
114
|
+
{ label: "List all", method: "GET", path: "/api/v1/schedules" },
|
|
115
|
+
{ label: "List by kind", method: "GET", path: "/api/v1/schedules", query: { kind: "signal" } },
|
|
116
|
+
{
|
|
117
|
+
label: "Create",
|
|
118
|
+
method: "POST",
|
|
119
|
+
path: "/api/v1/schedules",
|
|
120
|
+
body: { kind: "signal", target: "<signalName>", interval: "5m", enabled: true, input: {} },
|
|
121
|
+
},
|
|
122
|
+
]}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
package/src/config/schema.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { SignalQueueAdapter } from "station-signal";
|
|
2
2
|
import type { BroadcastQueueAdapter } from "station-broadcast";
|
|
3
|
+
import type { ScheduleAdapter } from "station-schedules";
|
|
4
|
+
import type { ApiKeyStorageAdapter } from "../server/auth/keys.js";
|
|
3
5
|
|
|
4
6
|
export interface AuthConfig {
|
|
5
7
|
username: string;
|
|
6
8
|
password: string;
|
|
7
9
|
sessionTtlMs?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Pluggable storage backend for API keys. Defaults to a SQLite store at
|
|
12
|
+
* `<dataDir>/station-keys.db`. Provide a custom adapter to host keys in
|
|
13
|
+
* Postgres, MySQL, Redis, etc.
|
|
14
|
+
*/
|
|
15
|
+
keyStorage?: ApiKeyStorageAdapter;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
export interface RunnerConfig {
|
|
@@ -27,6 +35,11 @@ export interface StationConfig {
|
|
|
27
35
|
host: string;
|
|
28
36
|
adapter?: SignalQueueAdapter;
|
|
29
37
|
broadcastAdapter?: BroadcastQueueAdapter;
|
|
38
|
+
/**
|
|
39
|
+
* Optional schedule storage adapter. When provided, runtime-editable
|
|
40
|
+
* schedules are persisted here and reconciled by both runners.
|
|
41
|
+
*/
|
|
42
|
+
scheduleAdapter?: ScheduleAdapter;
|
|
30
43
|
signalsDir?: string;
|
|
31
44
|
broadcastsDir?: string;
|
|
32
45
|
stationDir: string;
|
|
@@ -85,6 +98,7 @@ export function resolveConfig(input: StationUserConfig): StationConfig {
|
|
|
85
98
|
host: input.host ?? envHost ?? DEFAULTS.host,
|
|
86
99
|
adapter: input.adapter,
|
|
87
100
|
broadcastAdapter: input.broadcastAdapter,
|
|
101
|
+
scheduleAdapter: input.scheduleAdapter,
|
|
88
102
|
signalsDir: input.signalsDir,
|
|
89
103
|
broadcastsDir: input.broadcastsDir,
|
|
90
104
|
stationDir: input.stationDir ?? DEFAULTS.stationDir,
|
package/src/index.ts
CHANGED
|
@@ -5,3 +5,5 @@ export function defineConfig(config: StationUserConfig): StationUserConfig {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export type { StationConfig, StationUserConfig, AuthConfig, DeployConfig } from "./config/schema.js";
|
|
8
|
+
export { resolveConfig } from "./config/schema.js";
|
|
9
|
+
export { loadConfig } from "./config/loader.js";
|
package/src/server/auth/keys.ts
CHANGED
|
@@ -13,14 +13,49 @@ export interface ApiKey {
|
|
|
13
13
|
revoked: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export
|
|
16
|
+
export type ApiKeyPublic = Omit<ApiKey, "keyHash">;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pluggable storage backend for API keys. Implementations only persist and
|
|
20
|
+
* query records — hashing, key generation, and verification logic live in
|
|
21
|
+
* the KeyStore. May be sync or async; the KeyStore awaits all results.
|
|
22
|
+
*/
|
|
23
|
+
export interface ApiKeyStorageAdapter {
|
|
24
|
+
insert(record: ApiKey): Promise<void> | void;
|
|
25
|
+
findByHash(keyHash: string): Promise<ApiKey | null> | ApiKey | null;
|
|
26
|
+
list(): Promise<ApiKeyPublic[]> | ApiKeyPublic[];
|
|
27
|
+
touch(id: string, lastUsedIso: string): Promise<void> | void;
|
|
28
|
+
revoke(id: string): Promise<boolean> | boolean;
|
|
29
|
+
close?(): Promise<void> | void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── SQLite default ─────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export interface SqliteKeyStorageOptions {
|
|
35
|
+
dbPath: string;
|
|
36
|
+
/** Override the table name (default: "api_keys"). */
|
|
37
|
+
tableName?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default ApiKeyStorageAdapter backed by better-sqlite3. Used by the Station
|
|
42
|
+
* server when no `keyStorage` is configured. For Postgres / MySQL / Redis,
|
|
43
|
+
* implement `ApiKeyStorageAdapter` and pass it to `KeyStore` directly.
|
|
44
|
+
*/
|
|
45
|
+
export class SqliteKeyStorage implements ApiKeyStorageAdapter {
|
|
17
46
|
private db: Database.Database;
|
|
47
|
+
private table: string;
|
|
18
48
|
|
|
19
|
-
constructor(
|
|
20
|
-
|
|
49
|
+
constructor(options: SqliteKeyStorageOptions) {
|
|
50
|
+
const tableName = options.tableName ?? "api_keys";
|
|
51
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
52
|
+
throw new Error(`Invalid table name "${tableName}"`);
|
|
53
|
+
}
|
|
54
|
+
this.table = tableName;
|
|
55
|
+
this.db = new Database(options.dbPath);
|
|
21
56
|
this.db.pragma("journal_mode = WAL");
|
|
22
57
|
this.db.exec(`
|
|
23
|
-
CREATE TABLE IF NOT EXISTS
|
|
58
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
24
59
|
id TEXT PRIMARY KEY,
|
|
25
60
|
name TEXT NOT NULL,
|
|
26
61
|
key_hash TEXT NOT NULL UNIQUE,
|
|
@@ -34,79 +69,179 @@ export class KeyStore {
|
|
|
34
69
|
`);
|
|
35
70
|
}
|
|
36
71
|
|
|
72
|
+
insert(record: ApiKey): void {
|
|
73
|
+
this.db.prepare(`
|
|
74
|
+
INSERT INTO ${this.table}
|
|
75
|
+
(id, name, key_hash, key_prefix, scopes, created_at, last_used, expires_at, revoked)
|
|
76
|
+
VALUES
|
|
77
|
+
(@id, @name, @key_hash, @key_prefix, @scopes, @created_at, @last_used, @expires_at, @revoked)
|
|
78
|
+
`).run({
|
|
79
|
+
id: record.id,
|
|
80
|
+
name: record.name,
|
|
81
|
+
key_hash: record.keyHash,
|
|
82
|
+
key_prefix: record.keyPrefix,
|
|
83
|
+
scopes: JSON.stringify(record.scopes),
|
|
84
|
+
created_at: record.createdAt,
|
|
85
|
+
last_used: record.lastUsed,
|
|
86
|
+
expires_at: record.expiresAt,
|
|
87
|
+
revoked: record.revoked ? 1 : 0,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
findByHash(keyHash: string): ApiKey | null {
|
|
92
|
+
const row = this.db
|
|
93
|
+
.prepare(`SELECT id, name, key_hash, key_prefix, scopes, created_at, last_used, expires_at, revoked
|
|
94
|
+
FROM ${this.table} WHERE key_hash = ?`)
|
|
95
|
+
.get(keyHash) as Record<string, unknown> | undefined;
|
|
96
|
+
return row ? rowToApiKey(row) : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
list(): ApiKeyPublic[] {
|
|
100
|
+
const rows = this.db
|
|
101
|
+
.prepare(`SELECT id, name, key_prefix, scopes, created_at, last_used, expires_at, revoked
|
|
102
|
+
FROM ${this.table} ORDER BY created_at DESC`)
|
|
103
|
+
.all() as Record<string, unknown>[];
|
|
104
|
+
return rows.map((row) => ({
|
|
105
|
+
id: row.id as string,
|
|
106
|
+
name: row.name as string,
|
|
107
|
+
keyPrefix: row.key_prefix as string,
|
|
108
|
+
scopes: JSON.parse(row.scopes as string),
|
|
109
|
+
createdAt: row.created_at as string,
|
|
110
|
+
lastUsed: (row.last_used as string | null) ?? null,
|
|
111
|
+
expiresAt: (row.expires_at as string | null) ?? null,
|
|
112
|
+
revoked: Boolean(row.revoked),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
touch(id: string, lastUsedIso: string): void {
|
|
117
|
+
this.db.prepare(`UPDATE ${this.table} SET last_used = ? WHERE id = ?`).run(lastUsedIso, id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
revoke(id: string): boolean {
|
|
121
|
+
const result = this.db.prepare(`UPDATE ${this.table} SET revoked = 1 WHERE id = ?`).run(id);
|
|
122
|
+
return result.changes > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
close(): void {
|
|
126
|
+
this.db.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rowToApiKey(row: Record<string, unknown>): ApiKey {
|
|
131
|
+
return {
|
|
132
|
+
id: row.id as string,
|
|
133
|
+
name: row.name as string,
|
|
134
|
+
keyHash: row.key_hash as string,
|
|
135
|
+
keyPrefix: row.key_prefix as string,
|
|
136
|
+
scopes: JSON.parse(row.scopes as string),
|
|
137
|
+
createdAt: row.created_at as string,
|
|
138
|
+
lastUsed: (row.last_used as string | null) ?? null,
|
|
139
|
+
expiresAt: (row.expires_at as string | null) ?? null,
|
|
140
|
+
revoked: Boolean(row.revoked),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── In-memory storage for tests / ephemeral deployments ────────────
|
|
145
|
+
|
|
146
|
+
export class MemoryKeyStorage implements ApiKeyStorageAdapter {
|
|
147
|
+
private records = new Map<string, ApiKey>();
|
|
148
|
+
|
|
149
|
+
insert(record: ApiKey): void {
|
|
150
|
+
this.records.set(record.id, { ...record });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
findByHash(keyHash: string): ApiKey | null {
|
|
154
|
+
for (const r of this.records.values()) {
|
|
155
|
+
if (r.keyHash === keyHash) return { ...r };
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
list(): ApiKeyPublic[] {
|
|
161
|
+
return Array.from(this.records.values())
|
|
162
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
163
|
+
.map((r) => {
|
|
164
|
+
const { keyHash: _h, ...rest } = r;
|
|
165
|
+
return rest;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
touch(id: string, lastUsedIso: string): void {
|
|
170
|
+
const r = this.records.get(id);
|
|
171
|
+
if (r) r.lastUsed = lastUsedIso;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
revoke(id: string): boolean {
|
|
175
|
+
const r = this.records.get(id);
|
|
176
|
+
if (!r) return false;
|
|
177
|
+
r.revoked = true;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── KeyStore — owns crypto, delegates persistence ──────────────────
|
|
183
|
+
|
|
184
|
+
export class KeyStore {
|
|
185
|
+
private storage: ApiKeyStorageAdapter;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Pass an `ApiKeyStorageAdapter` for any backend. The string overload is
|
|
189
|
+
* retained for backwards compatibility — it constructs a SqliteKeyStorage
|
|
190
|
+
* at the given path.
|
|
191
|
+
*/
|
|
192
|
+
constructor(storageOrDbPath: ApiKeyStorageAdapter | string) {
|
|
193
|
+
if (typeof storageOrDbPath === "string") {
|
|
194
|
+
this.storage = new SqliteKeyStorage({ dbPath: storageOrDbPath });
|
|
195
|
+
} else {
|
|
196
|
+
this.storage = storageOrDbPath;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
37
200
|
/** Generate a new API key. Returns the full key (only shown once) and the stored record. */
|
|
38
|
-
create(name: string, scopes: string[] = ["trigger", "read"]): { key: string; record: ApiKey } {
|
|
201
|
+
async create(name: string, scopes: string[] = ["trigger", "read"]): Promise<{ key: string; record: ApiKey }> {
|
|
39
202
|
const id = crypto.randomUUID();
|
|
40
203
|
const rawKey = `sk_live_${crypto.randomBytes(16).toString("hex")}`;
|
|
41
204
|
const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
42
205
|
const keyPrefix = rawKey.slice(0, 12);
|
|
43
206
|
const createdAt = new Date().toISOString();
|
|
44
207
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`).run(id, name, keyHash, keyPrefix, JSON.stringify(scopes), createdAt);
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
key: rawKey,
|
|
52
|
-
record: { id, name, keyHash, keyPrefix, scopes, createdAt, lastUsed: null, expiresAt: null, revoked: false },
|
|
208
|
+
const record: ApiKey = {
|
|
209
|
+
id, name, keyHash, keyPrefix, scopes, createdAt,
|
|
210
|
+
lastUsed: null, expiresAt: null, revoked: false,
|
|
53
211
|
};
|
|
212
|
+
await this.storage.insert(record);
|
|
213
|
+
return { key: rawKey, record };
|
|
54
214
|
}
|
|
55
215
|
|
|
56
216
|
/** Verify an API key. Returns the key record if valid, null otherwise. */
|
|
57
|
-
verify(rawKey: string): ApiKey | null {
|
|
217
|
+
async verify(rawKey: string): Promise<ApiKey | null> {
|
|
58
218
|
const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
219
|
+
const record = await this.storage.findByHash(keyHash);
|
|
220
|
+
if (!record) return null;
|
|
221
|
+
if (record.revoked) return null;
|
|
222
|
+
if (record.expiresAt && new Date(record.expiresAt) < new Date()) return null;
|
|
63
223
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
224
|
+
// Touch is best-effort — don't block verification on the write. Wrap in
|
|
225
|
+
// an explicit deferred so a synchronous throw from a sync `touch()` is
|
|
226
|
+
// also swallowed, matching the async case.
|
|
227
|
+
Promise.resolve()
|
|
228
|
+
.then(() => this.storage.touch(record.id, new Date().toISOString()))
|
|
229
|
+
.catch(() => {});
|
|
67
230
|
|
|
68
|
-
|
|
69
|
-
this.db.prepare("UPDATE api_keys SET last_used = ? WHERE id = ?").run(new Date().toISOString(), row.id);
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
id: row.id as string,
|
|
73
|
-
name: row.name as string,
|
|
74
|
-
keyHash: row.key_hash as string,
|
|
75
|
-
keyPrefix: row.key_prefix as string,
|
|
76
|
-
scopes: JSON.parse(row.scopes as string),
|
|
77
|
-
createdAt: row.created_at as string,
|
|
78
|
-
lastUsed: row.last_used as string | null,
|
|
79
|
-
expiresAt: row.expires_at as string | null,
|
|
80
|
-
revoked: Boolean(row.revoked),
|
|
81
|
-
};
|
|
231
|
+
return record;
|
|
82
232
|
}
|
|
83
233
|
|
|
84
234
|
/** List all keys (without hashes). */
|
|
85
|
-
list():
|
|
86
|
-
|
|
87
|
-
SELECT id, name, key_prefix, scopes, created_at, last_used, expires_at, revoked
|
|
88
|
-
FROM api_keys ORDER BY created_at DESC
|
|
89
|
-
`).all() as Record<string, unknown>[];
|
|
90
|
-
|
|
91
|
-
return rows.map((row) => ({
|
|
92
|
-
id: row.id as string,
|
|
93
|
-
name: row.name as string,
|
|
94
|
-
keyPrefix: row.key_prefix as string,
|
|
95
|
-
scopes: JSON.parse(row.scopes as string),
|
|
96
|
-
createdAt: row.created_at as string,
|
|
97
|
-
lastUsed: row.last_used as string | null,
|
|
98
|
-
expiresAt: row.expires_at as string | null,
|
|
99
|
-
revoked: Boolean(row.revoked),
|
|
100
|
-
}));
|
|
235
|
+
async list(): Promise<ApiKeyPublic[]> {
|
|
236
|
+
return this.storage.list();
|
|
101
237
|
}
|
|
102
238
|
|
|
103
239
|
/** Revoke a key by ID. */
|
|
104
|
-
revoke(id: string): boolean {
|
|
105
|
-
|
|
106
|
-
return result.changes > 0;
|
|
240
|
+
async revoke(id: string): Promise<boolean> {
|
|
241
|
+
return this.storage.revoke(id);
|
|
107
242
|
}
|
|
108
243
|
|
|
109
|
-
close(): void {
|
|
110
|
-
this.
|
|
244
|
+
async close(): Promise<void> {
|
|
245
|
+
if (this.storage.close) await this.storage.close();
|
|
111
246
|
}
|
|
112
247
|
}
|