feat: Sync to OSS repo

This commit is contained in:
Wanjohi
2026-08-06 22:13:51 +03:00
parent 46d2a56180
commit 3faac3008f
144 changed files with 27561 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
import { PropsWithChildren } from 'hono/jsx';
import { getTheme } from './theme.js';
import css from './css.js';
export function Layout(
props: PropsWithChildren<{
size?: 'small';
}>
) {
const theme = getTheme();
function get(key: 'primary' | 'background' | 'logo', mode: 'light' | 'dark') {
if (!theme) return;
if (!theme[key]) return;
if (typeof theme[key] === 'string') return theme[key];
return theme[key][mode] as string | undefined;
}
const radius = (() => {
if (theme?.radius === 'none') return '0';
if (theme?.radius === 'sm') return '1';
if (theme?.radius === 'md') return '1.25';
if (theme?.radius === 'lg') return '1.5';
if (theme?.radius === 'full') return '1000000000001';
return '1';
})();
const hasLogo = get('logo', 'light') && get('logo', 'dark');
return (
<html
style={{
'--color-background-light': get('background', 'light'),
'--color-background-dark': get('background', 'dark'),
'--color-primary-light': get('primary', 'light'),
'--color-primary-dark': get('primary', 'dark'),
'--font-family': theme?.font?.family,
'--font-scale': theme?.font?.scale,
'--border-radius': radius
}}>
<head>
<title>{theme?.title || 'OpenAuthJS'}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{theme?.favicon ? (
<link rel="icon" href={theme?.favicon} />
) : (
<>
<link rel="icon" href="https://openauth.js.org/favicon.ico" sizes="48x48" />
<link
rel="icon"
href="https://openauth.js.org/favicon.svg"
media="(prefers-color-scheme: light)"
/>
<link
rel="icon"
href="https://openauth.js.org/favicon-dark.svg"
media="(prefers-color-scheme: dark)"
/>
<link
rel="shortcut icon"
href="https://openauth.js.org/favicon.svg"
type="image/svg+xml"
/>
</>
)}
<style dangerouslySetInnerHTML={{ __html: css }} />
{theme?.css && <style dangerouslySetInnerHTML={{ __html: theme.css }} />}
</head>
<body>
<div data-component="root">
<div data-component="center" data-size={props.size}>
{hasLogo ? (
<>
<img data-component="logo" src={get('logo', 'light')} data-mode="light" />
<img data-component="logo" src={get('logo', 'dark')} data-mode="dark" />
</>
) : (
ICON_OPENAUTH
)}
{props.children}
</div>
</div>
</body>
</html>
);
}
const ICON_OPENAUTH = (
<svg
data-component="logo-default"
width="51"
height="51"
viewBox="0 0 51 51"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 50.2303V0.12854H50.1017V50.2303H0ZM3.08002 11.8326H11.7041V3.20856H3.08002V11.8326ZM14.8526 11.8326H23.4766V3.20856H14.8526V11.8326ZM26.5566 11.8326H35.1807V3.20856H26.5566V11.8326ZM38.3292 11.8326H47.0217V3.20856H38.3292V11.8326ZM3.08002 23.6052H11.7041V14.9811H3.08002V23.6052ZM14.8526 23.6052H23.4766V14.9811H14.8526V23.6052ZM26.5566 23.6052H35.1807V14.9811H26.5566V23.6052ZM38.3292 23.6052H47.0217V14.9811H38.3292V23.6052ZM3.08002 35.3092H11.7041V26.6852H3.08002V35.3092ZM14.8526 35.3092H23.4766V26.6852H14.8526V35.3092ZM26.5566 35.3092H35.1807V26.6852H26.5566V35.3092ZM38.3292 35.3092H47.0217V26.6852H38.3292V35.3092ZM3.08002 47.1502H11.7041V38.3893H3.08002V47.1502ZM14.8526 47.1502H23.4766V38.3893H14.8526V47.1502ZM26.5566 47.1502H35.1807V38.3893H26.5566V47.1502ZM38.3292 47.1502H47.0217V38.3893H38.3292V47.1502Z"
fill="currentColor"
/>
</svg>
);

View File

