@valentinkolb/cloud 0.3.1 → 0.4.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valentinkolb/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Modular Hono+SolidJS framework for building per-app docker services behind a dynamic gateway. Powers cloud.stuve-ulm.de.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,8 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
39
|
-
"@scalar/hono-api-reference": "^0.10.9",
|
|
40
|
-
"@scalar/openapi-to-markdown": "^0.5.6",
|
|
41
39
|
"@tabler/icons-webfont": "^3.36.1",
|
|
42
40
|
"@tailwindcss/typography": "^0.5.19",
|
|
43
41
|
"@valentinkolb/ssr": "0.9.0",
|
|
@@ -8,6 +8,7 @@ import { createConfig as createSsrConfig } from "@valentinkolb/ssr";
|
|
|
8
8
|
import { createSSRHandler, routes } from "@valentinkolb/ssr/hono";
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
import { serveStatic } from "hono/bun";
|
|
11
|
+
import { generateSpecs } from "hono-openapi";
|
|
11
12
|
import type { SsrConfig } from "@valentinkolb/ssr";
|
|
12
13
|
import type {
|
|
13
14
|
AppMeta,
|
|
@@ -107,6 +108,23 @@ export type AppOptions<S extends AppSettingsMap = {}> = {
|
|
|
107
108
|
* prefix-trie from these strings.
|
|
108
109
|
*/
|
|
109
110
|
routes: readonly string[];
|
|
111
|
+
/**
|
|
112
|
+
* Gateway-relative URL at which this app's OpenAPI 3.x JSON spec is
|
|
113
|
+
* served, e.g. `"/api/notebooks/openapi.json"`. Opt-in: only set this
|
|
114
|
+
* for apps whose api router is documented with `middleware.openapi()`
|
|
115
|
+
* (i.e. `describeRoute()`) and worth surfacing in the api-docs aggregator.
|
|
116
|
+
*
|
|
117
|
+
* Pair this with `app.start({ openapi: <api router> })` — `defineApp`
|
|
118
|
+
* generates the spec from that router at boot, mounts it on the
|
|
119
|
+
* framework server (before the user's fetch, so it bypasses any
|
|
120
|
+
* auth/rate-limit middleware), and advertises the URL via the registry
|
|
121
|
+
* so `app-api-docs` picks it up automatically.
|
|
122
|
+
*
|
|
123
|
+
* The path must be reachable through the gateway — usually that means
|
|
124
|
+
* the standard form `"/api/<id>/openapi.json"` (covered by the
|
|
125
|
+
* `/api/<id>` entry in `routes`).
|
|
126
|
+
*/
|
|
127
|
+
openapi?: string;
|
|
110
128
|
/**
|
|
111
129
|
* Project root used by the SSR plugin to discover island/client files.
|
|
112
130
|
* Defaults to `process.cwd()`. Override only if you run the entrypoint
|
|
@@ -134,6 +152,18 @@ export type StartOptions = {
|
|
|
134
152
|
* might register.
|
|
135
153
|
*/
|
|
136
154
|
fetch: (req: Request) => Response | Promise<Response>;
|
|
155
|
+
/**
|
|
156
|
+
* Hono router to scan for OpenAPI route metadata. When set together with
|
|
157
|
+
* `defineApp({ openapi: "..." })`, the framework generates an OpenAPI
|
|
158
|
+
* spec from this router and mounts it at the configured URL on the
|
|
159
|
+
* framework server (before the user's fetch, so the spec stays public).
|
|
160
|
+
*
|
|
161
|
+
* Pass the BARE api router — the one with `describeRoute()` annotations.
|
|
162
|
+
* `generateSpecs` walks the route tree without executing middleware, so
|
|
163
|
+
* the inner auth/rate-limit `.use(...)` calls don't matter for spec
|
|
164
|
+
* generation; they only run if the router is hit by an actual request.
|
|
165
|
+
*/
|
|
166
|
+
openapi?: Hono<any>;
|
|
137
167
|
lifecycle?: AppLifecycle;
|
|
138
168
|
capabilities?: AppCapabilities;
|
|
139
169
|
port?: number;
|
|
@@ -258,6 +288,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
258
288
|
nav: opts.nav,
|
|
259
289
|
legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
|
|
260
290
|
widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
|
|
291
|
+
openapi: opts.openapi,
|
|
261
292
|
};
|
|
262
293
|
|
|
263
294
|
// ── 3. start() — builds and boots the Hono server ────────────────────
|
|
@@ -266,6 +297,11 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
266
297
|
const baseUrl = opts.baseUrl;
|
|
267
298
|
const log = logger("app");
|
|
268
299
|
|
|
300
|
+
// OpenAPI advertised in the registry only when there's a router to
|
|
301
|
+
// derive the spec from. The mount block lower down uses the same
|
|
302
|
+
// flag so the registry never points at a URL that 404s.
|
|
303
|
+
const advertiseOpenapi = !!(opts.openapi && startOpts.openapi);
|
|
304
|
+
|
|
269
305
|
// Registry entry
|
|
270
306
|
const entry: AppRegistryEntry = {
|
|
271
307
|
id: meta.id,
|
|
@@ -294,6 +330,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
294
330
|
: undefined,
|
|
295
331
|
legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
296
332
|
widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
|
|
333
|
+
openapi: advertiseOpenapi ? opts.openapi : undefined,
|
|
297
334
|
};
|
|
298
335
|
|
|
299
336
|
// Heartbeat
|
|
@@ -306,11 +343,13 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
306
343
|
// watcher is a module-level singleton, only one runs per process.
|
|
307
344
|
await ensureRuntimeWatcher();
|
|
308
345
|
|
|
309
|
-
// Build Hono server. Framework owns
|
|
346
|
+
// Build Hono server. Framework owns these mounts (registered first so
|
|
310
347
|
// they take precedence over any catch-all in the user's fetch):
|
|
311
348
|
// /_ssr/* island chunks (SSR adapter)
|
|
312
349
|
// /public/* serveStatic + terminal 404
|
|
313
350
|
// /api/_internal/search only when capabilities.search is declared
|
|
351
|
+
// <opts.openapi> OpenAPI JSON spec, when both opts.openapi
|
|
352
|
+
// and startOpts.openapi are set
|
|
314
353
|
const ssrMountPath = config.basePath ? `${config.basePath}/_ssr` : "/_ssr";
|
|
315
354
|
|
|
316
355
|
const server = new Hono()
|
|
@@ -336,6 +375,32 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
336
375
|
});
|
|
337
376
|
}
|
|
338
377
|
|
|
378
|
+
// OpenAPI spec mount. Registered on the framework server (before the
|
|
379
|
+
// user-fetch catch-all below) so it bypasses any auth / rate-limit
|
|
380
|
+
// middleware on the api router — the spec must stay reachable without
|
|
381
|
+
// a session for `app-api-docs` to render it.
|
|
382
|
+
//
|
|
383
|
+
// The `servers` override is load-bearing: hono-openapi walks the
|
|
384
|
+
// BARE api router and emits paths relative to its own root (e.g.
|
|
385
|
+
// `/{id}`, `/{id}/notes`), without the `/api/<id>` prefix it ends
|
|
386
|
+
// up under in the user's outer router. We derive that prefix from
|
|
387
|
+
// `opts.openapi` (everything before the trailing `/openapi.json`)
|
|
388
|
+
// so combined Scalar URLs resolve to the real public paths.
|
|
389
|
+
if (advertiseOpenapi) {
|
|
390
|
+
const apiPrefix = opts.openapi!.replace(/\/openapi\.json$/, "") || "/";
|
|
391
|
+
const spec = await generateSpecs(startOpts.openapi!, {
|
|
392
|
+
documentation: {
|
|
393
|
+
info: {
|
|
394
|
+
title: meta.name,
|
|
395
|
+
version: "0.0.1",
|
|
396
|
+
description: meta.description,
|
|
397
|
+
},
|
|
398
|
+
servers: [{ url: apiPrefix }],
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
server.get(opts.openapi!, (c) => c.json(spec));
|
|
402
|
+
}
|
|
403
|
+
|
|
339
404
|
// User's fetch handles everything else. The framework doesn't inject any
|
|
340
405
|
// context vars here — the user's router is expected to register the
|
|
341
406
|
// middlewares it needs (middleware.runtime, middleware.settings, …).
|
|
@@ -33,6 +33,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[]): CloudRunt
|
|
|
33
33
|
searchHelp: e.search?.help,
|
|
34
34
|
searchTagHelp: e.search?.tagHelp,
|
|
35
35
|
legalLinks: e.legalLinks ? e.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
36
|
+
openapi: e.openapi,
|
|
36
37
|
}),
|
|
37
38
|
),
|
|
38
39
|
});
|
package/src/api/index.ts
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* Apps that need a typed client to these routes import from
|
|
9
9
|
* `@valentinkolb/cloud/clients/core`. The client and the routes share their
|
|
10
10
|
* type via `CoreApiType` below.
|
|
11
|
+
*
|
|
12
|
+
* The OpenAPI spec for these routes is generated by `defineApp` (driven by
|
|
13
|
+
* core's `openapi: "/api/openapi.json"` opt-in) — this file no longer mounts
|
|
14
|
+
* any docs UI; the api-docs aggregator at `/app/api-docs` is the only consumer.
|
|
11
15
|
*/
|
|
12
16
|
import { Hono } from "hono";
|
|
13
|
-
import { Scalar } from "@scalar/hono-api-reference";
|
|
14
|
-
import { generateSpecs } from "hono-openapi";
|
|
15
17
|
import { prettyJSON } from "hono/pretty-json";
|
|
16
|
-
import { createMarkdownFromOpenApi } from "@scalar/openapi-to-markdown";
|
|
17
|
-
import { openApiMeta } from "../server";
|
|
18
18
|
import authRoutes from "./auth";
|
|
19
19
|
import meRoutes from "./me";
|
|
20
20
|
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
@@ -41,26 +41,11 @@ const buildCoreApi = () => {
|
|
|
41
41
|
export type CoreApiType = ReturnType<typeof buildCoreApi>;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Build the core router
|
|
45
|
-
*
|
|
44
|
+
* Build the core router. The core-app calls this and mounts the returned
|
|
45
|
+
* router under `/api`.
|
|
46
46
|
*/
|
|
47
|
-
export const createCoreApiRouter =
|
|
47
|
+
export const createCoreApiRouter = () => {
|
|
48
48
|
const api = buildCoreApi();
|
|
49
|
-
|
|
50
|
-
const spec = await generateSpecs(api, openApiMeta);
|
|
51
|
-
const llmsTxt = await createMarkdownFromOpenApi(JSON.stringify(spec));
|
|
52
|
-
|
|
53
|
-
api.get("/openapi.json", (c) => c.json(spec));
|
|
54
|
-
api.get(
|
|
55
|
-
"/docs",
|
|
56
|
-
Scalar({
|
|
57
|
-
theme: "saturn",
|
|
58
|
-
url: "/api/openapi.json",
|
|
59
|
-
hideClientButton: true,
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
62
|
-
|
|
63
49
|
api.all("/*", (c) => c.json({ message: "API route not found" }, 404));
|
|
64
|
-
|
|
65
|
-
return { api, llmsTxt };
|
|
50
|
+
return { api };
|
|
66
51
|
};
|
package/src/contracts/app.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type AppMeta = {
|
|
|
43
43
|
* silently skip rendering for the current user.
|
|
44
44
|
*/
|
|
45
45
|
widgets?: WidgetEndpoint[];
|
|
46
|
+
/** Gateway-relative URL where this app's OpenAPI JSON is served, or undefined. */
|
|
47
|
+
openapi?: string;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
export type WidgetEndpoint = {
|