@adonis-agora/authkit-server 1.0.0-rc.0 → 1.0.0-rc.10

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 (177) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +4 -5
  3. package/build/commands/agora_doctor.d.ts +22 -0
  4. package/build/commands/agora_doctor.js +109 -0
  5. package/build/commands/clients_create.d.ts +4 -0
  6. package/build/commands/clients_create.js +97 -0
  7. package/build/commands/commands.json +78 -0
  8. package/build/commands/doctor.js +8 -74
  9. package/build/host/views/account/apps.edge +1 -5
  10. package/build/host/views/account/extension.edge +77 -0
  11. package/build/host/views/account/mfa.edge +2 -0
  12. package/build/host/views/account/partials/extension_actions.edge +14 -0
  13. package/build/host/views/account/partials/extension_block.edge +197 -0
  14. package/build/host/views/account/partials/extension_value.edge +8 -0
  15. package/build/host/views/account/partials/nav.edge +14 -0
  16. package/build/host/views/account/security.edge +1 -5
  17. package/build/host/views/account/tokens.edge +2 -2
  18. package/build/host/views/agents/consent.edge +11 -2
  19. package/build/host/views/agents/done.edge +1 -1
  20. package/build/host/views/partials/styles.edge +1 -1
  21. package/build/index.d.ts +14 -12
  22. package/build/index.js +8 -9
  23. package/build/providers/authkit_server_provider.js +36 -8
  24. package/build/src/account/extension.d.ts +10 -0
  25. package/build/src/account/extension.js +9 -0
  26. package/build/src/account/http.d.ts +77 -0
  27. package/build/src/account/http.js +381 -0
  28. package/build/src/account/mount.d.ts +40 -0
  29. package/build/src/account/mount.js +171 -0
  30. package/build/src/account/registry.d.ts +41 -0
  31. package/build/src/account/registry.js +81 -0
  32. package/build/src/account/remote.d.ts +72 -0
  33. package/build/src/account/remote.js +406 -0
  34. package/build/src/account/runtime.d.ts +289 -0
  35. package/build/src/account/runtime.js +641 -0
  36. package/build/src/accounts/account_store.d.ts +6 -0
  37. package/build/src/accounts/lucid_store/organizations.js +6 -0
  38. package/build/src/agents/config.d.ts +9 -13
  39. package/build/src/agents/config.js +17 -11
  40. package/build/src/agents/delegation_service.js +3 -3
  41. package/build/src/agents/delegation_store.d.ts +3 -2
  42. package/build/src/agents/delegation_store.js +6 -5
  43. package/build/src/agents/extension.d.ts +261 -0
  44. package/build/src/agents/extension.js +40 -0
  45. package/build/src/agents/middleware.js +1 -1
  46. package/build/src/agents/protocol.d.ts +4 -4
  47. package/build/src/agents/registry_store.d.ts +3 -2
  48. package/build/src/agents/registry_store.js +1 -1
  49. package/build/src/agents/runtime.d.ts +26 -7
  50. package/build/src/agents/runtime.js +110 -75
  51. package/build/src/audit/audit_sink.d.ts +1 -1
  52. package/build/src/audit/audit_sink.js +10 -1
  53. package/build/src/console_authorize.d.ts +56 -0
  54. package/build/src/console_authorize.js +85 -0
  55. package/build/src/console_switcher.d.ts +132 -0
  56. package/build/src/console_switcher.js +206 -0
  57. package/build/src/define_config.d.ts +54 -14
  58. package/build/src/define_config.js +17 -8
  59. package/build/src/dev_tools/contract.d.ts +72 -0
  60. package/build/src/dev_tools/contract.js +10 -0
  61. package/build/src/dev_tools/gate.d.ts +17 -0
  62. package/build/src/dev_tools/gate.js +69 -0
  63. package/build/src/dev_tools/register.d.ts +11 -0
  64. package/build/src/dev_tools/register.js +25 -0
  65. package/build/src/dev_tools/tools.d.ts +24 -0
  66. package/build/src/dev_tools/tools.js +384 -0
  67. package/build/src/doctor/agora_checks.d.ts +15 -0
  68. package/build/src/doctor/agora_checks.js +150 -0
  69. package/build/src/doctor/agora_doctor.d.ts +115 -0
  70. package/build/src/doctor/agora_doctor.js +692 -0
  71. package/build/src/doctor/checks.d.ts +7 -1
  72. package/build/src/doctor/checks.js +47 -1
  73. package/build/src/doctor/doctor_input.d.ts +27 -0
  74. package/build/src/doctor/doctor_input.js +86 -0
  75. package/build/src/doctor/index.d.ts +5 -0
  76. package/build/src/doctor/index.js +4 -0
  77. package/build/src/host/access_token_verifier.js +7 -0
  78. package/build/src/host/account_api/account_api_controller.js +8 -6
  79. package/build/src/host/account_session_sign_in.d.ts +12 -0
  80. package/build/src/host/account_session_sign_in.js +19 -0
  81. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  82. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  83. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  84. package/build/src/host/admin_api/dto.d.ts +3 -1
  85. package/build/src/host/admin_api/dto.js +2 -0
  86. package/build/src/host/admin_authorize.d.ts +8 -0
  87. package/build/src/host/admin_authorize.js +25 -0
  88. package/build/src/host/admin_clients_service.d.ts +20 -0
  89. package/build/src/host/admin_clients_service.js +65 -1
  90. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  91. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  92. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  93. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  94. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  95. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  96. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  97. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  98. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  99. package/build/src/host/admin_console_warning.d.ts +8 -0
  100. package/build/src/host/admin_console_warning.js +18 -0
  101. package/build/src/host/admin_sessions_service.js +8 -5
  102. package/build/src/host/admin_validators.d.ts +29 -0
  103. package/build/src/host/admin_validators.js +30 -0
  104. package/build/src/host/auth_host_config.d.ts +14 -2
  105. package/build/src/host/console_denied_page.d.ts +6 -0
  106. package/build/src/host/console_denied_page.js +18 -0
  107. package/build/src/host/console_sso.d.ts +24 -0
  108. package/build/src/host/console_sso.js +70 -0
  109. package/build/src/host/controllers/account_apps_controller.js +15 -11
  110. package/build/src/host/controllers/account_session_controller.js +10 -1
  111. package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
  112. package/build/src/host/controllers/console_sso_controller.js +61 -0
  113. package/build/src/host/controllers/interaction_controller.js +3 -1
  114. package/build/src/host/csrf.d.ts +3 -3
  115. package/build/src/host/csrf.js +3 -13
  116. package/build/src/host/default_mailer.d.ts +11 -6
  117. package/build/src/host/default_mailer.js +16 -12
  118. package/build/src/host/i18n.d.ts +41 -14
  119. package/build/src/host/i18n.js +55 -13
  120. package/build/src/host/rate_limit.d.ts +8 -0
  121. package/build/src/host/rate_limit.js +9 -0
  122. package/build/src/host/register_auth_host.d.ts +16 -0
  123. package/build/src/host/register_auth_host.js +230 -88
  124. package/build/src/host/renderers/edge_renderer.js +5 -0
  125. package/build/src/host/renderers/inertia_renderer.js +7 -1
  126. package/build/src/host/screen_props.d.ts +39 -8
  127. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  128. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  129. package/build/src/host/ui-dist/index.html +2 -2
  130. package/build/src/provider/build_provider.js +43 -6
  131. package/build/src/provider/oidc_service.d.ts +1 -1
  132. package/build/src/provider/oidc_service.js +1 -1
  133. package/build/src/schema/ensure.d.ts +14 -0
  134. package/build/src/schema/ensure.js +43 -150
  135. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  136. package/build/src/service_tenants/client_metadata.js +80 -0
  137. package/build/src/service_tenants/config.d.ts +44 -0
  138. package/build/src/service_tenants/config.js +44 -0
  139. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  140. package/build/src/service_tenants/provider_hooks.js +124 -0
  141. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  142. package/build/src/service_tenants/tenant_verifier.js +104 -0
  143. package/build/src/tenancy/tenant_labels.d.ts +30 -0
  144. package/build/src/tenancy/tenant_labels.js +64 -0
  145. package/build/types.d.ts +3 -1
  146. package/package.json +12 -9
  147. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  148. package/build/src/agents/poppy/client_registry.js +0 -260
  149. package/build/src/agents/poppy/config.d.ts +0 -396
  150. package/build/src/agents/poppy/config.js +0 -228
  151. package/build/src/agents/poppy/dpop.d.ts +0 -61
  152. package/build/src/agents/poppy/dpop.js +0 -158
  153. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  154. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  155. package/build/src/agents/poppy/errors.d.ts +0 -25
  156. package/build/src/agents/poppy/errors.js +0 -25
  157. package/build/src/agents/poppy/replay.d.ts +0 -25
  158. package/build/src/agents/poppy/replay.js +0 -106
  159. package/build/src/agents/poppy/resource.d.ts +0 -100
  160. package/build/src/agents/poppy/resource.js +0 -129
  161. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  162. package/build/src/agents/poppy/safe_fetch.js +0 -262
  163. package/build/src/agents/poppy/service.d.ts +0 -293
  164. package/build/src/agents/poppy/service.js +0 -1458
  165. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  166. package/build/src/agents/poppy/session_jwt.js +0 -133
  167. package/build/src/agents/poppy/store.d.ts +0 -106
  168. package/build/src/agents/poppy/store.js +0 -261
  169. package/build/src/agents/poppy/web_session.d.ts +0 -39
  170. package/build/src/agents/poppy/web_session.js +0 -122
  171. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  172. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  173. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  174. package/build/src/host/controllers/poppy_controller.js +0 -219
  175. package/build/src/host/poppy_session_key.d.ts +0 -9
  176. package/build/src/host/poppy_session_key.js +0 -9
  177. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -1 +1 @@
