@adonis-agora/authkit-server 0.76.0 → 0.78.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 (88) hide show
  1. package/README.md +149 -0
  2. package/build/host/views/agents/consent.edge +13 -2
  3. package/build/host/views/agents/done.edge +3 -0
  4. package/build/host/views/mfa-challenge.edge +42 -3
  5. package/build/index.d.ts +19 -1
  6. package/build/index.js +13 -1
  7. package/build/providers/authkit_server_provider.js +15 -1
  8. package/build/src/accounts/account_store.d.ts +2 -0
  9. package/build/src/accounts/lucid_account_store.js +2 -0
  10. package/build/src/adapters/adapter_contract.d.ts +2 -0
  11. package/build/src/adapters/database_adapter.d.ts +1 -0
  12. package/build/src/adapters/database_adapter.js +26 -0
  13. package/build/src/adapters/redis_adapter.d.ts +1 -0
  14. package/build/src/adapters/redis_adapter.js +30 -0
  15. package/build/src/agents/config.d.ts +20 -3
  16. package/build/src/agents/config.js +19 -4
  17. package/build/src/agents/delegation_service.js +18 -8
  18. package/build/src/agents/middleware.js +2 -1
  19. package/build/src/agents/poppy/client_registry.d.ts +45 -0
  20. package/build/src/agents/poppy/client_registry.js +226 -0
  21. package/build/src/agents/poppy/config.d.ts +362 -0
  22. package/build/src/agents/poppy/config.js +201 -0
  23. package/build/src/agents/poppy/dpop.d.ts +61 -0
  24. package/build/src/agents/poppy/dpop.js +158 -0
  25. package/build/src/agents/poppy/errors.d.ts +20 -0
  26. package/build/src/agents/poppy/errors.js +22 -0
  27. package/build/src/agents/poppy/replay.d.ts +25 -0
  28. package/build/src/agents/poppy/replay.js +106 -0
  29. package/build/src/agents/poppy/resource.d.ts +95 -0
  30. package/build/src/agents/poppy/resource.js +125 -0
  31. package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
  32. package/build/src/agents/poppy/safe_fetch.js +256 -0
  33. package/build/src/agents/poppy/service.d.ts +280 -0
  34. package/build/src/agents/poppy/service.js +1452 -0
  35. package/build/src/agents/poppy/store.d.ts +109 -0
  36. package/build/src/agents/poppy/store.js +260 -0
  37. package/build/src/agents/poppy/web_session.d.ts +32 -0
  38. package/build/src/agents/poppy/web_session.js +104 -0
  39. package/build/src/agents/protocol.d.ts +8 -4
  40. package/build/src/agents/runtime.d.ts +17 -1
  41. package/build/src/agents/runtime.js +138 -13
  42. package/build/src/define_config.d.ts +24 -0
  43. package/build/src/define_config.js +14 -0
  44. package/build/src/host/account_api/account_api_controller.d.ts +12 -1
  45. package/build/src/host/account_api/account_api_controller.js +10 -5
  46. package/build/src/host/admin_sessions_service.js +8 -5
  47. package/build/src/host/auth_host_config.d.ts +2 -1
  48. package/build/src/host/controllers/account_apps_controller.js +13 -3
  49. package/build/src/host/controllers/account_security_controller.js +8 -4
  50. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  51. package/build/src/host/controllers/interaction_controller.js +352 -24
  52. package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
  53. package/build/src/host/controllers/poppy_consent_controller.js +173 -0
  54. package/build/src/host/controllers/poppy_controller.d.ts +25 -0
  55. package/build/src/host/controllers/poppy_controller.js +194 -0
  56. package/build/src/host/csrf.d.ts +7 -0
  57. package/build/src/host/csrf.js +16 -1
  58. package/build/src/host/custom_login.d.ts +36 -0
  59. package/build/src/host/custom_login.js +76 -0
  60. package/build/src/host/custom_login_completion.d.ts +4 -0
  61. package/build/src/host/custom_login_completion.js +33 -0
  62. package/build/src/host/custom_mfa.d.ts +77 -0
  63. package/build/src/host/custom_mfa.js +268 -0
  64. package/build/src/host/i18n.d.ts +8 -0
  65. package/build/src/host/i18n.js +14 -6
  66. package/build/src/host/idp_session_bridge.d.ts +5 -3
  67. package/build/src/host/idp_session_bridge.js +5 -3
  68. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  69. package/build/src/host/oidc_rp_guard.js +15 -2
  70. package/build/src/host/persistent_rp_session.d.ts +8 -0
  71. package/build/src/host/persistent_rp_session.js +74 -0
  72. package/build/src/host/poppy_session_key.d.ts +9 -0
  73. package/build/src/host/poppy_session_key.js +9 -0
  74. package/build/src/host/register_auth_host.js +72 -0
  75. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  76. package/build/src/host/whatsapp_code_sender.js +9 -0
  77. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  78. package/build/src/host/whatsapp_senders/http.js +33 -0
  79. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  80. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  81. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  82. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  83. package/build/src/provider/oidc_service.d.ts +5 -0
  84. package/build/src/provider/oidc_service.js +15 -4
  85. package/build/src/schema/ensure.js +124 -0
  86. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  87. package/package.json +2 -1
  88. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
