@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2

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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/app/docs/App.tsx +72 -0
  4. package/app/docs/api/client.ts +303 -0
  5. package/app/docs/api/contract.ts +24 -0
  6. package/app/docs/api/docs.ts +38 -0
  7. package/app/docs/api/errors.ts +144 -0
  8. package/app/docs/boot.tsx +58 -0
  9. package/app/docs/components/Badge.tsx +65 -0
  10. package/app/docs/components/Button.tsx +37 -0
  11. package/app/docs/components/Card.tsx +41 -0
  12. package/app/docs/components/ConfirmDialog.tsx +173 -0
  13. package/app/docs/components/CopyButton.tsx +68 -0
  14. package/app/docs/components/Dialog.tsx +230 -0
  15. package/app/docs/components/EmptyState.tsx +34 -0
  16. package/app/docs/components/ErrorView.tsx +70 -0
  17. package/app/docs/components/Field.tsx +93 -0
  18. package/app/docs/components/JsonEditor.tsx +151 -0
  19. package/app/docs/components/JsonView.tsx +338 -0
  20. package/app/docs/components/KeyValue.tsx +63 -0
  21. package/app/docs/components/ProblemBanner.tsx +116 -0
  22. package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
  23. package/app/docs/components/Spinner.tsx +33 -0
  24. package/app/docs/components/Table.tsx +33 -0
  25. package/app/docs/components/Tabs.tsx +226 -0
  26. package/app/docs/components/classNames.ts +4 -0
  27. package/app/docs/components/components.css +691 -0
  28. package/app/docs/components/copy.ts +89 -0
  29. package/app/docs/components/fieldContext.ts +29 -0
  30. package/app/docs/components/inputValues.ts +142 -0
  31. package/app/docs/components/inputs.tsx +387 -0
  32. package/app/docs/components/jsonParse.ts +338 -0
  33. package/app/docs/components/listKeys.ts +30 -0
  34. package/app/docs/config.ts +86 -0
  35. package/app/docs/context.ts +33 -0
  36. package/app/docs/embed.ts +74 -0
  37. package/app/docs/env.d.ts +2 -0
  38. package/app/docs/format.ts +9 -0
  39. package/app/docs/hooks/useUrlTab.ts +16 -0
  40. package/app/docs/main.tsx +11 -0
  41. package/app/docs/queryClient.ts +54 -0
  42. package/app/docs/router.tsx +194 -0
  43. package/app/docs/routing.ts +194 -0
  44. package/app/docs/screens/docs/document.ts +27 -0
  45. package/app/docs/screens/docs/http/OperationView.tsx +407 -0
  46. package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
  47. package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
  48. package/app/docs/screens/docs/http/http.css +405 -0
  49. package/app/docs/screens/docs/http/index.tsx +395 -0
  50. package/app/docs/screens/docs/http/model.ts +604 -0
  51. package/app/docs/screens/docs/http/tryIt.ts +455 -0
  52. package/app/docs/screens/docs/permissions.ts +6 -0
  53. package/app/docs/screens/docs/schema/Prose.tsx +43 -0
  54. package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
  55. package/app/docs/screens/docs/schema/index.ts +30 -0
  56. package/app/docs/screens/docs/schema/resolve.ts +466 -0
  57. package/app/docs/screens/docs/schema/schema.css +137 -0
  58. package/app/docs/screens/docs/schema/skeleton.ts +92 -0
  59. package/app/docs/screens/docs/viewer.tsx +154 -0
  60. package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
  61. package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
  62. package/app/docs/screens/docs/ws/format.ts +82 -0
  63. package/app/docs/screens/docs/ws/index.tsx +286 -0
  64. package/app/docs/screens/docs/ws/items.tsx +786 -0
  65. package/app/docs/screens/docs/ws/model.ts +1178 -0
  66. package/app/docs/screens/docs/ws/panels.tsx +440 -0
  67. package/app/docs/screens/docs/ws/ws.css +231 -0
  68. package/app/docs/shortcuts.ts +94 -0
  69. package/app/docs/styles/base.css +847 -0
  70. package/app/docs/styles/tokens.css +143 -0
  71. package/app/docs/theme.ts +56 -0
  72. package/app/kit/unauthorized.ts +222 -0
  73. package/dist/docs/assets/main-0rchxtv5.js +18 -0
  74. package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
  75. package/dist/docs/assets/main-cqgyvs86.css +1 -0
  76. package/dist/docs/manifest.json +26 -0
  77. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
  78. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
  79. package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
  80. package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
  81. package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
  82. package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
  83. package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
  84. package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
  85. package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
  86. package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
  87. package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
  88. package/dist/jobs/assets/index-02427smp.css +1 -0
  89. package/dist/jobs/assets/index-09z2cjft.js +4 -0
  90. package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
  91. package/dist/jobs/assets/index-18bn0gvy.css +1 -0
  92. package/dist/jobs/assets/index-47pxmyxn.js +4 -0
  93. package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
  94. package/dist/jobs/assets/index-4e55z986.js +4 -0
  95. package/dist/jobs/assets/index-4e55z986.js.map +11 -0
  96. package/dist/jobs/assets/index-5c933424.js +4 -0
  97. package/dist/jobs/assets/index-5c933424.js.map +18 -0
  98. package/dist/jobs/assets/index-5kbyqspm.css +1 -0
  99. package/dist/jobs/assets/index-5ng37hk6.css +1 -0
  100. package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
  101. package/dist/jobs/assets/index-71qs5qyx.css +1 -0
  102. package/dist/jobs/assets/index-btpg4r8b.css +1 -0
  103. package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
  104. package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
  105. package/dist/jobs/assets/index-e0yy2skq.css +1 -0
  106. package/dist/jobs/assets/index-hs60b25v.js +10 -0
  107. package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
  108. package/dist/jobs/assets/index-k2smrr70.js +4 -0
  109. package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
  110. package/dist/jobs/assets/index-q24jw3w9.js +4 -0
  111. package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
  112. package/dist/jobs/assets/index-qfb80xen.css +1 -0
  113. package/dist/jobs/assets/index-rtynx6wj.js +4 -0
  114. package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
  115. package/dist/jobs/assets/live-6k7s6358.js +4 -0
  116. package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
  117. package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
  118. package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
  119. package/dist/jobs/assets/main-3yj6bv90.js +5 -0
  120. package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
  121. package/dist/jobs/assets/main-4g69fn3x.js +5 -0
  122. package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
  123. package/dist/jobs/assets/main-4gte3eqq.js +5 -0
  124. package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
  125. package/dist/jobs/assets/main-4j1fcevh.js +5 -0
  126. package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
  127. package/dist/jobs/assets/main-54qwkz28.css +1 -0
  128. package/dist/jobs/assets/main-5grjvsy7.js +5 -0
  129. package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
  130. package/dist/jobs/assets/main-5zbx60gq.js +5 -0
  131. package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
  132. package/dist/jobs/assets/main-9prgjhg4.js +5 -0
  133. package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
  134. package/dist/jobs/assets/main-a250ffk7.js +17 -0
  135. package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
  136. package/dist/jobs/assets/main-d6ge7w08.js +5 -0
  137. package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
  138. package/dist/jobs/assets/main-gngzht43.js +5 -0
  139. package/dist/jobs/assets/main-gngzht43.js.map +10 -0
  140. package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
  141. package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
  142. package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
  143. package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
  144. package/dist/jobs/assets/main-jkvd92z9.js +5 -0
  145. package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
  146. package/dist/jobs/assets/main-jv5xqd13.js +5 -0
  147. package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
  148. package/dist/jobs/assets/main-jxre7w93.js +5 -0
  149. package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
  150. package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
  151. package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
  152. package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
  153. package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
  154. package/dist/jobs/assets/main-pegp0ygb.js +5 -0
  155. package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
  156. package/dist/jobs/assets/main-qr573853.js +5 -0
  157. package/dist/jobs/assets/main-qr573853.js.map +12 -0
  158. package/dist/jobs/assets/main-rq97rdr4.js +4 -0
  159. package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
  160. package/dist/jobs/assets/main-szjdmydc.js +5 -0
  161. package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
  162. package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
  163. package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
  164. package/dist/jobs/assets/main-ts19j4n0.js +5 -0
  165. package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
  166. package/dist/jobs/assets/main-w93x1dzs.js +5 -0
  167. package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
  168. package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
  169. package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
  170. package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
  171. package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
  172. package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
  173. package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
  174. package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
  175. package/dist/jobs/assets/validation-ysjfy072.js +4 -0
  176. package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
  177. package/dist/jobs/manifest.json +511 -0
  178. package/dts/docs/assets.d.ts +144 -0
  179. package/dts/docs/assets.d.ts.map +1 -0
  180. package/dts/docs/browser/config.d.ts +61 -0
  181. package/dts/docs/browser/config.d.ts.map +1 -0
  182. package/dts/docs/browser/index.d.ts +12 -0
  183. package/dts/docs/browser/index.d.ts.map +1 -0
  184. package/dts/docs/docsUi.d.ts +42 -0
  185. package/dts/docs/docsUi.d.ts.map +1 -0
  186. package/dts/docs/hosted.d.ts +22 -0
  187. package/dts/docs/hosted.d.ts.map +1 -0
  188. package/dts/docs/index.d.ts +18 -0
  189. package/dts/docs/index.d.ts.map +1 -0
  190. package/dts/docs/shell.d.ts +57 -0
  191. package/dts/docs/shell.d.ts.map +1 -0
  192. package/dts/docs/types.d.ts +137 -0
  193. package/dts/docs/types.d.ts.map +1 -0
  194. package/dts/index.d.ts +21 -0
  195. package/dts/index.d.ts.map +1 -0
  196. package/dts/jobs/assets.d.ts +157 -0
  197. package/dts/jobs/assets.d.ts.map +1 -0
  198. package/dts/jobs/browser/config.d.ts +67 -0
  199. package/dts/jobs/browser/config.d.ts.map +1 -0
  200. package/dts/jobs/browser/index.d.ts +10 -0
  201. package/dts/jobs/browser/index.d.ts.map +1 -0
  202. package/dts/jobs/browser/unauthorized.d.ts +9 -0
  203. package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
  204. package/dts/jobs/index.d.ts +24 -0
  205. package/dts/jobs/index.d.ts.map +1 -0
  206. package/dts/jobs/jobsUi.d.ts +63 -0
  207. package/dts/jobs/jobsUi.d.ts.map +1 -0
  208. package/dts/jobs/shell.d.ts +77 -0
  209. package/dts/jobs/shell.d.ts.map +1 -0
  210. package/dts/jobs/types.d.ts +146 -0
  211. package/dts/jobs/types.d.ts.map +1 -0
  212. package/dts/kit/browser/shared.d.ts +10 -0
  213. package/dts/kit/browser/shared.d.ts.map +1 -0
  214. package/dts/kit/browser/unauthorized.d.ts +74 -0
  215. package/dts/kit/browser/unauthorized.d.ts.map +1 -0
  216. package/dts/kit/unauthorized.d.ts +47 -0
  217. package/dts/kit/unauthorized.d.ts.map +1 -0
  218. package/guides/docs.md +170 -0
  219. package/guides/jobs.md +1120 -0
  220. package/lib/docs/assets.ts +427 -0
  221. package/lib/docs/browser/config.ts +66 -0
  222. package/lib/docs/browser/index.ts +24 -0
  223. package/lib/docs/docsUi.ts +623 -0
  224. package/lib/docs/hosted.ts +31 -0
  225. package/lib/docs/index.ts +37 -0
  226. package/lib/docs/shell.ts +156 -0
  227. package/lib/docs/types.ts +149 -0
  228. package/lib/index.ts +27 -0
  229. package/lib/jobs/assets.ts +448 -0
  230. package/lib/jobs/browser/config.ts +65 -0
  231. package/lib/jobs/browser/index.ts +9 -0
  232. package/lib/jobs/browser/unauthorized.ts +11 -0
  233. package/lib/jobs/index.ts +41 -0
  234. package/lib/jobs/jobsUi.ts +709 -0
  235. package/lib/jobs/shell.ts +205 -0
  236. package/lib/jobs/types.ts +162 -0
  237. package/lib/kit/browser/shared.ts +11 -0
  238. package/lib/kit/browser/unauthorized.ts +114 -0
  239. package/lib/kit/unauthorized.ts +222 -0
  240. package/package.json +198 -4
