@adonis-agora/authkit-server 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/ui_preset.js +15 -1
- package/build/host/views/account/confirm.edge +118 -52
- package/build/host/views/account/mfa.edge +1 -1
- package/build/host/views/login.edge +2 -4
- package/build/host/views/mfa-challenge.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +5 -1
- package/build/index.js +15 -1
- package/build/src/define_config.d.ts +51 -0
- package/build/src/define_config.js +8 -0
- package/build/src/host/assets/webauthn.js +2 -0
- package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
- package/build/src/host/controllers/account_confirm_controller.js +42 -130
- package/build/src/host/controllers/account_session_controller.js +20 -4
- package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
- package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
- package/build/src/host/i18n.d.ts +14 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/impersonation_session.js +15 -0
- package/build/src/host/rate_limit.d.ts +10 -0
- package/build/src/host/rate_limit.js +6 -0
- package/build/src/host/register_auth_host.d.ts +19 -0
- package/build/src/host/register_auth_host.js +72 -4
- package/build/src/host/sudo/index.d.ts +47 -0
- package/build/src/host/sudo/index.js +41 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
- package/build/src/host/sudo/methods/magic_link.js +174 -0
- package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
- package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
- package/build/src/host/sudo/methods/passkey.d.ts +28 -0
- package/build/src/host/sudo/methods/passkey.js +139 -0
- package/build/src/host/sudo/methods/password.d.ts +19 -0
- package/build/src/host/sudo/methods/password.js +93 -0
- package/build/src/host/sudo/runtime.d.ts +141 -0
- package/build/src/host/sudo/runtime.js +327 -0
- package/build/src/host/sudo/types.d.ts +93 -0
- package/build/src/host/sudo/types.js +1 -0
- package/build/src/host/sudo_mode.d.ts +116 -6
- package/build/src/host/sudo_mode.js +133 -7
- package/package.json +6 -2
- package/build/stubs/ui/edge/views/consent.edge +0 -13
- package/build/stubs/ui/edge/views/login.edge +0 -19
- package/stubs/ui/edge/views/consent.edge +0 -13
- package/stubs/ui/edge/views/login.edge +0 -19
|
@@ -13,7 +13,21 @@ export function uiStubPaths(preset) {
|
|
|
13
13
|
case 'headless':
|
|
14
14
|
return [];
|
|
15
15
|
case 'edge':
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Views são donas-da-lib (disco `authkit::`); nada a scaffoldar.
|
|
18
|
+
*
|
|
19
|
+
* Quem quer customizar usa `node ace authkit:eject --views`, que copia as
|
|
20
|
+
* views REAIS de `src/host/views` — com i18n, CSRF, passkey e o CSS já
|
|
21
|
+
* compilado em `partials/styles.edge`, sem nenhuma requisição externa.
|
|
22
|
+
*
|
|
23
|
+
* Houve stubs Edge de login/consent aqui, mas nenhum caminho de código os
|
|
24
|
+
* publicava (este `case` sempre devolveu `[]`). Como código morto que
|
|
25
|
+
* mesmo assim ia no pacote, ficaram para trás do resto: carregavam o
|
|
26
|
+
* Tailwind Play CDN muito depois de as views da lib terem migrado para o
|
|
27
|
+
* CSS compilado. Foram removidos em vez de ressuscitados — o scaffold era
|
|
28
|
+
* uma duplicata inferior das views da lib (sem `_csrf`, inclusive).
|
|
29
|
+
*/
|
|
30
|
+
return [];
|
|
17
31
|
case 'react':
|
|
18
32
|
return [
|
|
19
33
|
'ui/react/components/auth_shell.tsx',
|
|
@@ -5,66 +5,132 @@
|
|
|
5
5
|
<body class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
|
|
6
6
|
<div class="w-full max-w-sm">
|
|
7
7
|
<div class="rounded-2xl bg-white p-8 shadow-sm ring-1 ring-black/5">
|
|
8
|
-
<div class="
|
|
8
|
+
<div class="mx-auto w-full space-y-6">
|
|
9
9
|
<h1 class="text-xl font-semibold text-gray-900">{{ t('account.confirm.title') }}</h1>
|
|
10
|
-
<p class="mt-1 text-sm text-gray-500">{{ t('account.confirm.intro') }}</p>
|
|
11
|
-
</div>
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
@if(notice)
|
|
12
|
+
<div class="rounded-lg bg-blue-50 px-4 py-3 text-sm text-blue-800 ring-1 ring-blue-200">{{ notice }}</div>
|
|
13
|
+
@end
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@else
|
|
20
|
-
@if(!passwordless)
|
|
21
|
-
<form method="POST" action="/account/confirm">
|
|
22
|
-
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
23
|
-
@if(returnTo)
|
|
24
|
-
<input type="hidden" name="return_to" value="{{ returnTo }}">
|
|
25
|
-
@end
|
|
26
|
-
<div class="mb-4">
|
|
27
|
-
<label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.confirm.password_label') }}</label>
|
|
28
|
-
<input id="password" type="password" name="password" autofocus
|
|
29
|
-
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400">
|
|
30
|
-
</div>
|
|
31
|
-
<button type="submit"
|
|
32
|
-
class="w-full rounded-lg bg-gray-900 py-2 text-sm font-medium text-white hover:bg-gray-700">{{ t('account.confirm.submit') }}</button>
|
|
33
|
-
</form>
|
|
15
|
+
@if(error)
|
|
16
|
+
<div class="rounded-lg bg-red-50 px-4 py-3 text-sm text-red-800 ring-1 ring-red-200">{{ error }}</div>
|
|
34
17
|
@end
|
|
35
18
|
|
|
36
|
-
@if(
|
|
37
|
-
<div class="
|
|
38
|
-
|
|
39
|
-
class="w-full rounded-lg border border-gray-200 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">{{ t('account.confirm.passkey_button') }}</button>
|
|
19
|
+
@if(!methods || methods.length === 0)
|
|
20
|
+
<div class="rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-800 ring-1 ring-amber-200">
|
|
21
|
+
{{ t('account.confirm.no_methods') }}
|
|
40
22
|
</div>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
23
|
+
@else
|
|
24
|
+
@each(method in methods)
|
|
25
|
+
<div class="rounded-lg border p-4 {{ method.id === preferredId ? 'border-gray-900 ring-1 ring-gray-900' : 'border-gray-200' }}">
|
|
26
|
+
@if(method.id === preferredId)
|
|
27
|
+
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-gray-500">{{ t('account.confirm.preferred_badge') }}</p>
|
|
28
|
+
@end
|
|
29
|
+
@if(method.kind === 'redirect')
|
|
30
|
+
<a href="{{ method.endpoint }}"
|
|
31
|
+
class="block w-full rounded-lg bg-gray-900 py-2 text-center text-sm font-medium text-white hover:bg-gray-700">
|
|
32
|
+
{{ t(method.labelKey) }}
|
|
33
|
+
</a>
|
|
34
|
+
@elseif(method.kind === 'webauthn')
|
|
35
|
+
{{--
|
|
36
|
+
WebAuthn é um handshake, não um submit: o navegador precisa
|
|
37
|
+
buscar as options, assinar o challenge e SÓ ENTÃO postar a
|
|
38
|
+
assertion. Renderizado como form comum, o campo `response` vai
|
|
39
|
+
vazio e o handler recusa sempre — o botão nunca funciona.
|
|
40
|
+
|
|
41
|
+
Nada aqui é hardcoded por método: o endpoint de options sai do
|
|
42
|
+
`action` do próprio form (`<endpoint>/options`) e o csrf do
|
|
43
|
+
hidden que já está nele. A tela não conhece o id 'passkey'.
|
|
44
|
+
--}}
|
|
45
|
+
<div data-authkit-webauthn>
|
|
46
|
+
<button type="button" data-webauthn-start
|
|
47
|
+
class="w-full rounded-lg bg-gray-900 py-2 text-sm font-medium text-white hover:bg-gray-700">
|
|
48
|
+
{{ t(method.labelKey) }}
|
|
49
|
+
</button>
|
|
50
|
+
<form method="POST" action="{{ method.endpoint }}" data-webauthn-form class="hidden">
|
|
51
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
52
|
+
@if(returnTo)
|
|
53
|
+
<input type="hidden" name="return_to" value="{{ returnTo }}">
|
|
54
|
+
@end
|
|
55
|
+
<input type="hidden" name="response" data-webauthn-response>
|
|
56
|
+
</form>
|
|
57
|
+
{{--
|
|
58
|
+
A mensagem vem por `data-message` (atributo HTML, escapado
|
|
59
|
+
pelo Edge) em vez de interpolada dentro do <script>: dentro
|
|
60
|
+
de um <script> as entidades HTML NÃO são decodificadas, então
|
|
61
|
+
um catálogo i18n com apóstrofo quebraria o literal JS.
|
|
62
|
+
--}}
|
|
63
|
+
<p data-webauthn-error data-message="{{ t('account.confirm.passkey_error') }}"
|
|
64
|
+
class="mt-2 hidden text-sm text-red-700"></p>
|
|
65
|
+
</div>
|
|
66
|
+
@else
|
|
67
|
+
<form method="POST" action="{{ method.endpoint }}" class="space-y-3">
|
|
68
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
69
|
+
@if(returnTo)
|
|
70
|
+
<input type="hidden" name="return_to" value="{{ returnTo }}">
|
|
71
|
+
@end
|
|
72
|
+
|
|
73
|
+
@each(field in (method.fields || []))
|
|
74
|
+
<label class="block text-sm font-medium text-gray-700" for="{{ field.name }}">
|
|
75
|
+
{{ t(field.labelKey) }}
|
|
76
|
+
</label>
|
|
77
|
+
<input id="{{ field.name }}" name="{{ field.name }}" type="{{ field.type }}" required
|
|
78
|
+
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm">
|
|
79
|
+
@end
|
|
80
|
+
|
|
81
|
+
<button type="submit"
|
|
82
|
+
class="w-full rounded-lg bg-gray-900 py-2 text-sm font-medium text-white hover:bg-gray-700">
|
|
83
|
+
{{ t(method.labelKey) }}
|
|
84
|
+
</button>
|
|
85
|
+
</form>
|
|
86
|
+
@end
|
|
87
|
+
</div>
|
|
88
|
+
@end
|
|
65
89
|
@end
|
|
66
|
-
|
|
90
|
+
</div>
|
|
67
91
|
</div>
|
|
68
92
|
</div>
|
|
93
|
+
|
|
94
|
+
{{--
|
|
95
|
+
UM script para TODOS os blocos `kind: 'webauthn'` — o import do
|
|
96
|
+
@simplewebauthn/browser é caro e repeti-lo por método não traria nada. Só
|
|
97
|
+
é emitido quando existe ao menos um bloco desses na tela.
|
|
98
|
+
--}}
|
|
99
|
+
@if(methods && methods.some((m) => m.kind === 'webauthn'))
|
|
100
|
+
<script type="module">
|
|
101
|
+
import { startAuthentication } from '/authkit/assets/webauthn.js'
|
|
102
|
+
|
|
103
|
+
for (const block of document.querySelectorAll('[data-authkit-webauthn]')) {
|
|
104
|
+
const form = block.querySelector('[data-webauthn-form]')
|
|
105
|
+
const field = block.querySelector('[data-webauthn-response]')
|
|
106
|
+
const errorBox = block.querySelector('[data-webauthn-error]')
|
|
107
|
+
|
|
108
|
+
block.querySelector('[data-webauthn-start]')?.addEventListener('click', async () => {
|
|
109
|
+
try {
|
|
110
|
+
// Endpoint de options DERIVADO do descritor, não hardcoded.
|
|
111
|
+
const optionsResponse = await fetch(form.getAttribute('action') + '/options', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'x-csrf-token': form.querySelector('input[name="_csrf"]').value },
|
|
114
|
+
})
|
|
115
|
+
if (!optionsResponse.ok) throw new Error('authkit: options recusadas')
|
|
116
|
+
|
|
117
|
+
const optionsJSON = await optionsResponse.json()
|
|
118
|
+
const assertion = await startAuthentication({ optionsJSON })
|
|
119
|
+
// O handler lê `request.input('response')` como STRING e faz
|
|
120
|
+
// JSON.parse — daí o stringify aqui.
|
|
121
|
+
field.value = JSON.stringify(assertion)
|
|
122
|
+
form.submit()
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// Falha no handshake (usuário cancelou, options recusadas, sem
|
|
125
|
+
// autenticador). Sem esta mensagem o botão simplesmente não faz
|
|
126
|
+
// nada e o usuário não tem como saber que deve tentar outro método.
|
|
127
|
+
console.error(error)
|
|
128
|
+
errorBox.textContent = errorBox.getAttribute('data-message')
|
|
129
|
+
errorBox.classList.remove('hidden')
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
@end
|
|
69
135
|
</body>
|
|
70
136
|
</html>
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
|
|
116
116
|
@if(passkeysSupported)
|
|
117
117
|
<script type="module">
|
|
118
|
-
import { startRegistration } from '
|
|
118
|
+
import { startRegistration } from '/authkit/assets/webauthn.js'
|
|
119
119
|
const btn = document.getElementById('passkey-add')
|
|
120
120
|
const errEl = document.getElementById('passkey-error')
|
|
121
121
|
const csrf = '{{ csrfToken }}'
|
|
@@ -83,9 +83,7 @@
|
|
|
83
83
|
const supported = await PublicKeyCredential.isConditionalMediationAvailable?.()
|
|
84
84
|
if (!supported) return
|
|
85
85
|
|
|
86
|
-
const { startAuthentication } = await import(
|
|
87
|
-
'https://cdn.jsdelivr.net/npm/@simplewebauthn/browser@13/dist/bundle/index.js'
|
|
88
|
-
)
|
|
86
|
+
const { startAuthentication } = await import('/authkit/assets/webauthn.js')
|
|
89
87
|
const csrf = document.querySelector('input[name="_csrf"]')?.value ?? ''
|
|
90
88
|
const optRes = await fetch('/auth/interaction/{{ uid }}/passkey/options', {
|
|
91
89
|
method: 'POST',
|
|
@@ -221,7 +219,7 @@
|
|
|
221
219
|
</button>
|
|
222
220
|
<p id="passkey-error" class="mt-3 hidden text-sm text-red-600">{{ t('mfa_challenge.passkey_error') }}</p>
|
|
223
221
|
<script type="module">
|
|
224
|
-
import { startAuthentication } from '
|
|
222
|
+
import { startAuthentication } from '/authkit/assets/webauthn.js'
|
|
225
223
|
const btn = document.getElementById('passkey-button')
|
|
226
224
|
const errEl = document.getElementById('passkey-error')
|
|
227
225
|
const csrf = document.querySelector('#passkey-form input[name="_csrf"]').value
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
@if(passkeyAvailable)
|
|
74
74
|
<script type="module">
|
|
75
|
-
import { startAuthentication } from '
|
|
75
|
+
import { startAuthentication } from '/authkit/assets/webauthn.js'
|
|
76
76
|
const btn = document.getElementById('passkey-button')
|
|
77
77
|
const errEl = document.getElementById('passkey-error')
|
|
78
78
|
const csrf = document.querySelector('#passkey-form input[name="_csrf"]').value
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
{{-- Gerado por scripts/build_host_css.mjs — não editar à mão. --}}
|
|
2
|
-
<style>*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-16{height:4rem}.h-20{height:5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-6{height:1.5rem}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-6{width:1.5rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity,1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-amber-300{--tw-border-opacity:1;border-color:rgb(252 211 77/var(--tw-border-opacity,1))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity,1))}.border-emerald-300{--tw-border-opacity:1;border-color:rgb(110 231 183/var(--tw-border-opacity,1))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity,1))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-amber-900{--tw-bg-opacity:1;background-color:rgb(120 53 15/var(--tw-bg-opacity,1))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-emerald-100{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity,1))}.bg-emerald-50{--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-700{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-\[0\.4em\]{letter-spacing:.4em}.tracking-wide{letter-spacing:.025em}.tracking-widest{letter-spacing:.1em}.text-amber-50{--tw-text-opacity:1;color:rgb(255 251 235/var(--tw-text-opacity,1))}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-amber-700{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity,1))}.text-emerald-600{--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity,1))}.text-emerald-900{--tw-text-opacity:1;color:rgb(6 78 59/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.opacity-50{opacity:.5}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-amber-200{--tw-ring-opacity:1;--tw-ring-color:rgb(253 230 138/var(--tw-ring-opacity,1))}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity,1))}.ring-black\/5{--tw-ring-color:rgba(0,0,0,.05)}.ring-emerald-200{--tw-ring-opacity:1;--tw-ring-color:rgb(167 243 208/var(--tw-ring-opacity,1))}.ring-gray-900{--tw-ring-opacity:1;--tw-ring-color:rgb(17 24 39/var(--tw-ring-opacity,1))}.ring-red-200{--tw-ring-opacity:1;--tw-ring-color:rgb(254 202 202/var(--tw-ring-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.last\:border-0:last-child{border-width:0}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.hover\:bg-red-50:hover{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-gray-400:focus{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity,1))}.focus\:border-gray-900:focus{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity,1))}.focus\:border-red-600:focus{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity,1))}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-gray-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(156 163 175/var(--tw-ring-opacity,1))}.focus\:ring-gray-900:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(17 24 39/var(--tw-ring-opacity,1))}</style>
|
|
2
|
+
<style>*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-16{height:4rem}.h-20{height:5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-6{height:1.5rem}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-6{width:1.5rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity,1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-amber-300{--tw-border-opacity:1;border-color:rgb(252 211 77/var(--tw-border-opacity,1))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity,1))}.border-emerald-300{--tw-border-opacity:1;border-color:rgb(110 231 183/var(--tw-border-opacity,1))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-gray-900{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity,1))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity,1))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-amber-900{--tw-bg-opacity:1;background-color:rgb(120 53 15/var(--tw-bg-opacity,1))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-emerald-100{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity,1))}.bg-emerald-50{--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-700{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-\[0\.4em\]{letter-spacing:.4em}.tracking-wide{letter-spacing:.025em}.tracking-widest{letter-spacing:.1em}.text-amber-50{--tw-text-opacity:1;color:rgb(255 251 235/var(--tw-text-opacity,1))}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-amber-700{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity,1))}.text-emerald-600{--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity,1))}.text-emerald-900{--tw-text-opacity:1;color:rgb(6 78 59/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.opacity-50{opacity:.5}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-amber-200{--tw-ring-opacity:1;--tw-ring-color:rgb(253 230 138/var(--tw-ring-opacity,1))}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity,1))}.ring-black\/5{--tw-ring-color:rgba(0,0,0,.05)}.ring-blue-200{--tw-ring-opacity:1;--tw-ring-color:rgb(191 219 254/var(--tw-ring-opacity,1))}.ring-emerald-200{--tw-ring-opacity:1;--tw-ring-color:rgb(167 243 208/var(--tw-ring-opacity,1))}.ring-gray-900{--tw-ring-opacity:1;--tw-ring-color:rgb(17 24 39/var(--tw-ring-opacity,1))}.ring-red-200{--tw-ring-opacity:1;--tw-ring-color:rgb(254 202 202/var(--tw-ring-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.last\:border-0:last-child{border-width:0}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.hover\:bg-red-50:hover{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-gray-900:focus{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity,1))}.focus\:border-red-600:focus{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity,1))}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-gray-900:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(17 24 39/var(--tw-ring-opacity,1))}</style>
|
package/build/index.d.ts
CHANGED
|
@@ -113,6 +113,10 @@ export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/co
|
|
|
113
113
|
export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
|
|
114
114
|
export { rememberAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
115
115
|
export type { StartImpersonationParams, ImpersonationState, } from './src/host/impersonation_session.js';
|
|
116
|
-
export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
116
|
+
export { SUDO_SESSION_KEY, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
117
117
|
export type { SudoModeSetting, ResolvedSudoModeSetting, } from './src/host/sudo_mode.js';
|
|
118
|
+
export { sudoMethods } from './src/host/sudo/index.js';
|
|
119
|
+
export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY } from './src/host/sudo/runtime.js';
|
|
120
|
+
export { sudoContextFrom } from './src/host/sudo/runtime.js';
|
|
121
|
+
export type { SudoMethod, SudoContext, SudoMethodDescriptor, SudoRouteHelpers, } from './src/host/sudo/types.js';
|
|
118
122
|
export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
|
package/build/index.js
CHANGED
|
@@ -77,7 +77,21 @@ export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
|
|
|
77
77
|
// src/host/impersonation_session.ts.
|
|
78
78
|
export { rememberAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
79
79
|
// Sudo mode — helpers for host controllers that require step-up authentication.
|
|
80
|
-
export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
80
|
+
export { SUDO_SESSION_KEY, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
81
|
+
// SPI de métodos de confirmação de identidade (sudo mode).
|
|
82
|
+
//
|
|
83
|
+
// `completeSudo` é público porque o host PRECISA chamá-lo: o `oidcStepUp` não
|
|
84
|
+
// registra rotas — quem valida o grant é o callback do host, e é lá que a
|
|
85
|
+
// concessão acontece. Sem exportá-lo, o host improvisaria com `markSudo` e
|
|
86
|
+
// perderia o audit `sudo.confirmed`, a preferência de método e o redirect para
|
|
87
|
+
// o `return_to`.
|
|
88
|
+
export { sudoMethods } from './src/host/sudo/index.js';
|
|
89
|
+
export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY } from './src/host/sudo/runtime.js';
|
|
90
|
+
// Montador do `SudoContext` que `completeSudo`/`failSudo` recebem. Exportado
|
|
91
|
+
// junto porque sem ele o host teria de montar o contexto na mão (resolver o
|
|
92
|
+
// service do container, carregar a conta, validar o `return_to`) — e um
|
|
93
|
+
// `returnTo` montado na mão é um open redirect esperando acontecer.
|
|
94
|
+
export { sudoContextFrom } from './src/host/sudo/runtime.js';
|
|
81
95
|
// @adonisjs/auth integration (opt-in) — user provider for config/auth.ts's
|
|
82
96
|
// sessionGuard(), backed by authkit's own accountStore. Pair with
|
|
83
97
|
// `adonisAuth: { guard: '...' }` in config/authkit.ts so the account console's
|
|
@@ -4,6 +4,7 @@ import { adapters, type AdapterFactory, type OidcAdapterClass } from "./adapters
|
|
|
4
4
|
import type { AccountStore, AuthAccount } from "./accounts/account_store.js";
|
|
5
5
|
import type { PatStore } from "./pat/pat_store.js";
|
|
6
6
|
import type { AuditSink } from "./audit/audit_sink.js";
|
|
7
|
+
import type { SudoMethod } from "./host/sudo/types.js";
|
|
7
8
|
import { type EventsConfigInput } from "./events/dispatcher.js";
|
|
8
9
|
import type { BrandingConfig } from "./host/branding.js";
|
|
9
10
|
import { type AuthMessages, type I18nConfig } from "./host/i18n.js";
|
|
@@ -40,6 +41,15 @@ export interface MailHooks {
|
|
|
40
41
|
magicUrl: string;
|
|
41
42
|
token: string;
|
|
42
43
|
}) => Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Distinto de
|
|
46
|
+
* `onMagicLink`: aquele autentica, este só concede sudo a quem já está
|
|
47
|
+
* logado. Sem este hook, `sudoMethods.magicLink()` fica indisponível.
|
|
48
|
+
*/
|
|
49
|
+
onSudoLink?: (data: {
|
|
50
|
+
email: string;
|
|
51
|
+
sudoUrl: string;
|
|
52
|
+
}) => Promise<void>;
|
|
43
53
|
/**
|
|
44
54
|
* Disparado num login bem-sucedido a partir de um dispositivo NOVO (sem cookie
|
|
45
55
|
* de dispositivo confiável válido para a conta). Best-effort, fire-and-forget:
|
|
@@ -164,6 +174,23 @@ export interface ResolvedRateLimitConfig {
|
|
|
164
174
|
* apertado o suficiente para conter um ataque de força bruta por IP.
|
|
165
175
|
*/
|
|
166
176
|
adminIp: RateLimitBucket;
|
|
177
|
+
/**
|
|
178
|
+
* Bucket das rotas dos métodos de sudo (`/account/confirm/*`). SEPARADO do
|
|
179
|
+
* `login` de propósito, mesmo tendo os MESMOS limites: os dois orçamentos
|
|
180
|
+
* respondem a perguntas diferentes.
|
|
181
|
+
*
|
|
182
|
+
* `login` conta tentativas de um usuário ANÔNIMO adivinhando credenciais.
|
|
183
|
+
* `sudo` conta reprovas de identidade de um usuário JÁ AUTENTICADO, que
|
|
184
|
+
* chegou na tela de confirmação para uma operação sensível. Compartilhar o
|
|
185
|
+
* bucket cruza os dois: quem erra a senha algumas vezes no `/account/confirm`
|
|
186
|
+
* gasta o orçamento de login do próprio IP (e passa a não conseguir logar em
|
|
187
|
+
* outra aba), e um ataque de credencial no login tranca a confirmação de quem
|
|
188
|
+
* está legitimamente logado atrás do mesmo NAT.
|
|
189
|
+
*
|
|
190
|
+
* Os limites são os mesmos do login (10/min) porque não há razão para
|
|
191
|
+
* afrouxar — o ponto é separar a CONTAGEM, não o teto.
|
|
192
|
+
*/
|
|
193
|
+
sudo: RateLimitBucket;
|
|
167
194
|
store?: string;
|
|
168
195
|
}
|
|
169
196
|
export declare function resolveRateLimit(input?: RateLimitConfigInput): ResolvedRateLimitConfig;
|
|
@@ -630,6 +657,24 @@ export interface AuthServerConfigInput {
|
|
|
630
657
|
mail?: MailHooks;
|
|
631
658
|
/** Sink de auditoria (best-effort). Opcional — quando ausente, auditoria é no-op. */
|
|
632
659
|
audit?: AuditSink;
|
|
660
|
+
/**
|
|
661
|
+
* Métodos de confirmação de identidade (sudo mode). A ordem do array é a
|
|
662
|
+
* ordem de exibição; o último método usado com sucesso é promovido ao topo.
|
|
663
|
+
*
|
|
664
|
+
* Ausente → a lista que `registerAuthHost` MONTOU (que por sua vez cai em
|
|
665
|
+
* `[password(), passkey()]` sem `AuthHostOptions.sudoMethods`, o
|
|
666
|
+
* comportamento histórico). É a mesma resposta que os handlers dão nesse
|
|
667
|
+
* caso — "vale o que tem rota" —, e é o que impede a tela de oferecer um
|
|
668
|
+
* método sem endpoint ou de esconder um que funciona.
|
|
669
|
+
*
|
|
670
|
+
* Host passwordless (autentica por OIDC/magic link) DEVE incluir ao menos um
|
|
671
|
+
* método que não exija credencial previamente cadastrada — `oidcStepUp()` ou
|
|
672
|
+
* `magicLink()` — senão o usuário fica sem caminho para exportar/excluir os
|
|
673
|
+
* próprios dados.
|
|
674
|
+
*/
|
|
675
|
+
sudo?: {
|
|
676
|
+
methods?: SudoMethod[];
|
|
677
|
+
};
|
|
633
678
|
/**
|
|
634
679
|
* Eventos/webhooks: o host observa CADA evento de auditoria via callback
|
|
635
680
|
* in-process (`onEvent`) e/ou POST de webhook (`webhook`). Best-effort, nunca
|
|
@@ -822,6 +867,8 @@ export interface ResolvedServerConfig {
|
|
|
822
867
|
accountStore: AccountStore;
|
|
823
868
|
patStore?: PatStore;
|
|
824
869
|
mountPath: string;
|
|
870
|
+
/** Destino default pós-confirmação do console (sudo mode) e demais fallbacks de conta. Default: '/account/security'. */
|
|
871
|
+
accountHome?: string;
|
|
825
872
|
render?: AuthHostRenderer;
|
|
826
873
|
branding?: BrandingConfig;
|
|
827
874
|
social?: AuthSocialConfig;
|
|
@@ -833,6 +880,10 @@ export interface ResolvedServerConfig {
|
|
|
833
880
|
notifications: ResolvedNotificationsConfig;
|
|
834
881
|
mail?: MailHooks;
|
|
835
882
|
audit?: AuditSink;
|
|
883
|
+
/** Métodos de sudo configurados pelo host. Ausente → defaults do controller. */
|
|
884
|
+
sudo?: {
|
|
885
|
+
methods?: SudoMethod[];
|
|
886
|
+
};
|
|
836
887
|
mfaIssuer: string;
|
|
837
888
|
/** RP de WebAuthn resolvido (sempre presente; derivado do issuer por default). */
|
|
838
889
|
webauthn: ResolvedWebauthnConfig;
|
|
@@ -15,6 +15,8 @@ const RATE_LIMIT_DEFAULTS = {
|
|
|
15
15
|
login: { points: 10, duration: "1 min" },
|
|
16
16
|
introspection: { points: 60, duration: "1 min" },
|
|
17
17
|
adminIp: { points: 30, duration: "1 min" },
|
|
18
|
+
// Mesmos limites do login, bucket separado — ver `ResolvedRateLimitConfig.sudo`.
|
|
19
|
+
sudo: { points: 10, duration: "1 min" },
|
|
18
20
|
};
|
|
19
21
|
export function resolveRateLimit(input) {
|
|
20
22
|
const enabled = input?.enabled ?? true;
|
|
@@ -23,6 +25,7 @@ export function resolveRateLimit(input) {
|
|
|
23
25
|
login: RATE_LIMIT_DEFAULTS.login,
|
|
24
26
|
introspection: RATE_LIMIT_DEFAULTS.introspection,
|
|
25
27
|
adminIp: RATE_LIMIT_DEFAULTS.adminIp,
|
|
28
|
+
sudo: RATE_LIMIT_DEFAULTS.sudo,
|
|
26
29
|
store: input?.store,
|
|
27
30
|
};
|
|
28
31
|
}
|
|
@@ -317,6 +320,7 @@ export function defineConfig(config) {
|
|
|
317
320
|
accountStore: config.accountStore,
|
|
318
321
|
patStore: config.patStore,
|
|
319
322
|
mountPath: config.mountPath ?? "/oidc",
|
|
323
|
+
accountHome: config.accountHome,
|
|
320
324
|
// Default de runtime: sem isso, `render` ficava `undefined` e TODA
|
|
321
325
|
// request a `/account/*`/`/auth/interaction/*` estourava um 500 sem
|
|
322
326
|
// contexto (os controllers fazem `cfg.render!(...)`). `edgeRenderer()`
|
|
@@ -332,6 +336,10 @@ export function defineConfig(config) {
|
|
|
332
336
|
lockout: resolveLockout(config.lockout),
|
|
333
337
|
notifications: resolveNotifications(),
|
|
334
338
|
mail: config.mail,
|
|
339
|
+
// Propaga os métodos de sudo para o config resolvido. Sem esta linha o
|
|
340
|
+
// campo existiria só no config de ENTRADA e `cfg.sudo` seria sempre
|
|
341
|
+
// undefined em runtime — exatamente o bug que `accountHome` tinha.
|
|
342
|
+
sudo: config.sudo,
|
|
335
343
|
audit: (() => {
|
|
336
344
|
// Sempre compõe o fan-out: além de onEvent/webhook (quando configurados),
|
|
337
345
|
// o sink composto republica TODO evento de auditoria no barramento de
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function l(e){let t=new Uint8Array(e),r="";for(let n of t)r+=String.fromCharCode(n);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function h(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,i=t.padEnd(t.length+r,"="),n=atob(i),c=new ArrayBuffer(n.length),f=new Uint8Array(c);for(let a=0;a<n.length;a++)f[a]=n.charCodeAt(a);return c}function p(){return C.stubThis(globalThis?.PublicKeyCredential!==void 0&&typeof globalThis.PublicKeyCredential=="function")}var C={stubThis:e=>e};function R(e){let{id:t}=e;return{...e,id:h(t),transports:e.transports}}function w(e){return e==="localhost"||/^((xn--[a-z0-9-]+|[a-z0-9]+(-[a-z0-9]+)*)\.)+([a-z]{2,}|xn--[a-z0-9-]+)$/i.test(e)}var o=class extends Error{constructor({message:t,code:r,cause:i,name:n}){super(t,{cause:i}),Object.defineProperty(this,"code",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.name=n??i.name,this.code=r}};function I({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(r.authenticatorSelection?.requireResidentKey===!0)return new o({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new o({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return r.pubKeyCredParams.filter(n=>n.type==="public-key").length===0?new o({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new o({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rp.id!==i)return new o({message:`The RP ID "${r.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(r.user.id.byteLength<1||r.user.id.byteLength>64)return new o({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}var S=class{constructor(){Object.defineProperty(this,"controller",{enumerable:!0,configurable:!0,writable:!0,value:void 0})}createNewAbortSignal(){if(this.controller){let r=new Error("Cancelling existing WebAuthn API call for new one");r.name="AbortError",this.controller.abort(r)}let t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){let t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}},A=new S;var x=["cross-platform","platform"];function E(e){if(e&&!(x.indexOf(e)<0))return e}async function N(e){!e.optionsJSON&&e.challenge&&(console.warn("startRegistration() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useAutoRegister:r=!1}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let i={...t,challenge:h(t.challenge),user:{...t.user,id:h(t.user.id)},excludeCredentials:t.excludeCredentials?.map(R)},n={};r&&(n.mediation="conditional"),n.publicKey=i,n.signal=A.createNewAbortSignal();let c;try{c=await navigator.credentials.create(n)}catch(u){throw I({error:u,options:n})}if(!c)throw new Error("Registration was not completed");let{id:f,rawId:a,response:s,type:y}=c,d;typeof s.getTransports=="function"&&(d=s.getTransports());let g;if(typeof s.getPublicKeyAlgorithm=="function")try{g=s.getPublicKeyAlgorithm()}catch(u){O("getPublicKeyAlgorithm()",u)}let b;if(typeof s.getPublicKey=="function")try{let u=s.getPublicKey();u!==null&&(b=l(u))}catch(u){O("getPublicKey()",u)}let m;if(typeof s.getAuthenticatorData=="function")try{m=l(s.getAuthenticatorData())}catch(u){O("getAuthenticatorData()",u)}return{id:f,rawId:l(a),response:{attestationObject:l(s.attestationObject),clientDataJSON:l(s.clientDataJSON),transports:d,publicKeyAlgorithm:g,publicKey:b,authenticatorData:m},type:y,clientExtensionResults:c.getClientExtensionResults(),authenticatorAttachment:E(c.authenticatorAttachment)}}function O(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.
|
|
2
|
+
`,t)}function _(){if(!p())return T.stubThis(new Promise(t=>t(!1)));let e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?T.stubThis(new Promise(t=>t(!1))):T.stubThis(e.isConditionalMediationAvailable())}var T={stubThis:e=>e};function P({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rpId!==i)return new o({message:`The RP ID "${r.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function D(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useBrowserAutofill:r=!1,verifyBrowserAutofillInput:i=!0}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let n;t.allowCredentials?.length!==0&&(n=t.allowCredentials?.map(R));let c={...t,challenge:h(t.challenge),allowCredentials:n},f={};if(r){if(!await _())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&i)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');f.mediation="conditional",c.allowCredentials=[]}f.publicKey=c,f.signal=A.createNewAbortSignal();let a;try{a=await navigator.credentials.get(f)}catch(m){throw P({error:m,options:f})}if(!a)throw new Error("Authentication was not completed");let{id:s,rawId:y,response:d,type:g}=a,b;return d.userHandle&&(b=l(d.userHandle)),{id:s,rawId:l(y),response:{authenticatorData:l(d.authenticatorData),clientDataJSON:l(d.clientDataJSON),signature:l(d.signature),userHandle:b},type:g,clientExtensionResults:a.getClientExtensionResults(),authenticatorAttachment:E(a.authenticatorAttachment)}}function v(){return p()?PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable():new Promise(e=>e(!1))}export{A as WebAuthnAbortService,o as WebAuthnError,p as browserSupportsWebAuthn,_ as browserSupportsWebAuthnAutofill,v as platformAuthenticatorIsAvailable,D as startAuthentication,N as startRegistration};
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sudo mode — tela de confirmação de identidade (/account/confirm).
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* POST /account/confirm/passkey → verifica a resposta da passkey e marca o sudo.
|
|
4
|
+
* O GET lista os métodos DISPONÍVEIS para a conta (SPI `SudoMethod`); a
|
|
5
|
+
* verificação de cada um vive no próprio método, nas rotas que ele registra.
|
|
6
|
+
* Este controller não verifica credencial nem chama `markSudo`.
|
|
8
7
|
*
|
|
9
8
|
* A tela está atrás do `accountGuard` (requer sessão de conta ativa).
|
|
10
|
-
* Após confirmação, redireciona para `return_to` (validado) ou para `/account/tokens`.
|
|
11
9
|
*/
|
|
12
10
|
import '../augmentations.js';
|
|
13
11
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
12
|
+
import { sudoContextFrom } from '../sudo/runtime.js';
|
|
13
|
+
/**
|
|
14
|
+
* Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
|
|
15
|
+
* `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
|
|
16
|
+
* antigo segue valendo para quem já o importava.
|
|
17
|
+
*/
|
|
18
|
+
export { sudoContextFrom };
|
|
14
19
|
export default class AccountConfirmController {
|
|
15
|
-
show(ctx: HttpContext): Promise<
|
|
16
|
-
confirm(ctx: HttpContext): Promise<void>;
|
|
17
|
-
passkeyOptions(ctx: HttpContext): Promise<any>;
|
|
18
|
-
passkeyConfirm(ctx: HttpContext): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* Verifica se a conta é "passwordless" (sem hash de senha definido).
|
|
21
|
-
* Fail-safe: retorna `false` quando não é possível determinar.
|
|
22
|
-
*/
|
|
23
|
-
private isPasswordless;
|
|
20
|
+
show(ctx: HttpContext): Promise<unknown>;
|
|
24
21
|
}
|