@adonis-agora/authkit-server 0.74.0 → 0.76.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 (79) hide show
  1. package/README.md +3 -0
  2. package/build/host/views/account/apps.edge +30 -0
  3. package/build/host/views/agents/consent.edge +60 -0
  4. package/build/host/views/agents/done.edge +24 -0
  5. package/build/host/views/consent.edge +9 -1
  6. package/build/host/views/partials/styles.edge +1 -1
  7. package/build/index.d.ts +9 -0
  8. package/build/index.js +4 -0
  9. package/build/providers/authkit_server_provider.js +75 -50
  10. package/build/src/agents/agent_identity.d.ts +33 -0
  11. package/build/src/agents/agent_identity.js +156 -0
  12. package/build/src/agents/config.d.ts +99 -0
  13. package/build/src/agents/config.js +101 -0
  14. package/build/src/agents/delegation_service.d.ts +154 -0
  15. package/build/src/agents/delegation_service.js +394 -0
  16. package/build/src/agents/delegation_store.d.ts +93 -0
  17. package/build/src/agents/delegation_store.js +222 -0
  18. package/build/src/agents/middleware.d.ts +73 -0
  19. package/build/src/agents/middleware.js +113 -0
  20. package/build/src/agents/protocol.d.ts +62 -0
  21. package/build/src/agents/protocol.js +51 -0
  22. package/build/src/agents/runtime.d.ts +36 -0
  23. package/build/src/agents/runtime.js +65 -0
  24. package/build/src/agents/signer.d.ts +34 -0
  25. package/build/src/agents/signer.js +70 -0
  26. package/build/src/audit/audit_sink.d.ts +1 -1
  27. package/build/src/audit/audit_sink.js +5 -0
  28. package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
  29. package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
  30. package/build/src/define_config.d.ts +34 -3
  31. package/build/src/define_config.js +35 -1
  32. package/build/src/host/access_token_verifier.d.ts +7 -0
  33. package/build/src/host/access_token_verifier.js +8 -0
  34. package/build/src/host/account_api/account_api_controller.d.ts +12 -0
  35. package/build/src/host/account_api/account_api_controller.js +40 -0
  36. package/build/src/host/admin_api/dto.d.ts +1 -1
  37. package/build/src/host/admin_sessions_service.d.ts +2 -0
  38. package/build/src/host/admin_sessions_service.js +18 -0
  39. package/build/src/host/auth_host_config.d.ts +4 -0
  40. package/build/src/host/bearer_account.d.ts +17 -0
  41. package/build/src/host/bearer_account.js +10 -0
  42. package/build/src/host/client_names.d.ts +11 -0
  43. package/build/src/host/client_names.js +12 -0
  44. package/build/src/host/console_session.js +3 -5
  45. package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
  46. package/build/src/host/controllers/account_apps_controller.js +39 -3
  47. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  48. package/build/src/host/controllers/account_session_controller.js +2 -1
  49. package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
  50. package/build/src/host/controllers/agent_consent_controller.js +138 -0
  51. package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
  52. package/build/src/host/controllers/agent_oauth_controller.js +110 -0
  53. package/build/src/host/controllers/interaction_controller.js +28 -0
  54. package/build/src/host/csrf.d.ts +8 -18
  55. package/build/src/host/csrf.js +28 -2
  56. package/build/src/host/i18n.d.ts +54 -0
  57. package/build/src/host/i18n.js +54 -0
  58. package/build/src/host/impersonation_session.d.ts +5 -0
  59. package/build/src/host/impersonation_session.js +37 -4
  60. package/build/src/host/oidc_bearer_guard.js +10 -1
  61. package/build/src/host/redirect_exact.d.ts +12 -0
  62. package/build/src/host/redirect_exact.js +17 -0
  63. package/build/src/host/register_auth_host.js +48 -7
  64. package/build/src/host/request_url.d.ts +10 -0
  65. package/build/src/host/request_url.js +17 -0
  66. package/build/src/host/sudo/methods/magic_link.js +2 -1
  67. package/build/src/host/sudo/runtime.js +3 -2
  68. package/build/src/host/sudo_mode.js +4 -4
  69. package/build/src/mcp/mcp_oauth.d.ts +85 -0
  70. package/build/src/mcp/mcp_oauth.js +154 -0
  71. package/build/src/provider/build_provider.js +20 -1
  72. package/build/src/provider/oidc_service.d.ts +8 -0
  73. package/build/src/provider/oidc_service.js +11 -0
  74. package/build/src/provider/token_exchange.d.ts +26 -0
  75. package/build/src/provider/token_exchange.js +44 -3
  76. package/build/src/schema/ensure.js +92 -0
  77. package/build/stubs/ui/react/pages/consent.tsx +17 -1
  78. package/package.json +1 -1
  79. package/stubs/ui/react/pages/consent.tsx +17 -1
