@btst/stack 1.6.0 → 1.7.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.
Files changed (41) hide show
  1. package/dist/api/index.cjs +7 -1
  2. package/dist/api/index.d.cts +2 -2
  3. package/dist/api/index.d.mts +2 -2
  4. package/dist/api/index.d.ts +2 -2
  5. package/dist/api/index.mjs +7 -1
  6. package/dist/client/index.d.cts +1 -1
  7. package/dist/client/index.d.mts +1 -1
  8. package/dist/client/index.d.ts +1 -1
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/packages/better-stack/src/plugins/open-api/api/generator.cjs +300 -0
  13. package/dist/packages/better-stack/src/plugins/open-api/api/generator.mjs +284 -0
  14. package/dist/packages/better-stack/src/plugins/open-api/api/plugin.cjs +115 -0
  15. package/dist/packages/better-stack/src/plugins/open-api/api/plugin.mjs +113 -0
  16. package/dist/packages/better-stack/src/plugins/open-api/db.cjs +7 -0
  17. package/dist/packages/better-stack/src/plugins/open-api/db.mjs +5 -0
  18. package/dist/packages/better-stack/src/plugins/open-api/logo.cjs +8 -0
  19. package/dist/packages/better-stack/src/plugins/open-api/logo.mjs +6 -0
  20. package/dist/plugins/api/index.d.cts +2 -2
  21. package/dist/plugins/api/index.d.mts +2 -2
  22. package/dist/plugins/api/index.d.ts +2 -2
  23. package/dist/plugins/client/index.d.cts +2 -2
  24. package/dist/plugins/client/index.d.mts +2 -2
  25. package/dist/plugins/client/index.d.ts +2 -2
  26. package/dist/plugins/open-api/api/index.cjs +9 -0
  27. package/dist/plugins/open-api/api/index.d.cts +95 -0
  28. package/dist/plugins/open-api/api/index.d.mts +95 -0
  29. package/dist/plugins/open-api/api/index.d.ts +95 -0
  30. package/dist/plugins/open-api/api/index.mjs +2 -0
  31. package/dist/shared/{stack.ByOugz9d.d.cts → stack.CSce37mX.d.cts} +15 -2
  32. package/dist/shared/{stack.ByOugz9d.d.mts → stack.CSce37mX.d.mts} +15 -2
  33. package/dist/shared/{stack.ByOugz9d.d.ts → stack.CSce37mX.d.ts} +15 -2
  34. package/package.json +14 -1
  35. package/src/api/index.ts +14 -2
  36. package/src/plugins/open-api/api/generator.ts +433 -0
  37. package/src/plugins/open-api/api/index.ts +8 -0
  38. package/src/plugins/open-api/api/plugin.ts +243 -0
  39. package/src/plugins/open-api/db.ts +7 -0
  40. package/src/plugins/open-api/logo.ts +7 -0
  41. package/src/types.ts +15 -1