@@ -0,0 +1,198 @@
/**
* Configure the UI that's used by the Code provider.
*
* ```ts {1,7-12}
* import { CodeUI } from "@openauthjs/openauth/ui/code"
* import { CodeProvider } from "@openauthjs/openauth/provider/code"
*
* export default issuer({
* providers: {
* code: CodeAdapter(
* CodeUI({
* copy: {
* code_info: "We'll send a pin code to your email"
* },
* sendCode: (claims, code) => console.log(claims.email, code)
* })
* )
* },
* // ...
* })
* ```
*
* @packageDocumentation
*/
/** @jsxImportSource hono/jsx */
import { UnknownStateError } from '../error.js';
import { CodeProviderOptions } from '../provider/code.js';
import { Layout } from './base.js';
import { FormAlert } from './form.js';
const DEFAULT_COPY = {
/**
* Copy for the email input.
*/
email_placeholder: 'Email',
/**
* Error message when the email is invalid.
*/
email_invalid: 'Email address is not valid',
/**
* Copy for the continue button.
*/
button_continue: 'Continue',
/**
* Copy informing that the pin code will be emailed.
*/
code_info: "We'll send a pin code to your email.",
/**
* Copy for the pin code input.
*/
code_placeholder: 'Code',
/**
* Error message when the code is invalid.
*/
code_invalid: 'Invalid code',
/**
* Copy for when the code was sent.
*/
code_sent: 'Code sent to ',
/**
* Copy for when the code was resent.
*/
code_resent: 'Code resent to ',
/**
* Copy for the link to resend the code.
*/
code_didnt_get: "Didn't get code?",
/**
* Copy for the resend button.
*/
code_resend: 'Resend'
};
export type CodeUICopy = typeof DEFAULT_COPY;
/**
* Configure the password UI.
*/
export interface CodeUIOptions {
/**
* Callback to send the pin code to the user.
*
* The `claims` object contains the email or phone number of the user. You can send the code
* using this.
*
* @example
* ```ts
* async (claims, code) => {
* // Send the code via the claim
* }
* ```
*/
sendCode: (claims: Record<string, string>, code: string) => Promise<void>;
/**
* Custom copy for the UI.
*/
copy?: Partial<CodeUICopy>;
/**
* The mode to use for the input.
* @default "email"
*/
mode?: 'email' | 'phone';
}
/**
* Creates a UI for the Code provider flow.
* @param props - Configure the UI.
*/
export function CodeUI(props: CodeUIOptions): CodeProviderOptions {
const copy = {
...DEFAULT_COPY,
...props.copy
};
const mode = props.mode ?? 'email';
return {
sendCode: props.sendCode,
length: 6,
request: async (_req, state, _form, error): Promise<Response> => {
if (state.type === 'start') {
const jsx = (
<Layout>
<form data-component="form" method="post">
{error?.type === 'invalid_claim' && <FormAlert message={copy.email_invalid} />}
<input type="hidden" name="action" value="request" />
<input
data-component="input"
autofocus
type={mode === 'email' ? 'email' : 'tel'}
name={mode === 'email' ? 'email' : 'phone'}
inputmode={mode === 'email' ? 'email' : 'numeric'}
required
placeholder={copy.email_placeholder}
/>
<button data-component="button">{copy.button_continue}</button>
</form>
<p data-component="form-footer">{copy.code_info}</p>
</Layout>
);
return new Response(jsx.toString(), {
headers: {
'Content-Type': 'text/html'
}
});
}
if (state.type === 'code') {
const jsx = (
<Layout>
<form data-component="form" class="form" method="post">
{error?.type === 'invalid_code' && <FormAlert message={copy.code_invalid} />}
{state.type === 'code' && (
<FormAlert
message={(state.resend ? copy.code_resent : copy.code_sent) + state.claims.email}
color="success"
/>
)}
<input type="hidden" name="action" value="verify" />
<input
data-component="input"
autofocus
minLength={6}
maxLength={6}
type="text"
name="code"
required
inputmode="numeric"
autocomplete="one-time-code"
placeholder={copy.code_placeholder}
/>
<button data-component="button">{copy.button_continue}</button>
</form>
<form method="post">
{Object.entries(state.claims).map(([key, value]) => (
<input key={key} type="hidden" name={key} value={value} className="hidden" />
))}
<input type="hidden" name="action" value="request" />
<div data-component="form-footer">
<span>
{copy.code_didnt_get} <button data-component="link">{copy.code_resend}</button>
</span>
</div>
</form>
</Layout>
);
return new Response(jsx.toString(), {
headers: {
'Content-Type': 'text/html'
}
});
}
throw new UnknownStateError();
}
};
}

247
packages/auth/src/ui/css.ts Normal file
View File