package/README.md CHANGED
@@ -11,6 +11,9 @@ wrapper around [`oidc-provider`](https://github.com/panva/node-oidc-provider).
11
11
  - **Passwordless** — magic-link email login and passkey-first login.
12
12
  - **Protocol extensions** — Device Flow (RFC 8628), DPoP (RFC 9449), PAR (RFC 9126), step-up
13
13
  auth via `acr_values`, Dynamic Client Registration (RFC 7591/7592).
14
+ - **Personal agents** — AI assistants acting for your users: agent identity by signed JWT
15
+ (JWKS, no shared secrets) and user-approved, revocable delegation with scopes you define,
16
+ over a pluggable protocol adapter ([PACT](https://openpactprotocol.org) built in).
14
17
  - **Native apps** — RFC 8252 native clients (`authkit:clients:create --native`: private-use
15
18
  scheme / claimed https / loopback redirects, always public + PKCE) and the `@adonisjs/auth`
16
19
  guards `oidcRpGuard` (session) and `oidcBearerGuard` (`Authorization: Bearer` access tokens,
@@ -56,5 +56,35 @@
56
56
  @end
57
57
  </div>
58
58
  @end
59
+
60
+ @if(agents)
61
+ <h2 class="mt-8 mb-1 text-base font-semibold text-gray-900">{{ t('account.apps.agents_title') }}</h2>
62
+ <p class="mb-3 text-sm text-gray-500">{{ t('account.apps.agents_intro') }}</p>
63
+ <div class="overflow-hidden rounded-xl bg-white shadow-xs ring-1 ring-black/5">
64
+ @if(agents.length === 0)
65
+ <p class="p-6 text-sm text-gray-500">{{ t('account.apps.agents_empty') }}</p>
66
+ @else
67
+ @each(agent in agents)
68
+ <div class="flex items-center justify-between border-b border-gray-100 p-4 last:border-0">
69
+ <div>
70
+ <p class="text-sm font-medium text-gray-900">{{ agent.name }}</p>
71
+ <ul class="mt-1 text-xs text-gray-500">
72
+ @each(scope in agent.scopes)
73
+ <li>{{ scope.description }}</li>
74
+ @end
75
+ </ul>
76
+ </div>
77
+ <form method="POST" action="{{ accountPaths?.apps ?? '/account/apps' }}/agents/{{ agent.id }}/revoke"
78
+ onsubmit="return confirm('{{ t('account.apps.revoke_confirm') }}')">
79
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
80
+ <button type="submit" class="rounded-lg border border-red-300 px-3 py-1.5 text-sm font-semibold text-red-700 hover:bg-red-50">
81
+ {{ t('account.apps.revoke') }}
82
+ </button>
83
+ </form>
84
+ </div>
85
+ @end
86
+ @end
87
+ </div>
88
+ @end
59
89
  </div>
60
90
  </body></html>
@@ -0,0 +1,60 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('agents.consent.page_title') }}</title>
3
+ @include('authkit::partials/styles')
4
+ @include('authkit::partials/submit_lock')
5
+ </head>
6
+ <body class="min-h-screen flex items-center justify-center bg-gray-50">
7
+ <div class="w-full max-w-md bg-white p-8 rounded-2xl shadow-xl ring-1 ring-black/5">
8
+ @if(request)
9
+ <form method="POST" action="{{ action }}">
10
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
11
+ <input type="hidden" name="user_code" value="{{ request.userCode }}">
12
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.consent.title', { agent: request.agentName }) }}</h1>
13
+ <p class="mt-2 text-sm text-gray-600">{{ t('agents.consent.body', { origin: request.agentOrigin }) }}</p>
14
+ @if(account && account.email)
15
+ <p class="mt-2 text-xs text-gray-500">{{ t('agents.consent.signed_in_as', { email: account.email }) }}</p>
16
+ @end
17
+
18
+ <fieldset class="mt-6 space-y-3">
19
+ <legend class="text-sm font-medium text-gray-900">{{ t('agents.consent.scopes_label') }}</legend>
20
+ @each(scope in request.scopes)
21
+ <label class="flex items-start gap-3 rounded-lg border border-gray-200 p-3">
22
+ <input type="checkbox" name="scope" value="{{ scope.id }}" checked class="mt-0.5">
23
+ <span class="text-sm text-gray-800">{{ scope.description }}</span>
24
+ </label>
25
+ @end
26
+ </fieldset>
27
+
28
+ <p class="mt-4 text-xs text-gray-500">{{ t('agents.consent.code', { code: request.userCode }) }}</p>
29
+
30
+ <div class="mt-6 flex gap-3">
31
+ <button type="submit" name="decision" value="deny"
32
+ class="flex-1 rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
33
+ {{ t('agents.consent.deny') }}
34
+ </button>
35
+ <button type="submit" name="decision" value="allow"
36
+ class="flex-1 rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
37
+ {{ t('agents.consent.allow') }}
38
+ </button>
39
+ </div>
40
+ </form>
41
+ @else
42
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.consent.enter_code_title') }}</h1>
43
+ @if(error)
44
+ <p class="mt-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800">{{ t(error) }}</p>
45
+ @end
46
+ @if(error !== 'agents.consent.impersonating')
47
+ <form method="GET" action="{{ action }}" class="mt-4">
48
+ <label class="block text-sm text-gray-600" for="user_code">{{ t('agents.consent.enter_code_body') }}</label>
49
+ <input id="user_code" name="user_code" autocomplete="one-time-code" autocapitalize="characters"
50
+ class="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2 text-center font-mono text-lg tracking-widest"
51
+ placeholder="XXXX-XXXX" required>
52
+ <button type="submit"
53
+ class="mt-4 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
54
+ {{ t('agents.consent.continue') }}
55
+ </button>
56
+ </form>
57
+ @end
58
+ @end
59
+ </div>
60
+ </body></html>
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('agents.consent.page_title') }}</title>
3
+ @include('authkit::partials/styles')
4
+ </head>
5
+ <body class="min-h-screen flex items-center justify-center bg-gray-50">
6
+ <div class="w-full max-w-md bg-white p-8 rounded-2xl shadow-xl ring-1 ring-black/5">
7
+ @if(status === 'approved')
8
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.approved_title', { agent: agentName }) }}</h1>
9
+ <ul class="mt-4 space-y-1 text-sm text-gray-700">
10
+ @each(scope in scopes)
11
+ <li>• {{ scope.description }}</li>
12
+ @end
13
+ </ul>
14
+ <p class="mt-4 text-sm text-gray-600">{{ t('agents.done.return') }}</p>
15
+ <p class="mt-2 text-xs text-gray-500">{{ t('agents.done.revoke_hint') }}</p>
16
+ @elseif(status === 'denied')
17
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.denied_title') }}</h1>
18
+ <p class="mt-2 text-sm text-gray-600">{{ t('agents.done.return') }}</p>
19
+ @else
20
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.expired_title') }}</h1>
21
+ <p class="mt-2 text-sm text-gray-600">{{ t('agents.done.expired_body') }}</p>
22
+ @end
23
+ </div>
24
+ </body></html>
@@ -9,8 +9,16 @@
9
9
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
10
10
  <h1 class="text-xl font-semibold text-gray-900">{{ t('consent.title') }}</h1>