@@ -0,0 +1,243 @@
1
+ import { defineBackendPlugin } from "@btst/stack/plugins/api";
2
+ import { createEndpoint } from "@btst/stack/plugins/api";
3
+ import { openApiSchema } from "../db";
4
+ import { generateOpenAPISchema } from "./generator";
5
+ import { logo } from "../logo";
6
+ import type { BetterStackContext } from "../../../types";
7
+
8
+ /**
9
+ * Scalar API Reference themes
10
+ */
11
+ export type ScalarTheme =
12
+ | "alternate"
13
+ | "default"
14
+ | "moon"
15
+ | "purple"
16
+ | "solarized"
17
+ | "bluePlanet"
18
+ | "saturn"
19
+ | "kepler"
20
+ | "mars"
21
+ | "deepSpace"
22
+ | "laserwave"
23
+ | "none";
24
+
25
+ /**
26
+ * OpenAPI plugin configuration options
27
+ */
28
+ export interface OpenAPIOptions {
29
+ /**
30
+ * The path to the OpenAPI reference page
31
+ * This path is relative to the API base path
32
+ * @default "/reference"
33
+ */
34
+ path?: string;
35
+
36
+ /**
37
+ * Disable the default HTML reference page
38
+ * Only the JSON schema endpoint will be available
39
+ * @default false
40
+ */
41
+ disableDefaultReference?: boolean;
42
+
43
+ /**
44
+ * Theme for the Scalar API Reference page
45
+ * @default "default"
46
+ */
47
+ theme?: ScalarTheme;
48
+
49
+ /**
50
+ * CSP nonce for inline scripts
51
+ * Required for strict Content Security Policy
52
+ */
53
+ nonce?: string;
54
+
55
+ /**
56
+ * Custom title for the API documentation
57
+ * @default "Better Stack API"
58
+ */
59
+ title?: string;
60
+
61
+ /**
62
+ * Custom description for the API documentation
63
+ */
64
+ description?: string;
65
+
66
+ /**
67
+ * API version string
68
+ * @default "1.0.0"
69
+ */
70
+ version?: string;
71
+ }
72
+
73
+ /**
74
+ * Escape HTML entities to prevent XSS and ensure proper rendering
75
+ */
76
+ function escapeHtml(str: string): string {
77
+ return str
78
+ .replace(/&/g, "&")
79
+ .replace(/</g, "&lt;")
80
+ .replace(/>/g, "&gt;")
81
+ .replace(/"/g, "&quot;")
82
+ .replace(/'/g, "&#39;");
83
+ }
84
+
85
+ /**
86
+ * Escape JSON for safe embedding in HTML script tags.
87
+ * Replaces < with \u003c to prevent </script> from closing the tag prematurely.
88
+ * This is valid JSON and will be parsed correctly.
89
+ */
90
+ function escapeJsonForHtml(json: string): string {
91
+ return json.replace(/</g, "\\u003c");
92
+ }
93
+
94
+ /**
95
+ * Generate the HTML page for Scalar API Reference
96
+ */
97
+ function getScalarHTML(
98
+ schema: Record<string, any>,
99
+ theme: ScalarTheme = "default",
100
+ nonce?: string,
101
+ ): string {
102
+ const nonceAttr = nonce ? ` nonce="${escapeHtml(nonce)}"` : "";
103
+ const encodedLogo = encodeURIComponent(logo);
104
+
105
+ const title = schema.info?.title || "API Reference";
106
+ const description = schema.info?.description || "API Reference";
107
+
108
+ return `<!doctype html>
109
+ <html>
110
+ <head>
111
+ <title>${escapeHtml(title)}</title>
112
+ <meta charset="utf-8" />
113
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
114
+ </head>
115
+ <body>
116
+ <script
117
+ id="api-reference"
118
+ type="application/json"${nonceAttr}>
119
+ ${escapeJsonForHtml(JSON.stringify(schema))}
120
+ </script>
121
+ <script${nonceAttr}>
122
+ var configuration = {
123
+ favicon: "data:image/svg+xml;utf8,${encodedLogo}",
124
+ theme: "${theme}",
125
+ metaData: {
126
+ title: ${JSON.stringify(title)},
127
+ description: ${JSON.stringify(description)},
128
+ }
129
+ }
130
+
131
+ document.getElementById('api-reference').dataset.configuration =
132
+ JSON.stringify(configuration)
133
+ </script>
134
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"${nonceAttr}></script>
135
+ </body>
136
+ </html>`;
137
+ }
138
+
139
+ /**
140
+ * OpenAPI plugin for Better Stack
141
+ *
142
+ * Automatically generates OpenAPI 3.1 documentation for all registered plugins.
143
+ * Provides both a JSON schema endpoint and an interactive Scalar UI reference page.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const { handler } = betterStack({
148
+ * basePath: "/api/data",
149
+ * plugins: {
150
+ * blog: blogBackendPlugin(),
151
+ * cms: cmsBackendPlugin({ ... }),
152
+ * openApi: openApiBackendPlugin({ theme: "moon" }),
153
+ * },
154
+ * adapter: (db) => createMemoryAdapter(db)({}),
155
+ * });
156
+ *
157
+ * // Access:
158
+ * // - GET /api/data/open-api/schema - JSON schema
159
+ * // - GET /api/data/reference - Interactive Scalar UI
160
+ * ```
161
+ */
162
+ export const openApiBackendPlugin = (options?: OpenAPIOptions) => {
163
+ const referencePath = options?.path ?? "/reference";
164
+
165
+ // Store context for use in endpoint handlers
166
+ let storedContext: BetterStackContext | null = null;
167
+
168
+ return defineBackendPlugin({
169
+ name: "open-api",
170
+ dbPlugin: openApiSchema,
171
+
172
+ routes: (_adapter, context) => {
173
+ // Store context for endpoint handlers
174
+ storedContext = context ?? null;
175
+
176
+ const generateSchema = createEndpoint(
177
+ "/open-api/schema",
178
+ {
179
+ method: "GET",
180
+ },
181
+ async (ctx) => {
182
+ if (!storedContext) {
183
+ throw ctx.error(500, {
184
+ message: "OpenAPI context not available",
185
+ });
186
+ }
187
+
188
+ const schema = generateOpenAPISchema(storedContext, {
189
+ title: options?.title,
190
+ description: options?.description,
191
+ version: options?.version,
192
+ });
193
+
194
+ return schema;
195
+ },
196
+ );
197
+
198
+ const reference = createEndpoint(
199
+ referencePath,
200
+ {
201
+ method: "GET",
202
+ },
203
+ async (ctx) => {
204
+ if (options?.disableDefaultReference) {
205
+ throw ctx.error(404, {
206
+ message: "Reference page is disabled",
207
+ });
208
+ }
209
+
210
+ if (!storedContext) {
211
+ throw ctx.error(500, {
212
+ message: "OpenAPI context not available",
213
+ });
214
+ }
215
+
216
+ const schema = generateOpenAPISchema(storedContext, {
217
+ title: options?.title,
218
+ description: options?.description,
219
+ version: options?.version,
220
+ });
221
+
222
+ return new Response(
223
+ getScalarHTML(schema, options?.theme, options?.nonce),
224
+ {
225
+ headers: {
226
+ "Content-Type": "text/html; charset=utf-8",
227
+ },
228
+ },
229
+ );
230
+ },
231
+ );
232
+
233
+ return {
234
+ generateSchema,
235
+ reference,
236
+ } as const;
237
+ },
238
+ });
239
+ };
240
+
241
+ export type OpenApiRouter = ReturnType<
242
+ ReturnType<typeof openApiBackendPlugin>["routes"]
243
+ >;
@@ -0,0 +1,7 @@
1
+ import { createDbPlugin } from "@btst/db";
2
+
3
+ /**
4
+ * OpenAPI plugin doesn't require any database tables
5
+ * It only introspects other plugins' endpoints
6
+ */
7
+ export const openApiSchema = createDbPlugin("openApi", {});
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Better Stack logo SVG for use in Scalar API Reference
3
+ */
4
+ export const logo = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
5
+ <rect width="100" height="100" rx="20" fill="#0ea5e9"/>
6
+ <path d="M25 35h50M25 50h50M25 65h35" stroke="white" stroke-width="8" stroke-linecap="round"/>
7
+ </svg>`;
package/src/types.ts CHANGED
@@ -2,6 +2,19 @@ import type { Route, createRouter } from "@btst/yar";
2
2
  import type { Adapter, DatabaseDefinition, DbPlugin } from "@btst/db";
3
3
  import type { Endpoint, Router } from "better-call";
4
4
 
5
+ /**
6
+ * Context passed to backend plugins during route creation
7
+ * Provides access to all registered plugins for introspection (used by openAPI plugin)
8
+ */
9
+ export interface BetterStackContext {
10
+ /** All registered backend plugins */
11
+ plugins: Record<string, BackendPlugin<any>>;
12
+ /** The API base path (e.g., "/api/data") */
13
+ basePath: string;
14
+ /** The database adapter */
15
+ adapter: Adapter;
16
+ }
17
+
5
18
  /**
6
19
  * Backend plugin definition
7
20
  * Defines API routes and data access for a feature
@@ -23,8 +36,9 @@ export interface BackendPlugin<
23
36
  *
24
37
  * @param adapter - Better DB adapter instance with methods:
25
38
  * create, update, updateMany, delete, deleteMany, findOne, findMany, count
39
+ * @param context - Optional context with access to all plugins (for introspection)
26
40
  */
27
- routes: (adapter: Adapter) => TRoutes;
41
+ routes: (adapter: Adapter, context?: BetterStackContext) => TRoutes;
28
42
  dbPlugin: DbPlugin;
29
43
  }
30
44