@@ -0,0 +1,247 @@
export default `:root {
--color-background-dark: #0e0e11;
--color-background-light: #ffffff;
--color-primary-dark: #6772e5;
--color-primary-light: #6772e5;
--color-background-success-dark: oklch(0.3 0.04 172);
--color-background-success-light: oklch(from var(--color-background-success-dark) 0.83 c h);
--color-success-dark: oklch(from var(--color-background-success-dark) 0.92 c h);
--color-success-light: oklch(from var(--color-background-success-dark) 0.25 c h);
--color-background-error-dark: oklch(0.32 0.07 15);
--color-background-error-light: oklch(from var(--color-background-error-dark) 0.92 c h);
--color-error-dark: oklch(from var(--color-background-error-dark) 0.92 c h);
--color-error-light: oklch(from var(--color-background-error-dark) 0.25 c h);
--border-radius: 0;
--color-background: var(--color-background-dark);
--color-primary: var(--color-primary-dark);
--color-background-success: var(--color-background-success-dark);
--color-success: var(--color-success-dark);
--color-background-error: var(--color-background-error-dark);
--color-error: var(--color-error-dark);
@media (prefers-color-scheme: light) {
--color-background: var(--color-background-light);
--color-primary: var(--color-primary-light);
--color-background-success: var(--color-background-success-light);
--color-success: var(--color-success-light);
--color-background-error: var(--color-background-error-light);
--color-error: var(--color-error-light);
}
--color-high: oklch(from var(--color-background) clamp(0, calc((l - 0.714) * -1000), 1) 0 0);
--color-low: oklch(from var(--color-background) clamp(0, calc((l - 0.714) * 1000), 1) 0 0);
--lightness-high: color-mix(in oklch, var(--color-high) 0%, oklch(var(--color-high) 0 0));
--lightness-low: color-mix(in oklch, var(--color-low) 0%, oklch(var(--color-low) 0 0));
--font-family:
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-scale: 1;
--font-size-xs: calc(0.75rem * var(--font-scale));
--font-size-sm: calc(0.875rem * var(--font-scale));
--font-size-md: calc(1rem * var(--font-scale));
--font-size-lg: calc(1.125rem * var(--font-scale));
--font-size-xl: calc(1.25rem * var(--font-scale));
--font-size-2xl: calc(1.5rem * var(--font-scale));
}
[data-component='root'] {
font-family: var(--font-family);
background-color: var(--color-background);
padding: 1rem;
color: white;
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
user-select: none;
color: var(--color-high);
}
[data-component='center'] {
width: 380px;
display: flex;
flex-direction: column;
gap: 1.5rem;
&[data-size='small'] {
width: 300px;
}
}
[data-component='link'] {
text-decoration: underline;
text-underline-offset: 0.125rem;
font-weight: 600;
}
[data-component='label'] {
display: flex;
gap: 0.75rem;
flex-direction: column;
font-size: var(--font-size-xs);
}
[data-component='logo'] {
margin: 0 auto;
height: 2.5rem;
width: auto;
display: none;
@media (prefers-color-scheme: light) {
&[data-mode='light'] {
display: block;
}
}
@media (prefers-color-scheme: dark) {
&[data-mode='dark'] {
display: block;
}
}
}
[data-component='logo-default'] {
margin: 0 auto;
height: 2.5rem;
width: auto;
@media (prefers-color-scheme: light) {
color: var(--color-high);
}
@media (prefers-color-scheme: dark) {
color: var(--color-high);
}
}
[data-component='input'] {
width: 100%;
height: 2.5rem;
padding: 0 1rem;
border: 1px solid transparent;
--background: oklch(
from var(--color-background) calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c
h
);
background: var(--background);
border-color: oklch(
from var(--color-background)
calc(clamp(0.22, l + (-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06), 0.88)) c h
);
border-radius: calc(var(--border-radius) * 0.25rem);
font-size: var(--font-size-sm);
outline: none;
&:focus {
border-color: oklch(
from var(--color-background)
calc(clamp(0.3, l + (-0.2 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.1), 0.7)) c h
);
}
&:user-invalid:not(:focus) {
border-color: oklch(0.4 0.09 7.91);
}
}
[data-component='button'] {
height: 2.5rem;
cursor: pointer;
border: 0;
font-weight: 500;
font-size: var(--font-size-sm);
border-radius: calc(var(--border-radius) * 0.25rem);
display: flex;
gap: 0.75rem;
align-items: center;
justify-content: center;
background: var(--color-primary);
color: oklch(from var(--color-primary) clamp(0, calc((l - 0.714) * -1000), 1) 0 0);
&[data-color='ghost'] {
background: transparent;
color: var(--color-high);
border: 1px solid
oklch(
from var(--color-background)
calc(clamp(0.22, l + (-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06), 0.88)) c h
);
}
[data-slot='icon'] {
width: 16px;
height: 16px;
svg {
width: 100%;
height: 100%;
}
}
}
[data-component='form'] {
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
margin: 0;
}
[data-component='form-alert'] {
height: 2.5rem;
display: flex;
align-items: center;
padding: 0 1rem;
border-radius: calc(var(--border-radius) * 0.25rem);
background: var(--color-background-error);
color: var(--color-error);
text-align: left;
font-size: 0.75rem;
gap: 0.5rem;
&[data-color='success'] {
background: var(--color-background-success);
color: var(--color-success);
[data-slot='icon-success'] {
display: block;
}
[data-slot='icon-danger'] {
display: none;
}
}
&:has([data-slot='message']:empty) {
display: none;
}
[data-slot='icon-success'],
[data-slot='icon-danger'] {
width: 1rem;
height: 1rem;
}
[data-slot='icon-success'] {
display: none;
}
}
[data-component='form-footer'] {
display: flex;
gap: 1rem;
font-size: 0.75rem;
align-items: center;
justify-content: center;
&:has(> :nth-child(2)) {
justify-content: space-between;
}
}`;

