@adonis-agora/authkit-server 0.44.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 +71 -3
- package/build/src/define_config.js +11 -0
- package/build/src/host/account_api/account_api_controller.js +2 -2
- package/build/src/host/account_deletion_ops.d.ts +2 -2
- package/build/src/host/account_deletion_ops.js +3 -3
- package/build/src/host/account_deletion_service.js +1 -1
- package/build/src/host/assets/webauthn.js +2 -0
- package/build/src/host/avatar_storage.d.ts +40 -14
- package/build/src/host/avatar_storage.js +221 -72
- 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_security_controller.js +3 -3
- 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/durable/account_deletion_workflow.js +1 -1
- 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 +10 -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;
|
|
@@ -249,19 +276,36 @@ export declare function resolveDeviceFlow(input?: DeviceFlowConfigInput): Resolv
|
|
|
249
276
|
*/
|
|
250
277
|
export interface UploadsConfigInput {
|
|
251
278
|
avatars?: {
|
|
252
|
-
/**
|
|
279
|
+
/**
|
|
280
|
+
* Backend de storage do avatar. Default: `'auto'` (media se
|
|
281
|
+
* `@adonis-agora/media` estiver presente/configurado, senão o drive builtin).
|
|
282
|
+
* `'builtin'` força o `@adonisjs/drive`; `'media'` força o media (degrada para
|
|
283
|
+
* o input de URL se indisponível).
|
|
284
|
+
*/
|
|
285
|
+
storage?: "auto" | "builtin" | "media";
|
|
286
|
+
/** Disk do `@adonisjs/drive` a usar (backend builtin). Default: o disk DEFAULT do app. */
|
|
253
287
|
disk?: string;
|
|
254
|
-
/** Diretório/prefixo das chaves. Default: 'authkit/avatars'. */
|
|
288
|
+
/** Diretório/prefixo das chaves (backend builtin). Default: 'authkit/avatars'. */
|
|
255
289
|
directory?: string;
|
|
290
|
+
/** Collection single-file do media (backend media). Default: 'avatar'. */
|
|
291
|
+
collection?: string;
|
|
292
|
+
/** ownerType do media (backend media). Default: 'AuthAccount'. */
|
|
293
|
+
ownerType?: string;
|
|
256
294
|
/** Tamanho máximo em MB. Default: 5. */
|
|
257
295
|
maxSizeMb?: number;
|
|
258
296
|
};
|
|
259
297
|
}
|
|
260
298
|
export interface ResolvedUploadsConfig {
|
|
261
299
|
avatars: {
|
|
262
|
-
/**
|
|
300
|
+
/** Backend ativo (default 'auto'). */
|
|
301
|
+
storage: "auto" | "builtin" | "media";
|
|
302
|
+
/** Disk explícito; `undefined` = disk DEFAULT do app (backend builtin). */
|
|
263
303
|
disk?: string;
|
|
264
304
|
directory: string;
|
|
305
|
+
/** Collection single-file do media (backend media). */
|
|
306
|
+
collection: string;
|
|
307
|
+
/** ownerType do media (backend media). */
|
|
308
|
+
ownerType: string;
|
|
265
309
|
maxSizeMb: number;
|
|
266
310
|
};
|
|
267
311
|
}
|
|
@@ -613,6 +657,24 @@ export interface AuthServerConfigInput {
|
|
|
613
657
|
mail?: MailHooks;
|
|
614
658
|
/** Sink de auditoria (best-effort). Opcional — quando ausente, auditoria é no-op. */
|
|
615
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
|
+
};
|
|
616
678
|
/**
|
|
617
679
|
* Eventos/webhooks: o host observa CADA evento de auditoria via callback
|
|
618
680
|
* in-process (`onEvent`) e/ou POST de webhook (`webhook`). Best-effort, nunca
|
|
@@ -805,6 +867,8 @@ export interface ResolvedServerConfig {
|
|
|
805
867
|
accountStore: AccountStore;
|
|
806
868
|
patStore?: PatStore;
|
|
807
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;
|
|
808
872
|
render?: AuthHostRenderer;
|
|
809
873
|
branding?: BrandingConfig;
|
|
810
874
|
social?: AuthSocialConfig;
|
|
@@ -816,6 +880,10 @@ export interface ResolvedServerConfig {
|
|
|
816
880
|
notifications: ResolvedNotificationsConfig;
|
|
817
881
|
mail?: MailHooks;
|
|
818
882
|
audit?: AuditSink;
|
|
883
|
+
/** Métodos de sudo configurados pelo host. Ausente → defaults do controller. */
|
|
884
|
+
sudo?: {
|
|
885
|
+
methods?: SudoMethod[];
|
|
886
|
+
};
|
|
819
887
|
mfaIssuer: string;
|
|
820
888
|
/** RP de WebAuthn resolvido (sempre presente; derivado do issuer por default). */
|
|
821
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
|
}
|
|
@@ -67,8 +70,11 @@ export function resolveDeviceFlow(input) {
|
|
|
67
70
|
export function resolveUploads(input) {
|
|
68
71
|
return {
|
|
69
72
|
avatars: {
|
|
73
|
+
storage: input?.avatars?.storage ?? "auto",
|
|
70
74
|
disk: input?.avatars?.disk,
|
|
71
75
|
directory: input?.avatars?.directory ?? "authkit/avatars",
|
|
76
|
+
collection: input?.avatars?.collection ?? "avatar",
|
|
77
|
+
ownerType: input?.avatars?.ownerType ?? "AuthAccount",
|
|
72
78
|
maxSizeMb: input?.avatars?.maxSizeMb ?? 5,
|
|
73
79
|
},
|
|
74
80
|
};
|
|
@@ -314,6 +320,7 @@ export function defineConfig(config) {
|
|
|
314
320
|
accountStore: config.accountStore,
|
|
315
321
|
patStore: config.patStore,
|
|
316
322
|
mountPath: config.mountPath ?? "/oidc",
|
|
323
|
+
accountHome: config.accountHome,
|
|
317
324
|
// Default de runtime: sem isso, `render` ficava `undefined` e TODA
|
|
318
325
|
// request a `/account/*`/`/auth/interaction/*` estourava um 500 sem
|
|
319
326
|
// contexto (os controllers fazem `cfg.render!(...)`). `edgeRenderer()`
|
|
@@ -329,6 +336,10 @@ export function defineConfig(config) {
|
|
|
329
336
|
lockout: resolveLockout(config.lockout),
|
|
330
337
|
notifications: resolveNotifications(),
|
|
331
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,
|
|
332
343
|
audit: (() => {
|
|
333
344
|
// Sempre compõe o fan-out: além de onEvent/webhook (quando configurados),
|
|
334
345
|
// o sink composto republica TODO evento de auditoria no barramento de
|
|
@@ -40,7 +40,7 @@ import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '.
|
|
|
40
40
|
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
41
41
|
import { requireSudo, isSudoActive, SUDO_MODE_DEFAULTS, resolveEffectiveSudoMode } from '../sudo_mode.js';
|
|
42
42
|
import { translate } from '../i18n.js';
|
|
43
|
-
import { storeAvatar,
|
|
43
|
+
import { storeAvatar, isAvatarUploadSupported, AvatarUploadError } from '../avatar_storage.js';
|
|
44
44
|
import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail, } from '../default_mailer.js';
|
|
45
45
|
import { ACTIVE_ORG_COOKIE } from '../active_org_cookie.js';
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
@@ -138,7 +138,7 @@ export default class AccountApiController {
|
|
|
138
138
|
passkeysSupported: supportsPasskeys(cfg.accountStore),
|
|
139
139
|
orgsSupported: supportsOrganizations(cfg.accountStore),
|
|
140
140
|
tokensSupported: !!cfg.patStore,
|
|
141
|
-
avatarUploadSupported: await
|
|
141
|
+
avatarUploadSupported: await isAvatarUploadSupported(cfg.uploads),
|
|
142
142
|
sessionsSupported: new AdminSessionsService(service).canList,
|
|
143
143
|
},
|
|
144
144
|
};
|
|
@@ -61,8 +61,8 @@ export declare function removeFromOrgs(cfg: ResolvedServerConfig, accountId: str
|
|
|
61
61
|
orgMemberships: number;
|
|
62
62
|
orgInvitations: number;
|
|
63
63
|
}>;
|
|
64
|
-
/** 7) Apaga o avatar no drive
|
|
65
|
-
export declare function deleteAccountAvatar(cfg: ResolvedServerConfig, avatarUrl: string | null): Promise<{
|
|
64
|
+
/** 7) Apaga o avatar no backend ativo (drive OU media; best-effort, fail-safe). */
|
|
65
|
+
export declare function deleteAccountAvatar(cfg: ResolvedServerConfig, accountId: string, avatarUrl: string | null): Promise<{
|
|
66
66
|
avatarDeleted: boolean;
|
|
67
67
|
}>;
|
|
68
68
|
/** 8) Anonimiza o histórico de audit da conta (quando o sink suporta). */
|
|
@@ -92,9 +92,9 @@ export async function removeFromOrgs(cfg, accountId) {
|
|
|
92
92
|
orgInvitations: orgResult.invitations,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
/** 7) Apaga o avatar no drive
|
|
96
|
-
export async function deleteAccountAvatar(cfg, avatarUrl) {
|
|
97
|
-
const avatarDeleted = await deleteAvatar(cfg.uploads, avatarUrl);
|
|
95
|
+
/** 7) Apaga o avatar no backend ativo (drive OU media; best-effort, fail-safe). */
|
|
96
|
+
export async function deleteAccountAvatar(cfg, accountId, avatarUrl) {
|
|
97
|
+
const avatarDeleted = await deleteAvatar(cfg.uploads, accountId, avatarUrl);
|
|
98
98
|
return { avatarDeleted };
|
|
99
99
|
}
|
|
100
100
|
/** 8) Anonimiza o histórico de audit da conta (quando o sink suporta). */
|
|
@@ -114,7 +114,7 @@ export class AccountDeletionService {
|
|
|
114
114
|
}
|
|
115
115
|
// 7) Avatar no drive (best-effort, fail-safe).
|
|
116
116
|
try {
|
|
117
|
-
result.avatarDeleted = (await deleteAccountAvatar(cfg, snapshot.avatarUrl)).avatarDeleted;
|
|
117
|
+
result.avatarDeleted = (await deleteAccountAvatar(cfg, accountId, snapshot.avatarUrl)).avatarDeleted;
|
|
118
118
|
}
|
|
119
119
|
catch {
|
|
120
120
|
/* best-effort */
|