kernelcms 0.1.0 → 0.2.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/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  serve
4
- } from "./chunk-Z2RKB4LF.js";
4
+ } from "./chunk-TBZIZTVM.js";
5
5
  import {
6
6
  generateTypes,
7
7
  initKernel
@@ -93,10 +93,12 @@ async function run(argv) {
93
93
  const server = await serve(kernel, {
94
94
  port,
95
95
  apiKey: process.env.KERNEL_API_KEY,
96
- cors: true
96
+ cors: true,
97
+ admin: true
97
98
  });
98
99
  console.log(`
99
100
  KernelCMS dev server`);
101
+ console.log(` \u279C Admin: ${server.url}/admin`);
100
102
  console.log(` \u279C API: ${server.url}${kernel.config.routes.api}`);
101
103
  console.log(` \u279C Health: ${server.url}${kernel.config.routes.api}/health`);
102
104
  console.log(` \u279C Collections: ${kernel.config.collections.map((c) => c.slug).join(", ") || "(none)"}`);
@@ -0,0 +1,725 @@
1
+ import {
2
+ BadRequestError,
3
+ ForbiddenError,
4
+ NotFoundError,
5
+ UnauthorizedError,
6
+ describeConfig,
7
+ isKernelError
8
+ } from "./chunk-O5TO5JFA.js";
9
+
10
+ // ../server/src/index.ts
11
+ import { createServer } from "http";
12
+
13
+ // ../server/src/admin-ui.ts
14
+ var CSS = `
15
+ :root {
16
+ --bg: #ffffff; --fg: #111827; --muted: #6b7280; --line: #e5e7eb;
17
+ --line-2: #f3f4f6; --accent: #111827; --accent-fg: #ffffff;
18
+ --danger: #b91c1c; --radius: 12px; --shadow: 0 1px 2px rgba(16,24,40,.04), 0 4px 16px rgba(16,24,40,.06);
19
+ }
20
+ * { box-sizing: border-box; }
21
+ html, body { margin: 0; height: 100%; }
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
24
+ color: var(--fg); background: var(--bg); -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5;
25
+ }
26
+ #app { min-height: 100%; }
27
+ .muted { color: var(--muted); }
28
+ .err { color: var(--danger); font-size: 13px; min-height: 18px; }
29
+ .help { display: block; color: var(--muted); font-size: 12px; margin-top: 4px; }
30
+ .spacer { flex: 1; }
31
+
32
+ /* buttons + inputs */
33
+ .btn {
34
+ appearance: none; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-fg);
35
+ font: inherit; font-weight: 550; padding: 9px 16px; border-radius: 9px; cursor: pointer; transition: opacity .15s ease;
36
+ }
37
+ .btn:hover { opacity: .88; }
38
+ .btn:disabled { opacity: .5; cursor: default; }
39
+ .btn.ghost { background: transparent; color: var(--fg); border-color: var(--line); }
40
+ .btn.danger { background: transparent; color: var(--danger); border-color: var(--line); }
41
+ input, textarea, select {
42
+ font: inherit; color: var(--fg); width: 100%; padding: 9px 11px; border: 1px solid var(--line);
43
+ border-radius: 9px; background: #fff; outline: none; transition: border-color .15s ease, box-shadow .15s ease;
44
+ }
45
+ input:focus, textarea:focus, select:focus { border-color: #9ca3af; box-shadow: 0 0 0 3px rgba(17,24,39,.06); }
46
+ textarea { resize: vertical; }
47
+ textarea.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
48
+ input[type=checkbox] { width: auto; }
49
+ .fld { display: block; margin-bottom: 16px; }
50
+ .fld .lbl { display: block; font-weight: 550; margin-bottom: 6px; font-size: 13px; }
51
+ .opts { display: flex; flex-wrap: wrap; gap: 8px 18px; }
52
+ .opt { display: inline-flex; align-items: center; gap: 6px; font-weight: 400; }
53
+
54
+ /* auth screen */
55
+ .auth-wrap { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: #fafafa; }
56
+ .card { width: 100%; max-width: 380px; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); padding: 32px; box-shadow: var(--shadow); }
57
+ .brand { font-weight: 650; letter-spacing: -.01em; }
58
+ .card h1 { font-size: 20px; margin: 18px 0 4px; letter-spacing: -.02em; }
59
+ .card .sub { color: var(--muted); margin: 0 0 22px; }
60
+ .form .btn { width: 100%; margin-top: 6px; }
61
+
62
+ /* app shell */
63
+ .shell { display: flex; flex-direction: column; min-height: 100vh; }
64
+ .top { display: flex; align-items: center; gap: 14px; padding: 14px 22px; border-bottom: 1px solid var(--line); }
65
+ .top .who { color: var(--muted); font-size: 13px; }
66
+ .body { display: flex; flex: 1; align-items: stretch; }
67
+ .side { width: 232px; border-right: 1px solid var(--line); padding: 18px 12px; background: #fcfcfc; }
68
+ .nav-h { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 14px 10px 6px; }
69
+ .nav-i { display: block; padding: 8px 10px; border-radius: 8px; color: var(--fg); text-decoration: none; cursor: pointer; }
70
+ .nav-i:hover { background: var(--line-2); }
71
+ .nav-i.active { background: #eef2f7; font-weight: 550; }
72
+ .main { flex: 1; padding: 26px 32px; max-width: 920px; }
73
+ .page-h { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
74
+ .page-h h2 { font-size: 18px; margin: 0; letter-spacing: -.01em; }
75
+ .back { color: var(--muted); cursor: pointer; text-decoration: none; }
76
+ .back:hover { color: var(--fg); }
77
+
78
+ /* table */
79
+ .tablewrap { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
80
+ .tbl { width: 100%; border-collapse: collapse; }
81
+ .tbl th { text-align: left; font-size: 12px; color: var(--muted); font-weight: 550; padding: 11px 16px; background: #fcfcfc; border-bottom: 1px solid var(--line); }
82
+ .tbl td { padding: 12px 16px; border-bottom: 1px solid var(--line-2); }
83
+ .tbl tbody tr:last-child td { border-bottom: none; }
84
+ .tbl tbody tr.row { cursor: pointer; }
85
+ .tbl tbody tr.row:hover { background: #fafafa; }
86
+ .tbl td:last-child { color: var(--muted); text-align: right; width: 1px; }
87
+
88
+ .editor { max-width: 640px; }
89
+ .actions { display: flex; gap: 10px; margin-top: 8px; }
90
+ `;
91
+ var APP = `
92
+ var K = window.__KERNEL__; var API = K.api; var ADMIN = K.admin;
93
+ var TOKEN_KEY = "kernelcms.token";
94
+ var state = { schema: null, status: null, user: null, activeNav: null };
95
+
96
+ function getToken() { return localStorage.getItem(TOKEN_KEY); }
97
+ function setToken(t) { if (t) localStorage.setItem(TOKEN_KEY, t); else localStorage.removeItem(TOKEN_KEY); }
98
+
99
+ function el(tag, attrs, kids) {
100
+ var n = document.createElement(tag);
101
+ if (attrs) for (var k in attrs) {
102
+ var v = attrs[k];
103
+ if (v == null) continue;
104
+ if (k === "class") n.className = v;
105
+ else if (k === "text") n.textContent = v;
106
+ else if (k.slice(0, 2) === "on") n.addEventListener(k.slice(2).toLowerCase(), v);
107
+ else if (k === "value") n.value = v;
108
+ else if (k === "checked") n.checked = !!v;
109
+ else n.setAttribute(k, v);
110
+ }
111
+ if (kids != null) {
112
+ var arr = Array.isArray(kids) ? kids : [kids];
113
+ for (var i = 0; i < arr.length; i++) {
114
+ var c = arr[i];
115
+ if (c == null) continue;
116
+ n.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
117
+ }
118
+ }
119
+ return n;
120
+ }
121
+ function mount(node) { var a = document.getElementById("app"); a.textContent = ""; a.appendChild(node); }
122
+ function field(label, input, help) {
123
+ return el("label", { class: "fld" }, [el("span", { class: "lbl", text: label }), input, help ? el("span", { class: "help", text: help }) : null]);
124
+ }
125
+
126
+ function api(method, path, body) {
127
+ var headers = { "Content-Type": "application/json" };
128
+ var t = getToken(); if (t) headers["Authorization"] = "Bearer " + t;
129
+ return fetch(API + path, { method: method, headers: headers, body: body === undefined ? undefined : JSON.stringify(body) })
130
+ .then(function (res) {
131
+ return res.text().then(function (txt) {
132
+ var data = txt ? JSON.parse(txt) : null;
133
+ if (!res.ok) {
134
+ if (res.status === 401) { setToken(null); go("/login"); }
135
+ var m = data && data.error ? data.error.message : "HTTP " + res.status;
136
+ throw new Error(m);
137
+ }
138
+ return data;
139
+ });
140
+ });
141
+ }
142
+
143
+ function go(path) { history.pushState({}, "", path); render(); }
144
+ window.addEventListener("popstate", render);
145
+
146
+ function render() {
147
+ api("GET", "/_admin/status").then(function (status) {
148
+ state.status = status;
149
+ var path = location.pathname;
150
+ if (!getToken()) {
151
+ if (path !== "/login") history.replaceState({}, "", "/login");
152
+ return authScreen();
153
+ }
154
+ if (path === "/login") history.replaceState({}, "", ADMIN);
155
+ if (status.authCollection) {
156
+ return api("GET", "/" + status.authCollection + "/me")
157
+ .then(function (r) { state.user = r.user; appScreen(); })
158
+ .catch(function () { setToken(null); authScreen(); });
159
+ }
160
+ return appScreen();
161
+ }).catch(function (ex) {
162
+ mount(el("div", { class: "auth-wrap" }, el("div", { class: "card" }, el("p", { class: "err", text: ex.message }))));
163
+ });
164
+ }
165
+
166
+ function authScreen() {
167
+ var isSetup = state.status.needsSetup;
168
+ var err = el("div", { class: "err" });
169
+ var email = el("input", { type: "email", placeholder: "you@example.com", autocomplete: "username" });
170
+ var pw = el("input", { type: "password", placeholder: "Password", autocomplete: isSetup ? "new-password" : "current-password" });
171
+ var confirm = isSetup ? el("input", { type: "password", placeholder: "Confirm password", autocomplete: "new-password" }) : null;
172
+ var btn = el("button", { class: "btn", text: isSetup ? "Create account" : "Sign in" });
173
+ function submit() {
174
+ err.textContent = "";
175
+ var e = email.value.trim(), p = pw.value;
176
+ if (!e || !p) { err.textContent = "Email and password are required."; return; }
177
+ if (isSetup && p !== confirm.value) { err.textContent = "Passwords do not match."; return; }
178
+ if (isSetup && p.length < 8) { err.textContent = "Password must be at least 8 characters."; return; }
179
+ btn.disabled = true; btn.textContent = "Please wait\\u2026";
180
+ var path = isSetup ? "/_admin/setup" : "/" + state.status.authCollection + "/login";
181
+ api("POST", path, { email: e, password: p }).then(function (res) {
182
+ setToken(res.token); state.user = res.user; go(ADMIN);
183
+ }).catch(function (ex) {
184
+ err.textContent = ex.message; btn.disabled = false; btn.textContent = isSetup ? "Create account" : "Sign in";
185
+ });
186
+ }
187
+ btn.addEventListener("click", submit);
188
+ [email, pw, confirm].forEach(function (i) { if (i) i.addEventListener("keydown", function (ev) { if (ev.key === "Enter") submit(); }); });
189
+ var card = el("div", { class: "card" }, [
190
+ el("div", { class: "brand", text: "KernelCMS" }),
191
+ el("h1", { text: isSetup ? "Create admin account" : "Sign in" }),
192
+ el("p", { class: "sub", text: isSetup ? "Set up the first administrator to get started." : "Welcome back." }),
193
+ el("div", { class: "form" }, [
194
+ field("Email", email), field("Password", pw), confirm ? field("Confirm password", confirm) : null, err, btn,
195
+ ]),
196
+ ]);
197
+ mount(el("div", { class: "auth-wrap" }, card));
198
+ }
199
+
200
+ function appScreen() {
201
+ api("GET", "/_config").then(function (schema) {
202
+ state.schema = schema;
203
+ var cols = schema.collections.filter(function (c) { return !c.hidden; });
204
+ var nav = el("nav", { class: "nav" });
205
+ nav.appendChild(el("div", { class: "nav-h", text: "Collections" }));
206
+ cols.forEach(function (c) {
207
+ nav.appendChild(el("a", { class: "nav-i", "data-key": "c:" + c.slug, onClick: function () { selectCollection(c.slug); } }, c.labels.plural));
208
+ });
209
+ if (schema.globals.length) {
210
+ nav.appendChild(el("div", { class: "nav-h", text: "Globals" }));
211
+ schema.globals.forEach(function (g) {
212
+ nav.appendChild(el("a", { class: "nav-i", "data-key": "g:" + g.slug, onClick: function () { selectGlobal(g.slug); } }, g.label));
213
+ });
214
+ }
215
+ var top = el("header", { class: "top" }, [
216
+ el("div", { class: "brand", text: "KernelCMS" }),
217
+ el("div", { class: "spacer" }),
218
+ el("span", { class: "who", text: state.user ? state.user.email : "" }),
219
+ el("button", { class: "btn ghost", text: "Sign out", onClick: function () { setToken(null); state.user = null; go("/login"); } }),
220
+ ]);
221
+ var main = el("main", { class: "main" });
222
+ state._main = main; state._nav = nav;
223
+ mount(el("div", { class: "shell" }, [top, el("div", { class: "body" }, [el("aside", { class: "side" }, nav), main])]));
224
+ if (cols[0]) selectCollection(cols[0].slug);
225
+ else if (schema.globals[0]) selectGlobal(schema.globals[0].slug);
226
+ else main.appendChild(el("p", { class: "muted", text: "No collections configured." }));
227
+ }).catch(function (ex) {
228
+ mount(el("div", { class: "auth-wrap" }, el("div", { class: "card" }, el("p", { class: "err", text: ex.message }))));
229
+ });
230
+ }
231
+
232
+ function setActive(key) {
233
+ state.activeNav = key;
234
+ var items = state._nav.querySelectorAll(".nav-i");
235
+ for (var i = 0; i < items.length; i++) items[i].classList.toggle("active", items[i].getAttribute("data-key") === key);
236
+ }
237
+
238
+ function collBySlug(slug) { return state.schema.collections.filter(function (c) { return c.slug === slug; })[0]; }
239
+
240
+ function selectCollection(slug) {
241
+ setActive("c:" + slug);
242
+ var coll = collBySlug(slug);
243
+ var main = state._main; main.textContent = "";
244
+ main.appendChild(el("div", { class: "page-h" }, [
245
+ el("h2", { text: coll.labels.plural }),
246
+ el("div", { class: "spacer" }),
247
+ el("button", { class: "btn", text: "New " + coll.labels.singular, onClick: function () { openEditor(coll, null); } }),
248
+ ]));
249
+ var wrap = el("div", { class: "tablewrap" }, el("p", { class: "muted", text: "Loading\\u2026" }));
250
+ main.appendChild(wrap);
251
+ api("GET", "/" + slug + "?limit=100").then(function (res) {
252
+ wrap.textContent = "";
253
+ if (!res.docs.length) { wrap.appendChild(el("p", { class: "muted", text: "No records yet." })); return; }
254
+ var title = coll.useAsTitle;
255
+ var head = el("tr", null, [el("th", { text: title }), el("th", { text: "Updated" }), el("th", { text: "" })]);
256
+ var tb = el("tbody");
257
+ res.docs.forEach(function (doc) {
258
+ var t = doc[title]; if (t == null || t === "") t = "(untitled)";
259
+ tb.appendChild(el("tr", { class: "row", onClick: function () { openEditor(coll, doc.id); } }, [
260
+ el("td", { text: String(t) }),
261
+ el("td", { text: doc.updatedAt ? new Date(doc.updatedAt).toLocaleString() : "" }),
262
+ el("td", { text: "\\u203A" }),
263
+ ]));
264
+ });
265
+ wrap.appendChild(el("table", { class: "tbl" }, [el("thead", null, head), tb]));
266
+ }).catch(function (ex) { wrap.textContent = ""; wrap.appendChild(el("p", { class: "err", text: ex.message })); });
267
+ }
268
+
269
+ function inputForField(f, value) {
270
+ var t = f.type;
271
+ if (t === "textarea" || t === "richText" || t === "code") { var ta = el("textarea", { rows: "6" }); ta.value = value != null ? String(value) : ""; return ta; }
272
+ if (t === "json") { var tj = el("textarea", { rows: "6", class: "mono" }); tj.value = value != null ? JSON.stringify(value, null, 2) : ""; return tj; }
273
+ if (t === "boolean" || t === "checkbox") { return el("input", { type: "checkbox", checked: !!value }); }
274
+ if (t === "number") { var nn = el("input", { type: "number" }); if (value != null) nn.value = String(value); return nn; }
275
+ if (t === "date") { var dd = el("input", { type: "datetime-local" }); if (value) dd.value = toLocalInput(value); return dd; }
276
+ if (t === "select" || t === "radio") {
277
+ if (f.hasMany) {
278
+ var box = el("div", { class: "opts" });
279
+ (f.options || []).forEach(function (o) {
280
+ var cb = el("input", { type: "checkbox", value: o.value });
281
+ if (Array.isArray(value) && value.indexOf(o.value) >= 0) cb.checked = true;
282
+ box.appendChild(el("label", { class: "opt" }, [cb, document.createTextNode(o.label)]));
283
+ });
284
+ box.setAttribute("data-multi", "1");
285
+ return box;
286
+ }
287
+ var s = el("select");
288
+ s.appendChild(el("option", { value: "", text: "\\u2014" }));
289
+ (f.options || []).forEach(function (o) {
290
+ var op = el("option", { value: o.value, text: o.label });
291
+ if (value === o.value) op.selected = true;
292
+ s.appendChild(op);
293
+ });
294
+ return s;
295
+ }
296
+ if (t === "password") { return el("input", { type: "password", placeholder: "\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022" }); }
297
+ var inp = el("input", { type: t === "email" ? "email" : "text" });
298
+ if (value != null && typeof value !== "object") inp.value = String(value);
299
+ return inp;
300
+ }
301
+
302
+ function readField(f, input) {
303
+ var t = f.type;
304
+ if (t === "boolean" || t === "checkbox") return input.checked;
305
+ if (t === "number") return input.value === "" ? undefined : Number(input.value);
306
+ if (t === "json") { if (input.value.trim() === "") return undefined; return JSON.parse(input.value); }
307
+ if (t === "date") return input.value ? new Date(input.value).toISOString() : undefined;
308
+ if ((t === "select" || t === "radio") && f.hasMany) {
309
+ var vals = []; var cbs = input.querySelectorAll("input[type=checkbox]");
310
+ for (var i = 0; i < cbs.length; i++) if (cbs[i].checked) vals.push(cbs[i].value);
311
+ return vals;
312
+ }
313
+ if (t === "password") return input.value === "" ? undefined : input.value;
314
+ var v = input.value; return v === "" ? undefined : v;
315
+ }
316
+
317
+ function openEditor(coll, id) {
318
+ var isNew = !id;
319
+ var main = state._main; main.textContent = "";
320
+ main.appendChild(el("div", { class: "page-h" }, [
321
+ el("a", { class: "back", text: "\\u2039 " + coll.labels.plural, onClick: function () { selectCollection(coll.slug); } }),
322
+ el("h2", { text: isNew ? "New " + coll.labels.singular : "Edit " + coll.labels.singular }),
323
+ ]));
324
+ var wrap = el("div", { class: "editor" }); main.appendChild(wrap);
325
+ function build(doc) {
326
+ wrap.textContent = "";
327
+ var inputs = {};
328
+ coll.fields.forEach(function (f) {
329
+ if (f.name === "id") return;
330
+ var input = inputForField(f, doc ? doc[f.name] : undefined);
331
+ inputs[f.name] = { f: f, input: input };
332
+ wrap.appendChild(field(f.label + (f.required ? " *" : ""), input, f.admin && f.admin.description));
333
+ });
334
+ var err = el("div", { class: "err" });
335
+ var save = el("button", { class: "btn", text: isNew ? "Create" : "Save changes" });
336
+ var del = isNew ? null : el("button", { class: "btn danger", text: "Delete" });
337
+ save.addEventListener("click", function () {
338
+ err.textContent = ""; var payload = {};
339
+ try { for (var k in inputs) { var val = readField(inputs[k].f, inputs[k].input); if (val !== undefined) payload[k] = val; } }
340
+ catch (ex) { err.textContent = "Invalid JSON: " + ex.message; return; }
341
+ save.disabled = true; save.textContent = "Saving\\u2026";
342
+ var p = isNew ? api("POST", "/" + coll.slug, payload) : api("PATCH", "/" + coll.slug + "/" + id, payload);
343
+ p.then(function () { selectCollection(coll.slug); })
344
+ .catch(function (ex) { err.textContent = ex.message; save.disabled = false; save.textContent = isNew ? "Create" : "Save changes"; });
345
+ });
346
+ if (del) del.addEventListener("click", function () {
347
+ if (!window.confirm("Delete this record? This cannot be undone.")) return;
348
+ api("DELETE", "/" + coll.slug + "/" + id).then(function () { selectCollection(coll.slug); })
349
+ .catch(function (ex) { err.textContent = ex.message; });
350
+ });
351
+ wrap.appendChild(err);
352
+ wrap.appendChild(el("div", { class: "actions" }, [save, del]));
353
+ }
354
+ if (isNew) build(null);
355
+ else {
356
+ wrap.appendChild(el("p", { class: "muted", text: "Loading\\u2026" }));
357
+ api("GET", "/" + coll.slug + "/" + id).then(build)
358
+ .catch(function (ex) { wrap.textContent = ""; wrap.appendChild(el("p", { class: "err", text: ex.message })); });
359
+ }
360
+ }
361
+
362
+ function selectGlobal(slug) {
363
+ setActive("g:" + slug);
364
+ var g = state.schema.globals.filter(function (x) { return x.slug === slug; })[0];
365
+ var main = state._main; main.textContent = "";
366
+ main.appendChild(el("div", { class: "page-h" }, el("h2", { text: g.label })));
367
+ var wrap = el("div", { class: "editor" }, el("p", { class: "muted", text: "Loading\\u2026" }));
368
+ main.appendChild(wrap);
369
+ api("GET", "/globals/" + slug).then(function (doc) {
370
+ wrap.textContent = ""; var inputs = {};
371
+ g.fields.forEach(function (f) {
372
+ var input = inputForField(f, doc ? doc[f.name] : undefined);
373
+ inputs[f.name] = { f: f, input: input };
374
+ wrap.appendChild(field(f.label, input, f.admin && f.admin.description));
375
+ });
376
+ var err = el("div", { class: "err" });
377
+ var save = el("button", { class: "btn", text: "Save changes" });
378
+ save.addEventListener("click", function () {
379
+ err.textContent = ""; var payload = {};
380
+ try { for (var k in inputs) { var val = readField(inputs[k].f, inputs[k].input); if (val !== undefined) payload[k] = val; } }
381
+ catch (ex) { err.textContent = "Invalid JSON: " + ex.message; return; }
382
+ save.disabled = true; save.textContent = "Saving\\u2026";
383
+ api("POST", "/globals/" + slug, payload).then(function () {
384
+ save.disabled = false; save.textContent = "Saved \\u2713"; setTimeout(function () { save.textContent = "Save changes"; }, 1400);
385
+ }).catch(function (ex) { err.textContent = ex.message; save.disabled = false; save.textContent = "Save changes"; });
386
+ });
387
+ wrap.appendChild(err);
388
+ wrap.appendChild(el("div", { class: "actions" }, save));
389
+ }).catch(function (ex) { wrap.textContent = ""; wrap.appendChild(el("p", { class: "err", text: ex.message })); });
390
+ }
391
+
392
+ function toLocalInput(v) {
393
+ try {
394
+ var d = new Date(v); function p(n) { return ("0" + n).slice(-2); }
395
+ return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + "T" + p(d.getHours()) + ":" + p(d.getMinutes());
396
+ } catch (e) { return ""; }
397
+ }
398
+
399
+ render();
400
+ `;
401
+ function renderAdminPage(opts) {
402
+ const cfg = JSON.stringify({ api: opts.apiBase, admin: opts.adminBase });
403
+ return '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8" />\n<meta name="viewport" content="width=device-width, initial-scale=1" />\n<meta name="robots" content="noindex" />\n<title>KernelCMS Admin</title>\n<style>' + CSS + '</style>\n</head>\n<body>\n<div id="app"></div>\n<script>window.__KERNEL__ = ' + cfg + ';</script>\n<script type="module">' + APP + "</script>\n</body>\n</html>\n";
404
+ }
405
+
406
+ // ../server/src/index.ts
407
+ function authCollectionSlug(kernel) {
408
+ const configured = kernel.config.admin?.user;
409
+ if (configured && kernel.config.collectionsBySlug[configured]?.auth) return configured;
410
+ const found = kernel.config.collections.find((c) => c.auth);
411
+ return found ? found.slug : null;
412
+ }
413
+ function adminBaseOf(options) {
414
+ if (!options.admin) return null;
415
+ if (options.admin === true) return "/admin";
416
+ return options.admin.path ?? "/admin";
417
+ }
418
+ function html(body, status = 200) {
419
+ return new Response(body, { status, headers: { "content-type": "text/html; charset=utf-8" } });
420
+ }
421
+ function createRequestHandler(kernel, options = {}) {
422
+ const apiBase = kernel.config.routes.api;
423
+ const adminBase = adminBaseOf(options);
424
+ return async function handle(request) {
425
+ if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }), options, request);
426
+ if (adminBase && request.method === "GET") {
427
+ const { pathname } = new URL(request.url);
428
+ if (pathname === "/") return withCors(new Response(null, { status: 302, headers: { location: adminBase } }), options, request);
429
+ if (pathname === "/login" || pathname === adminBase || pathname.startsWith(adminBase + "/")) {
430
+ return withCors(html(renderAdminPage({ apiBase, adminBase })), options, request);
431
+ }
432
+ }
433
+ try {
434
+ const response = await route(kernel, options, request, apiBase);
435
+ return withCors(response, options, request);
436
+ } catch (err) {
437
+ return withCors(errorResponse(err), options, request);
438
+ }
439
+ };
440
+ }
441
+ async function route(kernel, options, request, apiBase) {
442
+ const url = new URL(request.url);
443
+ if (!url.pathname.startsWith(apiBase)) {
444
+ return json({ error: { code: "NOT_FOUND", message: `No route for ${url.pathname}` } }, 404);
445
+ }
446
+ const segments = url.pathname.slice(apiBase.length).split("/").filter(Boolean);
447
+ const { user, overrideAccess } = await resolveAuth(kernel, options, request);
448
+ const locale = url.searchParams.get("locale") ?? void 0;
449
+ const depth = toNum(url.searchParams.get("depth"));
450
+ const base = { req: { user, ...locale ? { locale } : {} }, overrideAccess, ...depth !== void 0 ? { depth } : {} };
451
+ const method = request.method;
452
+ if (segments.length === 0) {
453
+ return json({
454
+ name: "KernelCMS",
455
+ collections: kernel.config.collections.map((c) => c.slug),
456
+ globals: kernel.config.globals.map((g) => g.slug)
457
+ });
458
+ }
459
+ if (segments.length === 1 && segments[0] === "health") {
460
+ const health = await kernel.db.health();
461
+ return json({ status: "ok", db: health }, health.status === "ok" ? 200 : 503);
462
+ }
463
+ if (segments.length === 1 && segments[0] === "_config") {
464
+ return json(describeConfig(kernel.config));
465
+ }
466
+ if (segments[0] === "_admin") {
467
+ const slug = authCollectionSlug(kernel);
468
+ if (segments[1] === "status" && segments.length === 2 && method === "GET") {
469
+ const needsSetup = slug ? await kernel.count({ collection: slug, overrideAccess: true }) === 0 : false;
470
+ return json({ needsSetup, authCollection: slug });
471
+ }
472
+ if (segments[1] === "setup" && segments.length === 2 && method === "POST") {
473
+ if (!slug) throw new BadRequestError("No auth collection is configured.");
474
+ const existing = await kernel.count({ collection: slug, overrideAccess: true });
475
+ if (existing > 0) throw new ForbiddenError("Setup has already been completed.");
476
+ const body = await readBody(request);
477
+ const email = String(body.email ?? "");
478
+ const password = String(body.password ?? "");
479
+ const collection2 = kernel.config.collectionsBySlug[slug];
480
+ const data = { email, password };
481
+ if (collection2?.fields.some((f) => f.name === "roles")) data.roles = ["admin"];
482
+ await kernel.create({ collection: slug, data, overrideAccess: true });
483
+ return json(await kernel.login({ collection: slug, email, password }), 201);
484
+ }
485
+ return json({ error: { code: "NOT_FOUND", message: `No route for ${url.pathname}` } }, 404);
486
+ }
487
+ if (segments[0] === "globals" && segments.length === 2) {
488
+ const slug = segments[1];
489
+ if (method === "GET") return json(await kernel.findGlobal({ slug, ...base }));
490
+ if (method === "POST" || method === "PATCH") {
491
+ const data = await readBody(request);
492
+ return json(await kernel.updateGlobal({ slug, data, ...base }));
493
+ }
494
+ return methodNotAllowed();
495
+ }
496
+ const collection = segments[0];
497
+ if (segments.length === 2 && (segments[1] === "login" || segments[1] === "me")) {
498
+ const authColl = kernel.config.collectionsBySlug[collection];
499
+ if (authColl?.auth) {
500
+ if (segments[1] === "login" && method === "POST") {
501
+ const body = await readBody(request);
502
+ return json(
503
+ await kernel.login({
504
+ collection,
505
+ email: String(body.email ?? ""),
506
+ password: String(body.password ?? "")
507
+ })
508
+ );
509
+ }
510
+ if (segments[1] === "me" && method === "GET") {
511
+ if (!user) throw new UnauthorizedError();
512
+ return json({ user });
513
+ }
514
+ }
515
+ }
516
+ if (segments.length === 1) {
517
+ if (method === "GET") {
518
+ const result = await kernel.find({
519
+ collection,
520
+ where: parseWhere(url.searchParams),
521
+ sort: url.searchParams.get("sort") ?? void 0,
522
+ limit: toNum(url.searchParams.get("limit")),
523
+ page: toNum(url.searchParams.get("page")),
524
+ ...base
525
+ });
526
+ return json(result);
527
+ }
528
+ if (method === "POST") {
529
+ const data = await readBody(request);
530
+ return json(await kernel.create({ collection, data, ...base }), 201);
531
+ }
532
+ return methodNotAllowed();
533
+ }
534
+ if (segments.length === 2) {
535
+ const id = segments[1];
536
+ if (method === "GET") {
537
+ const doc = await kernel.findByID({ collection, id, ...base });
538
+ if (!doc) throw new NotFoundError();
539
+ return json(doc);
540
+ }
541
+ if (method === "PATCH" || method === "PUT") {
542
+ const data = await readBody(request);
543
+ const doc = await kernel.update({ collection, id, data, ...base });
544
+ if (!doc) throw new NotFoundError();
545
+ return json(doc);
546
+ }
547
+ if (method === "DELETE") {
548
+ const doc = await kernel.delete({ collection, id, ...base });
549
+ if (!doc) throw new NotFoundError();
550
+ return json(doc);
551
+ }
552
+ return methodNotAllowed();
553
+ }
554
+ return json({ error: { code: "NOT_FOUND", message: `No route for ${url.pathname}` } }, 404);
555
+ }
556
+ async function resolveAuth(kernel, options, request) {
557
+ const auth = request.headers.get("authorization");
558
+ if (options.apiKey && auth === `Bearer ${options.apiKey}`) {
559
+ return { user: { id: "system", roles: ["admin"], collection: "system" }, overrideAccess: true };
560
+ }
561
+ if (options.getUser) {
562
+ const user = await options.getUser(request);
563
+ if (user) return { user, overrideAccess: false };
564
+ }
565
+ if (auth?.startsWith("Bearer ")) {
566
+ const user = await kernel.authenticate(auth.slice("Bearer ".length));
567
+ if (user) return { user, overrideAccess: false };
568
+ }
569
+ return { user: null, overrideAccess: false };
570
+ }
571
+ async function readBody(request) {
572
+ let parsed;
573
+ try {
574
+ parsed = await request.json();
575
+ } catch {
576
+ throw new BadRequestError("Request body must be valid JSON.");
577
+ }
578
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
579
+ throw new BadRequestError("Request body must be a JSON object.");
580
+ }
581
+ return parsed;
582
+ }
583
+ function coerce(value) {
584
+ if (value === "true") return true;
585
+ if (value === "false") return false;
586
+ if (value === "null") return null;
587
+ if (value !== "" && /^-?\d+(\.\d+)?$/.test(value)) return Number(value);
588
+ return value;
589
+ }
590
+ function setDeep(root, path, value) {
591
+ let node = root;
592
+ for (let i = 0; i < path.length - 1; i++) {
593
+ const key = path[i];
594
+ if (typeof node[key] !== "object" || node[key] === null) node[key] = {};
595
+ node = node[key];
596
+ }
597
+ node[path[path.length - 1]] = value;
598
+ }
599
+ function parseWhere(params) {
600
+ const raw = params.get("where");
601
+ if (raw) {
602
+ try {
603
+ return JSON.parse(raw);
604
+ } catch {
605
+ throw new BadRequestError("`where` must be valid JSON.");
606
+ }
607
+ }
608
+ const root = {};
609
+ let found = false;
610
+ for (const [key, value] of params) {
611
+ if (!key.startsWith("where[")) continue;
612
+ found = true;
613
+ const path = key.slice("where".length).split(/[[\]]+/).filter(Boolean);
614
+ setDeep(root, path, coerce(value));
615
+ }
616
+ return found ? root : void 0;
617
+ }
618
+ function json(data, status = 200) {
619
+ return new Response(JSON.stringify(data), {
620
+ status,
621
+ headers: { "content-type": "application/json; charset=utf-8" }
622
+ });
623
+ }
624
+ function methodNotAllowed() {
625
+ return json({ error: { code: "BAD_REQUEST", message: "Method not allowed." } }, 405);
626
+ }
627
+ function errorResponse(err) {
628
+ if (isKernelError(err)) {
629
+ const response = json(err.toJSON(), err.status);
630
+ const retryAfter = err.retryAfter;
631
+ if (typeof retryAfter === "number") response.headers.set("retry-after", String(retryAfter));
632
+ return response;
633
+ }
634
+ console.error("[kernel] unhandled error:", err);
635
+ return json({ error: { code: "INTERNAL", message: "Internal server error." } }, 500);
636
+ }
637
+ function withCors(response, options, request) {
638
+ if (!options.cors) return response;
639
+ const origin = request.headers.get("origin");
640
+ let allow = null;
641
+ let credentials = false;
642
+ if (Array.isArray(options.cors)) {
643
+ if (origin && options.cors.includes(origin)) {
644
+ allow = origin;
645
+ credentials = true;
646
+ }
647
+ } else {
648
+ allow = origin ?? "*";
649
+ }
650
+ if (!allow) return response;
651
+ response.headers.set("access-control-allow-origin", allow);
652
+ if (credentials) response.headers.set("access-control-allow-credentials", "true");
653
+ response.headers.set("access-control-allow-headers", "Content-Type, Authorization");
654
+ response.headers.set("access-control-allow-methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS");
655
+ response.headers.set("vary", "Origin");
656
+ return response;
657
+ }
658
+ function toNum(value) {
659
+ if (value === null) return void 0;
660
+ const n = Number(value);
661
+ return Number.isNaN(n) ? void 0 : n;
662
+ }
663
+ function toNodeListener(handler) {
664
+ return (req, res) => {
665
+ const chunks = [];
666
+ req.on("data", (chunk) => chunks.push(chunk));
667
+ req.on("error", () => {
668
+ res.statusCode = 400;
669
+ res.end();
670
+ });
671
+ req.on("end", () => {
672
+ void (async () => {
673
+ const headers = new Headers();
674
+ for (const [key, value] of Object.entries(req.headers)) {
675
+ if (typeof value === "string") headers.set(key, value);
676
+ else if (Array.isArray(value)) headers.set(key, value.join(", "));
677
+ }
678
+ const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
679
+ const method = req.method ?? "GET";
680
+ const hasBody = method !== "GET" && method !== "HEAD" && chunks.length > 0;
681
+ const request = new Request(url, {
682
+ method,
683
+ headers,
684
+ body: hasBody ? Buffer.concat(chunks) : void 0
685
+ });
686
+ try {
687
+ const response = await handler(request);
688
+ res.statusCode = response.status;
689
+ response.headers.forEach((value, key) => res.setHeader(key, value));
690
+ const body = Buffer.from(await response.arrayBuffer());
691
+ res.end(body);
692
+ } catch (err) {
693
+ console.error("[kernel] listener error:", err);
694
+ res.statusCode = 500;
695
+ res.setHeader("content-type", "application/json");
696
+ res.end(JSON.stringify({ error: { code: "INTERNAL", message: "Internal server error." } }));
697
+ }
698
+ })();
699
+ });
700
+ };
701
+ }
702
+ async function serve(kernel, options = {}) {
703
+ const handler = createRequestHandler(kernel, options);
704
+ const server = createServer(toNodeListener(handler));
705
+ const requested = options.port ?? 3e3;
706
+ await new Promise((resolve, reject) => {
707
+ server.once("error", reject);
708
+ server.listen(requested, () => resolve());
709
+ });
710
+ const address = server.address();
711
+ const port = typeof address === "object" && address ? address.port : requested;
712
+ return {
713
+ url: `http://localhost:${port}`,
714
+ port,
715
+ close: () => new Promise((resolve) => server.close(() => resolve()))
716
+ };
717
+ }
718
+
719
+ export {
720
+ authCollectionSlug,
721
+ createRequestHandler,
722
+ parseWhere,
723
+ toNodeListener,
724
+ serve
725
+ };
package/dist/server.d.ts CHANGED
@@ -9,7 +9,16 @@ interface HandlerOptions {
9
9
  getUser?: (request: Request) => Promise<AuthUser | null> | AuthUser | null;
10
10
  /** CORS: true reflects the request origin; an array allow-lists origins. */
11
11
  cors?: boolean | string[];
12
+ /**
13
+ * Serve the built-in admin UI. `true` mounts it at `/admin` (and `/login`);
14
+ * pass `{ path }` to change the base. Disabled when omitted.
15
+ */
16
+ admin?: boolean | {
17
+ path?: string;
18
+ };
12
19
  }