View File

@@ -0,0 +1,35 @@
/** @jsxImportSource hono/jsx */
export function FormAlert(props: { message?: string; color?: 'danger' | 'success' }) {
return (
<div data-component="form-alert" data-color={props.color}>
<svg
data-slot="icon-success"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<svg
data-slot="icon-danger"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
/>
</svg>
<span data-slot="message">{props.message}</span>
</div>
);
}

View File

@@ -0,0 +1,86 @@
/** @jsxImportSource hono/jsx */
export const ICON_GITHUB = (
<svg
viewBox="0 0 256 250"
width="256"
height="250"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid">
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
</svg>
);
export const ICON_GOOGLE = (
<svg
width="256"
height="262"
viewBox="0 0 256 262"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid">
<path
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
fill="#4285F4"
/>
<path
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
fill="#34A853"
/>
<path
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
fill="#FBBC05"
/>
<path
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
fill="#EB4335"
/>
</svg>
);
export const ICON_EMAIL = (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"
/>
</svg>
);
export const ICON_SLACK = (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.79948 0C7.47279 0.000978593 6.39909 1.07547 6.40007 2.39951C6.39909 3.72355 7.47377 4.79804 8.80046 4.79902H11.2009V2.40049C11.2018 1.07645 10.1271 0.00195719 8.79948 0ZM8.79948 6.4H2.40039C1.07371 6.40098 -0.000977873 7.47547 2.67973e-06 8.79951C-0.00195842 10.1235 1.07273 11.198 2.39941 11.2H8.79948C10.1262 11.199 11.2009 10.1245 11.1999 8.80049C11.2009 7.47547 10.1262 6.40098 8.79948 6.4Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M24.0007 8.79951C24.0016 7.47547 22.9269 6.40098 21.6003 6.4C20.2736 6.40098 19.1989 7.47547 19.1999 8.79951V11.2H21.6003C22.9269 11.199 24.0016 10.1245 24.0007 8.79951ZM17.6006 8.79951V2.39951C17.6016 1.07645 16.5279 0.00195719 15.2012 0C13.8745 0.000978593 12.7998 1.07547 12.8008 2.39951V8.79951C12.7988 10.1235 13.8735 11.198 15.2002 11.2C16.5269 11.199 17.6016 10.1245 17.6006 8.79951Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.1992 23.9998C16.5259 23.9988 17.6006 22.9243 17.5996 21.6003C17.6006 20.2763 16.5259 19.2018 15.1992 19.2008H12.7988V21.6003C12.7978 22.9234 13.8725 23.9978 15.1992 23.9998ZM15.1992 17.5988H21.5993C22.926 17.5978 24.0007 16.5234 23.9997 15.1993C24.0016 13.8753 22.927 12.8008 21.6003 12.7988H15.2002C13.8735 12.7998 12.7988 13.8743 12.7998 15.1983C12.7988 16.5234 13.8725 17.5978 15.1992 17.5988Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 15.1993C-0.000979882 16.5234 1.07371 17.5978 2.40039 17.5988C3.72708 17.5978 4.80177 16.5234 4.80079 15.1993V12.7998H2.40039C1.07371 12.8008 -0.000979882 13.8753 0 15.1993ZM6.40007 15.1993V21.5993C6.3981 22.9234 7.47279 23.9978 8.79948 23.9998C10.1262 23.9988 11.2009 22.9243 11.1999 21.6003V15.2013C11.2018 13.8772 10.1271 12.8027 8.80046 12.8008C7.47279 12.8008 6.39909 13.8753 6.40007 15.1993Z"
fill="currentColor"
/>
</g>
</svg>
);

View File

