@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.
Files changed (52) hide show
  1. package/build/commands/ui_preset.js +15 -1
  2. package/build/host/views/account/confirm.edge +118 -52
  3. package/build/host/views/account/mfa.edge +1 -1
  4. package/build/host/views/login.edge +2 -4
  5. package/build/host/views/mfa-challenge.edge +1 -1
  6. package/build/host/views/partials/styles.edge +1 -1
  7. package/build/index.d.ts +5 -1
  8. package/build/index.js +15 -1
  9. package/build/src/define_config.d.ts +71 -3
  10. package/build/src/define_config.js +11 -0
  11. package/build/src/host/account_api/account_api_controller.js +2 -2
  12. package/build/src/host/account_deletion_ops.d.ts +2 -2
  13. package/build/src/host/account_deletion_ops.js +3 -3
  14. package/build/src/host/account_deletion_service.js +1 -1
  15. package/build/src/host/assets/webauthn.js +2 -0
  16. package/build/src/host/avatar_storage.d.ts +40 -14
  17. package/build/src/host/avatar_storage.js +221 -72
  18. package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
  19. package/build/src/host/controllers/account_confirm_controller.js +42 -130
  20. package/build/src/host/controllers/account_security_controller.js +3 -3
  21. package/build/src/host/controllers/account_session_controller.js +20 -4
  22. package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
  23. package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
  24. package/build/src/host/durable/account_deletion_workflow.js +1 -1
  25. package/build/src/host/i18n.d.ts +14 -0
  26. package/build/src/host/i18n.js +16 -0
  27. package/build/src/host/impersonation_session.js +15 -0
  28. package/build/src/host/rate_limit.d.ts +10 -0
  29. package/build/src/host/rate_limit.js +6 -0
  30. package/build/src/host/register_auth_host.d.ts +19 -0
  31. package/build/src/host/register_auth_host.js +72 -4
  32. package/build/src/host/sudo/index.d.ts +47 -0
  33. package/build/src/host/sudo/index.js +41 -0
  34. package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
  35. package/build/src/host/sudo/methods/magic_link.js +174 -0
  36. package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
  37. package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
  38. package/build/src/host/sudo/methods/passkey.d.ts +28 -0
  39. package/build/src/host/sudo/methods/passkey.js +139 -0
  40. package/build/src/host/sudo/methods/password.d.ts +19 -0
  41. package/build/src/host/sudo/methods/password.js +93 -0
  42. package/build/src/host/sudo/runtime.d.ts +141 -0
  43. package/build/src/host/sudo/runtime.js +327 -0
  44. package/build/src/host/sudo/types.d.ts +93 -0
  45. package/build/src/host/sudo/types.js +1 -0
  46. package/build/src/host/sudo_mode.d.ts +116 -6
  47. package/build/src/host/sudo_mode.js +133 -7
  48. package/package.json +10 -2
  49. package/build/stubs/ui/edge/views/consent.edge +0 -13
  50. package/build/stubs/ui/edge/views/login.edge +0 -19
  51. package/stubs/ui/edge/views/consent.edge +0 -13
  52. 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
- return []; // views são donas-da-lib (disco authkit::); nada a scaffoldar
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="mb-6">
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
- @if(error)
14
- <div class="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700 ring-1 ring-red-200">{{ error }}</div>
15
- @end
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
- @if(passwordless && !passkeyAvailable)
18
- <div class="rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-800 ring-1 ring-amber-200">{{ t('account.confirm.passwordless_notice') }}</div>
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(passkeyAvailable)
37
- <div class="{{ !passwordless ? 'mt-4 border-t border-gray-100 pt-4' : '' }}">
38
- <button type="button" id="confirm-passkey-btn"
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
- <form id="passkey-confirm-form" method="POST" action="/account/confirm/passkey" class="hidden">
42
- <input type="hidden" name="_csrf" value="{{ csrfToken }}">
43
- @if(returnTo)
44
- <input type="hidden" name="return_to" value="{{ returnTo }}">
45
- @end
46
- <input type="hidden" name="response" id="passkey-confirm-response">
47
- </form>
48
- <script type="module">
49
- import { startAuthentication } from 'https://cdn.jsdelivr.net/npm/@simplewebauthn/browser@13/dist/bundle/index.esm.js';
50
- document.getElementById('confirm-passkey-btn')?.addEventListener('click', async () => {
51
- try {
52
- const optRes = await fetch('/account/confirm/passkey/options', {
53
- method: 'POST',
54
- headers: { 'x-csrf-token': document.querySelector('[name="_csrf"]').value },
55
- });
56
- const opts = await optRes.json();
57
- const assertionResult = await startAuthentication({ optionsJSON: opts });
58
- document.getElementById('passkey-confirm-response').value = JSON.stringify(assertionResult);
59
- document.getElementById('passkey-confirm-form').submit();
60
- } catch (e) {
61
- console.error(e);
62
- }
63
- });
64
- </script>
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
- @end
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 'https://cdn.jsdelivr.net/npm/@simplewebauthn/browser@13/dist/bundle/index.js'
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 'https://cdn.jsdelivr.net/npm/@simplewebauthn/browser@13/dist/bundle/index.js'
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 'https://cdn.jsdelivr.net/npm/@simplewebauthn/browser@13/dist/bundle/index.js'
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
- /** Disk do `@adonisjs/drive` a usar. Default: o disk DEFAULT do app. */
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
- /** Disk explícito; `undefined` = disk DEFAULT do app. */
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, isDriveAvailable, AvatarUploadError } from '../avatar_storage.js';
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 isDriveAvailable(),
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 (best-effort, fail-safe). */
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 (best-effort, fail-safe). */
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 */