@adonis-agora/authkit-server 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) 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 +51 -0
  10. package/build/src/define_config.js +8 -0
  11. package/build/src/host/assets/webauthn.js +2 -0
  12. package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
  13. package/build/src/host/controllers/account_confirm_controller.js +42 -130
  14. package/build/src/host/controllers/account_session_controller.js +20 -4
  15. package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
  16. package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
  17. package/build/src/host/i18n.d.ts +14 -0
  18. package/build/src/host/i18n.js +16 -0
  19. package/build/src/host/impersonation_session.js +15 -0
  20. package/build/src/host/rate_limit.d.ts +10 -0
  21. package/build/src/host/rate_limit.js +6 -0
  22. package/build/src/host/register_auth_host.d.ts +19 -0
  23. package/build/src/host/register_auth_host.js +72 -4
  24. package/build/src/host/sudo/index.d.ts +47 -0
  25. package/build/src/host/sudo/index.js +41 -0
  26. package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
  27. package/build/src/host/sudo/methods/magic_link.js +174 -0
  28. package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
  29. package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
  30. package/build/src/host/sudo/methods/passkey.d.ts +28 -0
  31. package/build/src/host/sudo/methods/passkey.js +139 -0
  32. package/build/src/host/sudo/methods/password.d.ts +19 -0
  33. package/build/src/host/sudo/methods/password.js +93 -0
  34. package/build/src/host/sudo/runtime.d.ts +141 -0
  35. package/build/src/host/sudo/runtime.js +327 -0
  36. package/build/src/host/sudo/types.d.ts +93 -0
  37. package/build/src/host/sudo/types.js +1 -0
  38. package/build/src/host/sudo_mode.d.ts +116 -6
  39. package/build/src/host/sudo_mode.js +133 -7
  40. package/package.json +6 -2
  41. package/build/stubs/ui/edge/views/consent.edge +0 -13
  42. package/build/stubs/ui/edge/views/login.edge +0 -19
  43. package/stubs/ui/edge/views/consent.edge +0 -13
  44. 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;