@@ -0,0 +1,390 @@
/**
* Configure the UI that's used by the Password provider.
*
* ```ts {1,7-12}
* import { PasswordUI } from "@openauthjs/openauth/ui/password"
* import { PasswordProvider } from "@openauthjs/openauth/provider/password"
*
* export default issuer({
* providers: {
* password: PasswordAdapter(
* PasswordUI({
* copy: {
* error_email_taken: "This email is already taken."
* },
* sendCode: (email, code) => console.log(email, code)
* })
* )
* },
* // ...
* })
* ```
*
* @packageDocumentation
*/
/** @jsxImportSource hono/jsx */
import {
PasswordChangeError,
PasswordConfig,
PasswordLoginError,
PasswordRegisterError
} from '../provider/password.js';
import { Layout } from './base.js';
import './form.js';
import { FormAlert } from './form.js';
const DEFAULT_COPY = {
/**
* Error message when email is already taken.
*/
error_email_taken: 'There is already an account with this email.',
/**
* Error message when the confirmation code is incorrect.
*/
error_invalid_code: 'Code is incorrect.',
/**
* Error message when the email is invalid.
*/
error_invalid_email: 'Email is not valid.',
/**
* Error message when the password is incorrect.
*/
error_invalid_password: 'Password is incorrect.',
/**
* Error message when the passwords do not match.
*/
error_password_mismatch: 'Passwords do not match.',
/**
* Error message when the user enters a password that fails validation.
*/
error_validation_error: 'Password does not meet requirements.',
/**
* Title of the register page.
*/
register_title: 'Welcome to the app',
/**
* Description of the register page.
*/
register_description: 'Sign in with your email',
/**
* Title of the login page.
*/
login_title: 'Welcome to the app',
/**
* Description of the login page.
*/
login_description: 'Sign in with your email',
/**
* Copy for the register button.
*/
register: 'Register',
/**
* Copy for the register link.
*/
register_prompt: "Don't have an account?",
/**
* Copy for the login link.
*/
login_prompt: 'Already have an account?',
/**
* Copy for the login button.
*/
login: 'Login',
/**
* Copy for the forgot password link.
*/
change_prompt: 'Forgot password?',
/**
* Copy for the resend code button.
*/
code_resend: 'Resend code',
/**
* Copy for the "Back to" link.
*/
code_return: 'Back to',
/**
* Copy for the logo.
* @internal
*/
logo: 'A',
/**
* Copy for the email input.
*/
input_email: 'Email',
/**
* Copy for the password input.
*/
input_password: 'Password',
/**
* Copy for the code input.
*/
input_code: 'Code',
/**
* Copy for the repeat password input.
*/
input_repeat: 'Repeat password',
/**
* Copy for the continue button.
*/
button_continue: 'Continue'
} satisfies {
[key in `error_${
| PasswordLoginError['type']
| PasswordRegisterError['type']
| PasswordChangeError['type']}`]: string;
} & Record<string, string>;
type PasswordUICopy = typeof DEFAULT_COPY;
/**
* Configure the password UI.
*/
export interface PasswordUIOptions extends Pick<PasswordConfig, 'sendCode' | 'validatePassword'> {
/**
* Custom copy for the UI.
*/
copy?: Partial<PasswordUICopy>;
}
/**
* Creates a UI for the Password provider flow.
* @param input - Configure the UI.
*/
export function PasswordUI(input: PasswordUIOptions): PasswordConfig {
const copy = {
...DEFAULT_COPY,
...input.copy
};
return {
validatePassword: input.validatePassword,
sendCode: input.sendCode,
login: async (_req, form, error): Promise<Response> => {
const jsx = (
<Layout>
<form data-component="form" method="post">
<FormAlert message={error?.type && copy?.[`error_${error.type}`]} />
<input
data-component="input"
type="email"
name="email"
required
placeholder={copy.input_email}
autofocus={!error}
value={form?.get('email')?.toString()}
/>
<input
data-component="input"
autofocus={error?.type === 'invalid_password'}
required
type="password"
name="password"
placeholder={copy.input_password}
autoComplete="current-password"
/>
<button data-component="button">{copy.button_continue}</button>
<div data-component="form-footer">
<span>
{copy.register_prompt}{' '}
<a data-component="link" href="register">
{copy.register}
</a>
</span>
<a data-component="link" href="change">
{copy.change_prompt}
</a>
</div>
</form>
</Layout>
);
return new Response(jsx.toString(), {
status: error ? 401 : 200,
headers: {
'Content-Type': 'text/html'
}
});
},
register: async (_req, state, form, error): Promise<Response> => {
const emailError = ['invalid_email', 'email_taken'].includes(error?.type || '');
const passwordError = ['invalid_password', 'password_mismatch', 'validation_error'].includes(
error?.type || ''
);
const jsx = (
<Layout>
<form data-component="form" method="post">
<FormAlert
message={
error?.type
? error.type === 'validation_error'
? (error.message ?? copy?.[`error_${error.type}`])
: copy?.[`error_${error.type}`]
: undefined
}
/>
{state.type === 'start' && (
<>
<input type="hidden" name="action" value="register" />
<input
data-component="input"
autofocus={!error || emailError}
type="email"
name="email"
value={!emailError ? form?.get('email')?.toString() : ''}
required
placeholder={copy.input_email}
/>
<input
data-component="input"
autofocus={passwordError}
type="password"
name="password"
placeholder={copy.input_password}
required
value={!passwordError ? form?.get('password')?.toString() : ''}
autoComplete="new-password"
/>
<input
data-component="input"
type="password"
name="repeat"
required
autofocus={passwordError}
placeholder={copy.input_repeat}
autoComplete="new-password"
/>
<button data-component="button">{copy.button_continue}</button>
<div data-component="form-footer">
<span>
{copy.login_prompt}{' '}
<a data-component="link" href="authorize">
{copy.login}
</a>
</span>
</div>
</>
)}
{state.type === 'code' && (
<>
<input type="hidden" name="action" value="verify" />
<input
data-component="input"
autofocus
name="code"
minLength={6}
maxLength={6}
required
placeholder={copy.input_code}
autoComplete="one-time-code"
/>
<button data-component="button">{copy.button_continue}</button>
</>
)}
</form>
</Layout>
) as string;
return new Response(jsx.toString(), {
headers: {
'Content-Type': 'text/html'
}
});
},
change: async (_req, state, form, error): Promise<Response> => {
const passwordError = ['invalid_password', 'password_mismatch', 'validation_error'].includes(
error?.type || ''
);
const jsx = (
<Layout>
<form data-component="form" method="post" replace>
<FormAlert
message={
error?.type
? error.type === 'validation_error'
? (error.message ?? copy?.[`error_${error.type}`])
: copy?.[`error_${error.type}`]
: undefined
}
/>
{state.type === 'start' && (
<>
<input type="hidden" name="action" value="code" />
<input
data-component="input"
autofocus
type="email"
name="email"
required
value={form?.get('email')?.toString()}
placeholder={copy.input_email}
/>
</>
)}
{state.type === 'code' && (
<>
<input type="hidden" name="action" value="verify" />
<input
data-component="input"
autofocus
name="code"
minLength={6}
maxLength={6}
required
placeholder={copy.input_code}
autoComplete="one-time-code"
/>
</>
)}
{state.type === 'update' && (
<>
<input type="hidden" name="action" value="update" />
<input
data-component="input"
autofocus
type="password"
name="password"
placeholder={copy.input_password}
required
value={!passwordError ? form?.get('password')?.toString() : ''}
autoComplete="new-password"
/>
<input
data-component="input"
type="password"
name="repeat"
required
value={!passwordError ? form?.get('password')?.toString() : ''}
placeholder={copy.input_repeat}
autoComplete="new-password"
/>
</>
)}
<button data-component="button">{copy.button_continue}</button>
</form>
{state.type === 'code' && (
<form method="post">
<input type="hidden" name="action" value="code" />
<input type="hidden" name="email" value={state.email} />
{state.type === 'code' && (
<div data-component="form-footer">
<span>
{copy.code_return}{' '}
<a data-component="link" href="authorize">
{copy.login.toLowerCase()}
</a>
</span>
<button data-component="link">{copy.code_resend}</button>
</div>
)}
</form>
)}
</Layout>
);
return new Response(jsx.toString(), {
status: error ? 400 : 200,
headers: {
'Content-Type': 'text/html'
}
});
}
};
}