1
- :root{--bg0:#07070d;--bg1:#0c0c16;--bg2:#12121f;--bg3:#181828;--line:#9797c41a;--line-strong:#9797c42e;--text:#e9e9f4;--muted:#9292ac;--faint:#5b5b74;--accent:#625fff;--accent-soft:#625fff24;--accent-hover:#625fff38;--green:#3ecf8e;--green-soft:#3ecf8e1f;--red:#ff5470;--red-soft:#ff54701f;--amber:#ffb454;--amber-soft:#ffb4541f;--blue:#4cc2ff;--blue-soft:#4cc2ff1f;--sans:"Sora", system-ui, -apple-system, sans-serif;--mono:"JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;--sidebar-w:240px;--radius:10px;--topbar-h:54px}html.light{--bg0:#f7f7fb;--bg1:#fff;--bg2:#f0f0f6;--bg3:#e7e7f0;--line:#2828461a;--line-strong:#2828462e;--text:#1c1c28;--muted:#55556c;--faint:#8a8aa0;--accent-soft:#625fff1f;--accent-hover:#625fff2e;--amber-soft:#c4821e24;--green-soft:#1ea06424;--red-soft:#dc32501f;--blue-soft:#2896d21f}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%}body{font-family:var(--sans);background:var(--bg0);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:13px;overflow:hidden}body:before{content:"";pointer-events:none;z-index:0;background:radial-gradient(1100px 500px at 15% -10%,#625fff1a,#0000 60%),radial-gradient(900px 420px at 90% 0,#3ecf8e0a,#0000 55%);position:fixed;inset:0}html.light body:before{display:none}::selection{background:#625fff4d}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{border:2px solid var(--bg0);background:#26263a;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#33334d}::-webkit-scrollbar-track{background:0 0}html.light ::-webkit-scrollbar-thumb{background:#cfcfe0}html.light ::-webkit-scrollbar-thumb:hover{background:#b8b8cc}#root{z-index:1;position:relative}.app-shell{height:100vh;display:flex;overflow:hidden}.sidebar{width:var(--sidebar-w);border-right:1px solid var(--line);-webkit-backdrop-filter:blur(10px);background:#0a0a12bf;flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}html.light .sidebar{background:#ffffffd9}.main{flex-direction:column;flex:1;min-width:0;display:flex;overflow:hidden}.topbar{height:var(--topbar-h);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(8px);background:#09091099;flex-shrink:0;align-items:center;gap:12px;padding:0 20px;display:flex}html.light .topbar{background:#ffffffb3}.content{flex:1;padding:24px 24px 60px;overflow-y:auto}.brand{border-bottom:1px solid var(--line);flex-shrink:0;align-items:center;gap:10px;padding:18px 18px 14px;display:flex}.brand-logo{background:var(--accent-soft);border:1px solid #625fff4d;border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;width:30px;height:30px;display:flex;position:relative}.brand-logo svg{width:16px;height:16px;color:var(--accent)}.brand-logo .ring{border:1px solid #625fff59;border-radius:12px;animation:3s ease-out infinite ring-pulse;position:absolute;inset:-3px}@keyframes ring-pulse{0%{opacity:.9;transform:scale(.85)}70%{opacity:0;transform:scale(1.15)}to{opacity:0;transform:scale(1.15)}}.brand-text{overflow:hidden}.brand-name{letter-spacing:.03em;font-size:14px;font-weight:700}.brand-name span{color:var(--accent)}.brand-sub{color:var(--faint);letter-spacing:.08em;text-transform:uppercase;margin-top:1px;font-size:10px;font-weight:500}.nav{flex:1;padding:10px 10px 8px;overflow-y:auto}.nav-section{letter-spacing:.14em;text-transform:uppercase;color:var(--faint);padding:12px 8px 5px;font-size:9.5px;font-weight:600}.nav-item{color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none;border:1px solid #0000;border-radius:8px;align-items:center;gap:9px;padding:7px 9px;font-size:12.5px;font-weight:500;text-decoration:none;transition:background .12s,color .12s;display:flex}.nav-item:hover{color:var(--text);background:#9797c40f}.nav-item.active{background:var(--accent-soft);color:var(--text);border-color:#625fff38}.nav-item.active .nav-ico{color:var(--accent)}.nav-ico{width:15px;height:15px;color:var(--faint);flex-shrink:0;transition:color .12s}.nav-item:hover .nav-ico{color:var(--muted)}.sidebar-foot{border-top:1px solid var(--line);flex-direction:column;flex-shrink:0;gap:10px;padding:12px 14px;display:flex}.user-row{cursor:default;border-radius:8px;align-items:center;gap:9px;padding:6px 8px;display:flex}.avatar{background:var(--accent-soft);width:28px;height:28px;color:var(--accent);border:1px solid #625fff4d;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;font-size:10.5px;font-weight:600;display:flex}.user-info{flex:1;min-width:0}.user-email{color:var(--text);text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:500;overflow:hidden}.user-role{color:var(--faint);font-size:9.5px;font-family:var(--mono)}.foot-actions{gap:6px;display:flex}.topbar-title{color:var(--text);font-size:14px;font-weight:600}.topbar-spacer{flex:1}.theme-btn{border:1px solid var(--line);background:var(--bg2);width:32px;height:32px;color:var(--muted);cursor:pointer;border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;transition:all .13s;display:flex}.theme-btn:hover{background:var(--bg3);color:var(--text);border-color:var(--line-strong)}.theme-btn svg{width:14px;height:14px}.btn{background:var(--bg2);border:1px solid var(--line);height:32px;color:var(--muted);font-family:var(--sans);cursor:pointer;white-space:nowrap;border-radius:8px;align-items:center;gap:7px;padding:0 13px;font-size:12px;font-weight:500;text-decoration:none;transition:all .13s;display:inline-flex}.btn:hover{color:var(--text);border-color:var(--line-strong);background:var(--bg3)}.btn svg{flex-shrink:0;width:13px;height:13px}.btn.btn-primary{background:var(--accent);color:#fff;border-color:#0000}.btn.btn-primary:hover{background:#7472ff}.btn.btn-danger:hover{color:var(--red);background:var(--red-soft);border-color:#ff547066}.btn.btn-sm{border-radius:6px;height:26px;padding:0 10px;font-size:11px}.btn.btn-ghost{background:0 0;border-color:#0000}.btn.btn-ghost:hover{background:#9797c40f}.btn:disabled,.btn[disabled]{opacity:.45;pointer-events:none}.cards{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px;display:grid}.card{border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(#9797c40a,#9797c403);padding:16px 18px 14px;animation:.45s cubic-bezier(.2,.7,.3,1) both rise}.card:nth-child(2){animation-delay:40ms}.card:nth-child(3){animation-delay:80ms}.card:nth-child(4){animation-delay:.12s}.card:nth-child(5){animation-delay:.16s}@keyframes rise{0%{opacity:0;transform:translateY(8px)}}.card-label{letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-bottom:10px;font-size:10px;font-weight:600}.card-value{font-family:var(--mono);letter-spacing:-.02em;color:var(--text);font-size:26px;font-weight:600;line-height:1}.card-hint{color:var(--faint);font-size:10.5px;font-family:var(--mono);margin-top:8px}.card-hint b{color:var(--muted);font-weight:500}.card.c-accent .card-value{color:var(--accent)}.card.c-green .card-value{color:var(--green)}.card.c-amber .card-value{color:var(--amber)}.card.c-red .card-value{color:var(--red)}.card.c-blue .card-value{color:var(--blue)}.panel{border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(#9797c409,#9797c403);margin-bottom:16px;animation:.5s cubic-bezier(.2,.7,.3,1) both rise;overflow:hidden}.panel-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:10px;padding:12px 16px;display:flex}.panel-head h3{letter-spacing:.02em;font-size:12.5px;font-weight:600}.panel-head .meta{font-family:var(--mono);color:var(--faint);font-size:10.5px}.panel-body{padding:12px 16px 16px}.grid-2{grid-template-columns:1fr 1fr;gap:16px;display:grid}@media (width<=1100px){.grid-2{grid-template-columns:1fr}}.page-header{margin-bottom:20px}.page-title{letter-spacing:.01em;margin-bottom:4px;font-size:18px;font-weight:700}.page-sub{color:var(--faint);font-size:12px}.page-actions{align-items:center;gap:8px;display:flex}.page-header-row{justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px;display:flex}.tbl-wrap{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:12.5px}thead th{text-align:left;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);border-bottom:1px solid var(--line);white-space:nowrap;padding:8px 12px;font-size:10px;font-weight:600}tbody tr{border-bottom:1px solid var(--line);cursor:pointer;transition:background .1s}tbody tr:last-child{border-bottom:0}tbody tr:hover{background:#9797c40a}tbody td{color:var(--muted);vertical-align:middle;padding:10px 12px}tbody td b{color:var(--text);font-weight:500}tbody td.no-click{cursor:default}.badge{height:18px;font-family:var(--mono);white-space:nowrap;border-radius:99px;align-items:center;padding:0 8px;font-size:10px;font-weight:500;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--accent);border:1px solid #625fff38}.badge-green{background:var(--green-soft);color:var(--green);border:1px solid #3ecf8e38}.badge-red{background:var(--red-soft);color:var(--red);border:1px solid #ff547038}.badge-amber{background:var(--amber-soft);color:var(--amber);border:1px solid #ffb45438}.badge-muted{color:var(--muted);border:1px solid var(--line);background:#9797c412}.badge-blue{background:var(--blue-soft);color:var(--blue);border:1px solid #4cc2ff38}.input{background:var(--bg2);border:1px solid var(--line);width:100%;height:34px;color:var(--text);font-family:var(--sans);border-radius:8px;outline:none;padding:0 12px;font-size:13px;transition:border-color .15s,box-shadow .15s}.input:focus{border-color:#625fff80;box-shadow:0 0 0 3px #625fff1c}.input::placeholder{color:var(--faint)}.input-mono{font-family:var(--mono);font-size:12px}textarea.input{resize:vertical;height:auto;padding:10px 12px}.search-input{position:relative}.search-input input{padding-left:32px}.search-ico{width:14px;height:14px;color:var(--faint);pointer-events:none;position:absolute;top:50%;left:10px;transform:translateY(-50%)}.field{margin-bottom:14px}.field label{color:var(--muted);letter-spacing:.02em;margin-bottom:5px;font-size:11px;font-weight:500;display:block}.field .hint{color:var(--faint);margin-top:4px;font-size:10.5px}.checkbox-row{cursor:pointer;align-items:center;gap:9px;padding:7px 0;display:flex}.checkbox-row input[type=checkbox]{width:14px;height:14px;accent-color:var(--accent);cursor:pointer}.checkbox-row .chk-label{color:var(--muted);font-size:12.5px}.checkbox-row:hover .chk-label{color:var(--text)}.toggle-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;padding:10px 0;display:flex}.toggle-row:last-child{border-bottom:0}.toggle-info{flex:1;min-width:0}.toggle-label{color:var(--text);font-size:12.5px;font-weight:500}.toggle-desc{color:var(--faint);margin-top:2px;font-size:11px}.toggle{flex-shrink:0;width:38px;height:20px;position:relative}.toggle input{opacity:0;width:0;height:0;position:absolute}.toggle-track{background:var(--bg3);border:1px solid var(--line-strong);cursor:pointer;border-radius:99px;transition:background .2s;position:absolute;inset:0}.toggle input:checked+.toggle-track{background:var(--accent);border-color:var(--accent)}.toggle-thumb{background:var(--faint);pointer-events:none;border-radius:50%;width:14px;height:14px;transition:transform .2s,background .2s;position:absolute;top:2px;left:2px}.toggle input:checked~.toggle-thumb{background:#fff;transform:translate(18px)}.modal-overlay{z-index:100;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#0009;justify-content:center;align-items:center;padding:20px;animation:.15s fade-in;display:flex;position:fixed;inset:0}@keyframes fade-in{0%{opacity:0}}.modal{background:var(--bg1);border:1px solid var(--line-strong);border-radius:14px;flex-direction:column;width:100%;max-width:520px;max-height:90vh;animation:.2s cubic-bezier(.2,.8,.3,1) modal-in;display:flex;overflow:hidden}@keyframes modal-in{0%{opacity:0;transform:scale(.96)translateY(8px)}}.modal.modal-lg{max-width:760px}.modal-head{border-bottom:1px solid var(--line);flex-shrink:0;justify-content:space-between;align-items:center;padding:16px 20px;display:flex}.modal-head h2{font-size:14px;font-weight:600}.modal-close{width:28px;height:28px;color:var(--faint);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;transition:all .12s;display:flex}.modal-close:hover{color:var(--text);background:#9797c414}.modal-close svg{width:14px;height:14px}.modal-body{flex:1;padding:20px;overflow-y:auto}.modal-foot{border-top:1px solid var(--line);flex-shrink:0;justify-content:flex-end;gap:8px;padding:14px 20px;display:flex}.drawer-overlay{z-index:100;background:#00000080;animation:.15s fade-in;position:fixed;inset:0}.drawer{z-index:101;background:var(--bg1);border-left:1px solid var(--line-strong);flex-direction:column;width:480px;max-width:95vw;animation:.22s cubic-bezier(.2,.8,.3,1) drawer-in;display:flex;position:fixed;top:0;bottom:0;right:0;overflow:hidden}@keyframes drawer-in{0%{transform:translate(100%)}}.drawer-head{border-bottom:1px solid var(--line);flex-shrink:0;justify-content:space-between;align-items:center;padding:16px 20px;display:flex}.drawer-head h2{font-size:14px;font-weight:600}.drawer-body{flex:1;padding:20px;overflow-y:auto}.pagination{justify-content:flex-end;align-items:center;gap:6px;padding-top:16px;display:flex}.pag-info{font-family:var(--mono);color:var(--faint);margin-right:4px;font-size:11px}.empty-state{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:10px;padding:48px 24px;display:flex}.empty-state svg{width:36px;height:36px;color:var(--faint);margin-bottom:4px}.empty-state h4{color:var(--muted);font-size:14px;font-weight:600}.empty-state p{color:var(--faint);max-width:300px;font-size:12px}.spinner{border:2px solid var(--bg3);border-top-color:var(--accent);border-radius:50%;flex-shrink:0;width:20px;height:20px;animation:.7s linear infinite spin}.spinner.sm{width:14px;height:14px}.spinner.lg{border-width:3px;width:28px;height:28px}@keyframes spin{to{transform:rotate(360deg)}}.loading-row{color:var(--faint);justify-content:center;align-items:center;gap:12px;padding:40px;font-size:12px;display:flex}.error-box{background:var(--red-soft);border-radius:var(--radius);color:var(--red);border:1px solid #ff547040;padding:12px 16px;font-size:12px}.settings-section{margin-bottom:24px}.settings-section-head{border-bottom:1px solid var(--line);align-items:center;gap:10px;margin-bottom:4px;padding-bottom:10px;display:flex}.settings-section-head h3{font-size:13px;font-weight:600}.settings-section-head p{color:var(--faint);margin-top:1px;font-size:11px}.settings-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:20px;padding:12px 0;display:flex}.settings-row:last-child{border-bottom:0}.settings-info{flex:1;min-width:0}.settings-key{font-family:var(--mono);color:var(--text);font-size:12px;font-weight:500}.settings-desc{color:var(--faint);margin-top:2px;font-size:11px}.settings-badge{font-family:var(--mono);color:var(--faint);border:1px solid var(--line);background:#9797c412;border-radius:4px;margin-left:6px;padding:1px 6px;font-size:9.5px}.settings-control{flex-shrink:0;align-items:center;gap:8px;display:flex}.code{font-family:var(--mono);background:var(--bg2);border:1px solid var(--line);color:var(--muted);word-break:break-all;border-radius:6px;padding:2px 8px;font-size:11.5px}.sep{background:var(--line);height:1px;margin:16px 0}.flex{display:flex}.flex-center{align-items:center;display:flex}.gap-2{gap:8px}.gap-3{gap:12px}.ml-auto{margin-left:auto}.text-accent{color:var(--accent)}.text-muted{color:var(--muted)}.text-faint{color:var(--faint)}.text-green{color:var(--green)}.text-red{color:var(--red)}.text-amber{color:var(--amber)}.mono{font-family:var(--mono)}.text-sm{font-size:11px}.text-xs{font-size:10px}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.secret-box{background:var(--bg2);font-family:var(--mono);color:var(--accent);word-break:break-all;border:1px solid #625fff40;border-radius:8px;margin:8px 0;padding:12px 16px;font-size:12px}.secret-box small{color:var(--faint);font-size:10.5px;font-family:var(--sans);letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px;display:block}.chart-svg{width:100%;height:90px;display:block;overflow:visible}.toast-container{z-index:200;flex-direction:column-reverse;gap:8px;display:flex;position:fixed;bottom:20px;right:20px}.toast{background:var(--bg2);border:1px solid var(--line-strong);color:var(--text);border-radius:10px;align-items:center;gap:10px;min-width:260px;max-width:380px;padding:10px 14px;font-size:12.5px;animation:.22s cubic-bezier(.2,.8,.3,1) toast-in;display:flex;box-shadow:0 4px 24px #0006}.toast svg{flex-shrink:0;width:15px;height:15px}.toast.toast-success svg{color:var(--green)}.toast.toast-error svg{color:var(--red)}.toast.toast-info svg{color:var(--accent)}@keyframes toast-in{0%{opacity:0;transform:translate(16px)}}.chart-tip{background:var(--panel,var(--bg));border:1px solid var(--line);color:var(--text);border-radius:8px;padding:7px 10px;font-size:11.5px;box-shadow:0 6px 24px #00000040}.chart-tip-label{color:var(--faint);margin-bottom:4px;font-size:10px}.chart-tip-row{align-items:center;gap:6px;display:flex}.chart-tip-row b{font-weight:600}.chart-tip-dot{border-radius:2px;flex:none;width:7px;height:7px}
1
+ :root{--bg0:#07070d;--bg1:#0c0c16;--bg2:#12121f;--bg3:#181828;--line:#9797c41a;--line-strong:#9797c42e;--text:#e9e9f4;--muted:#9292ac;--faint:#5b5b74;--accent:#625fff;--accent-soft:#625fff24;--accent-hover:#625fff38;--green:#3ecf8e;--green-soft:#3ecf8e1f;--red:#ff5470;--red-soft:#ff54701f;--amber:#ffb454;--amber-soft:#ffb4541f;--blue:#4cc2ff;--blue-soft:#4cc2ff1f;--sans:"Sora", system-ui, -apple-system, sans-serif;--mono:"JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;--sidebar-w:240px;--radius:10px;--topbar-h:54px}html.light{--bg0:#f7f7fb;--bg1:#fff;--bg2:#f0f0f6;--bg3:#e7e7f0;--line:#2828461a;--line-strong:#2828462e;--text:#1c1c28;--muted:#55556c;--faint:#8a8aa0;--accent-soft:#625fff1f;--accent-hover:#625fff2e;--amber-soft:#c4821e24;--green-soft:#1ea06424;--red-soft:#dc32501f;--blue-soft:#2896d21f}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%}body{font-family:var(--sans);background:var(--bg0);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:13px;overflow:hidden}body:before{content:"";pointer-events:none;z-index:0;background:radial-gradient(1100px 500px at 15% -10%,#625fff1a,#0000 60%),radial-gradient(900px 420px at 90% 0,#3ecf8e0a,#0000 55%);position:fixed;inset:0}html.light body:before{display:none}::selection{background:#625fff4d}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{border:2px solid var(--bg0);background:#26263a;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#33334d}::-webkit-scrollbar-track{background:0 0}html.light ::-webkit-scrollbar-thumb{background:#cfcfe0}html.light ::-webkit-scrollbar-thumb:hover{background:#b8b8cc}#root{z-index:1;position:relative}.app-shell{height:100vh;display:flex;overflow:hidden}.sidebar{width:var(--sidebar-w);border-right:1px solid var(--line);-webkit-backdrop-filter:blur(10px);background:#0a0a12bf;flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}html.light .sidebar{background:#ffffffd9}.main{flex-direction:column;flex:1;min-width:0;display:flex;overflow:hidden}.topbar{height:var(--topbar-h);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(8px);background:#09091099;flex-shrink:0;align-items:center;gap:12px;padding:0 20px;display:flex}html.light .topbar{background:#ffffffb3}.content{flex:1;padding:24px 24px 60px;overflow-y:auto}.brand{border-bottom:1px solid var(--line);flex-shrink:0;align-items:center;gap:10px;padding:18px 18px 14px;display:flex}.brand-logo{background:var(--accent-soft);border:1px solid #625fff4d;border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;width:30px;height:30px;display:flex;position:relative}.brand-logo svg{width:16px;height:16px;color:var(--accent)}.brand-logo .ring{border:1px solid #625fff59;border-radius:12px;animation:3s ease-out infinite ring-pulse;position:absolute;inset:-3px}@keyframes ring-pulse{0%{opacity:.9;transform:scale(.85)}70%{opacity:0;transform:scale(1.15)}to{opacity:0;transform:scale(1.15)}}.brand-text{overflow:hidden}.brand-name{letter-spacing:.03em;font-size:14px;font-weight:700}.brand-name span{color:var(--accent)}.brand-sub{color:var(--faint);letter-spacing:.08em;text-transform:uppercase;margin-top:1px;font-size:10px;font-weight:500}.nav{flex:1;padding:10px 10px 8px;overflow-y:auto}.nav-section{letter-spacing:.14em;text-transform:uppercase;color:var(--faint);padding:12px 8px 5px;font-size:9.5px;font-weight:600}.nav-item{color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none;border:1px solid #0000;border-radius:8px;align-items:center;gap:9px;padding:7px 9px;font-size:12.5px;font-weight:500;text-decoration:none;transition:background .12s,color .12s;display:flex}.nav-item:hover{color:var(--text);background:#9797c40f}.nav-item.active{background:var(--accent-soft);color:var(--text);border-color:#625fff38}.nav-item.active .nav-ico{color:var(--accent)}.nav-ico{width:15px;height:15px;color:var(--faint);flex-shrink:0;transition:color .12s}.nav-item:hover .nav-ico{color:var(--muted)}.sidebar-foot{border-top:1px solid var(--line);flex-direction:column;flex-shrink:0;gap:10px;padding:12px 14px;display:flex}.user-row{cursor:default;border-radius:8px;align-items:center;gap:9px;padding:6px 8px;display:flex}.avatar{background:var(--accent-soft);width:28px;height:28px;color:var(--accent);border:1px solid #625fff4d;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;font-size:10.5px;font-weight:600;display:flex}.user-info{flex:1;min-width:0}.user-email{color:var(--text);text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:500;overflow:hidden}.user-role{color:var(--faint);font-size:9.5px;font-family:var(--mono)}.foot-actions{gap:6px;display:flex}.topbar-title{color:var(--text);font-size:14px;font-weight:600}.topbar-spacer{flex:1}.theme-btn{border:1px solid var(--line);background:var(--bg2);width:32px;height:32px;color:var(--muted);cursor:pointer;border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;transition:all .13s;display:flex}.theme-btn:hover{background:var(--bg3);color:var(--text);border-color:var(--line-strong)}.theme-btn svg{width:14px;height:14px}.btn{background:var(--bg2);border:1px solid var(--line);height:32px;color:var(--muted);font-family:var(--sans);cursor:pointer;white-space:nowrap;border-radius:8px;align-items:center;gap:7px;padding:0 13px;font-size:12px;font-weight:500;text-decoration:none;transition:all .13s;display:inline-flex}.btn:hover{color:var(--text);border-color:var(--line-strong);background:var(--bg3)}.btn svg{flex-shrink:0;width:13px;height:13px}.btn.btn-primary{background:var(--accent);color:#fff;border-color:#0000}.btn.btn-primary:hover{background:#7472ff}.btn.btn-danger:hover{color:var(--red);background:var(--red-soft);border-color:#ff547066}.btn.btn-sm{border-radius:6px;height:26px;padding:0 10px;font-size:11px}.btn.btn-ghost{background:0 0;border-color:#0000}.btn.btn-ghost:hover{background:#9797c40f}.btn:disabled,.btn[disabled]{opacity:.45;pointer-events:none}.cards{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px;display:grid}.card{border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(#9797c40a,#9797c403);padding:16px 18px 14px;animation:.45s cubic-bezier(.2,.7,.3,1) both rise}.card:nth-child(2){animation-delay:40ms}.card:nth-child(3){animation-delay:80ms}.card:nth-child(4){animation-delay:.12s}.card:nth-child(5){animation-delay:.16s}@keyframes rise{0%{opacity:0;transform:translateY(8px)}}.card-label{letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-bottom:10px;font-size:10px;font-weight:600}.card-value{font-family:var(--mono);letter-spacing:-.02em;color:var(--text);font-size:26px;font-weight:600;line-height:1}.card-hint{color:var(--faint);font-size:10.5px;font-family:var(--mono);margin-top:8px}.card-hint b{color:var(--muted);font-weight:500}.card.c-accent .card-value{color:var(--accent)}.card.c-green .card-value{color:var(--green)}.card.c-amber .card-value{color:var(--amber)}.card.c-red .card-value{color:var(--red)}.card.c-blue .card-value{color:var(--blue)}.panel{border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(#9797c409,#9797c403);margin-bottom:16px;animation:.5s cubic-bezier(.2,.7,.3,1) both rise;overflow:hidden}.panel-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:10px;padding:12px 16px;display:flex}.panel-head h3{letter-spacing:.02em;font-size:12.5px;font-weight:600}.panel-head .meta{font-family:var(--mono);color:var(--faint);font-size:10.5px}.panel-body{padding:12px 16px 16px}.grid-2{grid-template-columns:1fr 1fr;gap:16px;display:grid}@media (width<=1100px){.grid-2{grid-template-columns:1fr}}.page-header{margin-bottom:20px}.page-title{letter-spacing:.01em;margin-bottom:4px;font-size:18px;font-weight:700}.page-sub{color:var(--faint);font-size:12px}.page-actions{align-items:center;gap:8px;display:flex}.page-header-row{justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px;display:flex}.tbl-wrap{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:12.5px}thead th{text-align:left;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);border-bottom:1px solid var(--line);white-space:nowrap;padding:8px 12px;font-size:10px;font-weight:600}tbody tr{border-bottom:1px solid var(--line);cursor:pointer;transition:background .1s}tbody tr:last-child{border-bottom:0}tbody tr:hover{background:#9797c40a}tbody td{color:var(--muted);vertical-align:middle;padding:10px 12px}tbody td b{color:var(--text);font-weight:500}tbody td.no-click{cursor:default}.badge{height:18px;font-family:var(--mono);white-space:nowrap;border-radius:99px;align-items:center;padding:0 8px;font-size:10px;font-weight:500;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--accent);border:1px solid #625fff38}.badge-green{background:var(--green-soft);color:var(--green);border:1px solid #3ecf8e38}.badge-red{background:var(--red-soft);color:var(--red);border:1px solid #ff547038}.badge-amber{background:var(--amber-soft);color:var(--amber);border:1px solid #ffb45438}.badge-muted{color:var(--muted);border:1px solid var(--line);background:#9797c412}.badge-blue{background:var(--blue-soft);color:var(--blue);border:1px solid #4cc2ff38}.input{background:var(--bg2);border:1px solid var(--line);width:100%;height:34px;color:var(--text);font-family:var(--sans);border-radius:8px;outline:none;padding:0 12px;font-size:13px;transition:border-color .15s,box-shadow .15s}.input:focus{border-color:#625fff80;box-shadow:0 0 0 3px #625fff1c}.input::placeholder{color:var(--faint)}.input-mono{font-family:var(--mono);font-size:12px}textarea.input{resize:vertical;height:auto;padding:10px 12px}.search-input{position:relative}.search-input input{padding-left:32px}.search-ico{width:14px;height:14px;color:var(--faint);pointer-events:none;position:absolute;top:50%;left:10px;transform:translateY(-50%)}.field{margin-bottom:14px}.field label{color:var(--muted);letter-spacing:.02em;margin-bottom:5px;font-size:11px;font-weight:500;display:block}.field .hint{color:var(--faint);margin-top:4px;font-size:10.5px}.checkbox-row{cursor:pointer;align-items:center;gap:9px;padding:7px 0;display:flex}.checkbox-row input[type=checkbox]{width:14px;height:14px;accent-color:var(--accent);cursor:pointer}.checkbox-row .chk-label{color:var(--muted);font-size:12.5px}.checkbox-row:hover .chk-label{color:var(--text)}.toggle-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;padding:10px 0;display:flex}.toggle-row:last-child{border-bottom:0}.toggle-info{flex:1;min-width:0}.toggle-label{color:var(--text);font-size:12.5px;font-weight:500}.toggle-desc{color:var(--faint);margin-top:2px;font-size:11px}.toggle{flex-shrink:0;width:38px;height:20px;position:relative}.toggle input{opacity:0;width:0;height:0;position:absolute}.toggle-track{background:var(--bg3);border:1px solid var(--line-strong);cursor:pointer;border-radius:99px;transition:background .2s;position:absolute;inset:0}.toggle input:checked+.toggle-track{background:var(--accent);border-color:var(--accent)}.toggle-thumb{background:var(--faint);pointer-events:none;border-radius:50%;width:14px;height:14px;transition:transform .2s,background .2s;position:absolute;top:2px;left:2px}.toggle input:checked~.toggle-thumb{background:#fff;transform:translate(18px)}.modal-overlay{z-index:100;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#0009;justify-content:center;align-items:center;padding:20px;animation:.15s fade-in;display:flex;position:fixed;inset:0}@keyframes fade-in{0%{opacity:0}}.modal{background:var(--bg1);border:1px solid var(--line-strong);border-radius:14px;flex-direction:column;width:100%;max-width:520px;max-height:90vh;animation:.2s cubic-bezier(.2,.8,.3,1) modal-in;display:flex;overflow:hidden}@keyframes modal-in{0%{opacity:0;transform:scale(.96)translateY(8px)}}.modal.modal-lg{max-width:760px}.modal-head{border-bottom:1px solid var(--line);flex-shrink:0;justify-content:space-between;align-items:center;padding:16px 20px;display:flex}.modal-head h2{font-size:14px;font-weight:600}.modal-close{width:28px;height:28px;color:var(--faint);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;transition:all .12s;display:flex}.modal-close:hover{color:var(--text);background:#9797c414}.modal-close svg{width:14px;height:14px}.modal-body{flex:1;padding:20px;overflow-y:auto}.modal-foot{border-top:1px solid var(--line);flex-shrink:0;justify-content:flex-end;gap:8px;padding:14px 20px;display:flex}.drawer-overlay{z-index:100;background:#00000080;animation:.15s fade-in;position:fixed;inset:0}.drawer{z-index:101;background:var(--bg1);border-left:1px solid var(--line-strong);flex-direction:column;width:480px;max-width:95vw;animation:.22s cubic-bezier(.2,.8,.3,1) drawer-in;display:flex;position:fixed;top:0;bottom:0;right:0;overflow:hidden}@keyframes drawer-in{0%{transform:translate(100%)}}.drawer-head{border-bottom:1px solid var(--line);flex-shrink:0;justify-content:space-between;align-items:center;padding:16px 20px;display:flex}.drawer-head h2{font-size:14px;font-weight:600}.drawer-body{flex:1;padding:20px;overflow-y:auto}.pagination{justify-content:flex-end;align-items:center;gap:6px;padding-top:16px;display:flex}.pag-info{font-family:var(--mono);color:var(--faint);margin-right:4px;font-size:11px}.empty-state{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:10px;padding:48px 24px;display:flex}.empty-state svg{width:36px;height:36px;color:var(--faint);margin-bottom:4px}.empty-state h4{color:var(--muted);font-size:14px;font-weight:600}.empty-state p{color:var(--faint);max-width:300px;font-size:12px}.spinner{border:2px solid var(--bg3);border-top-color:var(--accent);border-radius:50%;flex-shrink:0;width:20px;height:20px;animation:.7s linear infinite spin}.spinner.sm{width:14px;height:14px}.spinner.lg{border-width:3px;width:28px;height:28px}@keyframes spin{to{transform:rotate(360deg)}}.loading-row{color:var(--faint);justify-content:center;align-items:center;gap:12px;padding:40px;font-size:12px;display:flex}.error-box{background:var(--red-soft);border-radius:var(--radius);color:var(--red);border:1px solid #ff547040;padding:12px 16px;font-size:12px}.settings-section{margin-bottom:24px}.settings-section-head{border-bottom:1px solid var(--line);align-items:center;gap:10px;margin-bottom:4px;padding-bottom:10px;display:flex}.settings-section-head h3{font-size:13px;font-weight:600}.settings-section-head p{color:var(--faint);margin-top:1px;font-size:11px}.settings-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:20px;padding:12px 0;display:flex}.settings-row:last-child{border-bottom:0}.settings-info{flex:1;min-width:0}.settings-key{font-family:var(--mono);color:var(--text);font-size:12px;font-weight:500}.settings-desc{color:var(--faint);margin-top:2px;font-size:11px}.settings-badge{font-family:var(--mono);color:var(--faint);border:1px solid var(--line);background:#9797c412;border-radius:4px;margin-left:6px;padding:1px 6px;font-size:9.5px}.settings-control{flex-shrink:0;align-items:center;gap:8px;display:flex}.code{font-family:var(--mono);background:var(--bg2);border:1px solid var(--line);color:var(--muted);word-break:break-all;border-radius:6px;padding:2px 8px;font-size:11.5px}.sep{background:var(--line);height:1px;margin:16px 0}.flex{display:flex}.flex-center{align-items:center;display:flex}.gap-2{gap:8px}.gap-3{gap:12px}.ml-auto{margin-left:auto}.text-accent{color:var(--accent)}.text-muted{color:var(--muted)}.text-faint{color:var(--faint)}.text-green{color:var(--green)}.text-red{color:var(--red)}.text-amber{color:var(--amber)}.mono{font-family:var(--mono)}.text-sm{font-size:11px}.text-xs{font-size:10px}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.secret-box{background:var(--bg2);font-family:var(--mono);color:var(--accent);word-break:break-all;border:1px solid #625fff40;border-radius:8px;margin:8px 0;padding:12px 16px;font-size:12px}.secret-box small{color:var(--faint);font-size:10.5px;font-family:var(--sans);letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px;display:block}.chart-svg{width:100%;height:90px;display:block;overflow:visible}.toast-container{z-index:200;flex-direction:column-reverse;gap:8px;display:flex;position:fixed;bottom:20px;right:20px}.toast{background:var(--bg2);border:1px solid var(--line-strong);color:var(--text);border-radius:10px;align-items:center;gap:10px;min-width:260px;max-width:380px;padding:10px 14px;font-size:12.5px;animation:.22s cubic-bezier(.2,.8,.3,1) toast-in;display:flex;box-shadow:0 4px 24px #0006}.toast svg{flex-shrink:0;width:15px;height:15px}.toast.toast-success svg{color:var(--green)}.toast.toast-error svg{color:var(--red)}.toast.toast-info svg{color:var(--accent)}@keyframes toast-in{0%{opacity:0;transform:translate(16px)}}.chart-tip{background:var(--panel,var(--bg));border:1px solid var(--line);color:var(--text);border-radius:8px;padding:7px 10px;font-size:11.5px;box-shadow:0 6px 24px #00000040}.chart-tip-label{color:var(--faint);margin-bottom:4px;font-size:10px}.chart-tip-row{align-items:center;gap:6px;display:flex}.chart-tip-row b{font-weight:600}.chart-tip-dot{border-radius:2px;flex:none;width:7px;height:7px}.brand.switcher{padding:10px 10px 8px;position:relative}.switcher-button{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:8px;align-items:center;gap:10px;padding:8px;display:flex}.switcher-button:hover,.switcher-button[aria-expanded=true]{background:var(--bg2);border-color:var(--line)}.switcher-button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.switcher-chevron{color:var(--faint);flex-shrink:0;margin-left:auto}.switcher-menu{z-index:50;background:var(--bg1);border:1px solid var(--line-strong);border-radius:8px;padding:4px;position:absolute;top:calc(100% - 2px);left:10px;right:10px}.switcher-item{color:var(--muted);border-radius:6px;grid-template-columns:16px 1fr auto;align-items:center;column-gap:10px;padding:7px 8px;font-size:12.5px;text-decoration:none;display:grid}.switcher-item .switcher-package{font-family:var(--mono);color:var(--faint);grid-column:2/4;font-size:10px}.switcher-item.current{color:var(--text)}.switcher-check{color:var(--accent)}a.switcher-item:hover,a.switcher-item:focus-visible{background:var(--accent-soft);color:var(--text);outline:none}.switcher-title{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.search-trigger{align-items:center;gap:6px;display:inline-flex}.search-trigger kbd{font-family:var(--mono);color:var(--faint);border:1px solid var(--line);border-radius:4px;padding:0 4px;font-size:10px}.search-overlay{align-items:flex-start;padding-top:12vh}.search-dialog{background:var(--bg1);border:1px solid var(--line-strong);border-radius:var(--radius);width:min(560px,100%);overflow:hidden}.search-input{border:0;border-bottom:1px solid var(--line);width:100%;color:var(--text);font:inherit;background:0 0;outline:none;padding:14px 16px;font-size:14px}.search-results{max-height:52vh;padding:6px;overflow-y:auto}.search-empty{color:var(--faint);padding:14px 10px;font-size:12.5px}.search-group+.search-group{margin-top:6px}.search-group-title{letter-spacing:.1em;text-transform:uppercase;color:var(--faint);align-items:center;gap:6px;padding:6px 10px 4px;font-size:10px;font-weight:600;display:flex}.search-hit{color:var(--text);border-radius:6px;flex-direction:column;gap:1px;padding:7px 10px;text-decoration:none;display:flex}.search-hit.active{background:var(--accent-soft)}.search-hit-title{font-size:13px}.search-hit-sub{color:var(--muted);font-size:11px}.consoles-panel{margin-top:16px}.console-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;display:grid}.console-card{border:1px solid var(--line);color:var(--text);border-radius:8px;align-items:center;gap:12px;padding:12px;text-decoration:none;display:flex}.console-card:hover,.console-card:focus-visible{border-color:var(--line-strong);background:var(--bg2);outline:none}.console-card-icon{background:var(--accent-soft);width:36px;height:36px;color:var(--accent);border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.console-card-text{flex-direction:column;min-width:0;display:flex}.console-card-title{font-size:13px;font-weight:600}.console-card-path{font-family:var(--mono);color:var(--faint);font-size:10.5px}@media (width<=640px){.switcher-menu{border-radius:0;position:fixed;top:64px;left:0;right:0}}
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
11
- <script type="module" crossorigin src="/__AUTHKIT_BASE__/assets/index-D0EIeyKK.js"></script>
12
- <link rel="stylesheet" crossorigin href="/__AUTHKIT_BASE__/assets/index-DTSmD4RU.css">
11
+ <script type="module" crossorigin src="/__AUTHKIT_BASE__/assets/index-Dq3dJVBv.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/__AUTHKIT_BASE__/assets/index-WNIyCEPA.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -1,7 +1,10 @@
1
1
  import * as oidc from 'oidc-provider';
2
2
  import { pickModelAdapterClass } from '../adapters/factory.js';
3
3
  import { normalizeActiveOrg, readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
4
+ import { consoleSsoClientMetadata } from '../host/console_sso.js';
4
5
  import { findMcpResource, mcpAuthorizeMiddleware } from '../mcp/mcp_oauth.js';
6
+ import { hideDisabledClients } from '../service_tenants/client_metadata.js';
7
+ import { createServiceTenantHooks } from '../service_tenants/provider_hooks.js';
5
8
  import { createDeviceSources } from './device_sources.js';
6
9
  import { createLogoutSources } from './logout_sources.js';
7
10
  import { registrationPolicyMiddleware } from './registration_policy.js';
@@ -78,21 +81,35 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
78
81
  return key ? { key, rc: at.resources[key] } : null;
79
82
  };
80
83
  const mcp = config.mcp;
81
- const resourceIndicatorFeatures = at.anyJwt || declaredResources.length > 0 || mcp.enabled
84
+ // Service tenants (`serviceTenants`): `client_credentials` clients with a partition,
85
+ // whose tokens are JWTs for a service-tenant resource. See `service_tenants/`.
86
+ const serviceTenants = createServiceTenantHooks({
87
+ serviceTenants: config.serviceTenants ?? { enabled: false, resources: {} },
88
+ AdapterClass: config.AdapterClass,
89
+ audit: config.audit,
90
+ });
91
+ const signAlg = config.jwks.keys[0]?.alg ?? 'RS256';
92
+ const resourceIndicatorFeatures = at.anyJwt || declaredResources.length > 0 || mcp.enabled || serviceTenants.enabled
82
93
  ? {
83
94
  resourceIndicators: {
84
95
  enabled: true,
85
- defaultResource: async (_ctx, _client, oneOf) => {
96
+ defaultResource: async (ctx, client, oneOf) => {
86
97
  // Nas trocas (code/refresh/device), o provider passa `oneOf` com as
87
98
  // resources já concedidas — devolvemos para não falhar a request.
88
99
  if (oneOf)
89
100
  return oneOf;
101
+ const serviceResource = await serviceTenants.defaultResource(ctx, client);
102
+ if (serviceResource)
103
+ return serviceResource;
90
104
  // Authorize/sem resource explícito: no modo JWT, liga ao resource
91
105
  // default (modo simples). Só opaco: sem resource (AT de sempre).
92
106
  return at.anyJwt ? at.audience : undefined;
93
107
  },
94
108
  useGrantedResource: async () => true,
95
- getResourceServerInfo: (_ctx, resourceIndicator, _client) => {
109
+ getResourceServerInfo: async (ctx, resourceIndicator, client) => {
110
+ const serviceInfo = await serviceTenants.resourceServerInfo(ctx, resourceIndicator, client, signAlg);
111
+ if (serviceInfo)
112
+ return serviceInfo;
96
113
  const found = findResource(resourceIndicator);
97
114
  const isDefault = at.anyJwt && resourceIndicator === at.audience;
98
115
  // Servidores MCP (`mcp`): os declarados e os registrados em runtime.
@@ -121,7 +138,7 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
121
138
  info.accessTokenTTL = ttl;
122
139
  if (format === 'jwt') {
123
140
  // Assina com a chave de assinatura corrente do keystore (mesma do JWKS).
124
- info.jwt = { sign: { alg: config.jwks.keys[0]?.alg ?? 'RS256' } };
141
+ info.jwt = { sign: { alg: signAlg } };
125
142
  }
126
143
  return info;
127
144
  },
@@ -129,16 +146,31 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
129
146
  }
130
147
  : {};
131
148
  const provider = new oidc.Provider(config.issuer, {
149
+ // Client interno do login do console pelo IdP (ver `host/console_sso.ts`).
150
+ // Estático: não passa pelo adapter, então não aparece no console de clients.
151
+ ...(config.accountSession?.acceptIdpSession
152
+ ? { clients: [consoleSsoClientMetadata(config.issuer)] }
153
+ : {}),
132
154
  // O ator (`act`, RFC 8693) dos access tokens de impersonation — ver
133
155
  // `impersonationExtraClaims` em `token_exchange.ts`. Sem isto o token trocado
134
156
  // não se distingue de um token do próprio alvo no resource server.
135
- extraTokenClaims: async (_ctx, token) => impersonationExtraClaims(token),
157
+ // Service tenants add `partition`/`capabilities` (see `service_tenants/`).
158
+ extraTokenClaims: async (ctx, token) => {
159
+ const actor = impersonationExtraClaims(token);
160
+ const tenant = await serviceTenants.extraClaims(ctx, token);
161
+ return actor || tenant ? { ...tenant, ...actor } : undefined;
162
+ },
136
163
  // Dispatcher por modelo (suportado pelo oidc-provider: `Adapter` aceita
137
164
  // função `(name) => adapter` além de classe). Session-scoped vai pro
138
165
  // `SessionAdapterClass`, o resto pro `AdapterClass` — mesma regra de
139
166
  // `pickModelAdapterClass`, usada também pelos serviços que instanciam
140
167
  // adapter na mão. Sem `session.adapter`, as duas classes são a mesma.
141
- adapter: ((name) => new (pickModelAdapterClass(name, config.AdapterClass, config.SessionAdapterClass))(name)),
168
+ // A disabled client (`authkit_disabled`) is "not found" for the provider: every
169
+ // endpoint answers `invalid_client` on the next request.
170
+ adapter: ((name) => {
171
+ const instance = new (pickModelAdapterClass(name, config.AdapterClass, config.SessionAdapterClass))(name);
172
+ return name === 'Client' ? hideDisabledClients(instance) : instance;
173
+ }),
142
174
  findAccount: options.findAccount,
143
175
  // Grant REAPROVEITADO (consent já lembrado): o org capturado no primeiro
144
176
  // consent ficaria VELHO se o usuário trocasse de org no console e voltasse a
@@ -238,6 +270,7 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
238
270
  backchannelLogout: { enabled: true },
239
271
  revocation: { enabled: true },
240
272
  introspection: { enabled: true },
273
+ ...(serviceTenants.enabled ? { clientCredentials: { enabled: true } } : {}),
241
274
  ...registrationFeatures,
242
275
  ...deviceFlowFeatures,
243
276
  ...dpopFeatures,
@@ -273,6 +306,10 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
273
306
  ? sessionTtlHolder.transientSec
274
307
  : sessionTtlHolder.rememberSec
275
308
  : config.ttl.session,
309
+ // `client_credentials` tokens: the resource's lifetime (a service-tenant resource
310
+ // always sets one), else the access-token TTL.
311
+ ClientCredentials: (_ctx, token) => token?.resourceServer?.accessTokenTTL ??
312
+ (tokenTtlHolder ? tokenTtlHolder.accessTokenSec : config.ttl.accessToken),
276
313
  Interaction: 3600,
277
314
  Grant: tokenTtlHolder
278
315
  ? () => tokenTtlHolder.refreshTokenSec
@@ -28,7 +28,7 @@ export declare class OidcService {
28
28
  };
29
29
  /**
30
30
  * @internal Segredo derivado da app key para um uso (`label`) — HMAC dos nonces
31
- * DPoP do Poppy, por exemplo. Igual em todas as instâncias, sem expor a chave.
31
+ * DPoP de uma extension de personal agents, por exemplo. Igual em todas as instâncias, sem expor a chave.
32
32
  */
33
33
  deriveSecret(label: string): Buffer;
34
34
  /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
@@ -44,7 +44,7 @@ export class OidcService {
44
44
  }
45
45
  /**
46
46
  * @internal Segredo derivado da app key para um uso (`label`) — HMAC dos nonces
47
- * DPoP do Poppy, por exemplo. Igual em todas as instâncias, sem expor a chave.
47
+ * DPoP de uma extension de personal agents, por exemplo. Igual em todas as instâncias, sem expor a chave.
48
48
  */
49
49
  deriveSecret(label) {
50
50
  return createHmac('sha256', this.#appKey).update(`authkit:${label}`).digest();
@@ -23,9 +23,15 @@
23
23
  * }
24
24
  * ```
25
25
  */
26
+ import type { AuthkitTable } from '../agents/extension.js';
26
27
  export interface EnsureSchemaOptions {
27
28
  /** Conexão Lucid a usar. Default: conexão primária. */
28
29
  connection?: string;
30
+ /**
31
+ * Tabelas de `personalAgents.extensions` (`tables` de cada uma), garantidas junto. O provider
32
+ * passa as do config no boot.
33
+ */
34
+ tables?: readonly AuthkitTable[];
29
35
  /**
30
36
  * Tabela da conta, onde a coluna opcional `login_methods` pertence. Vem do
31
37
  * metadado `accountStore.accountTable` (o provider repassa no boot). Undefined
@@ -51,6 +57,14 @@ export interface EnsureSchemaReport {
51
57
  ensured: boolean;
52
58
  };
53
59
  }
60
+ /**
61
+ * Garante tabelas lib-owned no formato {@link AuthkitTable} — o que `ensureAuthkitSchema` faz com as
62
+ * do authkit: cria as que faltam, adiciona colunas e índices novos, nunca dropa nem altera. Para
63
+ * uma extension expor o próprio `ensure*Schema()` para migrations do host. Estável no 1.x.
64
+ */
65
+ export declare function ensureAuthkitTables(db: any, tables: readonly AuthkitTable[], options?: {
66
+ connection?: string;
67
+ }): Promise<Pick<EnsureSchemaReport, 'created' | 'altered' | 'indexed'>>;
54
68
  /**
55
69
  * Garante que as tabelas do authkit existem e têm todas as colunas que esta
56
70
  * versão da lib espera. Aditivo e idempotente — seguro de rodar em todo boot
@@ -309,7 +309,7 @@ const TABLES = [
309
309
  name: 'auth_agent_registry',
310
310
  /**
311
311
  * Agentes liberados (`allowed`) ou bloqueados (`blocked`) pelo console admin,
312
- * somados ao `defineConfig` — PACT pelo `iss`, Poppy pelo `client_id`.
312
+ * somados ao `defineConfig` — PACT pelo `iss`; uma extension pelo identificador dela.
313
313
  */
314
314
  create: (t) => {
315
315
  t.string('id').primary();
@@ -333,135 +333,6 @@ const TABLES = [
333
333
  updated_at: (t) => t.dateTime('updated_at', { useTz: true, precision: 3 }).nullable(),
334
334
  },
335
335
  },
336
- /*
337
- * Personal Agent Protocol (Poppy). Os refresh tokens são grants de
338
- * `auth_agent_grants` (prefixo `pgrant_`) com o hash em
339
- * `auth_agent_refresh_tokens`; aqui ficam Sessions, access tokens, pedidos de
340
- * sign-in e os `jti` vistos. Mesmas regras de data das tabelas acima.
341
- */
342
- {
343
- name: 'auth_poppy_sessions',
344
- /**
345
- * Uma Session: um usuário do agente (`user_id`, opaco) num agente (`client_id`), com o
346
- * `agent_id` opcional. `account_id` é a conta que já usou. `grant_id` e `scope` ficaram da
347
- * Draft 0.1, sem uso (o schema só é aditivo).
348
- */
349
- create: (t) => {
350
- t.string('id').primary();
351
- t.string('client_id', 2048).notNullable();
352
- t.string('user_id').notNullable();
353
- t.string('agent_id').nullable();
354
- t.string('account_id').nullable().index();
355
- t.string('grant_id').nullable().index();
356
- t.text('scope').notNullable();
357
- t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable();
358
- t.dateTime('ended_at', { useTz: true, precision: 3 }).nullable();
359
- t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
360
- t.dateTime('updated_at', { useTz: true, precision: 3 }).notNullable();
361
- },
362
- columns: {
363
- client_id: (t) => t.string('client_id', 2048),
364
- user_id: (t) => t.string('user_id'),
365
- agent_id: (t) => t.string('agent_id').nullable(),
366
- account_id: (t) => t.string('account_id').nullable().index(),
367
- grant_id: (t) => t.string('grant_id').nullable().index(),
368
- scope: (t) => t.text('scope'),
369
- expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
370
- ended_at: (t) => t.dateTime('ended_at', { useTz: true, precision: 3 }).nullable(),
371
- created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
372
- updated_at: (t) => t.dateTime('updated_at', { useTz: true, precision: 3 }).nullable(),
373
- },
374
- },
375
- {
376
- name: 'auth_poppy_tokens',
377
- /**
378
- * Access tokens — só o hash. `jkt` = chave DPoP; `null` = Bearer. `session_id` e `user_id`
379
- * ficaram da Draft 0.1 e saem vazios: o sign-in não conhece a Session.
380
- */
381
- create: (t) => {
382
- t.string('token_hash', 64).primary();
383
- t.string('session_id').notNullable().index();
384
- t.string('client_id', 2048).notNullable();
385
- t.string('user_id').notNullable();
386
- t.string('account_id').nullable();
387
- t.string('grant_id').nullable().index();
388
- t.text('scope').notNullable();
389
- t.string('resource', 2048).nullable();
390
- t.string('jkt', 64).nullable();
391
- t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
392
- t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
393
- },
394
- columns: {
395
- session_id: (t) => t.string('session_id').index(),
396
- client_id: (t) => t.string('client_id', 2048),
397
- user_id: (t) => t.string('user_id'),
398
- account_id: (t) => t.string('account_id').nullable(),
399
- grant_id: (t) => t.string('grant_id').nullable().index(),
400
- scope: (t) => t.text('scope'),
401
- resource: (t) => t.string('resource', 2048).nullable(),
402
- jkt: (t) => t.string('jkt', 64).nullable(),
403
- expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
404
- created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
405
- },
406
- },
407
- {
408
- name: 'auth_poppy_requests',
409
- /**
410
- * Pedidos: `authorize` (Direct — vira código após o consentimento), `device` (RFC 8628),
411
- * `mediated` (aguardando o código de uso único) e `browser` (entrada de um navegador, §6).
412
- * Códigos só como hash. Linhas expiradas são apagadas no próximo pedido.
413
- */
414
- create: (t) => {
415
- t.string('id').primary();
416
- t.string('kind', 16).notNullable();
417
- t.string('code_hash', 64).nullable().unique();
418
- t.string('user_code', 16).nullable().unique();
419
- t.string('client_id', 2048).notNullable();
420
- t.string('session_id').nullable();
421
- t.string('account_id').nullable();
422
- t.text('requested_scope').notNullable();
423
- t.text('granted_scope').nullable();
424
- t.string('redirect_uri', 2048).nullable();
425
- t.string('code_challenge', 128).nullable();
426
- t.string('status', 16).notNullable();
427
- t.integer('attempts').notNullable().defaultTo(0);
428
- t.integer('interval_seconds').nullable();
429
- t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable();
430
- t.text('data').nullable();
431
- t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
432
- t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
433
- },
434
- columns: {
435
- kind: (t) => t.string('kind', 16),
436
- code_hash: (t) => t.string('code_hash', 64).nullable(),
437
- user_code: (t) => t.string('user_code', 16).nullable(),
438
- client_id: (t) => t.string('client_id', 2048),
439
- session_id: (t) => t.string('session_id').nullable(),
440
- account_id: (t) => t.string('account_id').nullable(),
441
- requested_scope: (t) => t.text('requested_scope'),
442
- granted_scope: (t) => t.text('granted_scope').nullable(),
443
- redirect_uri: (t) => t.string('redirect_uri', 2048).nullable(),
444
- code_challenge: (t) => t.string('code_challenge', 128).nullable(),
445
- status: (t) => t.string('status', 16),
446
- attempts: (t) => t.integer('attempts').defaultTo(0),
447
- interval_seconds: (t) => t.integer('interval_seconds').nullable(),
448
- last_polled_at: (t) => t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable(),
449
- data: (t) => t.text('data').nullable(),
450
- expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
451
- created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
452
- },
453
- },
454
- {
455
- name: 'auth_poppy_jtis',
456
- /** `jti` já aceitos (client assertions, Session JWTs, provas DPoP), até expirarem — anti-replay entre instâncias. */
457
- create: (t) => {
458
- t.string('key', 100).primary();
459
- t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
460
- },
461
- columns: {
462
- expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
463
- },
464
- },
465
336
  ];
466
337
  /** Linhas de um `rawQuery`, no formato de cada driver (pg, mysql2, sqlite). */
467
338
  function catalogRows(result) {
@@ -586,26 +457,9 @@ async function ensureIndex(conn, table, index) {
586
457
  throw error;
587
458
  }
588
459
  }
589
- /**
590
- * Garante que as tabelas do authkit existem e têm todas as colunas que esta
591
- * versão da lib espera. Aditivo e idempotente — seguro de rodar em todo boot
592
- * e dentro de migrations.
593
- *
594
- * `db` é o manager (`Database`, no boot) ou o client da própria migration
595
- * (`this.db`, um `QueryClient`/`TransactionClient`, usado como está).
596
- */
597
- export async function ensureAuthkitSchema(db, options = {}) {
598
- const conn = typeof db.connection !== 'function'
599
- ? db
600
- : options.connection
601
- ? db.connection(options.connection)
602
- : db.connection();
603
- const report = {
604
- created: [],
605
- altered: {},
606
- loginMethods: { table: options.accountTable ?? 'users', ensured: false },
607
- };
608
- for (const def of TABLES) {
460
+ /** Cria/completa as tabelas (o miolo de {@link ensureAuthkitSchema}). */
461
+ async function ensureTables(conn, tables, report) {
462
+ for (const def of tables) {
609
463
  if (!(await tableExists(conn, def.name))) {
610
464
  try {
611
465
  await conn.schema.createTable(def.name, def.create);
@@ -639,6 +493,45 @@ export async function ensureAuthkitSchema(db, options = {}) {
639
493
  report.indexed[def.name].push(index.name);
640
494
  }
641
495
  }
496
+ }
497
+ /**
498
+ * Garante tabelas lib-owned no formato {@link AuthkitTable} — o que `ensureAuthkitSchema` faz com as
499
+ * do authkit: cria as que faltam, adiciona colunas e índices novos, nunca dropa nem altera. Para
500
+ * uma extension expor o próprio `ensure*Schema()` para migrations do host. Estável no 1.x.
501
+ */
502
+ export async function ensureAuthkitTables(db, tables, options = {}) {
503
+ const conn = typeof db.connection !== 'function'
504
+ ? db
505
+ : options.connection
506
+ ? db.connection(options.connection)
507
+ : db.connection();
508
+ const report = {
509
+ created: [],
510
+ altered: {},
511
+ };
512
+ await ensureTables(conn, tables, report);
513
+ return report;
514
+ }
515
+ /**
516
+ * Garante que as tabelas do authkit existem e têm todas as colunas que esta
517
+ * versão da lib espera. Aditivo e idempotente — seguro de rodar em todo boot
518
+ * e dentro de migrations.
519
+ *
520
+ * `db` é o manager (`Database`, no boot) ou o client da própria migration
521
+ * (`this.db`, um `QueryClient`/`TransactionClient`, usado como está).
522
+ */
523
+ export async function ensureAuthkitSchema(db, options = {}) {
524
+ const conn = typeof db.connection !== 'function'
525
+ ? db
526
+ : options.connection
527
+ ? db.connection(options.connection)
528
+ : db.connection();
529
+ const report = {
530
+ created: [],
531
+ altered: {},
532
+ loginMethods: { table: options.accountTable ?? 'users', ensured: false },
533
+ };
534
+ await ensureTables(conn, [...TABLES, ...(options.tables ?? [])], report);
642
535
  // Coluna `login_methods` na tabela da CONTA — a tabela é HOST-owned (não está
643
536
  // em TABLES porque o nome/shape são decisão do app), mas a LIB consome a
644
537
  // coluna (LoginMethodsPreferenceCapability). Alguns deploys de host só migram
@@ -0,0 +1,39 @@
1
+ import type { OidcAdapter } from '../adapters/adapter_contract.js';
2
+ /**
3
+ * authkit-owned keys in a persisted `Client` payload. oidc-provider strips unknown
4
+ * metadata when it builds a `Client` (and when dynamic registration persists one), so
5
+ * these keys can only be written by authkit itself (console, Admin API, CLI) and are
6
+ * read straight from the adapter.
7
+ */
8
+ export declare const SERVICE_TENANT_KEY = "authkit_service_tenant";
9
+ export declare const CLIENT_DISABLED_KEY = "authkit_disabled";
10
+ /** The service-tenant part of a client. */
11
+ export interface ServiceTenantInfo {
12
+ /** Partition (tenant) the client acts as. Emitted as the `partition` claim. */
13
+ partition: string;
14
+ /** Granted capabilities. Emitted as the `capabilities` claim when non-empty. */
15
+ capabilities: string[];
16
+ }
17
+ /** Letters, digits, `.`, `_`, `:` and `-`; starts with a letter or digit; up to 128 chars. */
18
+ export declare const PARTITION_PATTERN: RegExp;
19
+ /** Like {@link PARTITION_PATTERN}, plus `*` and `/`. */
20
+ export declare const CAPABILITY_PATTERN: RegExp;
21
+ export declare const MAX_CAPABILITIES = 64;
22
+ /** Reads the service-tenant metadata from a raw client payload, or `null`. */
23
+ export declare function serviceTenantOf(payload: Record<string, unknown> | undefined | null): ServiceTenantInfo | null;
24
+ /** `true` when the raw client payload is disabled. */
25
+ export declare function isClientDisabled(payload: Record<string, unknown> | undefined | null): boolean;
26
+ /**
27
+ * Validates and normalizes service-tenant input. Returns an error message or the
28
+ * normalized value (trimmed, de-duplicated capabilities).
29
+ */
30
+ export declare function normalizeServiceTenant(input: {
31
+ partition: string;
32
+ capabilities?: string[];
33
+ }): ServiceTenantInfo | string;
34
+ /**
35
+ * Wraps the provider's `Client` adapter so a disabled client is "not found": every
36
+ * endpoint answers `invalid_client` from the next request on, on every instance that
37
+ * shares the adapter. Writes are untouched.
38
+ */
39
+ export declare function hideDisabledClients<T extends OidcAdapter>(adapter: T): T;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * authkit-owned keys in a persisted `Client` payload. oidc-provider strips unknown
3
+ * metadata when it builds a `Client` (and when dynamic registration persists one), so
4
+ * these keys can only be written by authkit itself (console, Admin API, CLI) and are
5
+ * read straight from the adapter.
6
+ */
7
+ export const SERVICE_TENANT_KEY = 'authkit_service_tenant';
8
+ export const CLIENT_DISABLED_KEY = 'authkit_disabled';
9
+ /** Letters, digits, `.`, `_`, `:` and `-`; starts with a letter or digit; up to 128 chars. */
10
+ export const PARTITION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
11
+ /** Like {@link PARTITION_PATTERN}, plus `*` and `/`. */
12
+ export const CAPABILITY_PATTERN = /^[A-Za-z0-9*][A-Za-z0-9._:*/-]{0,127}$/;
13
+ export const MAX_CAPABILITIES = 64;
14
+ /** Reads the service-tenant metadata from a raw client payload, or `null`. */
15
+ export function serviceTenantOf(payload) {
16
+ const raw = payload?.[SERVICE_TENANT_KEY];
17
+ if (!raw || typeof raw !== 'object')
18
+ return null;
19
+ if (typeof raw.partition !== 'string' || !PARTITION_PATTERN.test(raw.partition))
20
+ return null;
21
+ const capabilities = Array.isArray(raw.capabilities)
22
+ ? raw.capabilities.filter((c) => typeof c === 'string' && c.length > 0)
23
+ : [];
24
+ return { partition: raw.partition, capabilities };
25
+ }
26
+ /** `true` when the raw client payload is disabled. */
27
+ export function isClientDisabled(payload) {
28
+ return payload?.[CLIENT_DISABLED_KEY] === true;
29
+ }
30
+ /**
31
+ * Validates and normalizes service-tenant input. Returns an error message or the
32
+ * normalized value (trimmed, de-duplicated capabilities).
33
+ */
34
+ export function normalizeServiceTenant(input) {
35
+ // Called directly too (not only behind the HTTP validators): check the types here.
36
+ const rawPartition = input?.partition;
37
+ const partition = typeof rawPartition === 'string' ? rawPartition.trim() : '';
38
+ if (!PARTITION_PATTERN.test(partition)) {
39
+ return `partition "${partition}" is invalid: use letters, digits, ".", "_", ":" or "-" (max 128 chars)`;
40
+ }
41
+ const rawCapabilities = input.capabilities ?? [];
42
+ if (!Array.isArray(rawCapabilities))
43
+ return 'capabilities must be an array of strings';
44
+ const capabilities = [];
45
+ for (const raw of rawCapabilities) {
46
+ if (typeof raw !== 'string')
47
+ return 'capabilities must be an array of strings';
48
+ const cap = raw.trim();
49
+ if (!CAPABILITY_PATTERN.test(cap)) {
50
+ return `capability "${cap}" is invalid: use letters, digits, ".", "_", ":", "*", "/" or "-" (max 128 chars)`;
51
+ }
52
+ if (!capabilities.includes(cap))
53
+ capabilities.push(cap);
54
+ }
55
+ if (capabilities.length > MAX_CAPABILITIES) {
56
+ return `at most ${MAX_CAPABILITIES} capabilities per client`;
57
+ }
58
+ return { partition, capabilities };
59
+ }
60
+ /**
61
+ * Wraps the provider's `Client` adapter so a disabled client is "not found": every
62
+ * endpoint answers `invalid_client` from the next request on, on every instance that
63
+ * shares the adapter. Writes are untouched.
64
+ */
65
+ export function hideDisabledClients(adapter) {
66
+ return new Proxy(adapter, {
67
+ get(target, prop, receiver) {
68
+ if (prop === 'find') {
69
+ return async (id) => {
70
+ const payload = await target.find(id);
71
+ return isClientDisabled(payload)
72
+ ? undefined
73
+ : payload;
74
+ };
75
+ }
76
+ const value = Reflect.get(target, prop, receiver);
77
+ return typeof value === 'function' ? value.bind(target) : value;
78
+ },
79
+ });
80
+ }