@@ -630,6 +657,24 @@ export interface AuthServerConfigInput {
630
657
  mail?: MailHooks;
631
658
  /** Sink de auditoria (best-effort). Opcional — quando ausente, auditoria é no-op. */
632
659
  audit?: AuditSink;
660
+ /**
661
+ * Métodos de confirmação de identidade (sudo mode). A ordem do array é a
662
+ * ordem de exibição; o último método usado com sucesso é promovido ao topo.
663
+ *
664
+ * Ausente → a lista que `registerAuthHost` MONTOU (que por sua vez cai em
665
+ * `[password(), passkey()]` sem `AuthHostOptions.sudoMethods`, o
666
+ * comportamento histórico). É a mesma resposta que os handlers dão nesse
667
+ * caso — "vale o que tem rota" —, e é o que impede a tela de oferecer um
668
+ * método sem endpoint ou de esconder um que funciona.
669
+ *
670
+ * Host passwordless (autentica por OIDC/magic link) DEVE incluir ao menos um
671
+ * método que não exija credencial previamente cadastrada — `oidcStepUp()` ou
672
+ * `magicLink()` — senão o usuário fica sem caminho para exportar/excluir os
673
+ * próprios dados.
674
+ */
675
+ sudo?: {
676
+ methods?: SudoMethod[];
677
+ };
633
678
  /**
634
679
  * Eventos/webhooks: o host observa CADA evento de auditoria via callback
635
680
  * in-process (`onEvent`) e/ou POST de webhook (`webhook`). Best-effort, nunca
@@ -822,6 +867,8 @@ export interface ResolvedServerConfig {
822
867
  accountStore: AccountStore;
823
868
  patStore?: PatStore;
824
869
  mountPath: string;
870
+ /** Destino default pós-confirmação do console (sudo mode) e demais fallbacks de conta. Default: '/account/security'. */
871
+ accountHome?: string;
825
872
  render?: AuthHostRenderer;
826
873
  branding?: BrandingConfig;
827
874
  social?: AuthSocialConfig;
@@ -833,6 +880,10 @@ export interface ResolvedServerConfig {
833
880
  notifications: ResolvedNotificationsConfig;
834
881
  mail?: MailHooks;
835
882
  audit?: AuditSink;
883
+ /** Métodos de sudo configurados pelo host. Ausente → defaults do controller. */
884
+ sudo?: {
885
+ methods?: SudoMethod[];
886
+ };
836
887
  mfaIssuer: string;
837
888
  /** RP de WebAuthn resolvido (sempre presente; derivado do issuer por default). */
838
889
  webauthn: ResolvedWebauthnConfig;
@@ -15,6 +15,8 @@ const RATE_LIMIT_DEFAULTS = {
15
15
  login: { points: 10, duration: "1 min" },
16
16
  introspection: { points: 60, duration: "1 min" },
17
17
  adminIp: { points: 30, duration: "1 min" },
18
+ // Mesmos limites do login, bucket separado — ver `ResolvedRateLimitConfig.sudo`.
19
+ sudo: { points: 10, duration: "1 min" },
18
20
  };
19
21
  export function resolveRateLimit(input) {
20
22
  const enabled = input?.enabled ?? true;
@@ -23,6 +25,7 @@ export function resolveRateLimit(input) {
23
25
  login: RATE_LIMIT_DEFAULTS.login,
24
26
  introspection: RATE_LIMIT_DEFAULTS.introspection,
25
27
  adminIp: RATE_LIMIT_DEFAULTS.adminIp,
28
+ sudo: RATE_LIMIT_DEFAULTS.sudo,
26
29
  store: input?.store,
27
30
  };
28
31
  }
@@ -317,6 +320,7 @@ export function defineConfig(config) {
317
320
  accountStore: config.accountStore,
318
321
  patStore: config.patStore,
319
322
  mountPath: config.mountPath ?? "/oidc",
323
+ accountHome: config.accountHome,
320
324
  // Default de runtime: sem isso, `render` ficava `undefined` e TODA
321
325
  // request a `/account/*`/`/auth/interaction/*` estourava um 500 sem
322
326
  // contexto (os controllers fazem `cfg.render!(...)`). `edgeRenderer()`
@@ -332,6 +336,10 @@ export function defineConfig(config) {
332
336
  lockout: resolveLockout(config.lockout),
333
337
  notifications: resolveNotifications(),
334
338
  mail: config.mail,
339
+ // Propaga os métodos de sudo para o config resolvido. Sem esta linha o
340
+ // campo existiria só no config de ENTRADA e `cfg.sudo` seria sempre
341
+ // undefined em runtime — exatamente o bug que `accountHome` tinha.
342
+ sudo: config.sudo,
335
343
  audit: (() => {
336
344
  // Sempre compõe o fan-out: além de onEvent/webhook (quando configurados),
337
345
  // o sink composto republica TODO evento de auditoria no barramento de
@@ -0,0 +1,2 @@
1
+ function l(e){let t=new Uint8Array(e),r="";for(let n of t)r+=String.fromCharCode(n);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function h(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,i=t.padEnd(t.length+r,"="),n=atob(i),c=new ArrayBuffer(n.length),f=new Uint8Array(c);for(let a=0;a<n.length;a++)f[a]=n.charCodeAt(a);return c}function p(){return C.stubThis(globalThis?.PublicKeyCredential!==void 0&&typeof globalThis.PublicKeyCredential=="function")}var C={stubThis:e=>e};function R(e){let{id:t}=e;return{...e,id:h(t),transports:e.transports}}function w(e){return e==="localhost"||/^((xn--[a-z0-9-]+|[a-z0-9]+(-[a-z0-9]+)*)\.)+([a-z]{2,}|xn--[a-z0-9-]+)$/i.test(e)}var o=class extends Error{constructor({message:t,code:r,cause:i,name:n}){super(t,{cause:i}),Object.defineProperty(this,"code",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.name=n??i.name,this.code=r}};function I({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(r.authenticatorSelection?.requireResidentKey===!0)return new o({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new o({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return r.pubKeyCredParams.filter(n=>n.type==="public-key").length===0?new o({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new o({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rp.id!==i)return new o({message:`The RP ID "${r.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(r.user.id.byteLength<1||r.user.id.byteLength>64)return new o({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}var S=class{constructor(){Object.defineProperty(this,"controller",{enumerable:!0,configurable:!0,writable:!0,value:void 0})}createNewAbortSignal(){if(this.controller){let r=new Error("Cancelling existing WebAuthn API call for new one");r.name="AbortError",this.controller.abort(r)}let t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){let t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}},A=new S;var x=["cross-platform","platform"];function E(e){if(e&&!(x.indexOf(e)<0))return e}async function N(e){!e.optionsJSON&&e.challenge&&(console.warn("startRegistration() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useAutoRegister:r=!1}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let i={...t,challenge:h(t.challenge),user:{...t.user,id:h(t.user.id)},excludeCredentials:t.excludeCredentials?.map(R)},n={};r&&(n.mediation="conditional"),n.publicKey=i,n.signal=A.createNewAbortSignal();let c;try{c=await navigator.credentials.create(n)}catch(u){throw I({error:u,options:n})}if(!c)throw new Error("Registration was not completed");let{id:f,rawId:a,response:s,type:y}=c,d;typeof s.getTransports=="function"&&(d=s.getTransports());let g;if(typeof s.getPublicKeyAlgorithm=="function")try{g=s.getPublicKeyAlgorithm()}catch(u){O("getPublicKeyAlgorithm()",u)}let b;if(typeof s.getPublicKey=="function")try{let u=s.getPublicKey();u!==null&&(b=l(u))}catch(u){O("getPublicKey()",u)}let m;if(typeof s.getAuthenticatorData=="function")try{m=l(s.getAuthenticatorData())}catch(u){O("getAuthenticatorData()",u)}return{id:f,rawId:l(a),response:{attestationObject:l(s.attestationObject),clientDataJSON:l(s.clientDataJSON),transports:d,publicKeyAlgorithm:g,publicKey:b,authenticatorData:m},type:y,clientExtensionResults:c.getClientExtensionResults(),authenticatorAttachment:E(c.authenticatorAttachment)}}function O(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.
2
+ `,t)}function _(){if(!p())return T.stubThis(new Promise(t=>t(!1)));let e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?T.stubThis(new Promise(t=>t(!1))):T.stubThis(e.isConditionalMediationAvailable())}var T={stubThis:e=>e};function P({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rpId!==i)return new o({message:`The RP ID "${r.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function D(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useBrowserAutofill:r=!1,verifyBrowserAutofillInput:i=!0}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let n;t.allowCredentials?.length!==0&&(n=t.allowCredentials?.map(R));let c={...t,challenge:h(t.challenge),allowCredentials:n},f={};if(r){if(!await _())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&i)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');f.mediation="conditional",c.allowCredentials=[]}f.publicKey=c,f.signal=A.createNewAbortSignal();let a;try{a=await navigator.credentials.get(f)}catch(m){throw P({error:m,options:f})}if(!a)throw new Error("Authentication was not completed");let{id:s,rawId:y,response:d,type:g}=a,b;return d.userHandle&&(b=l(d.userHandle)),{id:s,rawId:l(y),response:{authenticatorData:l(d.authenticatorData),clientDataJSON:l(d.clientDataJSON),signature:l(d.signature),userHandle:b},type:g,clientExtensionResults:a.getClientExtensionResults(),authenticatorAttachment:E(a.authenticatorAttachment)}}function v(){return p()?PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable():new Promise(e=>e(!1))}export{A as WebAuthnAbortService,o as WebAuthnError,p as browserSupportsWebAuthn,_ as browserSupportsWebAuthnAutofill,v as platformAuthenticatorIsAvailable,D as startAuthentication,N as startRegistration};
@@ -1,24 +1,21 @@
1
1
  /**
2
2
  * Sudo mode — tela de confirmação de identidade (/account/confirm).
3
3
  *
4
- * GET /account/confirm → exibe o formulário de senha (e opção de passkey se disponível).
5
- * POST /account/confirm → verifica a senha e, se correta, marca o sudo na sessão.
6
- * POST /account/confirm/passkey/options gera as opções de autenticação.
7
- * POST /account/confirm/passkey → verifica a resposta da passkey e marca o sudo.
4
+ * O GET lista os métodos DISPONÍVEIS para a conta (SPI `SudoMethod`); a
5
+ * verificação de cada um vive no próprio método, nas rotas que ele registra.
6
+ * Este controller não verifica credencial nem chama `markSudo`.
8
7
  *
9
8
  * A tela está atrás do `accountGuard` (requer sessão de conta ativa).
10
- * Após confirmação, redireciona para `return_to` (validado) ou para `/account/tokens`.
11
9
  */
12
10
  import '../augmentations.js';
13
11
  import type { HttpContext } from '@adonisjs/core/http';
12
+ import { sudoContextFrom } from '../sudo/runtime.js';
13
+ /**
14
+ * Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
15
+ * `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
16
+ * antigo segue valendo para quem já o importava.
17
+ */
18
+ export { sudoContextFrom };
14
19
  export default class AccountConfirmController {
15
- show(ctx: HttpContext): Promise<any>;
16
- confirm(ctx: HttpContext): Promise<void>;
17
- passkeyOptions(ctx: HttpContext): Promise<any>;
18
- passkeyConfirm(ctx: HttpContext): Promise<void>;
19
- /**
20
- * Verifica se a conta é "passwordless" (sem hash de senha definido).
21
- * Fail-safe: retorna `false` quando não é possível determinar.
22
- */
23
- private isPasswordless;
20
+ show(ctx: HttpContext): Promise<unknown>;
24
21
  }