View File

@@ -0,0 +1,201 @@
/**
* The UI that's displayed when loading the root page of the OpenAuth server. You can configure
* which providers should be displayed in the select UI.
*
* ```ts
* import { Select } from "@openauthjs/openauth/ui/select"
*
* export default issuer({
* select: Select({
* providers: {
* github: {
* hide: true
* },
* google: {
* display: "Google"
* }
* }
* })
* // ...
* })
* ```
*
* @packageDocumentation
*/
/** @jsxImportSource hono/jsx */
import { Layout } from './base.js';
import { ICON_GITHUB, ICON_GOOGLE } from './icon.js';
export interface SelectProps {
/**
* An object with all the providers and their config; where the key is the provider name.
*
* @example
* ```ts
* {
* github: {
* hide: true
* },
* google: {
* display: "Google"
* }
* }
* ```
*/
providers?: Record<
string,
{
/**
* Whether to hide the provider from the select UI.
* @default false
*/
hide?: boolean;
/**
* The display name of the provider.
*/
display?: string;
}
>;
}
export function Select(props?: SelectProps) {
return async (providers: Record<string, string>, _req: Request): Promise<Response> => {
const jsx = (
<Layout>
<div data-component="form">
{Object.entries(providers).map(([key, type]) => {
const match = props?.providers?.[key];
if (match?.hide) return;
const icon = ICON[key];
return (
<a href={`/${key}/authorize`} data-component="button" data-color="ghost">
{icon && <i data-slot="icon">{icon}</i>}
Continue with {match?.display || DISPLAY[type] || type}
</a>
);
})}
</div>
</Layout>
);
return new Response(jsx.toString(), {
headers: {
'Content-Type': 'text/html'
}
});
};
}
const DISPLAY: Record<string, string> = {
twitch: 'Twitch',
google: 'Google',
github: 'GitHub',
apple: 'Apple',
x: 'X',
facebook: 'Facebook',
microsoft: 'Microsoft',
slack: 'Slack'
};
const ICON: Record<string, any> = {
code: (
<svg
fill="currentColor"
viewBox="0 0 52 52"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.55,36.91A6.55,6.55,0,1,1,2,43.45,6.54,6.54,0,0,1,8.55,36.91Zm17.45,0a6.55,6.55,0,1,1-6.55,6.54A6.55,6.55,0,0,1,26,36.91Zm17.45,0a6.55,6.55,0,1,1-6.54,6.54A6.54,6.54,0,0,1,43.45,36.91ZM8.55,19.45A6.55,6.55,0,1,1,2,26,6.55,6.55,0,0,1,8.55,19.45Zm17.45,0A6.55,6.55,0,1,1,19.45,26,6.56,6.56,0,0,1,26,19.45Zm17.45,0A6.55,6.55,0,1,1,36.91,26,6.55,6.55,0,0,1,43.45,19.45ZM8.55,2A6.55,6.55,0,1,1,2,8.55,6.54,6.54,0,0,1,8.55,2ZM26,2a6.55,6.55,0,1,1-6.55,6.55A6.55,6.55,0,0,1,26,2ZM43.45,2a6.55,6.55,0,1,1-6.54,6.55A6.55,6.55,0,0,1,43.45,2Z"
fill-rule="evenodd"
/>
</svg>
),
password: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
fill-rule="evenodd"
d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z"
clip-rule="evenodd"
/>
</svg>
),
twitch: (
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path
fill="currentColor"
d="M40.1 32L10 108.9v314.3h107V480h60.2l56.8-56.8h87l117-117V32H40.1zm357.8 254.1L331 353H224l-56.8 56.8V353H76.9V72.1h321v214zM331 149v116.9h-40.1V149H331zm-107 0v116.9h-40.1V149H224z"></path>
</svg>
),
google: ICON_GOOGLE,
github: ICON_GITHUB,
apple: (
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 814 1000">
<path
fill="currentColor"
d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z "
/>
</svg>
),
x: (
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1227">
<path
fill="currentColor"
d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z"
/>
</svg>
),
microsoft: (
<svg
role="img"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid">
<path fill="#F1511B" d="M121.666 121.666H0V0h121.666z" />
<path fill="#80CC28" d="M256 121.666H134.335V0H256z" />
<path fill="#00ADEF" d="M121.663 256.002H0V134.336h121.663z" />
<path fill="#FBBC09" d="M256 256.002H134.335V134.336H256z" />
</svg>
),
facebook: (
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="url(#a)">
<defs>
<linearGradient x1="50%" x2="50%" y1="97.078%" y2="0%" id="a">
<stop offset="0%" stop-color="#0062E0" />
<stop offset="100%" stop-color="#19AFFF" />
</linearGradient>
</defs>
<path d="M15 35.8C6.5 34.3 0 26.9 0 18 0 8.1 8.1 0 18 0s18 8.1 18 18c0 8.9-6.5 16.3-15 17.8l-1-.8h-4l-1 .8z" />
<path
fill="#FFF"
d="m25 23 .8-5H21v-3.5c0-1.4.5-2.5 2.7-2.5H26V7.4c-1.3-.2-2.7-.4-4-.4-4.1 0-7 2.5-7 7v4h-4.5v5H15v12.7c1 .2 2 .3 3 .3s2-.1 3-.3V23h4z"
/>
</svg>
),
slack: (
<svg
role="img"
enable-background="new 0 0 2447.6 2452.5"
viewBox="0 0 2447.6 2452.5"
xmlns="http://www.w3.org/2000/svg">
<g clip-rule="evenodd" fill-rule="evenodd">
<path
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
fill="#36c5f0"
/>
<path
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
fill="#2eb67d"
/>
<path
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
fill="#ecb22e"
/>
<path
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
fill="#e01e5a"
/>
</g>
</svg>
)
};

