@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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server half of the `unauthorized` option, shared by `docsUi()` and
|
|
3
|
+
* `jobsUi()`: validating it, telling a page navigation from any other
|
|
4
|
+
* request, and answering the shell's `303`. The browser half is
|
|
5
|
+
* `app/kit/unauthorized.ts` (`./docs/embed/unauthorized`); what both need is
|
|
6
|
+
* `lib/kit/browser/unauthorized.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import type { BunRequest, BunResponse } from "@kingsleyweb/bun-common";
|
|
9
|
+
import type {
|
|
10
|
+
UiUnauthorized,
|
|
11
|
+
UiUnauthorizedStatus,
|
|
12
|
+
} from "./browser/unauthorized";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_RETURN_PARAM,
|
|
15
|
+
DEFAULT_UNAUTHORIZED_STATUSES,
|
|
16
|
+
unauthorizedRedirectUrl,
|
|
17
|
+
} from "./browser/unauthorized";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds the error a refused option throws: each caller passes its own
|
|
21
|
+
* `ConfigError` (`docsUi()` bun-common's, `jobsUi()` bun-jobs'), so a refusal
|
|
22
|
+
* is the class that caller's users already catch.
|
|
23
|
+
*/
|
|
24
|
+
export type UnauthorizedConfigError = (
|
|
25
|
+
/** What is wrong, starting with the option's name. */
|
|
26
|
+
message: string,
|
|
27
|
+
/** The offending values, for the error's context. */
|
|
28
|
+
context: Record<string, unknown>,
|
|
29
|
+
) => Error;
|
|
30
|
+
|
|
31
|
+
/** A query parameter name `unauthorized.returnParam` may be. */
|
|
32
|
+
const RETURN_PARAM = /^[\w.~-]+$/;
|
|
33
|
+
|
|
34
|
+
/** What a redirect target may not contain anywhere: whitespace, a control character or `\`. */
|
|
35
|
+
const UNSAFE_TARGET_CHAR = /[\s\\\p{Cc}]/u;
|
|
36
|
+
|
|
37
|
+
/** Any origin, to parse a path target with `new URL()`. */
|
|
38
|
+
const PATH_TARGET_ORIGIN = "http://unauthorized.invalid";
|
|
39
|
+
|
|
40
|
+
/** Whether `path` is `base` or lies under it, segment-wise. */
|
|
41
|
+
function isWithin(path: string, base: string): boolean {
|
|
42
|
+
return path === base || path.startsWith(`${base}/`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validates `unauthorized.redirectTo`: a path starting with one `/`, or an
|
|
47
|
+
* absolute `http(s)://` URL without credentials. A path is refused when it is
|
|
48
|
+
* the UI's own `basePath` or under it: the UI would answer it, deny it, and
|
|
49
|
+
* redirect again. The comparison resolves `..` as a browser does, and is
|
|
50
|
+
* case-insensitive, because the router matches paths case-insensitively, as
|
|
51
|
+
* Express does (`/JOBS/login` reaches a UI at `/jobs`). It also decodes
|
|
52
|
+
* percent-escapes, which the router does not: stricter than needed, and
|
|
53
|
+
* refusing such a target costs nothing. An absolute URL on the UI's own host
|
|
54
|
+
* is caught by the app instead, which knows its origin.
|
|
55
|
+
*/
|
|
56
|
+
function resolveRedirectTarget(
|
|
57
|
+
redirectTo: unknown,
|
|
58
|
+
basePath: string,
|
|
59
|
+
fail: UnauthorizedConfigError,
|
|
60
|
+
): string {
|
|
61
|
+
const badTarget = (): Error =>
|
|
62
|
+
fail(
|
|
63
|
+
`unauthorized.redirectTo must be a path starting with "/" or an absolute http: or https: URL`,
|
|
64
|
+
{ redirectTo },
|
|
65
|
+
);
|
|
66
|
+
if (typeof redirectTo !== "string" || redirectTo === "") {
|
|
67
|
+
throw badTarget();
|
|
68
|
+
}
|
|
69
|
+
if (UNSAFE_TARGET_CHAR.test(redirectTo)) {
|
|
70
|
+
throw fail(
|
|
71
|
+
`unauthorized.redirectTo may not contain whitespace, control characters or "\\"`,
|
|
72
|
+
{ redirectTo },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (redirectTo.startsWith("/")) {
|
|
76
|
+
if (redirectTo.startsWith("//")) {
|
|
77
|
+
throw fail(
|
|
78
|
+
`unauthorized.redirectTo may not start with "//": a browser reads it as another host. Write the full https:// URL instead`,
|
|
79
|
+
{ redirectTo },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const { pathname } = new URL(redirectTo, PATH_TARGET_ORIGIN);
|
|
83
|
+
let decoded = pathname;
|
|
84
|
+
try {
|
|
85
|
+
decoded = decodeURIComponent(pathname);
|
|
86
|
+
} catch {
|
|
87
|
+
// A malformed escape: compare it as written.
|
|
88
|
+
}
|
|
89
|
+
const path = decoded.replace(/\/+$/, "").toLowerCase();
|
|
90
|
+
if (path !== "" && isWithin(path, basePath.toLowerCase())) {
|
|
91
|
+
throw fail(
|
|
92
|
+
`unauthorized.redirectTo ${redirectTo} is inside the UI's basePath ${basePath}: the UI would deny it again and redirect to itself, a redirect loop. Point it at a page outside ${basePath}`,
|
|
93
|
+
{ redirectTo, basePath },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return redirectTo;
|
|
97
|
+
}
|
|
98
|
+
if (!/^https?:\/\//i.test(redirectTo)) {
|
|
99
|
+
throw badTarget();
|
|
100
|
+
}
|
|
101
|
+
let url: URL;
|
|
102
|
+
try {
|
|
103
|
+
url = new URL(redirectTo);
|
|
104
|
+
} catch {
|
|
105
|
+
throw badTarget();
|
|
106
|
+
}
|
|
107
|
+
if (url.username !== "" || url.password !== "") {
|
|
108
|
+
throw fail("unauthorized.redirectTo may not carry credentials", {
|
|
109
|
+
redirectTo,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return redirectTo;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validates the `unauthorized` option and fills in its defaults; `null` when
|
|
117
|
+
* it is unset (the option is off). Every refusal is thrown as `fail` builds
|
|
118
|
+
* it.
|
|
119
|
+
*
|
|
120
|
+
* @param value The option as the host passed it.
|
|
121
|
+
* @param basePath The UI's normalised `basePath`, for the redirect-loop check.
|
|
122
|
+
* @param fail Builds the caller's own `ConfigError`.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveUnauthorized(
|
|
125
|
+
value: unknown,
|
|
126
|
+
basePath: string,
|
|
127
|
+
fail: UnauthorizedConfigError,
|
|
128
|
+
): UiUnauthorized | null {
|
|
129
|
+
if (value === undefined) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
133
|
+
throw fail("unauthorized must be an object with a `redirectTo`", {
|
|
134
|
+
value,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const option = value as Record<string, unknown>;
|
|
138
|
+
const redirectTo = resolveRedirectTarget(option.redirectTo, basePath, fail);
|
|
139
|
+
|
|
140
|
+
let returnParam: string | null = DEFAULT_RETURN_PARAM;
|
|
141
|
+
if (option.returnParam === false) {
|
|
142
|
+
returnParam = null;
|
|
143
|
+
} else if (option.returnParam !== undefined) {
|
|
144
|
+
if (
|
|
145
|
+
typeof option.returnParam !== "string" ||
|
|
146
|
+
!RETURN_PARAM.test(option.returnParam)
|
|
147
|
+
) {
|
|
148
|
+
throw fail(
|
|
149
|
+
`unauthorized.returnParam must be false or a name of letters, digits, "_", ".", "~" and "-"`,
|
|
150
|
+
{ returnParam: option.returnParam },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
returnParam = option.returnParam;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let statuses = DEFAULT_UNAUTHORIZED_STATUSES;
|
|
157
|
+
if (option.statuses !== undefined) {
|
|
158
|
+
const list: unknown = option.statuses;
|
|
159
|
+
if (
|
|
160
|
+
!Array.isArray(list) ||
|
|
161
|
+
list.length === 0 ||
|
|
162
|
+
!list.every((status) => status === 401 || status === 403)
|
|
163
|
+
) {
|
|
164
|
+
throw fail(
|
|
165
|
+
"unauthorized.statuses must be a non-empty array of 401 and 403 (leave unauthorized out to switch it off)",
|
|
166
|
+
{ statuses: list },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
statuses = Object.freeze(
|
|
170
|
+
[...new Set(list as UiUnauthorizedStatus[])].sort((a, b) => a - b),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze({ redirectTo, returnParam, statuses });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Whether a request is a browser navigation, the only kind a redirect helps:
|
|
178
|
+
* `Sec-Fetch-Mode: navigate` when the browser sends the header (every
|
|
179
|
+
* current one does), else an `Accept` naming `text/html`. A script's
|
|
180
|
+
* `fetch`, `curl` (whose `Accept` is the any-type wildcard) and a health
|
|
181
|
+
* probe are not, and keep the problem.
|
|
182
|
+
*/
|
|
183
|
+
export function isNavigation(req: BunRequest): boolean {
|
|
184
|
+
const mode = req.get("sec-fetch-mode");
|
|
185
|
+
if (typeof mode === "string" && mode !== "") {
|
|
186
|
+
return mode.toLowerCase() === "navigate";
|
|
187
|
+
}
|
|
188
|
+
return (req.get("accept") ?? "").toLowerCase().includes("text/html");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Whether a denial with `status` should be answered with the redirect: the
|
|
193
|
+
* option is on, the route is a page of the shell (not a bundle file, not a
|
|
194
|
+
* document the viewer fetches), the status is listed, and the request is a
|
|
195
|
+
* navigation.
|
|
196
|
+
*/
|
|
197
|
+
export function redirectsDenial(
|
|
198
|
+
redirect: UiUnauthorized | null,
|
|
199
|
+
shell: boolean,
|
|
200
|
+
status: number,
|
|
201
|
+
req: BunRequest,
|
|
202
|
+
): redirect is UiUnauthorized {
|
|
203
|
+
return (
|
|
204
|
+
redirect !== null &&
|
|
205
|
+
shell &&
|
|
206
|
+
(redirect.statuses as readonly number[]).includes(status) &&
|
|
207
|
+
isNavigation(req)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Answers `303 See Other` to the sign-in target, carrying the request's own path and query back. */
|
|
212
|
+
export function sendUnauthorizedRedirect(
|
|
213
|
+
req: BunRequest,
|
|
214
|
+
res: BunResponse,
|
|
215
|
+
redirect: UiUnauthorized,
|
|
216
|
+
): void {
|
|
217
|
+
res.status(303);
|
|
218
|
+
res.setHeader("Location", unauthorizedRedirectUrl(redirect, req.originalUrl));
|
|
219
|
+
res.setHeader("Cache-Control", "no-store");
|
|
220
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
221
|
+
void res.end();
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,200 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kingsleyweb/bun-ui",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "3.0.0-rc.2",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "Every bun-node browser UI in one package, one subpath each: the bun-jobs management UI (./jobs, jobsUi()) and the OpenAPI/AsyncAPI docs viewer (./docs, docsUi()), each served by a bun-common router with a prebuilt React bundle, hash-named immutable assets, SRI and a strict CSP",
|
|
7
|
+
"author": "okafor obinna <kingsloob1@gmail.com>",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"@kingsleyweb/source": "./lib/index.ts",
|
|
12
|
+
"types": "./dts/index.d.ts",
|
|
13
|
+
"default": "./lib/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./jobs": {
|
|
16
|
+
"@kingsleyweb/source": "./lib/jobs/index.ts",
|
|
17
|
+
"types": "./dts/jobs/index.d.ts",
|
|
18
|
+
"default": "./lib/jobs/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"./jobs/browser": {
|
|
21
|
+
"@kingsleyweb/source": "./lib/jobs/browser/index.ts",
|
|
22
|
+
"types": "./dts/jobs/browser/index.d.ts",
|
|
23
|
+
"default": "./lib/jobs/browser/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"./docs": {
|
|
26
|
+
"@kingsleyweb/source": "./lib/docs/index.ts",
|
|
27
|
+
"types": "./dts/docs/index.d.ts",
|
|
28
|
+
"default": "./lib/docs/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"./docs/browser": {
|
|
31
|
+
"@kingsleyweb/source": "./lib/docs/browser/index.ts",
|
|
32
|
+
"types": "./dts/docs/browser/index.d.ts",
|
|
33
|
+
"default": "./lib/docs/browser/index.ts"
|
|
34
|
+
},
|
|
35
|
+
"./docs/hosted": {
|
|
36
|
+
"@kingsleyweb/source": "./lib/docs/hosted.ts",
|
|
37
|
+
"types": "./dts/docs/hosted.d.ts",
|
|
38
|
+
"default": "./lib/docs/hosted.ts"
|
|
39
|
+
},
|
|
40
|
+
"./docs/embed": {
|
|
41
|
+
"@kingsleyweb/source": "./app/docs/embed.ts",
|
|
42
|
+
"types": "./app/docs/embed.ts",
|
|
43
|
+
"default": "./app/docs/embed.ts"
|
|
44
|
+
},
|
|
45
|
+
"./docs/embed/routing": {
|
|
46
|
+
"@kingsleyweb/source": "./app/docs/routing.ts",
|
|
47
|
+
"types": "./app/docs/routing.ts",
|
|
48
|
+
"default": "./app/docs/routing.ts"
|
|
49
|
+
},
|
|
50
|
+
"./docs/embed/router": {
|
|
51
|
+
"@kingsleyweb/source": "./app/docs/router.tsx",
|
|
52
|
+
"types": "./app/docs/router.tsx",
|
|
53
|
+
"default": "./app/docs/router.tsx"
|
|
54
|
+
},
|
|
55
|
+
"./docs/embed/context": {
|
|
56
|
+
"@kingsleyweb/source": "./app/docs/context.ts",
|
|
57
|
+
"types": "./app/docs/context.ts",
|
|
58
|
+
"default": "./app/docs/context.ts"
|
|
59
|
+
},
|
|
60
|
+
"./docs/embed/permissions": {
|
|
61
|
+
"@kingsleyweb/source": "./app/docs/screens/docs/permissions.ts",
|
|
62
|
+
"types": "./app/docs/screens/docs/permissions.ts",
|
|
63
|
+
"default": "./app/docs/screens/docs/permissions.ts"
|
|
64
|
+
},
|
|
65
|
+
"./docs/embed/client": {
|
|
66
|
+
"@kingsleyweb/source": "./app/docs/api/client.ts",
|
|
67
|
+
"types": "./app/docs/api/client.ts",
|
|
68
|
+
"default": "./app/docs/api/client.ts"
|
|
69
|
+
},
|
|
70
|
+
"./docs/embed/errors": {
|
|
71
|
+
"@kingsleyweb/source": "./app/docs/api/errors.ts",
|
|
72
|
+
"types": "./app/docs/api/errors.ts",
|
|
73
|
+
"default": "./app/docs/api/errors.ts"
|
|
74
|
+
},
|
|
75
|
+
"./docs/embed/unauthorized": {
|
|
76
|
+
"@kingsleyweb/source": "./app/kit/unauthorized.ts",
|
|
77
|
+
"types": "./app/kit/unauthorized.ts",
|
|
78
|
+
"default": "./app/kit/unauthorized.ts"
|
|
79
|
+
},
|
|
80
|
+
"./lib": {
|
|
81
|
+
"@kingsleyweb/source": "./lib/index.ts",
|
|
82
|
+
"types": "./dts/index.d.ts",
|
|
83
|
+
"default": "./lib/index.ts"
|
|
84
|
+
},
|
|
85
|
+
"./lib/jobs": {
|
|
86
|
+
"@kingsleyweb/source": "./lib/jobs/index.ts",
|
|
87
|
+
"types": "./dts/jobs/index.d.ts",
|
|
88
|
+
"default": "./lib/jobs/index.ts"
|
|
89
|
+
},
|
|
90
|
+
"./lib/jobs/browser": {
|
|
91
|
+
"@kingsleyweb/source": "./lib/jobs/browser/index.ts",
|
|
92
|
+
"types": "./dts/jobs/browser/index.d.ts",
|
|
93
|
+
"default": "./lib/jobs/browser/index.ts"
|
|
94
|
+
},
|
|
95
|
+
"./lib/docs": {
|
|
96
|
+
"@kingsleyweb/source": "./lib/docs/index.ts",
|
|
97
|
+
"types": "./dts/docs/index.d.ts",
|
|
98
|
+
"default": "./lib/docs/index.ts"
|
|
99
|
+
},
|
|
100
|
+
"./lib/docs/browser": {
|
|
101
|
+
"@kingsleyweb/source": "./lib/docs/browser/index.ts",
|
|
102
|
+
"types": "./dts/docs/browser/index.d.ts",
|
|
103
|
+
"default": "./lib/docs/browser/index.ts"
|
|
104
|
+
},
|
|
105
|
+
"./lib/*.ts": {
|
|
106
|
+
"@kingsleyweb/source": "./lib/*.ts",
|
|
107
|
+
"types": "./dts/*.d.ts",
|
|
108
|
+
"default": "./lib/*.ts"
|
|
109
|
+
},
|
|
110
|
+
"./lib/*.js": {
|
|
111
|
+
"@kingsleyweb/source": "./lib/*.ts",
|
|
112
|
+
"types": "./dts/*.d.ts",
|
|
113
|
+
"default": "./lib/*.ts"
|
|
114
|
+
},
|
|
115
|
+
"./lib/*": {
|
|
116
|
+
"@kingsleyweb/source": "./lib/*.ts",
|
|
117
|
+
"types": "./dts/*.d.ts",
|
|
118
|
+
"default": "./lib/*.ts"
|
|
119
|
+
},
|
|
120
|
+
"./package.json": "./package.json"
|
|
121
|
+
},
|
|
122
|
+
"main": "lib/index.ts",
|
|
123
|
+
"types": "dts/index.d.ts",
|
|
124
|
+
"sideEffects": [
|
|
125
|
+
"**/*.css",
|
|
126
|
+
"./app/*/main.tsx"
|
|
127
|
+
],
|
|
128
|
+
"publishConfig": {
|
|
129
|
+
"access": "public",
|
|
130
|
+
"registry": "https://registry.npmjs.org/"
|
|
131
|
+
},
|
|
132
|
+
"files": [
|
|
133
|
+
"app/docs",
|
|
134
|
+
"app/kit",
|
|
135
|
+
"dist",
|
|
136
|
+
"dts",
|
|
137
|
+
"guides",
|
|
138
|
+
"lib"
|
|
139
|
+
],
|
|
140
|
+
"engines": {
|
|
141
|
+
"bun": ">=1.4.2"
|
|
142
|
+
},
|
|
143
|
+
"scripts": {
|
|
144
|
+
"build": "bun scripts/build.ts",
|
|
145
|
+
"build:types": "bun scripts/build-declarations.ts",
|
|
146
|
+
"prepack": "bun scripts/build.ts && bun scripts/build-declarations.ts",
|
|
147
|
+
"test": "bun test --check --parallel --timings=bun-timings.json",
|
|
148
|
+
"test:serial": "bun test --check",
|
|
149
|
+
"test:timings": "rm -f bun-timings.json && bun test --check --parallel --timings=bun-timings.json --update-timings",
|
|
150
|
+
"check": "bun check && bun check -p app && bun check -p __tests__/jobs/app/pkg",
|
|
151
|
+
"gen:client": "bun-client generate --module scripts/jobs/routes-openapi.ts --out app/jobs/api/routes.gen.ts",
|
|
152
|
+
"check:client": "bun-client generate --module scripts/jobs/routes-openapi.ts --out app/jobs/api/routes.gen.ts --check",
|
|
153
|
+
"lint": "bun lint",
|
|
154
|
+
"format": "bun fmt",
|
|
155
|
+
"format:check": "bun fmt --check"
|
|
156
|
+
},
|
|
157
|
+
"peerDependencies": {
|
|
158
|
+
"@kingsleyweb/bun-jobs": "^3.0.0-rc.2",
|
|
159
|
+
"@types/bun": ">=1.4.2"
|
|
160
|
+
},
|
|
161
|
+
"peerDependenciesMeta": {
|
|
162
|
+
"@kingsleyweb/bun-jobs": {
|
|
163
|
+
"optional": true
|
|
164
|
+
},
|
|
165
|
+
"@types/bun": {
|
|
166
|
+
"optional": true
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"dependencies": {
|
|
170
|
+
"@kingsleyweb/bun-common": "^3.0.0-rc.2"
|
|
171
|
+
},
|
|
172
|
+
"devDependencies": {
|
|
173
|
+
"@antfu/eslint-config": "^9.0.0",
|
|
174
|
+
"@happy-dom/global-registrator": "^20.0.0",
|
|
175
|
+
"@kingsleyweb/bun-client": "^3.0.0-rc.2",
|
|
176
|
+
"@kingsleyweb/bun-jobs": "^3.0.0-rc.2",
|
|
177
|
+
"@kingsleyweb/bun-nest": "^3.0.0-rc.2",
|
|
178
|
+
"@nestjs/common": "^12.2.0",
|
|
179
|
+
"@nestjs/core": "^12.2.0",
|
|
180
|
+
"@tanstack/react-query": "^5.90.0",
|
|
181
|
+
"@testing-library/dom": "^10.4.0",
|
|
182
|
+
"@testing-library/react": "^16.3.0",
|
|
183
|
+
"@types/bun": "^1.4.2",
|
|
184
|
+
"@types/react": "^19.2.0",
|
|
185
|
+
"@types/react-dom": "^19.2.0",
|
|
186
|
+
"bun-types": "^1.4.3",
|
|
187
|
+
"eslint": "^10.5.0",
|
|
188
|
+
"eslint-config-prettier": "^10.0.1",
|
|
189
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
190
|
+
"eslint-plugin-react-hooks": "^7.0.0",
|
|
191
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
192
|
+
"happy-dom": "^20.0.0",
|
|
193
|
+
"prettier": "^3.8.4",
|
|
194
|
+
"react": "^19.2.0",
|
|
195
|
+
"react-dom": "^19.2.0",
|
|
196
|
+
"reflect-metadata": "^0.2.2",
|
|
197
|
+
"rxjs": "^7.8.1",
|
|
198
|
+
"typescript": "^6.0.3"
|
|
199
|
+
}
|
|
200
|
+
}
|