@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.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/app/docs/App.tsx +72 -0
- package/app/docs/api/client.ts +303 -0
- package/app/docs/api/contract.ts +24 -0
- package/app/docs/api/docs.ts +38 -0
- package/app/docs/api/errors.ts +144 -0
- package/app/docs/boot.tsx +58 -0
- package/app/docs/components/Badge.tsx +65 -0
- package/app/docs/components/Button.tsx +37 -0
- package/app/docs/components/Card.tsx +41 -0
- package/app/docs/components/ConfirmDialog.tsx +173 -0
- package/app/docs/components/CopyButton.tsx +68 -0
- package/app/docs/components/Dialog.tsx +230 -0
- package/app/docs/components/EmptyState.tsx +34 -0
- package/app/docs/components/ErrorView.tsx +70 -0
- package/app/docs/components/Field.tsx +93 -0
- package/app/docs/components/JsonEditor.tsx +151 -0
- package/app/docs/components/JsonView.tsx +338 -0
- package/app/docs/components/KeyValue.tsx +63 -0
- package/app/docs/components/ProblemBanner.tsx +116 -0
- package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
- package/app/docs/components/Spinner.tsx +33 -0
- package/app/docs/components/Table.tsx +33 -0
- package/app/docs/components/Tabs.tsx +226 -0
- package/app/docs/components/classNames.ts +4 -0
- package/app/docs/components/components.css +691 -0
- package/app/docs/components/copy.ts +89 -0
- package/app/docs/components/fieldContext.ts +29 -0
- package/app/docs/components/inputValues.ts +142 -0
- package/app/docs/components/inputs.tsx +387 -0
- package/app/docs/components/jsonParse.ts +338 -0
- package/app/docs/components/listKeys.ts +30 -0
- package/app/docs/config.ts +86 -0
- package/app/docs/context.ts +33 -0
- package/app/docs/embed.ts +74 -0
- package/app/docs/env.d.ts +2 -0
- package/app/docs/format.ts +9 -0
- package/app/docs/hooks/useUrlTab.ts +16 -0
- package/app/docs/main.tsx +11 -0
- package/app/docs/queryClient.ts +54 -0
- package/app/docs/router.tsx +194 -0
- package/app/docs/routing.ts +194 -0
- package/app/docs/screens/docs/document.ts +27 -0
- package/app/docs/screens/docs/http/OperationView.tsx +407 -0
- package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
- package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
- package/app/docs/screens/docs/http/http.css +405 -0
- package/app/docs/screens/docs/http/index.tsx +395 -0
- package/app/docs/screens/docs/http/model.ts +604 -0
- package/app/docs/screens/docs/http/tryIt.ts +455 -0
- package/app/docs/screens/docs/permissions.ts +6 -0
- package/app/docs/screens/docs/schema/Prose.tsx +43 -0
- package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
- package/app/docs/screens/docs/schema/index.ts +30 -0
- package/app/docs/screens/docs/schema/resolve.ts +466 -0
- package/app/docs/screens/docs/schema/schema.css +137 -0
- package/app/docs/screens/docs/schema/skeleton.ts +92 -0
- package/app/docs/screens/docs/viewer.tsx +154 -0
- package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
- package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
- package/app/docs/screens/docs/ws/format.ts +82 -0
- package/app/docs/screens/docs/ws/index.tsx +286 -0
- package/app/docs/screens/docs/ws/items.tsx +786 -0
- package/app/docs/screens/docs/ws/model.ts +1178 -0
- package/app/docs/screens/docs/ws/panels.tsx +440 -0
- package/app/docs/screens/docs/ws/ws.css +231 -0
- package/app/docs/shortcuts.ts +94 -0
- package/app/docs/styles/base.css +847 -0
- package/app/docs/styles/tokens.css +143 -0
- package/app/docs/theme.ts +56 -0
- package/app/kit/unauthorized.ts +222 -0
- package/dist/docs/assets/main-0rchxtv5.js +18 -0
- package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
- package/dist/docs/assets/main-cqgyvs86.css +1 -0
- package/dist/docs/manifest.json +26 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
- package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
- package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
- package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
- package/dist/jobs/assets/index-02427smp.css +1 -0
- package/dist/jobs/assets/index-09z2cjft.js +4 -0
- package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
- package/dist/jobs/assets/index-18bn0gvy.css +1 -0
- package/dist/jobs/assets/index-47pxmyxn.js +4 -0
- package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
- package/dist/jobs/assets/index-4e55z986.js +4 -0
- package/dist/jobs/assets/index-4e55z986.js.map +11 -0
- package/dist/jobs/assets/index-5c933424.js +4 -0
- package/dist/jobs/assets/index-5c933424.js.map +18 -0
- package/dist/jobs/assets/index-5kbyqspm.css +1 -0
- package/dist/jobs/assets/index-5ng37hk6.css +1 -0
- package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
- package/dist/jobs/assets/index-71qs5qyx.css +1 -0
- package/dist/jobs/assets/index-btpg4r8b.css +1 -0
- package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
- package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
- package/dist/jobs/assets/index-e0yy2skq.css +1 -0
- package/dist/jobs/assets/index-hs60b25v.js +10 -0
- package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
- package/dist/jobs/assets/index-k2smrr70.js +4 -0
- package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
- package/dist/jobs/assets/index-q24jw3w9.js +4 -0
- package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
- package/dist/jobs/assets/index-qfb80xen.css +1 -0
- package/dist/jobs/assets/index-rtynx6wj.js +4 -0
- package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
- package/dist/jobs/assets/live-6k7s6358.js +4 -0
- package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
- package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
- package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
- package/dist/jobs/assets/main-3yj6bv90.js +5 -0
- package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
- package/dist/jobs/assets/main-4g69fn3x.js +5 -0
- package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
- package/dist/jobs/assets/main-4gte3eqq.js +5 -0
- package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
- package/dist/jobs/assets/main-4j1fcevh.js +5 -0
- package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
- package/dist/jobs/assets/main-54qwkz28.css +1 -0
- package/dist/jobs/assets/main-5grjvsy7.js +5 -0
- package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
- package/dist/jobs/assets/main-5zbx60gq.js +5 -0
- package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
- package/dist/jobs/assets/main-9prgjhg4.js +5 -0
- package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
- package/dist/jobs/assets/main-a250ffk7.js +17 -0
- package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
- package/dist/jobs/assets/main-d6ge7w08.js +5 -0
- package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
- package/dist/jobs/assets/main-gngzht43.js +5 -0
- package/dist/jobs/assets/main-gngzht43.js.map +10 -0
- package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
- package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
- package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
- package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
- package/dist/jobs/assets/main-jkvd92z9.js +5 -0
- package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
- package/dist/jobs/assets/main-jv5xqd13.js +5 -0
- package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
- package/dist/jobs/assets/main-jxre7w93.js +5 -0
- package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
- package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
- package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
- package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
- package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
- package/dist/jobs/assets/main-pegp0ygb.js +5 -0
- package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
- package/dist/jobs/assets/main-qr573853.js +5 -0
- package/dist/jobs/assets/main-qr573853.js.map +12 -0
- package/dist/jobs/assets/main-rq97rdr4.js +4 -0
- package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
- package/dist/jobs/assets/main-szjdmydc.js +5 -0
- package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
- package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
- package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
- package/dist/jobs/assets/main-ts19j4n0.js +5 -0
- package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
- package/dist/jobs/assets/main-w93x1dzs.js +5 -0
- package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
- package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
- package/dist/jobs/assets/validation-ysjfy072.js +4 -0
- package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
- package/dist/jobs/manifest.json +511 -0
- package/dts/docs/assets.d.ts +144 -0
- package/dts/docs/assets.d.ts.map +1 -0
- package/dts/docs/browser/config.d.ts +61 -0
- package/dts/docs/browser/config.d.ts.map +1 -0
- package/dts/docs/browser/index.d.ts +12 -0
- package/dts/docs/browser/index.d.ts.map +1 -0
- package/dts/docs/docsUi.d.ts +42 -0
- package/dts/docs/docsUi.d.ts.map +1 -0
- package/dts/docs/hosted.d.ts +22 -0
- package/dts/docs/hosted.d.ts.map +1 -0
- package/dts/docs/index.d.ts +18 -0
- package/dts/docs/index.d.ts.map +1 -0
- package/dts/docs/shell.d.ts +57 -0
- package/dts/docs/shell.d.ts.map +1 -0
- package/dts/docs/types.d.ts +137 -0
- package/dts/docs/types.d.ts.map +1 -0
- package/dts/index.d.ts +21 -0
- package/dts/index.d.ts.map +1 -0
- package/dts/jobs/assets.d.ts +157 -0
- package/dts/jobs/assets.d.ts.map +1 -0
- package/dts/jobs/browser/config.d.ts +67 -0
- package/dts/jobs/browser/config.d.ts.map +1 -0
- package/dts/jobs/browser/index.d.ts +10 -0
- package/dts/jobs/browser/index.d.ts.map +1 -0
- package/dts/jobs/browser/unauthorized.d.ts +9 -0
- package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
- package/dts/jobs/index.d.ts +24 -0
- package/dts/jobs/index.d.ts.map +1 -0
- package/dts/jobs/jobsUi.d.ts +63 -0
- package/dts/jobs/jobsUi.d.ts.map +1 -0
- package/dts/jobs/shell.d.ts +77 -0
- package/dts/jobs/shell.d.ts.map +1 -0
- package/dts/jobs/types.d.ts +146 -0
- package/dts/jobs/types.d.ts.map +1 -0
- package/dts/kit/browser/shared.d.ts +10 -0
- package/dts/kit/browser/shared.d.ts.map +1 -0
- package/dts/kit/browser/unauthorized.d.ts +74 -0
- package/dts/kit/browser/unauthorized.d.ts.map +1 -0
- package/dts/kit/unauthorized.d.ts +47 -0
- package/dts/kit/unauthorized.d.ts.map +1 -0
- package/guides/docs.md +170 -0
- package/guides/jobs.md +1120 -0
- package/lib/docs/assets.ts +427 -0
- package/lib/docs/browser/config.ts +66 -0
- package/lib/docs/browser/index.ts +24 -0
- package/lib/docs/docsUi.ts +623 -0
- package/lib/docs/hosted.ts +31 -0
- package/lib/docs/index.ts +37 -0
- package/lib/docs/shell.ts +156 -0
- package/lib/docs/types.ts +149 -0
- package/lib/index.ts +27 -0
- package/lib/jobs/assets.ts +448 -0
- package/lib/jobs/browser/config.ts +65 -0
- package/lib/jobs/browser/index.ts +9 -0
- package/lib/jobs/browser/unauthorized.ts +11 -0
- package/lib/jobs/index.ts +41 -0
- package/lib/jobs/jobsUi.ts +709 -0
- package/lib/jobs/shell.ts +205 -0
- package/lib/jobs/types.ts +162 -0
- package/lib/kit/browser/shared.ts +11 -0
- package/lib/kit/browser/unauthorized.ts +114 -0
- package/lib/kit/unauthorized.ts +222 -0
- 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("&", "&")
|
|
26
|
+
.replaceAll("<", "<")
|
|
27
|
+
.replaceAll(">", ">")
|
|
28
|
+
.replaceAll('"', """)
|
|
29
|
+
.replaceAll("'", "'");
|
|
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";
|