View File

@@ -0,0 +1,318 @@
/**
* Use one of the built-in themes.
*
* @example
*
* ```ts
* import { THEME_SST } from "@openauthjs/openauth/ui/theme"
*
* export default issuer({
* theme: THEME_SST,
* // ...
* })
* ```
*
* Or define your own.
*
* ```ts
* import type { Theme } from "@openauthjs/openauth/ui/theme"
*
* const MY_THEME: Theme = {
* title: "Acne",
* radius: "none",
* favicon: "https://www.example.com/favicon.svg",
* // ...
* }
*
* export default issuer({
* theme: MY_THEME,
* // ...
* })
* ```
*
* @packageDocumentation
*/
/**
* A type to define values for light and dark mode.
*
* @example
* ```ts
* {
* light: "#FFF",
* dark: "#000"
* }
* ```
*/
export interface ColorScheme {
/**
* The value for dark mode.
*/
dark: string;
/**
* The value for light mode.
*/
light: string;
}
/**
* A type to define your custom theme.
*/
export interface Theme {
/**
* The name of your app. Also used as the title of the page.
*
* @example
* ```ts
* {
* title: "Acne"
* }
* ```
*/
title?: string;
/**
* A URL to the favicon of your app.
*
* @example
* ```ts
* {
* favicon: "https://www.example.com/favicon.svg"
* }
* ```
*/
favicon?: string;
/**
* The border radius of the UI elements.
*
* @example
* ```ts
* {
* radius: "none"
* }
* ```
*/
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
/**
* The primary color of the theme.
*
* Takes a color or both light and dark colors.
*
* @example
* ```ts
* {
* primary: "#FF5E00"
* }
* ```
*/
primary: string | ColorScheme;
/**
* The background color of the theme.
*
* Takes a color or both light and dark colors.
*
* @example
* ```ts
* {
* background: "#FFF"
* }
* ```
*/
background?: string | ColorScheme;
/**
* A URL to the logo of your app.
*
* Takes a single image or both light and dark mode versions.
*
* @example
* ```ts
* {
* logo: "https://www.example.com/logo.svg"
* }
* ```
*/
logo?: string | ColorScheme;
/**
* The font family and scale of the theme.
*/
font?: {
/**
* The font family of the theme.
*
* @example
* ```ts
* {
* font: {
* family: "Geist Mono, monospace"
* }
* }
* ```
*/
family?: string;
/**
* The font scale of the theme. Can be used to increase or decrease the font sizes across
* the UI.
*
* @default "1"
* @example
* ```ts
* {
* font: {
* scale: "1.25"
* }
* }
* ```
*/
scale?: string;
};
/**
* Custom CSS that's added to the page in a `<style>` tag.
*
* This can be used to import custom fonts.
*
* @example
* ```ts
* {
* css: `@import url('https://fonts.googleapis.com/css2?family=Rubik:wght@100;200;300;400;500;600;700;800;900&display=swap');`
* }
* ```
*/
css?: string;
}
/**
* Built-in default OpenAuth theme.
*/
export const THEME_OPENAUTH: Theme = {
title: 'OpenAuth',
radius: 'none',
background: {
dark: 'black',
light: 'white'
},
primary: {
dark: 'white',
light: 'black'
},
font: {
family: 'IBM Plex Sans, sans-serif'
},
css: `
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@100;200;300;400;500;600;700&display=swap');
`
};
/**
* Built-in theme based on [Terminal](https://terminal.shop).
*/
export const THEME_TERMINAL: Theme = {
title: 'terminal',
radius: 'none',
favicon: 'https://www.terminal.shop/favicon.svg',
logo: {
dark: 'https://www.terminal.shop/images/logo-white.svg',
light: 'https://www.terminal.shop/images/logo-black.svg'
},
primary: '#ff5e00',
background: {
dark: 'rgb(0, 0, 0)',
light: 'rgb(255, 255, 255)'
},
font: {
family: 'Geist Mono, monospace'
},
css: `
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100;200;300;400;500;600;700;800;900&display=swap');
`
};
/**
* Built-in theme based on [SST](https://sst.dev).
*/
export const THEME_SST: Theme = {
title: 'SST',
favicon: 'https://sst.dev/favicon.svg',
logo: {
dark: 'https://sst.dev/favicon.svg',
light: 'https://sst.dev/favicon.svg'
},
background: {
dark: '#1a1a2d',
light: 'rgb(255, 255, 255)'
},
primary: '#f3663f',
font: {
family: 'Rubik, sans-serif'
},
css: `
@import url('https://fonts.googleapis.com/css2?family=Rubik:wght@100;200;300;400;500;600;700;800;900&display=swap');
`
};
/**
* Built-in theme based on [Supabase](https://supabase.com).
*/
export const THEME_SUPABASE: Theme = {
title: 'Supabase',
logo: {
dark: 'https://supabase.com/dashboard/_next/image?url=%2Fdashboard%2Fimg%2Fsupabase-dark.svg&w=128&q=75',
light:
'https://supabase.com/dashboard/_next/image?url=%2Fdashboard%2Fimg%2Fsupabase-light.svg&w=128&q=75'
},
background: {
dark: '#171717',
light: '#f8f8f8'
},
primary: {
dark: '#006239',
light: '#72e3ad'
},
font: {
family: 'Varela Round, sans-serif'
},
css: `
@import url('https://fonts.googleapis.com/css2?family=Varela+Round:wght@100;200;300;400;500;600;700;800;900&display=swap');
`
};
/**
* Built-in theme based on [Vercel](https://vercel.com).
*/
export const THEME_VERCEL: Theme = {
title: 'Vercel',
logo: {
dark: 'https://vercel.com/mktng/_next/static/media/vercel-logotype-dark.e8c0a742.svg',
light: 'https://vercel.com/mktng/_next/static/media/vercel-logotype-light.700a8d26.svg'
},
background: {
dark: 'black',
light: 'white'
},
primary: {
dark: 'white',
light: 'black'
},
font: {
family: 'Geist, sans-serif'
},
css: `
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');
`
};
// i really don't wanna use async local storage for this so get over it
/**
* @internal
*/
export function setTheme(value: Theme) {
// @ts-ignore
globalThis.OPENAUTH_THEME = value;
}
/**
* @internal
*/
export function getTheme() {
// @ts-ignore
return globalThis.OPENAUTH_THEME || THEME_OPENAUTH;
}