20
+ /** The auth collection the admin UI logs into: configured admin.user, else the first auth collection. */
21
+ declare function authCollectionSlug(kernel: Kernel): string | null;
13
22
  type RequestHandler = (request: Request) => Promise<Response>;
14
23
  declare function createRequestHandler(kernel: Kernel, options?: HandlerOptions): RequestHandler;
15
24
  declare function parseWhere(params: URLSearchParams): Where | undefined;
@@ -24,4 +33,4 @@ interface RunningServer {
24
33
  }
25
34
  declare function serve(kernel: Kernel, options?: ServeOptions): Promise<RunningServer>;
26
35
 
27
- export { type HandlerOptions, type RunningServer, type ServeOptions, createRequestHandler, parseWhere, serve, toNodeListener };
36
+ export { type HandlerOptions, type RunningServer, type ServeOptions, authCollectionSlug, createRequestHandler, parseWhere, serve, toNodeListener };
package/dist/server.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import {
2
+ authCollectionSlug,
2
3
  createRequestHandler,
3
4
  parseWhere,
4
5
  serve,
5
6
  toNodeListener
6
- } from "./chunk-Z2RKB4LF.js";
7
+ } from "./chunk-TBZIZTVM.js";
7
8
  import "./chunk-O5TO5JFA.js";