@@ -0,0 +1,156 @@
1
+ import type { UiAssets } from "./assets";
2
+ /**
3
+ * The HTML shell: the one page every client route answers with, and the
4
+ * Content-Security-Policy it is served under.
5
+ *
6
+ * Everything in it is fixed per mount except the nonce, which is fresh per
7
+ * request. The configuration travels as a nonce'd
8
+ * `<script type="application/json">`, escaped for a script context, so a title
9
+ * containing `</script>` cannot end the element early.
10
+ */
11
+ import type { DocsUiConfig, DocsUiTheme } from "./browser/config";
12
+ import { Buffer } from "node:buffer";
13
+ import { DOCS_UI_CONFIG_ELEMENT_ID } from "./browser/config";
14
+
15
+ /** A fresh CSP nonce: 128 random bits, base64. */
16
+ export function createNonce(): string {
17
+ return Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString(
18
+ "base64",
19
+ );
20
+ }
21
+
22
+ /** Escapes text for an HTML text node or a quoted attribute value. */
23
+ export function escapeHtml(value: string): string {
24
+ return value
25
+ .replaceAll("&", "&amp;")
26
+ .replaceAll("<", "&lt;")
27
+ .replaceAll(">", "&gt;")
28
+ .replaceAll('"', "&quot;")
29
+ .replaceAll("'", "&#39;");
30
+ }
31
+
32
+ /**
33
+ * Serialises a value as JSON that is safe inside a `<script>` element: `<`,
34
+ * `>` and `&` become `<`/`>`/`&` (so neither `</script>` nor
35
+ * `<!--` can appear), and U+2028/U+2029 are escaped too. The result is still
36
+ * valid JSON that parses back to the same value.
37
+ */
38
+ export function jsonForScript(value: unknown): string {
39
+ return JSON.stringify(value)
40
+ .replaceAll("<", "\\u003c")
41
+ .replaceAll(">", "\\u003e")
42
+ .replaceAll("&", "\\u0026")
43
+ .replaceAll(/\u2028/g, "\\u2028")
44
+ .replaceAll(/\u2029/g, "\\u2029");
45
+ }
46
+
47
+ /** The `color-scheme` meta value for a theme, so the first paint matches it. */
48
+ function colorScheme(theme: DocsUiTheme): string {
49
+ return theme === "system" ? "light dark" : theme;
50
+ }
51
+
52
+ /** Inputs to {@link cspHeader}. */
53
+ export interface CspOptions {
54
+ /** This request's nonce. */
55
+ nonce: string;
56
+ /**
57
+ * `connect-src` sources after `'self'` — for a cross-origin document URL,
58
+ * its origin (see {@link documentOriginSource}). A source that is not a
59
+ * single CSP token (whitespace, `;` or `,`) is dropped rather than written
60
+ * into the header. Defaults to none.
61
+ */
62
+ connectSrc?: readonly string[];
63
+ }
64
+
65
+ /** One CSP source expression: no whitespace, `;` or `,` that could end it early. */
66
+ const CSP_TOKEN = /^[^\s;,]+$/;
67
+
68
+ /**
69
+ * The shell's Content-Security-Policy. Scripts only from this origin and
70
+ * carrying the nonce; styles only from files (no inline `<style>`, no `style`
71
+ * attributes in markup — setting `element.style` from script is still fine);
72
+ * connections to `'self'` plus `connectSrc`.
73
+ */
74
+ export function cspHeader(options: CspOptions): string {
75
+ const connect = [
76
+ "'self'",
77
+ ...(options.connectSrc ?? []).filter((source) => CSP_TOKEN.test(source)),
78
+ ];
79
+ return [
80
+ "default-src 'self'",
81
+ `script-src 'self' 'nonce-${options.nonce}'`,
82
+ "style-src 'self'",
83
+ "img-src 'self' data:",
84
+ `connect-src ${connect.join(" ")}`,
85
+ "base-uri 'none'",
86
+ "frame-ancestors 'none'",
87
+ "form-action 'self'",
88
+ ].join("; ");
89
+ }
90
+
91
+ /**
92
+ * The origin a cross-origin document URL adds to `connect-src`, or
93
+ * `undefined` for a same-origin one (`/jobs-api/openapi.json`). The viewer
94
+ * fetches the document from the page; a thunk is served same-origin by the
95
+ * router itself. Anything malformed is dropped rather than written into the
96
+ * header.
97
+ */
98
+ export function documentOriginSource(documentUrl: string): string | undefined {
99
+ if (!/^https?:\/\//i.test(documentUrl)) {
100
+ return undefined;
101
+ }
102
+ try {
103
+ return new URL(documentUrl).origin;
104
+ } catch {
105
+ return undefined;
106
+ }
107
+ }
108
+
109
+ /** A renderer for one mount: fixed parts computed once, the nonce per call. */
110
+ export type ShellRenderer = (nonce: string) => string;
111
+
112
+ /**
113
+ * Prepares the shell for a mount and a bundle. The returned function only
114
+ * splices a nonce in.
115
+ */
116
+ export function createShellRenderer(
117
+ config: DocsUiConfig,
118
+ assets: UiAssets,
119
+ ): ShellRenderer {
120
+ const href = (name: string): string =>
121
+ escapeHtml(`${config.assetsPath}/${encodeURIComponent(name)}`);
122
+ const integrity = (name: string): string =>
123
+ escapeHtml(assets.files.get(name)?.integrity ?? "");
124
+ const stylesheets = assets.entry.css
125
+ .map(
126
+ (name) =>
127
+ `<link rel="stylesheet" href="${href(name)}" integrity="${integrity(name)}">`,
128
+ )
129
+ .join("\n ");
130
+ const title = escapeHtml(config.title);
131
+ const json = jsonForScript(config);
132
+ const js = assets.entry.js;
133
+ const scheme = colorScheme(config.theme);
134
+
135
+ return (nonce) => {
136
+ const n = escapeHtml(nonce);
137
+ return `<!doctype html>
138
+ <html lang="en">
139
+ <head>
140
+ <meta charset="utf-8">
141
+ <meta name="viewport" content="width=device-width, initial-scale=1">
142
+ <meta name="color-scheme" content="${scheme}">
143
+ <meta property="csp-nonce" nonce="${n}">
144
+ <title>${title}</title>
145
+ <link rel="icon" href="data:,">
146
+ ${stylesheets}
147
+ <script type="application/json" id="${DOCS_UI_CONFIG_ELEMENT_ID}" nonce="${n}">${json}</script>
148
+ <script type="module" src="${href(js)}" integrity="${integrity(js)}" nonce="${n}"></script>
149
+ </head>
150
+ <body>
151
+ <div id="root"></div>
152
+ </body>
153
+ </html>
154
+ `;
155
+ };
156
+ }
@@ -0,0 +1,149 @@
1
+ import type {
2
+ BunRequest,
3
+ BunRouter,
4
+ LoggerLike,
5
+ RouterHandler,
6
+ } from "@kingsleyweb/bun-common";
7
+ import type { DocsUiConfig, DocsUiTheme } from "./browser/config";
8
+ import type { UnauthorizedOptions } from "../kit/browser/unauthorized";
9
+
10
+ /**
11
+ * Where to send the browser when an authorization check fails, instead of
12
+ * leaving it on a viewer that reports the failure. See
13
+ * {@link DocsUiOptions.unauthorized}; the fields are described on
14
+ * `UnauthorizedOptions`.
15
+ */
16
+ export type DocsUiUnauthorizedOptions = UnauthorizedOptions;
17
+
18
+ /** What {@link DocsUiOptions.authorize} is told about the request being decided. */
19
+ export interface DocsUiAuthorizeContext {
20
+ /** `true` for a bundle file under `<basePath>/assets/`, `false` for the HTML shell or the document a thunk serves. */
21
+ asset: boolean;
22
+ /** The request's full path, e.g. `"/docs/http/listQueues"` (no query string). */
23
+ path: string;
24
+ }
25
+
26
+ /**
27
+ * What `authorize` answers — the same shape as bun-jobs' API and UI take:
28
+ * `true`/`false`, or `{ allow: true }` / `{ allow: false, status?: 401 | 403,
29
+ * reason? }`. A denial is a 401 only when `status` is exactly `401`; any other
30
+ * `status` (a `429`, a `500`, a string) is a 403 — fail closed. Rate limiting
31
+ * belongs in `middleware`, which can answer 429 itself. Anything that is not
32
+ * one of these shapes denies with 403.
33
+ */
34
+ export type DocsUiAuthorizeResult =
35
+ boolean | { allow: boolean; status?: 401 | 403; reason?: string };
36
+
37
+ /**
38
+ * Decides whether a request may load the viewer. May be async; a throw (or a
39
+ * rejection) is a 500. A denial is 401 for `status: 401` and 403 for every
40
+ * other status — see {@link DocsUiAuthorizeResult}.
41
+ */
42
+ export type DocsUiAuthorize = (
43
+ req: BunRequest,
44
+ context: DocsUiAuthorizeContext,
45
+ ) => DocsUiAuthorizeResult | Promise<DocsUiAuthorizeResult>;
46
+
47
+ /**
48
+ * The one document the viewer shows, produced on demand. A thunk is served by
49
+ * the router at `<basePath>/document.json`, so the viewer always fetches
50
+ * same-origin and the strict CSP needs no exception for it.
51
+ */
52
+ export type DocsUiDocumentThunk = () =>
53
+ Record<string, unknown> | Promise<Record<string, unknown>>;
54
+
55
+ /** Options for {@link docsUi}. */
56
+ export interface DocsUiOptions {
57
+ /**
58
+ * The document to show: where the viewer fetches it — a same-origin
59
+ * absolute path (`"/jobs-api/openapi.json"`) or an `http(s)://` URL — or a
60
+ * thunk producing it, which the router serves at `<basePath>/document.json`.
61
+ *
62
+ * A URL follows the path rules of `basePath` (checked on the string as
63
+ * written, so `..`, a space or `%2F` is refused rather than normalised
64
+ * away) and may not carry credentials; a URL's origin is added to the CSP's
65
+ * `connect-src`. Whether it is an OpenAPI 3.1 or an AsyncAPI 3.0 document is
66
+ * read from the document itself (`openapi` / `asyncapi`).
67
+ */
68
+ document: string | DocsUiDocumentThunk;
69
+ /**
70
+ * Absolute path the viewer is mounted at: no trailing slash, not `"/"`,
71
+ * segments of letters, digits, `_`, `.`, `~` and `-`. Defaults to `"/docs"`.
72
+ */
73
+ basePath?: string;
74
+ /** Page title. Defaults to `"API docs"`. */
75
+ title?: string;
76
+ /**
77
+ * The specification extensions the viewers read, as their prefixes, first
78
+ * hit winning: a `<prefix>-<name>` extension on an operation, channel or
79
+ * scheme is read from the first prefix carrying it, and one under any other
80
+ * prefix is ignored. Defaults to `["x-bun-common", "x-bun-jobs"]`.
81
+ */
82
+ extensions?: readonly string[];
83
+ /**
84
+ * Header the try-it panel sends on every mutation (any non-empty value), as
85
+ * the documented API's CSRF rule, or `false` for none. Unset: none.
86
+ */
87
+ csrfHeader?: string | false;
88
+ /**
89
+ * Guards the shell, the bundle and a thunk's document. Same result shape as
90
+ * the bun-jobs API's, and fails closed the same way: a denial is 401 only
91
+ * for `status: 401` and 403 for any other status (`429` included), an
92
+ * unrecognised answer is 403 and a throw is 500. Omitted: everyone may load
93
+ * the page.
94
+ */
95
+ authorize?: DocsUiAuthorize;
96
+ /**
97
+ * Sends the browser to a sign-in (or any other) page when the caller is not
98
+ * authorized, instead of showing the failure. Off by default (today's
99
+ * behaviour: the shell answers a problem, the viewer shows the error).
100
+ *
101
+ * With it set, for a status in `statuses` (`[401]` by default):
102
+ *
103
+ * - **The shell:** a browser navigation (`GET`/`HEAD` of a page, not a
104
+ * bundle file and not `document.json`) that `authorize` denies answers
105
+ * `303 See Other` to `redirectTo`. A bundle file, the document, and a
106
+ * request that is not a navigation (a script's `fetch`, an `Accept`
107
+ * without `text/html`) keep the problem answer.
108
+ * - **The viewer:** a document read that fails with such a status sends
109
+ * the browser to `redirectTo` with `location.assign`, once per page
110
+ * load. The try-it panel's calls never do: their 401 is the documented
111
+ * API's answer to the request being tried, not the viewer's.
112
+ *
113
+ * A denial `middleware` answers itself is not redirected: only
114
+ * `authorize`'s. The target is fixed here, never read from the request, so
115
+ * it cannot be an open redirect. `jobsUi()` (`/jobs`) takes the same
116
+ * option.
117
+ */
118
+ unauthorized?: DocsUiUnauthorizedOptions;
119
+ /**
120
+ * Middleware run before every route, on the `GET`/`HEAD` requests the
121
+ * viewer answers (session loading, a rate limiter). Runs before
122
+ * `authorize`. Defaults to none.
123
+ */
124
+ middleware?: readonly RouterHandler[];
125
+ /** Initial colour scheme. Defaults to `"system"` (follows `prefers-color-scheme`). */
126
+ theme?: DocsUiTheme;
127
+ /**
128
+ * Where the bundle comes from. Unset (default): the prebuilt `dist/` when
129
+ * present, else an in-memory `Bun.build` on the first request. `true`:
130
+ * always build in memory. `false`: `dist/` is required — `docsUi()` throws
131
+ * a `ConfigError` without it.
132
+ */
133
+ dev?: boolean;
134
+ /** Logger, or anything `resolveLogger` accepts. Defaults to a console logger. */
135
+ logger?: LoggerLike;
136
+ }
137
+
138
+ /** A mountable API-docs viewer. */
139
+ export interface DocsUi {
140
+ /**
141
+ * Serves the shell, the bundle and a thunk's document. Mount with
142
+ * `use(ui.basePath, ui.router)`, on a bun-common or a bun-nest adapter.
143
+ */
144
+ readonly router: BunRouter;
145
+ /** The normalised `basePath`, no trailing slash. */
146
+ readonly basePath: string;
147
+ /** The configuration injected into every page, frozen. */
148
+ readonly config: Readonly<DocsUiConfig>;
149
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@kingsleyweb/bun-ui`: every bun-node browser UI, one subpath each.
3
+ *
4
+ * - `@kingsleyweb/bun-ui/jobs`: `jobsUi()`, the bun-jobs management UI
5
+ * (needs the optional peer `@kingsleyweb/bun-jobs`);
6
+ * `@kingsleyweb/bun-ui/jobs/browser`: its browser-safe config contract.
7
+ * - `@kingsleyweb/bun-ui/docs`: `docsUi()`, the viewer for one OpenAPI or
8
+ * AsyncAPI document; `/docs/browser` its browser-safe contract;
9
+ * `/docs/hosted` what bun-common's `"self-hosted"` viewer loads;
10
+ * `/docs/embed*` the viewer's screens, as source, for a host app to bundle.
11
+ *
12
+ * This root entry holds only what every UI shares and a browser may import:
13
+ * the theme type, the `unauthorized` option's types and defaults, and the
14
+ * assets' `Cache-Control`. It never imports a UI or a domain package, so
15
+ * importing it needs no peer.
16
+ */
17
+ export { ASSET_CACHE_CONTROL } from "./kit/browser/shared";
18
+ export type { UiTheme } from "./kit/browser/shared";
19
+ export {
20
+ DEFAULT_RETURN_PARAM,
21
+ DEFAULT_UNAUTHORIZED_STATUSES,
22
+ } from "./kit/browser/unauthorized";
23
+ export type {
24
+ UiUnauthorized,
25
+ UiUnauthorizedStatus,
26
+ UnauthorizedOptions,
27
+ } from "./kit/browser/unauthorized";