package/README.md CHANGED
@@ -14,6 +14,10 @@ wrapper around [`oidc-provider`](https://github.com/panva/node-oidc-provider).
14
14
  - **Personal agents** — AI assistants acting for your users: agent identity by signed JWT
15
15
  (JWKS, no shared secrets) and user-approved, revocable delegation with scopes you define,
16
16
  over a pluggable protocol adapter ([PACT](https://openpactprotocol.org) built in).
17
+ Plus the [Personal Agent Protocol ("Poppy")](https://personalagentprotocol.org/docs/spec):
18
+ discovery, DPoP-bound agent sessions, Direct/Device/Mediated sign-in, MCP and browser sessions.
19
+ ⚠️ **Experimental** — tracks the spec's Draft 0.1 and will change (possibly breaking, outside
20
+ semver majors) while the spec is a draft.
17
21
  - **Native apps** — RFC 8252 native clients (`authkit:clients:create --native`: private-use
18
22
  scheme / claimed https / loopback redirects, always public + PKCE) and the `@adonisjs/auth`
19
23
  guards `oidcRpGuard` (session) and `oidcBearerGuard` (`Authorization: Bearer` access tokens,
@@ -163,3 +167,148 @@ OTel para que as séries existam.
163
167
  ## Notas
164
168
  - Access tokens são opacos; ID tokens são JWT (assinados pelo JWKS gerido).
165
169
  - PKCE (S256) é obrigatório; refresh tokens são rotacionados.
170
+
171
+ ### Custom primary login methods
172
+
173
+ Register a class or instance for a host-specific authentication flow. AuthKit resolves classes through the request container, so normal Adonis `@inject()` dependencies work. The host validates and consumes the credential; AuthKit applies account policies, maintenance, MFA, audit, and OIDC session completion.
174
+
175
+ ```ts
176
+ import { inject } from '@adonisjs/core';
177
+ import type { HttpContext } from '@adonisjs/core/http';
178
+ import type { CustomLoginMethod } from '@adonis-agora/authkit-server';
179
+ import PhoneProofs from '#services/phone_proofs';
180
+
181
+ @inject()
182
+ export default class WhatsappLogin implements CustomLoginMethod {
183
+ readonly passwordless = true;
184
+ constructor(private proofs: PhoneProofs) {}
185
+
186
+ async authenticate(ctx: HttpContext) {
187
+ // Host service validates and atomically consumes the proof bound to this
188
+ // browser session and OIDC interaction. Never trust request.accountId.
189
+ const accountId = await this.proofs.consume(ctx);
190
+ return accountId ? { accountId } : null;
191
+ }
192
+ }
193
+ ```
194
+
195
+ ```ts
196
+ // config/authkit.ts
197
+ import WhatsappLogin from '#auth/whatsapp_login';
198
+ export default defineConfig({
199
+ // issuer, adapter, accountStore, ...
200
+ customLoginMethods: { whatsapp: WhatsappLogin },
201
+ });
202
+
203
+ // Native form controller, on the host's POST /auth/interaction/:uid/... route
204
+ return authenticateCustomLogin(ctx, 'whatsapp');
205
+ ```
206
+
207
+ `authenticate` returns `{ accountId }` only after a valid proof, or `null` to deny login. It may also return a host-validated `remember` boolean; persistence is disabled by default and subject to the runtime session policy. The same session policy applies after MFA. Returning an identity does not skip MFA. The method name is retained in the OIDC `amr` claim, including when a second factor follows. Methods default to password-based policy; set `passwordless = true` for OTP, hardware or external proofs that do not use the account password.
208
+
209
+ A method may implement `begin(ctx)` to initiate a challenge. Call `beginCustomLogin(ctx, 'whatsapp')` from the host route. Both dispatchers check the current OIDC interaction and its `:uid` before invoking the method. Method names contain lowercase letters, digits, `:`, `_` or `-`, start with a letter, and have at most 64 characters. Built-in factor names (`pwd`, `email`, `mfa`, `totp`, `webauthn`, `recovery`) are reserved. Registration does not automatically create routes or UI: the host owns validation, CSRF, rate limits, bot protection, delivery, proof expiry, replay protection, account lookup/signup and the response renderer. Use native form navigation for final completion so AuthKit can render MFA or redirect to the relying party.
210
+
211
+ For an arbitrary flow that already verified its credential, `completeCustomLogin(ctx, { accountId, method, passwordless })` is the trusted escape hatch. Do not expose it as an endpoint accepting an account ID from the browser. A phone-only account may represent absent email as an empty string in the legacy `AuthAccount` DTO; OIDC omits `email` and `email_verified` in that case. `AuthAccount.phone`, when supplied, must represent a verified phone identity.
212
+
213
+ Denied proofs and handler exceptions produce a `login.failure` audit event with the method ID and a sanitized reason; proof contents and exception messages are never included. Host-owned challenge/OTP verification endpoints must audit their own earlier failures.
214
+
215
+ ### WhatsApp OTP delivery providers
216
+
217
+ `whatsapp.sender` accepts any `WhatsappCodeSender` instance or class. It is separate from `customLoginMethods`: login classes verify identity, senders only deliver a code. Configuring a sender does not create an OTP store, route, validator, signup flow, or UI.
218
+
219
+ ```ts
220
+ import { inject } from '@adonisjs/core';
221
+ import type { WhatsappCodeInput, WhatsappCodeSender } from '@adonis-agora/authkit-server';
222
+ import MyWhatsappSdk from '#services/my_whatsapp_sdk';
223
+
224
+ @inject()
225
+ class MyProvider implements WhatsappCodeSender {
226
+ constructor(private sdk: MyWhatsappSdk) {}
227
+ async sendCode(input: WhatsappCodeInput): Promise<void> {
228
+ await this.sdk.sendText(input.phone, input.text ?? input.code);
229
+ }
230
+ }
231
+
232
+ export default defineConfig({
233
+ // issuer, adapter, accountStore, ...
234
+ customLoginMethods: { whatsapp: WhatsappLogin },
235
+ whatsapp: { sender: MyProvider }, // or an already-created instance
236
+ });
237
+ ```
238
+
239
+ After generating an OTP, resolve the sender through the request container:
240
+
241
+ ```ts
242
+ const service = await ctx.containerResolver.make('authkit.server');
243
+ const binding = service.config.whatsapp?.sender;
244
+ if (!binding) throw new Error('WhatsApp login is not configured');
245
+ const sender = await resolveWhatsappCodeSender(ctx.containerResolver, binding);
246
+ await sender.sendCode({
247
+ phone: '5511999999999', code, locale: 'pt-BR', expiresInSeconds: 300,
248
+ text: ctx.i18n.t('auth.code_message', { code }),
249
+ });
250
+ ```
251
+
252
+ The sender must reject when delivery fails. The host should activate its stored OTP only after delivery acceptance and enforce expiry, retries, request binding, replay protection and rate limits. Provider acceptance is not a delivery/read receipt. `text` is optional localized copy for providers supporting free-form messages; structured providers receive the original code independently.
253
+
254
+ Native adapters can be supplied as instances:
255
+
256
+ ```ts
257
+ whatsapp: {
258
+ sender: new WhatsmiauCodeSender({ apiKey, instanceName }),
259
+ }
260
+
261
+ whatsapp: {
262
+ sender: new MetaWhatsappCodeSender({
263
+ accessToken, phoneNumberId, apiVersion, templateName,
264
+ languageCode: 'pt_BR',
265
+ }),
266
+ }
267
+ ```
268
+
269
+ Whatsmiau's optional `baseUrl` includes the API version path and defaults to `https://api.whatsmiau.dev/v2`. Meta requires an explicit supported Graph API version and an approved authentication template with an OTP/copy-code button. Template language must exist for the configured template; `languageCode` overrides automatic locale mapping (`pt-BR` → `pt_BR`, `en` → `en_US`, `es` → `es`). Meta template text and displayed expiry are managed in the approved template; `expiresInSeconds` describes the host's verification TTL and does not change template expiry. Both native adapters use bounded requests, reject redirects and discard provider error bodies so credentials/OTPs do not enter error messages.
270
+
271
+
272
+ ### Custom MFA methods (classes or instances)
273
+
274
+ Register additional methods separately from primary login methods. Registration never enrolls an account: `isEnabled` must consult your enrollment store. A method may send a challenge in `begin`, describe its form fields, and verify a proof in `verify`.
275
+
276
+ ```ts
277
+ import { inject } from '@adonisjs/core'
278
+ import type { CustomMfaContext, CustomMfaMethod } from '@adonis-agora/authkit-server'
279
+
280
+ // Enrollment and challenge services belong to your application and are injected by Adonis.
281
+ @inject()
282
+ export class WhatsappMfa implements CustomMfaMethod {
283
+ readonly factorId = 'phone'
284
+ constructor(private enrollment: PhoneEnrollment, private challenges: PhoneChallenges) {}
285
+
286
+ async isEnabled({ accountId }: CustomMfaContext) {
287
+ return this.enrollment.hasVerifiedPhone(accountId)
288
+ }
289
+ async describe() {
290
+ return { label: 'WhatsApp', fields: [{ name: 'code', label: 'Code', inputMode: 'numeric' as const }] }
291
+ }
292
+ async begin({ accountId, challengeId }: CustomMfaContext) {
293
+ await this.challenges.sendWhatsapp(accountId, challengeId)
294
+ }
295
+ async verify({ ctx, accountId, challengeId }: CustomMfaContext) {
296
+ const { code } = await ctx.request.validateUsing(phoneCodeValidator)
297
+ return this.challenges.consume(accountId, challengeId, code)
298
+ }
299
+ }
300
+
301
+ // In defineConfig:
302
+ mfa: {
303
+ methods: { whatsapp: WhatsappMfa, sms: smsMfaInstance },
304
+ requiredFactors: 3,
305
+ }
306
+ ```
307
+
308
+ The sample enrollment/challenge services and Vine validator are host implementations. Bind proofs to both `accountId` and `challengeId`; expire them, hash stored codes, limit sends, and atomically consume successful proofs. Delivery can use the WhatsApp sender interface or any SMS/provider SDK. Proofs and provider credentials must never appear in descriptors.
309
+
310
+ `requiredFactors` counts the primary login plus distinct additional groups (2 by default, configurable from 2 through 8). Omit it to challenge only accounts with an enrolled method; setting it explicitly also requires unenrolled accounts to enroll before they can authenticate. This extension does not supply an enrollment screen. Native TOTP, recovery codes and passkeys remain available; recovery and TOTP share one group. WhatsApp and SMS using the same phone must share `factorId: 'phone'`. A WhatsApp primary login class should also expose that group, preventing the same phone from counting twice. Group names express application policy; they do not prove independent physical authentication factors.
311
+
312
+ AuthKit binds the challenge to the current login interaction and browser session, expires it after ten minutes, caps failed custom proofs at five, and completes OIDC only after the required number of groups. Edge and generated React challenge screens render custom descriptors and optional start buttons. The named POST routes `authkit.mfa.custom.begin` and `authkit.mfa.custom.verify` use the existing login throttling and CSRF pipeline. Public `beginCustomMfa`, `verifyCustomMfa`, and `customMfaViewProps` support host integrations; proof verification alone does not complete authentication. Use the registered routes for policy-controlled completion.
313
+
314
+ Without `mfa` configuration, existing native MFA behavior is preserved. Applications can ship this extension without enabling additional methods.
@@ -8,7 +8,16 @@
8
8
  @if(request)
9
9
  <form method="POST" action="{{ action }}">
10
10
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
11
- <input type="hidden" name="user_code" value="{{ request.userCode }}">
11
+ @if(request.hidden)
12
+ @each(field in request.hidden)
13
+ <input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
14
+ @end
15
+ @else
16
+ <input type="hidden" name="user_code" value="{{ request.userCode }}">
17
+ @end
18
+ @if(request.logoUri)
19
+ <img src="{{ request.logoUri }}" alt="" class="mb-4 h-12 w-12 rounded-lg" referrerpolicy="no-referrer">
20
+ @end
12
21
  <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.consent.title', { agent: request.agentName }) }}</h1>
13
22
  <p class="mt-2 text-sm text-gray-600">{{ t('agents.consent.body', { origin: request.agentOrigin }) }}</p>
14
23
  @if(account && account.email)
@@ -25,7 +34,9 @@
25
34
  @end
26
35
  </fieldset>
27
36
 
28
- <p class="mt-4 text-xs text-gray-500">{{ t('agents.consent.code', { code: request.userCode }) }}</p>
37
+ @if(request.userCode)
38
+ <p class="mt-4 text-xs text-gray-500">{{ t('agents.consent.code', { code: request.userCode }) }}</p>
39
+ @end
29
40
 
30
41
  <div class="mt-6 flex gap-3">
31
42
  <button type="submit" name="decision" value="deny"
@@ -16,6 +16,9 @@
16
16
  @elseif(status === 'denied')
17
17
  <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.denied_title') }}</h1>
18
18
  <p class="mt-2 text-sm text-gray-600">{{ t('agents.done.return') }}</p>
19
+ @elseif(status === 'invalid')
20
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.invalid_title') }}</h1>
21
+ <p class="mt-2 text-sm text-gray-600">{{ t('agents.done.invalid_body') }}</p>
19
22
  @else
20
23
  <h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.expired_title') }}</h1>
21
24
  <p class="mt-2 text-sm text-gray-600">{{ t('agents.done.expired_body') }}</p>
@@ -12,6 +12,12 @@
12
12
  {{ t('mfa_challenge.intro') }}
13
13
  </p>
14
14
 
15
+ @if(requiredMfaFactors > 2)
16
+ <p class="mt-3 text-sm text-gray-600" role="status">
17
+ {{ t('mfa_challenge.progress', { completed: 1 + (completedMfaMethods || []).length, required: requiredMfaFactors }) }}
18
+ </p>
19
+ @end
20
+
15
21
  @if(error)
16
22
  <p class="mt-4 text-sm text-red-600">{{ error }}</p>
17
23
  @end
@@ -21,7 +27,7 @@
21
27
  `totpAvailable === false` = a conta só tem passkey, sem app autenticador
22
28
  confirmado: pedir um código de 6 dígitos seria pedir o impossível. Ausente
23
29
  (stores que não reportam o estado) mantém o campo visível. --}}
24
- @if(!noEnrollment && totpAvailable !== false)
30
+ @if(!noEnrollment && !otpLocked && totpAvailable !== false)
25
31
  <div class="mt-6">
26
32
  <label for="code" class="mb-1 block text-sm font-medium text-gray-700">{{ t('mfa_challenge.code_label') }}</label>
27
33
  <input id="code" name="code" inputmode="numeric" autocomplete="one-time-code"
@@ -43,6 +49,39 @@
43
49
  @end
44
50
  </form>
45
51
 
52
+ @if(!noEnrollment)
53
+ @each(method in (customMfaMethods || []))
54
+ @if(!(completedMfaMethods || []).includes(method.id))
55
+ <section class="mt-6">
56
+ <h2 class="text-sm font-semibold text-gray-900">{{ method.label }}</h2>
57
+ @if(method.requiresBegin && !method.started)
58
+ <form method="POST" action="{{ method.beginUrl }}" class="mt-3">
59
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
60
+ <button type="submit" class="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
61
+ {{ t('mfa_challenge.start') }}
62
+ </button>
63
+ </form>
64
+ @else
65
+ <form method="POST" action="{{ method.verifyUrl }}" class="mt-3 space-y-3">
66
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
67
+ @each(field in (method.fields || []))
68
+ <div>
69
+ <label for="custom-{{ method.id }}-{{ field.name }}" class="mb-1 block text-sm font-medium text-gray-700">{{ field.label }}</label>
70
+ <input id="custom-{{ method.id }}-{{ field.name }}" name="{{ field.name }}"
71
+ type="{{ field.type || 'text' }}" inputmode="{{ field.inputMode || 'text' }}" autocomplete="{{ field.autoComplete || 'off' }}"
72
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-base outline-hidden focus:border-gray-900 focus:ring-2 focus:ring-gray-900">
73
+ </div>
74
+ @end
75
+ <button type="submit" class="w-full rounded-lg bg-gray-900 px-3 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
76
+ {{ t('mfa_challenge.submit') }}
77
+ </button>
78
+ </form>
79
+ @end
80
+ </section>
81
+ @end
82
+ @end
83
+ @end
84
+
46
85
  @if(passkeyAvailable && !noEnrollment)
47
86
  {{-- Passkey como alternativa ao código TOTP. O form é submetido por página
48
87
  inteira (não fetch) para que o 303 de volta ao client navegue o browser. --}}
@@ -61,7 +100,7 @@
61
100
 
62
101
  {{-- Códigos de recuperação só existem junto com o TOTP (são gerados no
63
102
  enrollment); sem ele, a seção não tem o que receber. --}}
64
- @if(!noEnrollment && totpAvailable !== false)
103
+ @if(!noEnrollment && !otpLocked && totpAvailable !== false)
65
104
  <details class="mt-6 text-sm text-gray-600">
66
105
  <summary class="cursor-pointer hover:underline">{{ t('mfa_challenge.recovery_summary') }}</summary>
67
106
  <form method="POST" action="/auth/interaction/{{ uid }}/mfa" class="mt-3">
@@ -77,7 +116,7 @@
77
116
  @end
78
117
  </div>
79
118
 
80
- @if(passkeyAvailable)
119
+ @if(passkeyAvailable && !noEnrollment)
81
120
  {{-- M12: script inline sem nonce é bloqueado por CSP `script-src 'self'` — extraído
82
121
  para asset same-origin (compartilhado com login.edge). --}}
83
122
  <script type="module" src="/authkit/assets/passkey_button.js"></script>
package/build/index.d.ts CHANGED
@@ -16,6 +16,15 @@ export type { PersonalAgentDelegationConfigInput, PersonalAgentRegistration, Per
16
16
  export type { DelegationContext, DelegationGrantSummary, DelegationReceipt, } from './src/agents/delegation_service.js';
17
17
  export type { PersonalAgentAuthOptions, PersonalAgentRequestContext, } from './src/agents/middleware.js';
18
18
  export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
19
+ export type { PoppyAgentProtocolEntry, PoppyApiEntry, PoppyBrowserLogin, PoppyClientsConfigInput, PoppyConfigInput, PoppyLimiter, PoppyMediatedConfigInput, PoppyMediatedField, PoppyMediatedResult, PoppyMediatedVerifyInput, PoppyPrincipalBase, PoppyReplayStore, ResolvedPoppyConfig, } from './src/agents/poppy/config.js';
20
+ export { POPPY_READ, POPPY_WRITE } from './src/agents/poppy/config.js';
21
+ export { accessTokenHash, type DpopNonceSource, type DpopResult, hmacNonceSource, type VerifyDpopInput, verifyDpopProof, } from './src/agents/poppy/dpop.js';
22
+ export { PoppyError, type PoppyErrorCode } from './src/agents/poppy/errors.js';
23
+ export { databaseReplayStore, memoryReplayStore, redisReplayStore, } from './src/agents/poppy/replay.js';
24
+ export { installPoppySlots, POPPY_AUTHENTICATE_SLOT, type PoppyAuthenticate, type PoppyAuthResult, type PoppyPrincipal, type PoppySlotRequest, poppyAuth, poppyOf, verifyPoppyRequest, writePoppyAuthError, } from './src/agents/poppy/resource.js';
25
+ export { isBlockedAddress, SafeFetchError, safeFetchJson, } from './src/agents/poppy/safe_fetch.js';
26
+ export type { PoppyAuthError, PoppyGrantSummary, PoppySessionState, PoppyTokenResponse, PoppyVerifyOptions, } from './src/agents/poppy/service.js';
27
+ export { type PoppyWebSessionMarker, poppyWebSession, poppyWebSessionOf, } from './src/agents/poppy/web_session.js';
19
28
  export type { BuiltinProtocolId, PactOptions, PersonalAgentProtocol, } from './src/agents/protocol.js';
20
29
  export { pact, pactProtocol } from './src/agents/protocol.js';
21
30
  export type { AuditEvent, AuditEventType, AuditPage, AuditSink, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
@@ -76,12 +85,16 @@ export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROU
76
85
  export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
77
86
  export type { AuthkitCsrfOptions } from './src/host/csrf.js';
78
87
  export { authkitCsrfExceptions } from './src/host/csrf.js';
88
+ export type { CustomLoginIdentity, CustomLoginMethod, CustomLoginMethodBinding, CustomLoginMethodConstructor, CustomLoginMethods, } from './src/host/custom_login.js';
89
+ export { authenticateCustomLogin, beginCustomLogin, completeCustomLogin, } from './src/host/custom_login.js';
90
+ export type { CustomMfaContext, CustomMfaDescription, CustomMfaDescriptor, CustomMfaField, CustomMfaMethod, CustomMfaMethodBinding, CustomMfaMethodConstructor, CustomMfaMethods, } from './src/host/custom_mfa.js';
91
+ export { beginCustomMfa, customMfaViewProps, verifyCustomMfa } from './src/host/custom_mfa.js';
79
92
  export { normalizeEmailIdentifier } from './src/host/email_identifier.js';
80
93
  export type { ResolveGeo } from './src/host/geo.js';
81
94
  export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
82
95
  export type { AuthMessages, I18nConfig } from './src/host/i18n.js';
83
96
  export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
84
- export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
97
+ export { ACCOUNT_IDP_SESSION_KEY, ensureConsoleSession, readIdpSession, } from './src/host/idp_session_bridge.js';
85
98
  export type { ImpersonationClientLike, ImpersonationPanel } from './src/host/impersonation.js';
86
99
  export { buildImpersonationPanel } from './src/host/impersonation.js';
87
100
  export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonationParams, StopImpersonationOptions, TokenExchangeResult, } from './src/host/impersonation_session.js';
@@ -93,6 +106,9 @@ export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.j
93
106
  export type { OidcRpGuardEvents, OidcRpGuardOptions } from './src/host/oidc_rp_guard.js';
94
107
  export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
95
108
  export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, type OtpLoginConfigInput, type OtpVerifyOutcome, type ResolvedOtpLoginConfig, resolveOtpLoginConfig, } from './src/host/otp_login.js';
109
+ export type { PersistentRpOptions } from './src/host/persistent_rp_session.js';
110
+ export { endRpSession } from './src/host/persistent_rp_session.js';
111
+ export { POPPY_WEB_SESSION_KEY } from './src/host/poppy_session_key.js';
96
112
  export type { AuthThrottles, ThrottleMiddleware } from './src/host/rate_limit.js';
97
113
  export { createAuthThrottles } from './src/host/rate_limit.js';
98
114
  export type { AccountScreensOptions, AuthHostOptions, AuthHostRouteMap, } from './src/host/register_auth_host.js';
@@ -117,6 +133,8 @@ export type { ParsedUserAgent } from './src/host/user_agent.js';
117
133
  export { parseUserAgent } from './src/host/user_agent.js';
118
134
  export type { ResolvedUserLoginMethods, UserLoginMethodKey, UserLoginMethods, } from './src/host/user_login_methods.js';
119
135
  export { normalizeUserLoginMethods, parseUserLoginMethodsPayload, resolveEffectiveUserLoginMethods, USER_LOGIN_METHOD_KEYS, } from './src/host/user_login_methods.js';
136
+ export type { MetaWhatsappCodeSenderOptions, WhatsappCodeInput, WhatsappCodeSender, WhatsappCodeSenderBinding, WhatsappCodeSenderConstructor, WhatsmiauCodeSenderOptions, } from './src/host/whatsapp_code_sender.js';
137
+ export { MetaWhatsappCodeSender, resolveWhatsappCodeSender, WhatsmiauCodeSender, } from './src/host/whatsapp_code_sender.js';
120
138
  export { MCP_CLIENT_REDIRECTS, MCP_RESOURCE_SCOPES, type McpOAuthConfigInput, type OAuthResourceRegistration, registeredOAuthResources, registerOAuthResource, } from './src/mcp/mcp_oauth.js';
121
139
  export { withAuditLog } from './src/mixins/with_audit_log.js';
122
140
  export { withAuthUser } from './src/mixins/with_auth_user.js';
package/build/index.js CHANGED
@@ -9,6 +9,13 @@ export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, d
9
9
  export { lucidStores } from './src/accounts/lucid_stores.js';
10
10
  export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
11
11
  export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
12
+ export { POPPY_READ, POPPY_WRITE } from './src/agents/poppy/config.js';
13
+ export { accessTokenHash, hmacNonceSource, verifyDpopProof, } from './src/agents/poppy/dpop.js';
14
+ export { PoppyError } from './src/agents/poppy/errors.js';
15
+ export { databaseReplayStore, memoryReplayStore, redisReplayStore, } from './src/agents/poppy/replay.js';
16
+ export { installPoppySlots, POPPY_AUTHENTICATE_SLOT, poppyAuth, poppyOf, verifyPoppyRequest, writePoppyAuthError, } from './src/agents/poppy/resource.js';
17
+ export { isBlockedAddress, SafeFetchError, safeFetchJson, } from './src/agents/poppy/safe_fetch.js';
18
+ export { poppyWebSession, poppyWebSessionOf, } from './src/agents/poppy/web_session.js';
12
19
  export { pact, pactProtocol } from './src/agents/protocol.js';
13
20
  export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
14
21
  export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
@@ -54,10 +61,12 @@ export { assertClientMetadata, ClientMetadataError, redirectUriProblem, } from '
54
61
  export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
55
62
  export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
56
63
  export { authkitCsrfExceptions } from './src/host/csrf.js';
64
+ export { authenticateCustomLogin, beginCustomLogin, completeCustomLogin, } from './src/host/custom_login.js';
65
+ export { beginCustomMfa, customMfaViewProps, verifyCustomMfa } from './src/host/custom_mfa.js';
57
66
  export { normalizeEmailIdentifier } from './src/host/email_identifier.js';
58
67
  export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
59
68
  export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
60
- export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
69
+ export { ACCOUNT_IDP_SESSION_KEY, ensureConsoleSession, readIdpSession, } from './src/host/idp_session_bridge.js';
61
70
  export { buildImpersonationPanel } from './src/host/impersonation.js';
62
71
  // Session impersonation — RP-side glue that routes through the IdP's RFC 8693
63
72
  // token-exchange (the IdP validates the admin role + audits). See
@@ -77,6 +86,8 @@ export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.j
77
86
  export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
78
87
  // Login por OTP (código digitável): config + helpers puros.
79
88
  export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, resolveOtpLoginConfig, } from './src/host/otp_login.js';
89
+ export { endRpSession } from './src/host/persistent_rp_session.js';
90
+ export { POPPY_WEB_SESSION_KEY } from './src/host/poppy_session_key.js';
80
91
  export { createAuthThrottles } from './src/host/rate_limit.js';
81
92
  export { registerAuthHost } from './src/host/register_auth_host.js';
82
93
  export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
@@ -110,6 +121,7 @@ export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices,
110
121
  export { parseUserAgent } from './src/host/user_agent.js';
111
122
  // Tipos de login POR USUÁRIO (self-service no console de conta).
112
123
  export { normalizeUserLoginMethods, parseUserLoginMethodsPayload, resolveEffectiveUserLoginMethods, USER_LOGIN_METHOD_KEYS, } from './src/host/user_login_methods.js';
124
+ export { MetaWhatsappCodeSender, resolveWhatsappCodeSender, WhatsmiauCodeSender, } from './src/host/whatsapp_code_sender.js';
113
125
  export { MCP_CLIENT_REDIRECTS, MCP_RESOURCE_SCOPES, registeredOAuthResources, registerOAuthResource, } from './src/mcp/mcp_oauth.js';
114
126
  export { withAuditLog } from './src/mixins/with_audit_log.js';
115
127
  export { withAuthUser } from './src/mixins/with_auth_user.js';
@@ -130,7 +130,14 @@ export default class AuthkitServerProvider {
130
130
  // Defaults estruturais de `config.routes` (o argumento ainda vence).
131
131
  routes: typeof config.routes === 'object' ? config.routes : undefined,
132
132
  lockedRouteOptions: config.lockedRouteOptions,
133
- personalAgents: config.personalAgents ? { prefix: config.personalAgents.prefix } : undefined,
133
+ personalAgents: config.personalAgents
134
+ ? {
135
+ prefix: config.personalAgents.prefix,
136
+ ...(config.personalAgents.poppy
137
+ ? { poppyPrefix: config.personalAgents.poppy.prefix }
138
+ : {}),
139
+ }
140
+ : undefined,
134
141
  // API headless — repassa pro registerAuthHost montar as rotas.
135
142
  headless: config.headless
136
143
  ? {
@@ -139,6 +146,13 @@ export default class AuthkitServerProvider {
139
146
  }
140
147
  : undefined,
141
148
  });
149
+ // Poppy: o slot global que o `@adonis-agora/agent` usa para autenticar o endpoint de
150
+ // conversas (e o MCP) sem importar este pacote — o contrato é o símbolo.
151
+ if (config.personalAgents?.poppy) {
152
+ const { installPoppySlots } = await import('../src/agents/poppy/resource.js');
153
+ const { poppyUrls } = await import('../src/agents/poppy/service.js');
154
+ installPoppySlots(poppyUrls(config.issuer, config.personalAgents.poppy.prefix).issuer);
155
+ }
142
156
  // Auto-montagem das rotas (`config.routes`). Chama a MESMA função exportada que o
143
157
  // `start/routes.ts` chamaria; não há segunda implementação. "As rotas não subiram" não pode
144
158
  // degradar em silêncio: uma falha aqui propaga.
@@ -3,6 +3,8 @@ import type { UserLoginMethods } from '../host/user_login_methods.js';
3
3
  export interface AuthAccount {
4
4
  id: string;
5
5
  email: string;
6
+ /** Host-verified phone identity. Omit until possession has been verified. */
7
+ phone?: string;
6
8
  globalRoles?: string[];
7
9
  name?: string;
8
10
  avatarUrl?: string;
@@ -182,6 +182,7 @@ export function lucidAccountStore(Model, options = {}) {
182
182
  // token (e para todo call-site tipado como string).
183
183
  id: String(row.id),
184
184
  email: row.email,
185
+ ...(row.phone && row.phoneVerifiedAt ? { phone: row.phone } : {}),
185
186
  globalRoles: row.globalRoles ?? [],
186
187
  name: row.fullName ?? undefined,
187
188
  avatarUrl: row.avatarUrl ?? undefined,
@@ -328,6 +329,7 @@ export async function lucidAccountStoreAsync(Model, options = {}) {
328
329
  // Ver a nota em `toAccount` acima: `sub` precisa ser string.
329
330
  id: String(row.id),
330
331
  email: row.email,
332
+ ...(row.phone && row.phoneVerifiedAt ? { phone: row.phone } : {}),
331
333
  globalRoles: row.globalRoles ?? [],
332
334
  name: row.fullName ?? undefined,
333
335
  avatarUrl: row.avatarUrl ?? undefined,
@@ -24,6 +24,8 @@ export interface OidcAdapter {
24
24
  findByUid(uid: string): Promise<OidcPayload | undefined>;
25
25
  consume(id: string): Promise<void>;
26
26
  destroy(id: string): Promise<void>;
27
+ /** Extend an existing remembered Session atomically; never recreate revoked records. */
28
+ renewSession?(id: string, expiresIn: number): Promise<boolean>;
27
29
  revokeByGrantId(grantId: string): Promise<void>;
28
30
  /**
29
31
  * Enumeração GENÉRICA dos artefatos do model deste adapter (id + payload). Usada
@@ -6,6 +6,7 @@ export declare class DatabaseAdapter implements OidcAdapter {
6
6
  private db;
7
7
  constructor(name: string, db: Database);
8
8
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
9
+ renewSession(id: string, expiresIn: number): Promise<boolean>;
9
10
  find(id: string): Promise<OidcPayload | undefined>;
10
11
  findByUid(uid: string): Promise<OidcPayload | undefined>;
11
12
  findByUserCode(userCode: string): Promise<OidcPayload | undefined>;
@@ -38,6 +38,32 @@ export class DatabaseAdapter {
38
38
  }
39
39
  return JSON.parse(record.payload);
40
40
  }
41
+ async renewSession(id, expiresIn) {
42
+ if (this.name !== 'Session' || !Number.isSafeInteger(expiresIn) || expiresIn < 1)
43
+ return false;
44
+ const row = await this.#query().where('id', id).first();
45
+ const payload = await this.#parse(row);
46
+ const now = Date.now();
47
+ if (!payload ||
48
+ payload.transient ||
49
+ typeof payload.exp !== 'number' ||
50
+ payload.exp <= now / 1000)
51
+ return false;
52
+ // Compare-and-set: concurrent logout or mutation cannot be undone by renewal.
53
+ const changed = await this.#query()
54
+ .where('id', id)
55
+ .where('payload', row.payload)
56
+ .where('expires_at', '>', new Date(now).toISOString())
57
+ .update({
58
+ payload: JSON.stringify({ ...payload, exp: Math.floor(now / 1000) + expiresIn }),
59
+ expires_at: new Date(now + expiresIn * 1000).toISOString(),
60
+ });
61
+ if (Number(changed) > 0)
62
+ return true;
63
+ // A parallel renewal is harmless; a deletion must remain deleted.
64
+ const current = await this.find(id);
65
+ return Boolean(current && !current.transient && typeof current.exp === 'number' && current.exp > now / 1000);
66
+ }
41
67
  async find(id) {
42
68
  return this.#parse(await this.#query().where('id', id).first());
43
69
  }
@@ -7,6 +7,7 @@ export declare class RedisAdapter implements OidcAdapter {
7
7
  private prefix;
8
8
  constructor(name: string, redis: Redis, prefix: string);
9
9
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
10
+ renewSession(id: string, expiresIn: number): Promise<boolean>;
10
11
  find(id: string): Promise<OidcPayload | undefined>;
11
12
  findByUid(uid: string): Promise<OidcPayload | undefined>;
12
13
  findByUserCode(userCode: string): Promise<OidcPayload | undefined>;
@@ -52,6 +52,36 @@ export class RedisAdapter {
52
52
  multi.expire(key, expiresIn);
53
53
  await multi.exec();
54
54
  }
55
+ async renewSession(id, expiresIn) {
56
+ if (this.name !== 'Session' || !Number.isSafeInteger(expiresIn) || expiresIn < 1)
57
+ return false;
58
+ for (let attempt = 0; attempt < 3; attempt++) {
59
+ const raw = await this.redis.get(this.#key(id));
60
+ if (!raw)
61
+ return false;
62
+ const payload = JSON.parse(raw);
63
+ const now = Math.floor(Date.now() / 1000);
64
+ if (payload.transient || typeof payload.exp !== 'number' || payload.exp <= now)
65
+ return false;
66
+ const updated = JSON.stringify({ ...payload, exp: now + expiresIn });
67
+ const result = await this.redis.eval(`
68
+ local raw = redis.call('GET', KEYS[1])
69
+ if not raw then return 0 end
70
+ if raw ~= ARGV[1] then return 2 end
71
+ redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
72
+ if ARGV[4] ~= '' then redis.call('SET', KEYS[2], ARGV[4], 'EX', ARGV[3]) end
73
+ return 1
74
+ `, 2, this.#key(id), payload.uid ? this.#uidKey(payload.uid) : this.#key(id), raw, updated, expiresIn, payload.uid ? id : '');
75
+ if (result !== 2)
76
+ return result === 1;
77
+ }
78
+ // Another request may have renewed while we were comparing the payload.
79
+ const current = await this.find(id);
80
+ return Boolean(current &&
81
+ !current.transient &&
82
+ typeof current.exp === 'number' &&
83
+ current.exp > Date.now() / 1000);
84
+ }
55
85
  async find(id) {
56
86
  const data = await this.redis.get(this.#key(id));
57
87
  if (!data)
@@ -12,6 +12,7 @@
12
12
  * recibos) não depende de protocolo; o formato do fio vem do adapter em
13
13
  * `protocol` (default: PACT — https://openpactprotocol.org). Ver `protocol.ts`.
14
14
  */
15
+ import { type PoppyConfigInput, type ResolvedPoppyConfig } from './poppy/config.js';
15
16
  import { type BuiltinProtocolId, type PersonalAgentProtocol } from './protocol.js';
16
17
  /** Registro de um personal agent conhecido (PACT §3.1). */
17
18
  export interface PersonalAgentRegistration {
@@ -55,10 +56,12 @@ export interface PersonalAgentsConfigInput {
55
56
  */
56
57
  protocol?: BuiltinProtocolId | PersonalAgentProtocol;
57
58
  /**
58
- * Valor que os personal agents põem no `aud` do JWT deles. Opaco e atribuído
59
- * por ESTE app no registro do agente — um valor por app, não derivado de URL.
59
+ * Valor que os personal agents põem no `aud` do JWT deles (PACT). Opaco e
60
+ * atribuído por ESTE app no registro do agente — um valor por app, não
61
+ * derivado de URL. Liga o PACT; obrigatório, a não ser num app só-Poppy
62
+ * (`poppy` sem `audience`).
60
63
  */
61
- audience: string;
64
+ audience?: string;
62
65
  /**
63
66
  * Agentes aceitos: uma lista estática, ou uma função que resolve pelo `iss`
64
67
  * (para quem guarda o registro no banco). Default: nenhum.
@@ -75,6 +78,15 @@ export interface PersonalAgentsConfigInput {
75
78
  delegation?: PersonalAgentDelegationConfigInput;
76
79
  /** Prefixo das rotas (`{prefix}/oauth/*`, `{prefix}/consent`). Default: `/agents`. */
77
80
  prefix?: string;
81
+ /**
82
+ * Personal Agent Protocol ("Poppy", https://personalagentprotocol.org) — o
83
+ * segundo protocolo, ao lado do PACT. Um app pode servir os dois. Ver
84
+ * `poppy/config.ts`.
85
+ *
86
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
87
+ * de forma incompatível fora de majors enquanto ela for draft.
88
+ */
89
+ poppy?: PoppyConfigInput;
78
90
  }
79
91
  export interface ResolvedPersonalAgentDelegationConfig {
80
92
  interfaceUrl: string;
@@ -85,12 +97,17 @@ export interface ResolvedPersonalAgentDelegationConfig {
85
97
  pollInterval: number;
86
98
  }
87
99
  export interface ResolvedPersonalAgentsConfig {
100
+ /** PACT ligado (`audience` configurado). */
101
+ pact: boolean;
88
102
  protocol: PersonalAgentProtocol;
103
+ /** `''` quando o PACT está desligado. */
89
104
  audience: string;
90
105
  resolveAgent: PersonalAgentResolver;
91
106
  open: boolean;
92
107
  delegation?: ResolvedPersonalAgentDelegationConfig;
93
108
  prefix: string;
109
+ /** Poppy resolvido (undefined = desligado). */
110
+ poppy?: ResolvedPoppyConfig;
94
111
  }
95
112
  export declare function normalizePersonalAgentsPrefix(prefix: string | undefined): string;
96
113
  export declare function resolvePersonalAgentsConfig(input: PersonalAgentsConfigInput | undefined): ResolvedPersonalAgentsConfig | undefined;