11
11
  <p class="mt-2 text-sm text-gray-600">
12
- {{{ t('consent.body', { app: brand && brand.appName ? brand.appName : params.client_id }) }}}
12
+ {{{ t('consent.body', { app: clientNameHtml }) }}}
13
13
  </p>
14
+ @if(scopes && scopes.length)
15
+ <p class="mt-4 text-sm font-medium text-gray-900">{{ t('consent.scopes_title') }}</p>
16
+ <ul class="mt-2 space-y-1 text-sm text-gray-600 list-disc list-inside">
17
+ @each(scope in scopes)
18
+ <li>{{ scope.label }}</li>
19
+ @end
20
+ </ul>
21
+ @end
14
22
 
15
23
  <button type="submit"
16
24
  class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
@@ -1,3 +1,3 @@
1
1
  {{-- Gerado por scripts/build_host_css.mjs — não editar à mão. --}}
2
2
  <style>/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
3
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-800:oklch(47.3% .137 46.201);--color-green-50:oklch(98.2% .018 155.826);--color-green-700:oklch(52.7% .154 150.069);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--tracking-widest:.1em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}button:not(:disabled),[role=button]:not(:disabled){cursor:pointer}}@layer components;@layer utilities{.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-6{margin-block:calc(var(--spacing) * 6)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-48{height:calc(var(--spacing) * 48)}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-48{width:calc(var(--spacing) * 48)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-sm{max-width:var(--container-sm)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-300{border-color:var(--color-blue-300)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-900{border-color:var(--color-gray-900)}.border-red-300{border-color:var(--color-red-300)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-600{background-color:var(--color-red-600)}.bg-white{background-color:var(--color-white)}.object-cover{object-fit:cover}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.4em\]{--tw-tracking:.4em;letter-spacing:.4em}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-800{color:var(--color-amber-800)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-emerald-200{--tw-ring-color:var(--color-emerald-200)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-red-600:focus{border-color:var(--color-red-600)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}</style>
3
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-800:oklch(47.3% .137 46.201);--color-green-50:oklch(98.2% .018 155.826);--color-green-700:oklch(52.7% .154 150.069);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--tracking-widest:.1em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}button:not(:disabled),[role=button]:not(:disabled){cursor:pointer}}@layer components;@layer utilities{.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-6{margin-block:calc(var(--spacing) * 6)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-48{height:calc(var(--spacing) * 48)}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-48{width:calc(var(--spacing) * 48)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-300{border-color:var(--color-blue-300)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-900{border-color:var(--color-gray-900)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-600{background-color:var(--color-red-600)}.bg-white{background-color:var(--color-white)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.4em\]{--tw-tracking:.4em;letter-spacing:.4em}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-800{color:var(--color-amber-800)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-emerald-200{--tw-ring-color:var(--color-emerald-200)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-red-600:focus{border-color:var(--color-red-600)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}</style>
package/build/index.d.ts CHANGED
@@ -10,6 +10,14 @@ export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account
10
10
  export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
11
11
  export type { LucidStoresModels, LucidStoresOptions, LucidStoresResult, } from './src/accounts/lucid_stores.js';
12
12
  export { lucidStores } from './src/accounts/lucid_stores.js';
13
+ export type { PersonalAgentIdentity } from './src/agents/agent_identity.js';
14
+ export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
15
+ export type { PersonalAgentDelegationConfigInput, PersonalAgentRegistration, PersonalAgentResolver, PersonalAgentsConfigInput, ResolvedPersonalAgentsConfig, } from './src/agents/config.js';
16
+ export type { DelegationContext, DelegationGrantSummary, DelegationReceipt, } from './src/agents/delegation_service.js';
17
+ export type { PersonalAgentAuthOptions, PersonalAgentRequestContext, } from './src/agents/middleware.js';
18
+ export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
19
+ export type { BuiltinProtocolId, PactOptions, PersonalAgentProtocol, } from './src/agents/protocol.js';
20
+ export { pact, pactProtocol } from './src/agents/protocol.js';
13
21
  export type { AuditEvent, AuditEventType, AuditPage, AuditSink, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
14
22
  export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
15
23
  export type { AdminApiConfigInput, AdminConfigInput, AuthHostRenderer, AuthMethodsConfigInput, AuthServerConfigInput, AuthSocialConfig, DynamicRegistrationConfigInput, LoginConfigInput, OrganizationsConfigInput, PasswordlessConfigInput, RateLimitBucket, RateLimitConfigInput, RegistrationConfigInput, ResolvedAdminApiConfig, ResolvedAdminConfig, ResolvedAuthMethodsConfig, ResolvedDynamicRegistrationConfig, ResolvedLoginConfig, ResolvedNotificationsConfig, ResolvedOrganizationsConfig, ResolvedPasswordlessConfig, ResolvedRateLimitConfig, ResolvedRegistrationConfig, ResolvedServerConfig, ResolvedWebauthnConfig, WebauthnConfigInput, } from './src/define_config.js';
@@ -109,6 +117,7 @@ export type { ParsedUserAgent } from './src/host/user_agent.js';
109
117
  export { parseUserAgent } from './src/host/user_agent.js';
110
118
  export type { ResolvedUserLoginMethods, UserLoginMethodKey, UserLoginMethods, } from './src/host/user_login_methods.js';
111
119
  export { normalizeUserLoginMethods, parseUserLoginMethodsPayload, resolveEffectiveUserLoginMethods, USER_LOGIN_METHOD_KEYS, } from './src/host/user_login_methods.js';
120
+ export { MCP_CLIENT_REDIRECTS, MCP_RESOURCE_SCOPES, type McpOAuthConfigInput, type OAuthResourceRegistration, registeredOAuthResources, registerOAuthResource, } from './src/mcp/mcp_oauth.js';
112
121
  export { withAuditLog } from './src/mixins/with_audit_log.js';
113
122
  export { withAuthUser } from './src/mixins/with_auth_user.js';
114
123
  export { withCredentials } from './src/mixins/with_credentials.js';
package/build/index.js CHANGED
@@ -7,6 +7,9 @@ export { supportsAccountDeletion, supportsAccountEmailRewrite, supportsAccountIm
7
7
  export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account_store.js';
8
8
  export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
9
9
  export { lucidStores } from './src/accounts/lucid_stores.js';
10
+ export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
11
+ export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
12
+ export { pact, pactProtocol } from './src/agents/protocol.js';
10
13
  export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
11
14
  export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
12
15
  export { buildWebhookBody, composeAuditSink, resolveEvents, signWebhookBody, } from './src/events/dispatcher.js';
@@ -107,6 +110,7 @@ export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices,
107
110
  export { parseUserAgent } from './src/host/user_agent.js';
108
111
  // Tipos de login POR USUÁRIO (self-service no console de conta).
109
112
  export { normalizeUserLoginMethods, parseUserLoginMethodsPayload, resolveEffectiveUserLoginMethods, USER_LOGIN_METHOD_KEYS, } from './src/host/user_login_methods.js';
113
+ export { MCP_CLIENT_REDIRECTS, MCP_RESOURCE_SCOPES, registeredOAuthResources, registerOAuthResource, } from './src/mcp/mcp_oauth.js';
110
114
  export { withAuditLog } from './src/mixins/with_audit_log.js';
111
115
  export { withAuthUser } from './src/mixins/with_auth_user.js';
112
116
  export { withCredentials } from './src/mixins/with_credentials.js';
@@ -47,58 +47,36 @@ export default class AuthkitServerProvider {
47
47
  if (this.app.config.get('authkit')) {
48
48
  resolveAppKey(this.app);
49
49
  }
50
- // Config locks: trava as settings definidas explicitamente no defineConfig
51
- // (config vence em runtime; a UI/Admin API não pode alterá-las). Fail-safe:
52
- // qualquer erro → sem locks (comportamento legado).
53
- let resolvedConfig = null;
54
- try {
55
- const value = this.app.config.get('authkit');
56
- if (value) {
57
- const config = (await configProvider.resolve(this.app, value));
58
- if (config) {
59
- resolvedConfig = config;
60
- if (config.lockedSettingKeys?.length) {
61
- const { setLockedSettingKeys } = await import('../src/host/config_locks.js');
62
- setLockedSettingKeys(config.lockedSettingKeys);
50
+ // Config locks + stash dos bits de routing + auto-montagem: tudo sai do config RESOLVIDO.
51
+ //
52
+ // A resolução pode falhar AQUI e dar certo um instante depois: o `jwks` de um keystore
53
+ // criptografado precisa do serviço de encryption, que nem sempre está pronto durante o boot
54
+ // dos providers. Antes, o catch engolia a falha e nada derivado do config valia — locks,
55
+ // `sudo.methods`, headless, personal agents — sem um aviso. Agora a falha aqui é tentada de
56
+ // novo no `booted` (todos os providers prontos, ANTES do preload `start/routes.ts`), e só a
57
+ // segunda falha desiste, com warning.
58
+ if (this.app.config.get('authkit')) {
59
+ let config;
60
+ try {
61
+ config = await this.#resolveConfig();
62
+ }
63
+ catch {
64
+ this.app.booted(async () => {
65
+ let late;
66
+ try {
67
+ late = await this.#resolveConfig();
63
68
  }
64
- // Stash dos bits de routing p/ o registerAuthHost ler do config (dedup).
65
- // boot() roda antes do preload start/routes.ts, então estará disponível lá.
66
- const { setAuthHostConfig } = await import('../src/host/auth_host_config.js');
67
- setAuthHostConfig({
68
- mountPath: config.mountPath,
69
- social: config.social,
70
- rateLimit: config.rateLimit,
71
- adminEnabled: config.admin.enabled,
72
- adminApiEnabled: config.adminApi.enabled,
73
- // `config.sudo.methods` passa a decidir também o que é MONTADO — sem
74
- // isto o host teria de repetir a lista no `registerAuthHost`, e as
75
- // duas divergiriam (tela oferecendo endpoint que dá 404).
76
- sudoMethods: config.sudo?.methods,
77
- // Defaults estruturais de `config.routes` (o argumento ainda vence).
78
- routes: typeof config.routes === 'object' ? config.routes : undefined,
79
- lockedRouteOptions: config.lockedRouteOptions,
80
- // API headless — repassa pro registerAuthHost montar as rotas.
81
- headless: config.headless
82
- ? {
83
- baseUrl: config.headless.baseUrl,
84
- resolveAccountId: config.headless.resolveAccountId,
85
- }
86
- : undefined,
87
- });
88
- }
69
+ catch (error) {
70
+ const logger = await this.app.container.make('logger');
71
+ logger.warn({ err: error }, 'authkit: config não resolveu no boot — locks, stash de rotas (registerAuthHost) e config.routes não foram aplicados');
72
+ return;
73
+ }
74
+ if (late)
75
+ await this.#applyResolvedConfig(late);
76
+ });
89
77
  }
90
- }
91
- catch {
92
- /* sem locks / sem stash → registerAuthHost cai em opts/defaults */
93
- }
94
- // Auto-montagem das rotas (`config.routes`). FORA do try/catch fail-safe
95
- // acima de propósito: "as rotas não subiram" não pode degradar em silêncio —
96
- // seria um app inteiro em 404 sem nenhuma pista. Chama a MESMA função
97
- // exportada que o `start/routes.ts` chamaria; não há segunda implementação.
98
- if (resolvedConfig?.routes) {
99
- const router = await this.app.container.make('router');
100
- const { autoMountAuthHost } = await import('../src/host/register_auth_host.js');
101
- autoMountAuthHost(router);
78
+ if (config)
79
+ await this.#applyResolvedConfig(config);
102
80
  }
103
81
  // Registra o disco "authkit" no edge.js para que os templates sejam referenciados
104
82
  // como `authkit::login`, `authkit::account/tokens`, etc.
@@ -123,6 +101,53 @@ export default class AuthkitServerProvider {
123
101
  // edge.js ausente (host headless/Inertia-only que não usa edgeRenderer) — ignora.
124
102
  }
125
103
  }
104
+ async #resolveConfig() {
105
+ const value = this.app.config.get('authkit');
106
+ return (await configProvider.resolve(this.app, value));
107
+ }
108
+ /**
109
+ * Aplica o que o boot deriva do config resolvido: os locks de settings, o stash que o
110
+ * `registerAuthHost` lê e a auto-montagem de `config.routes` (cuja falha propaga).
111
+ */
112
+ async #applyResolvedConfig(config) {
113
+ if (config.lockedSettingKeys?.length) {
114
+ const { setLockedSettingKeys } = await import('../src/host/config_locks.js');
115
+ setLockedSettingKeys(config.lockedSettingKeys);
116
+ }
117
+ // Stash dos bits de routing p/ o registerAuthHost ler do config (dedup). Roda antes do
118
+ // preload start/routes.ts (no boot, ou no `booted` quando o boot não conseguiu resolver).
119
+ const { setAuthHostConfig } = await import('../src/host/auth_host_config.js');
120
+ setAuthHostConfig({
121
+ mountPath: config.mountPath,
122
+ social: config.social,
123
+ rateLimit: config.rateLimit,
124
+ adminEnabled: config.admin.enabled,
125
+ adminApiEnabled: config.adminApi.enabled,
126
+ // `config.sudo.methods` passa a decidir também o que é MONTADO — sem isto o host teria de
127
+ // repetir a lista no `registerAuthHost`, e as duas divergiriam (tela oferecendo endpoint
128
+ // que dá 404).
129
+ sudoMethods: config.sudo?.methods,
130
+ // Defaults estruturais de `config.routes` (o argumento ainda vence).
131
+ routes: typeof config.routes === 'object' ? config.routes : undefined,
132
+ lockedRouteOptions: config.lockedRouteOptions,
133
+ personalAgents: config.personalAgents ? { prefix: config.personalAgents.prefix } : undefined,
134
+ // API headless — repassa pro registerAuthHost montar as rotas.
135
+ headless: config.headless
136
+ ? {
137
+ baseUrl: config.headless.baseUrl,
138
+ resolveAccountId: config.headless.resolveAccountId,
139
+ }
140
+ : undefined,
141
+ });
142
+ // Auto-montagem das rotas (`config.routes`). Chama a MESMA função exportada que o
143
+ // `start/routes.ts` chamaria; não há segunda implementação. "As rotas não subiram" não pode
144
+ // degradar em silêncio: uma falha aqui propaga.
145
+ if (config.routes) {
146
+ const router = await this.app.container.make('router');
147
+ const { autoMountAuthHost } = await import('../src/host/register_auth_host.js');
148
+ autoMountAuthHost(router);
149
+ }
150
+ }
126
151
  register() {
127
152
  // Entrega o app BOOTADO ao singleton de `services/main` (e ao
128
153
  // `recordSubRevocation` do AdminSessionsService) para que nunca precisem
@@ -0,0 +1,33 @@
1
+ import { type JWTPayload, type JWTVerifyGetKey } from 'jose';
2
+ import type { PersonalAgentRegistration, ResolvedPersonalAgentsConfig } from './config.js';
3
+ /** Quem está chamando: o agente e o usuário DELE (`sub`, opaco). */
4
+ export interface PersonalAgentIdentity {
5
+ /** `iss` do JWT — identifica o agente; também é o `client_id` OAuth dele. */
6
+ issuer: string;
7
+ /** Usuário do agente. Opaco e estável; NÃO é a conta neste app. */
8
+ sub: string;
9
+ /** Nome de exibição do agente (registro ou host do issuer). */
10
+ name: string;
11
+ claims: JWTPayload;
12
+ }
13
+ export interface PersonalAgentVerifierOptions {
14
+ /** Fonte das chaves por `jwksUri`. Default: `createRemoteJWKSet` com cache. */
15
+ getKey?: (jwksUri: string) => JWTVerifyGetKey;
16
+ /** `fetch` da descoberta OIDC no modo `open`. Default: o global. */
17
+ fetch?: typeof fetch;
18
+ now?: () => Date;
19
+ }
20
+ /** Extrai o token de um header `Authorization: Bearer <token>`. */
21
+ export declare function bearerToken(header: string | null | undefined): string | null;
22
+ export declare function displayNameFor(registration: PersonalAgentRegistration): string;
23
+ /**
24
+ * Verifica o JWT que um personal agent manda em `Authorization: Bearer`, com
25
+ * as regras do protocolo configurado (PACT §3.2: ES256/RS256, vida ≤ 300 s,
26
+ * 30 s de relógio). Devolve `null` para QUALQUER falha — o chamador responde
27
+ * 401 sem detalhe.
28
+ */
29
+ export declare class PersonalAgentVerifier {
30
+ #private;
31
+ constructor(cfg: ResolvedPersonalAgentsConfig, options?: PersonalAgentVerifierOptions);
32
+ verify(authorization: string | null | undefined): Promise<PersonalAgentIdentity | null>;
33
+ }
@@ -0,0 +1,156 @@
1
+ import { createRemoteJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify, } from 'jose';
2
+ /** Descoberta do modo `open`: quanto tempo um resultado vale, e quantos guardar. */
3
+ const DISCOVERY_TTL_MS = 60 * 60 * 1000;
4
+ const DISCOVERY_FAILURE_TTL_MS = 60 * 1000;
5
+ const MAX_CACHED_ISSUERS = 1000;
6
+ /** Map com teto: passando do limite, sai a entrada mais antiga (ordem de inserção). */
7
+ function setBounded(map, key, value) {
8
+ map.delete(key);
9
+ map.set(key, value);
10
+ if (map.size > MAX_CACHED_ISSUERS)
11
+ map.delete(map.keys().next().value);
12
+ }
13
+ /** Extrai o token de um header `Authorization: Bearer <token>`. */
14
+ export function bearerToken(header) {
15
+ if (!header)
16
+ return null;
17
+ const match = /^Bearer\s+(\S+)\s*$/i.exec(header);
18
+ return match ? match[1] : null;
19
+ }
20
+ export function displayNameFor(registration) {
21
+ if (registration.name)
22
+ return registration.name;
23
+ try {
24
+ return new URL(registration.issuer).host;
25
+ }
26
+ catch {
27
+ return registration.issuer;
28
+ }
29
+ }
30
+ /**
31
+ * Verifica o JWT que um personal agent manda em `Authorization: Bearer`, com
32
+ * as regras do protocolo configurado (PACT §3.2: ES256/RS256, vida ≤ 300 s,
33
+ * 30 s de relógio). Devolve `null` para QUALQUER falha — o chamador responde
34
+ * 401 sem detalhe.
35
+ */
36
+ export class PersonalAgentVerifier {
37
+ #cfg;
38
+ #getKey;
39
+ #fetch;
40
+ #now;
41
+ #remoteSets = new Map();
42
+ #discovered = new Map();
43
+ constructor(cfg, options = {}) {
44
+ this.#cfg = cfg;
45
+ this.#getKey = options.getKey ?? ((uri) => this.#remoteSet(uri));
46
+ this.#fetch = options.fetch ?? globalThis.fetch;
47
+ this.#now = options.now ?? (() => new Date());
48
+ }
49
+ async verify(authorization) {
50
+ const token = bearerToken(authorization);
51
+ if (!token)
52
+ return null;
53
+ const rules = this.#cfg.protocol.identity;
54
+ let issuer;
55
+ try {
56
+ if (!rules.algorithms.includes(decodeProtectedHeader(token).alg ?? ''))
57
+ return null;
58
+ issuer = decodeJwt(token).iss;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ if (typeof issuer !== 'string' || !issuer)
64
+ return null;
65
+ const registration = await this.#registrationFor(issuer);
66
+ if (!registration || registration.enabled === false)
67
+ return null;
68
+ const now = this.#now();
69
+ let payload;
70
+ try {
71
+ ({ payload } = await jwtVerify(token, this.#getKey(registration.jwksUri), {
72
+ algorithms: rules.algorithms,
73
+ issuer: registration.issuer,
74
+ audience: this.#cfg.audience,
75
+ clockTolerance: rules.clockSkewSeconds,
76
+ currentDate: now,
77
+ requiredClaims: ['sub', 'iat', 'exp'],
78
+ }));
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ // O que o `jwtVerify` não cobre: `aud` é UMA string, `iat` não está no
84
+ // futuro além da tolerância, e a vida do token é curta.
85
+ const nowSeconds = Math.floor(now.getTime() / 1000);
86
+ if (typeof payload.aud !== 'string')
87
+ return null;
88
+ if (typeof payload.sub !== 'string' || !payload.sub)
89
+ return null;
90
+ if (payload.iat > nowSeconds + rules.clockSkewSeconds)
91
+ return null;
92
+ if (payload.exp - payload.iat > rules.maxLifetimeSeconds)
93
+ return null;
94
+ return {
95
+ issuer: registration.issuer,
96
+ sub: payload.sub,
97
+ name: displayNameFor(registration),
98
+ claims: payload,
99
+ };
100
+ }
101
+ async #registrationFor(issuer) {
102
+ const registered = await this.#cfg.resolveAgent(issuer);
103
+ if (registered)
104
+ return registered;
105
+ if (!this.#cfg.open)
106
+ return null;
107
+ const jwksUri = await this.#discover(issuer);
108
+ return jwksUri ? { issuer, jwksUri } : null;
109
+ }
110
+ /**
111
+ * Modo `open`: `jwks_uri` via `{iss}/.well-known/openid-configuration`, só
112
+ * HTTPS. O `iss` vem de um JWT ainda NÃO verificado, então tudo é limitado:
113
+ * falhas também ficam em cache (um minuto — o mesmo `iss` não vira uma
114
+ * requisição de saída por request), acertos expiram (o `jwks_uri` pode
115
+ * mudar) e o cache tem teto de entradas.
116
+ */
117
+ #discover(issuer) {
118
+ const now = this.#now().getTime();
119
+ const cached = this.#discovered.get(issuer);
120
+ if (cached && cached.expiresAt > now)
121
+ return cached.result;
122
+ const result = this.#fetchJwksUri(issuer);
123
+ const entry = { result, expiresAt: now + DISCOVERY_TTL_MS };
124
+ setBounded(this.#discovered, issuer, entry);
125
+ result.then((uri) => {
126
+ if (!uri)
127
+ entry.expiresAt = now + DISCOVERY_FAILURE_TTL_MS;
128
+ });
129
+ return result;
130
+ }
131
+ async #fetchJwksUri(issuer) {
132
+ try {
133
+ const url = new URL(issuer);
134
+ if (url.protocol !== 'https:')
135
+ return null;
136
+ const res = await this.#fetch(`${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(5000), redirect: 'error' });
137
+ if (!res.ok)
138
+ return null;
139
+ const meta = (await res.json());
140
+ if (meta.issuer !== issuer || typeof meta.jwks_uri !== 'string')
141
+ return null;
142
+ return new URL(meta.jwks_uri).protocol === 'https:' ? meta.jwks_uri : null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ #remoteSet(jwksUri) {
149
+ let set = this.#remoteSets.get(jwksUri);
150
+ if (!set) {
151
+ set = createRemoteJWKSet(new URL(jwksUri));
152
+ setBounded(this.#remoteSets, jwksUri, set);
153
+ }
154
+ return set;
155
+ }
156
+ }