@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.
- package/README.md +149 -0
- package/build/host/views/agents/consent.edge +13 -2
- package/build/host/views/agents/done.edge +3 -0
- package/build/host/views/mfa-challenge.edge +42 -3
- package/build/index.d.ts +19 -1
- package/build/index.js +13 -1
- package/build/providers/authkit_server_provider.js +15 -1
- package/build/src/accounts/account_store.d.ts +2 -0
- package/build/src/accounts/lucid_account_store.js +2 -0
- package/build/src/adapters/adapter_contract.d.ts +2 -0
- package/build/src/adapters/database_adapter.d.ts +1 -0
- package/build/src/adapters/database_adapter.js +26 -0
- package/build/src/adapters/redis_adapter.d.ts +1 -0
- package/build/src/adapters/redis_adapter.js +30 -0
- package/build/src/agents/config.d.ts +20 -3
- package/build/src/agents/config.js +19 -4
- package/build/src/agents/delegation_service.js +18 -8
- package/build/src/agents/middleware.js +2 -1
- package/build/src/agents/poppy/client_registry.d.ts +45 -0
- package/build/src/agents/poppy/client_registry.js +226 -0
- package/build/src/agents/poppy/config.d.ts +362 -0
- package/build/src/agents/poppy/config.js +201 -0
- package/build/src/agents/poppy/dpop.d.ts +61 -0
- package/build/src/agents/poppy/dpop.js +158 -0
- package/build/src/agents/poppy/errors.d.ts +20 -0
- package/build/src/agents/poppy/errors.js +22 -0
- package/build/src/agents/poppy/replay.d.ts +25 -0
- package/build/src/agents/poppy/replay.js +106 -0
- package/build/src/agents/poppy/resource.d.ts +95 -0
- package/build/src/agents/poppy/resource.js +125 -0
- package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
- package/build/src/agents/poppy/safe_fetch.js +256 -0
- package/build/src/agents/poppy/service.d.ts +280 -0
- package/build/src/agents/poppy/service.js +1452 -0
- package/build/src/agents/poppy/store.d.ts +109 -0
- package/build/src/agents/poppy/store.js +260 -0
- package/build/src/agents/poppy/web_session.d.ts +32 -0
- package/build/src/agents/poppy/web_session.js +104 -0
- package/build/src/agents/protocol.d.ts +8 -4
- package/build/src/agents/runtime.d.ts +17 -1
- package/build/src/agents/runtime.js +138 -13
- package/build/src/define_config.d.ts +24 -0
- package/build/src/define_config.js +14 -0
- package/build/src/host/account_api/account_api_controller.d.ts +12 -1
- package/build/src/host/account_api/account_api_controller.js +10 -5
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/auth_host_config.d.ts +2 -1
- package/build/src/host/controllers/account_apps_controller.js +13 -3
- package/build/src/host/controllers/account_security_controller.js +8 -4
- package/build/src/host/controllers/interaction_controller.d.ts +13 -1
- package/build/src/host/controllers/interaction_controller.js +352 -24
- package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
- package/build/src/host/controllers/poppy_consent_controller.js +173 -0
- package/build/src/host/controllers/poppy_controller.d.ts +25 -0
- package/build/src/host/controllers/poppy_controller.js +194 -0
- package/build/src/host/csrf.d.ts +7 -0
- package/build/src/host/csrf.js +16 -1
- package/build/src/host/custom_login.d.ts +36 -0
- package/build/src/host/custom_login.js +76 -0
- package/build/src/host/custom_login_completion.d.ts +4 -0
- package/build/src/host/custom_login_completion.js +33 -0
- package/build/src/host/custom_mfa.d.ts +77 -0
- package/build/src/host/custom_mfa.js +268 -0
- package/build/src/host/i18n.d.ts +8 -0
- package/build/src/host/i18n.js +14 -6
- package/build/src/host/idp_session_bridge.d.ts +5 -3
- package/build/src/host/idp_session_bridge.js +5 -3
- package/build/src/host/oidc_rp_guard.d.ts +5 -1
- package/build/src/host/oidc_rp_guard.js +15 -2
- package/build/src/host/persistent_rp_session.d.ts +8 -0
- package/build/src/host/persistent_rp_session.js +74 -0
- package/build/src/host/poppy_session_key.d.ts +9 -0
- package/build/src/host/poppy_session_key.js +9 -0
- package/build/src/host/register_auth_host.js +72 -0
- package/build/src/host/whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_code_sender.js +9 -0
- package/build/src/host/whatsapp_senders/http.d.ts +5 -0
- package/build/src/host/whatsapp_senders/http.js +33 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
- package/build/src/provider/oidc_service.d.ts +5 -0
- package/build/src/provider/oidc_service.js +15 -4
- package/build/src/schema/ensure.js +124 -0
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
- package/package.json +2 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
59
|
-
* por ESTE app no registro do agente — um valor por app, não
|
|
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
|
|
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;
|