8
9
  export {
10
+ authCollectionSlug,
9
11
  createRequestHandler,
10
12
  parseWhere,
11
13
  serve,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelcms",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The TanStack-native, adapter-based, end-to-end type-safe headless CMS. Config-as-code, self-hosted.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,12 +59,12 @@
59
59
  "@types/pg": "^8.11.10",
60
60
  "tsup": "^8.3.5",
61
61
  "@kernel/core": "0.0.0",
62
- "@kernel/server": "0.0.0",
62
+ "@kernel/db": "0.0.0",
63
+ "@kernel/cli": "0.0.0",
63
64
  "@kernel/db-sqlite": "0.0.0",
64
65
  "@kernel/db-postgres": "0.0.0",
65
- "@kernel/client": "0.0.0",
66
- "@kernel/db": "0.0.0",
67
- "@kernel/cli": "0.0.0"
66
+ "@kernel/server": "0.0.0",
67
+ "@kernel/client": "0.0.0"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "tsup"
@@ -1,285 +0,0 @@
1
- import {
2
- BadRequestError,
3
- NotFoundError,
4
- UnauthorizedError,
5
- describeConfig,
6
- isKernelError
7
- } from "./chunk-O5TO5JFA.js";
8
-
9
- // ../server/src/index.ts
10
- import { createServer } from "http";
11
- function createRequestHandler(kernel, options = {}) {
12
- const apiBase = kernel.config.routes.api;
13
- return async function handle(request) {
14
- if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }), options, request);
15
- try {
16
- const response = await route(kernel, options, request, apiBase);
17
- return withCors(response, options, request);
18
- } catch (err) {
19
- return withCors(errorResponse(err), options, request);
20
- }
21
- };
22
- }
23
- async function route(kernel, options, request, apiBase) {
24
- const url = new URL(request.url);
25
- if (!url.pathname.startsWith(apiBase)) {
26
- return json({ error: { code: "NOT_FOUND", message: `No route for ${url.pathname}` } }, 404);
27
- }
28
- const segments = url.pathname.slice(apiBase.length).split("/").filter(Boolean);
29
- const { user, overrideAccess } = await resolveAuth(kernel, options, request);
30
- const locale = url.searchParams.get("locale") ?? void 0;
31
- const depth = toNum(url.searchParams.get("depth"));
32
- const base = { req: { user, ...locale ? { locale } : {} }, overrideAccess, ...depth !== void 0 ? { depth } : {} };
33
- const method = request.method;
34
- if (segments.length === 0) {
35
- return json({
36
- name: "KernelCMS",
37
- collections: kernel.config.collections.map((c) => c.slug),
38
- globals: kernel.config.globals.map((g) => g.slug)
39
- });
40
- }
41
- if (segments.length === 1 && segments[0] === "health") {
42
- const health = await kernel.db.health();
43
- return json({ status: "ok", db: health }, health.status === "ok" ? 200 : 503);
44
- }
45
- if (segments.length === 1 && segments[0] === "_config") {
46
- return json(describeConfig(kernel.config));
47
- }
48
- if (segments[0] === "globals" && segments.length === 2) {
49
- const slug = segments[1];
50
- if (method === "GET") return json(await kernel.findGlobal({ slug, ...base }));
51
- if (method === "POST" || method === "PATCH") {
52
- const data = await readBody(request);
53
- return json(await kernel.updateGlobal({ slug, data, ...base }));
54
- }
55
- return methodNotAllowed();
56
- }
57
- const collection = segments[0];
58
- if (segments.length === 2 && (segments[1] === "login" || segments[1] === "me")) {
59
- const authColl = kernel.config.collectionsBySlug[collection];
60
- if (authColl?.auth) {
61
- if (segments[1] === "login" && method === "POST") {
62
- const body = await readBody(request);
63
- return json(
64
- await kernel.login({
65
- collection,
66
- email: String(body.email ?? ""),
67
- password: String(body.password ?? "")
68
- })
69
- );
70
- }
71
- if (segments[1] === "me" && method === "GET") {
72
- if (!user) throw new UnauthorizedError();
73
- return json({ user });
74
- }
75
- }
76
- }
77
- if (segments.length === 1) {
78
- if (method === "GET") {
79
- const result = await kernel.find({
80
- collection,
81
- where: parseWhere(url.searchParams),
82
- sort: url.searchParams.get("sort") ?? void 0,
83
- limit: toNum(url.searchParams.get("limit")),
84
- page: toNum(url.searchParams.get("page")),
85
- ...base
86
- });
87
- return json(result);
88
- }
89
- if (method === "POST") {
90
- const data = await readBody(request);
91
- return json(await kernel.create({ collection, data, ...base }), 201);
92
- }
93
- return methodNotAllowed();
94
- }
95
- if (segments.length === 2) {
96
- const id = segments[1];
97
- if (method === "GET") {
98
- const doc = await kernel.findByID({ collection, id, ...base });
99
- if (!doc) throw new NotFoundError();
100
- return json(doc);
101
- }
102
- if (method === "PATCH" || method === "PUT") {
103
- const data = await readBody(request);
104
- const doc = await kernel.update({ collection, id, data, ...base });
105
- if (!doc) throw new NotFoundError();
106
- return json(doc);
107
- }
108
- if (method === "DELETE") {
109
- const doc = await kernel.delete({ collection, id, ...base });
110
- if (!doc) throw new NotFoundError();
111
- return json(doc);
112
- }
113
- return methodNotAllowed();
114
- }
115
- return json({ error: { code: "NOT_FOUND", message: `No route for ${url.pathname}` } }, 404);
116
- }
117
- async function resolveAuth(kernel, options, request) {
118
- const auth = request.headers.get("authorization");
119
- if (options.apiKey && auth === `Bearer ${options.apiKey}`) {
120
- return { user: { id: "system", roles: ["admin"], collection: "system" }, overrideAccess: true };
121
- }
122
- if (options.getUser) {
123
- const user = await options.getUser(request);
124
- if (user) return { user, overrideAccess: false };
125
- }
126
- if (auth?.startsWith("Bearer ")) {
127
- const user = await kernel.authenticate(auth.slice("Bearer ".length));
128
- if (user) return { user, overrideAccess: false };
129
- }
130
- return { user: null, overrideAccess: false };
131
- }
132
- async function readBody(request) {
133
- let parsed;
134
- try {
135
- parsed = await request.json();
136
- } catch {
137
- throw new BadRequestError("Request body must be valid JSON.");
138
- }
139
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
140
- throw new BadRequestError("Request body must be a JSON object.");
141
- }
142
- return parsed;
143
- }
144
- function coerce(value) {
145
- if (value === "true") return true;
146
- if (value === "false") return false;
147
- if (value === "null") return null;
148
- if (value !== "" && /^-?\d+(\.\d+)?$/.test(value)) return Number(value);
149
- return value;
150
- }
151
- function setDeep(root, path, value) {
152
- let node = root;
153
- for (let i = 0; i < path.length - 1; i++) {
154
- const key = path[i];
155
- if (typeof node[key] !== "object" || node[key] === null) node[key] = {};
156
- node = node[key];
157
- }
158
- node[path[path.length - 1]] = value;
159
- }
160
- function parseWhere(params) {
161
- const raw = params.get("where");
162
- if (raw) {
163
- try {
164
- return JSON.parse(raw);
165
- } catch {
166
- throw new BadRequestError("`where` must be valid JSON.");
167
- }
168
- }
169
- const root = {};
170
- let found = false;
171
- for (const [key, value] of params) {
172
- if (!key.startsWith("where[")) continue;
173
- found = true;
174
- const path = key.slice("where".length).split(/[[\]]+/).filter(Boolean);
175
- setDeep(root, path, coerce(value));
176
- }
177
- return found ? root : void 0;
178
- }
179
- function json(data, status = 200) {
180
- return new Response(JSON.stringify(data), {
181
- status,
182
- headers: { "content-type": "application/json; charset=utf-8" }
183
- });
184
- }
185
- function methodNotAllowed() {
186
- return json({ error: { code: "BAD_REQUEST", message: "Method not allowed." } }, 405);
187
- }
188
- function errorResponse(err) {
189
- if (isKernelError(err)) {
190
- const response = json(err.toJSON(), err.status);
191
- const retryAfter = err.retryAfter;
192
- if (typeof retryAfter === "number") response.headers.set("retry-after", String(retryAfter));
193
- return response;
194
- }
195
- console.error("[kernel] unhandled error:", err);
196
- return json({ error: { code: "INTERNAL", message: "Internal server error." } }, 500);
197
- }
198
- function withCors(response, options, request) {
199
- if (!options.cors) return response;
200
- const origin = request.headers.get("origin");
201
- let allow = null;
202
- let credentials = false;
203
- if (Array.isArray(options.cors)) {
204
- if (origin && options.cors.includes(origin)) {
205
- allow = origin;
206
- credentials = true;
207
- }
208
- } else {
209
- allow = origin ?? "*";
210
- }
211
- if (!allow) return response;
212
- response.headers.set("access-control-allow-origin", allow);
213
- if (credentials) response.headers.set("access-control-allow-credentials", "true");
214
- response.headers.set("access-control-allow-headers", "Content-Type, Authorization");
215
- response.headers.set("access-control-allow-methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS");
216
- response.headers.set("vary", "Origin");
217
- return response;
218
- }
219
- function toNum(value) {
220
- if (value === null) return void 0;
221
- const n = Number(value);
222
- return Number.isNaN(n) ? void 0 : n;
223
- }
224
- function toNodeListener(handler) {
225
- return (req, res) => {
226
- const chunks = [];
227
- req.on("data", (chunk) => chunks.push(chunk));
228
- req.on("error", () => {
229
- res.statusCode = 400;
230
- res.end();
231
- });
232
- req.on("end", () => {
233
- void (async () => {
234
- const headers = new Headers();
235
- for (const [key, value] of Object.entries(req.headers)) {
236
- if (typeof value === "string") headers.set(key, value);
237
- else if (Array.isArray(value)) headers.set(key, value.join(", "));
238
- }
239
- const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
240
- const method = req.method ?? "GET";
241
- const hasBody = method !== "GET" && method !== "HEAD" && chunks.length > 0;
242
- const request = new Request(url, {
243
- method,
244
- headers,
245
- body: hasBody ? Buffer.concat(chunks) : void 0
246
- });
247
- try {
248
- const response = await handler(request);
249
- res.statusCode = response.status;
250
- response.headers.forEach((value, key) => res.setHeader(key, value));
251
- const body = Buffer.from(await response.arrayBuffer());
252
- res.end(body);
253
- } catch (err) {
254
- console.error("[kernel] listener error:", err);
255
- res.statusCode = 500;
256
- res.setHeader("content-type", "application/json");
257
- res.end(JSON.stringify({ error: { code: "INTERNAL", message: "Internal server error." } }));
258
- }
259
- })();
260
- });
261
- };
262
- }
263
- async function serve(kernel, options = {}) {
264
- const handler = createRequestHandler(kernel, options);
265
- const server = createServer(toNodeListener(handler));
266
- const requested = options.port ?? 3e3;
267
- await new Promise((resolve, reject) => {
268
- server.once("error", reject);
269
- server.listen(requested, () => resolve());
270
- });
271
- const address = server.address();
272
- const port = typeof address === "object" && address ? address.port : requested;
273
- return {
274
- url: `http://localhost:${port}`,
275
- port,
276
- close: () => new Promise((resolve) => server.close(() => resolve()))
277
- };
278
- }
279
-
280
- export {
281
- createRequestHandler,
282
- parseWhere,
283
- toNodeListener,
284
- serve
285
- };